[gnome-builder/wip/chergert/refactor] land the refactor



commit 594935a45525add66d7305f82ca85be883cff49f
Author: Christian Hergert <chergert redhat com>
Date:   Tue Jan 8 21:21:37 2019 -0800

    land the refactor

 JOURNAL.md                                         |  315 ++
 build-aux/asan.supp                                |    8 -
 build-aux/flatpak/org.gnome.Builder.json           |   10 +-
 data/appdata/meson.build                           |   17 +
 .../{ => appdata}/org.gnome.Builder.appdata.xml.in |    0
 data/meson.build                                   |   28 +-
 .../builder-dark-refresh.style-scheme.xml          |  198 +
 data/style-schemes/builder-dark.style-scheme.xml   |    8 +-
 data/themes/Adwaita-dark.css                       |   26 -
 data/themes/Adwaita-shared.css                     |   78 -
 data/themes/Arc-Darker.css                         |    7 -
 data/themes/Arc-shared.css                         |   83 -
 data/themes/shared.css                             |  146 -
 data/themes/shared/shared-editor.css               |  133 -
 data/themes/shared/shared-greeter.css              |   15 -
 data/themes/shared/shared-layout.css               |   83 -
 data/themes/shared/shared-omnibar.css              |   50 -
 doc/help/meson.build                               |    2 +-
 doc/meson.build                                    |    8 +-
 doc/sdk/libide-docs.sgml                           |  571 ++-
 doc/sdk/meson.build                                |   69 +-
 meson.build                                        |   88 +-
 meson_options.txt                                  |  130 +-
 po/POTFILES.in                                     |  353 +-
 src/bug-buddy.c                                    |    6 +-
 src/bug-buddy.h                                    |    4 +-
 src/fusermount-wrapper.c                           |    6 +-
 src/{libide => }/gconstructor.h                    |    0
 src/gstyle/gstyle-animation.c                      |    4 +-
 src/gstyle/gstyle-animation.h                      |    4 +-
 src/gstyle/gstyle-cielab.c                         |    4 +-
 src/gstyle/gstyle-cielab.h                         |    4 +-
 src/gstyle/gstyle-color-component.c                |    4 +-
 src/gstyle/gstyle-color-component.h                |    4 +-
 src/gstyle/gstyle-color-convert.c                  |    6 +-
 src/gstyle/gstyle-color-convert.h                  |    4 +-
 src/gstyle/gstyle-color-filter.c                   |    4 +-
 src/gstyle/gstyle-color-filter.h                   |    4 +-
 src/gstyle/gstyle-color-item.c                     |    4 +-
 src/gstyle/gstyle-color-item.h                     |    4 +-
 src/gstyle/gstyle-color-panel-actions.c            |    4 +-
 src/gstyle/gstyle-color-panel-actions.h            |    4 +-
 src/gstyle/gstyle-color-panel-private.h            |    4 +-
 src/gstyle/gstyle-color-panel.c                    |    8 +-
 src/gstyle/gstyle-color-panel.h                    |    4 +-
 src/gstyle/gstyle-color-plane.c                    |    6 +-
 src/gstyle/gstyle-color-plane.h                    |    6 +-
 src/gstyle/gstyle-color-predefined.h               |    4 +-
 src/gstyle/gstyle-color-scale.c                    |    4 +-
 src/gstyle/gstyle-color-scale.h                    |    4 +-
 src/gstyle/gstyle-color-widget-actions.c           |    4 +-
 src/gstyle/gstyle-color-widget-actions.h           |    4 +-
 src/gstyle/gstyle-color-widget.c                   |    4 +-
 src/gstyle/gstyle-color-widget.h                   |    4 +-
 src/gstyle/gstyle-color.c                          |    4 +-
 src/gstyle/gstyle-color.h                          |    4 +-
 src/gstyle/gstyle-colorlexer.c                     |    4 +-
 src/gstyle/gstyle-colorlexer.h                     |    4 +-
 src/gstyle/gstyle-css-provider.c                   |    4 +-
 src/gstyle/gstyle-css-provider.h                   |    4 +-
 src/gstyle/gstyle-eyedropper.c                     |    4 +-
 src/gstyle/gstyle-eyedropper.h                     |    4 +-
 src/gstyle/gstyle-hsv.c                            |    4 +-
 src/gstyle/gstyle-hsv.h                            |    4 +-
 src/gstyle/gstyle-palette-widget.c                 |    4 +-
 src/gstyle/gstyle-palette-widget.h                 |    4 +-
 src/gstyle/gstyle-palette.c                        |    6 +-
 src/gstyle/gstyle-palette.h                        |    4 +-
 src/gstyle/gstyle-private.h                        |    4 +-
 src/gstyle/gstyle-rename-popover.c                 |    4 +-
 src/gstyle/gstyle-rename-popover.h                 |    4 +-
 src/gstyle/gstyle-revealer.c                       |    4 +-
 src/gstyle/gstyle-revealer.h                       |    4 +-
 src/gstyle/gstyle-slidein.c                        |    6 +-
 src/gstyle/gstyle-slidein.h                        |    4 +-
 src/gstyle/gstyle-types.h                          |    4 +-
 src/gstyle/gstyle-utils.c                          |    4 +-
 src/gstyle/gstyle-utils.h                          |    4 +-
 src/gstyle/gstyle-xyz.c                            |    4 +-
 src/gstyle/gstyle-xyz.h                            |    4 +-
 src/gstyle/gstyle.map                              |    6 -
 src/gstyle/meson.build                             |   43 +-
 src/gstyle/tests/test-gstyle-color-panel.c         |    4 +-
 src/gstyle/tests/test-gstyle-color-plane.c         |    4 +-
 src/gstyle/tests/test-gstyle-color-scale.c         |    4 +-
 src/gstyle/tests/test-gstyle-color-widget.c        |    4 +-
 src/gstyle/tests/test-gstyle-color.c               |    4 +-
 src/gstyle/tests/test-gstyle-filter.c              |    4 +-
 src/gstyle/tests/test-gstyle-palette-widget.c      |    4 +-
 src/gstyle/tests/test-gstyle-palette.c             |    4 +-
 src/gstyle/tests/test-gstyle-parse.c               |    4 +-
 src/libeditorconfig/ec_glob.c                      |  371 --
 src/libeditorconfig/ec_glob.h                      |   43 -
 src/libeditorconfig/editorconfig.c                 |  547 ---
 src/libeditorconfig/editorconfig.h                 |   37 -
 src/libeditorconfig/editorconfig/editorconfig.h    |  309 --
 .../editorconfig/editorconfig_handle.h             |  193 -
 src/libeditorconfig/editorconfig_handle.c          |  155 -
 src/libeditorconfig/editorconfig_handle.h          |   89 -
 src/libeditorconfig/global.h                       |   80 -
 src/libeditorconfig/ini.c                          |  200 -
 src/libeditorconfig/ini.h                          |   93 -
 src/libeditorconfig/meson.build                    |   49 -
 src/libeditorconfig/misc.c                         |  250 --
 src/libeditorconfig/misc.h                         |   62 -
 src/libeditorconfig/utarray.h                      |  232 --
 src/libide.deps                                    |    8 +
 src/libide/Ide-1.0.metadata                        |    2 -
 src/libide/Ide.py                                  |    2 +-
 src/libide/application/OVERVIEW.md                 |   44 -
 src/libide/application/ide-application-actions.c   |  509 ---
 src/libide/application/ide-application-actions.h   |   28 -
 src/libide/application/ide-application-addin.c     |  108 -
 src/libide/application/ide-application-addin.h     |   56 -
 src/libide/application/ide-application-color.c     |  230 --
 .../application/ide-application-command-line.c     |  494 ---
 src/libide/application/ide-application-credits.h   |  597 ---
 src/libide/application/ide-application-open.c      |  273 --
 src/libide/application/ide-application-plugins.c   |  482 ---
 src/libide/application/ide-application-private.h   |  101 -
 src/libide/application/ide-application-shortcuts.c |   73 -
 src/libide/application/ide-application-tests.c     |  206 -
 src/libide/application/ide-application-tests.h     |   40 -
 src/libide/application/ide-application-tool.c      |  108 -
 src/libide/application/ide-application-tool.h      |   70 -
 src/libide/application/ide-application.c           | 1053 -----
 src/libide/application/ide-application.h           |   83 -
 src/libide/application/meson.build                 |   31 -
 src/libide/buffers/OVERVIEW.md                     |   30 -
 src/libide/buffers/ide-buffer-addin.c              |  136 -
 src/libide/buffers/ide-buffer-addin.h              |   52 -
 src/libide/buffers/ide-buffer-change-monitor.c     |  134 -
 src/libide/buffers/ide-buffer-change-monitor.h     |   64 -
 src/libide/buffers/ide-buffer-manager.c            | 2344 -----------
 src/libide/buffers/ide-buffer-manager.h            |  115 -
 src/libide/buffers/ide-buffer-private.h            |   53 -
 src/libide/buffers/ide-buffer.c                    | 3530 -----------------
 src/libide/buffers/ide-buffer.h                    |  176 -
 src/libide/buffers/ide-unsaved-file.c              |  174 -
 src/libide/buffers/ide-unsaved-file.h              |   50 -
 src/libide/buffers/ide-unsaved-files.c             |  937 -----
 src/libide/buffers/ide-unsaved-files.h             |   79 -
 src/libide/buffers/meson.build                     |   32 -
 src/libide/buildconfig/OVERVIEW.md                 |   27 -
 src/libide/buildconfig/buildconfig.plugin          |    9 -
 .../ide-buildconfig-configuration-provider.c       |  773 ----
 .../ide-buildconfig-configuration-provider.h       |   29 -
 .../buildconfig/ide-buildconfig-configuration.c    |  170 -
 .../buildconfig/ide-buildconfig-configuration.h    |   43 -
 .../buildconfig/ide-buildconfig-pipeline-addin.c   |  115 -
 .../buildconfig/ide-buildconfig-pipeline-addin.h   |   29 -
 src/libide/buildconfig/ide-buildconfig-plugin.c    |   39 -
 src/libide/buildconfig/meson.build                 |   21 -
 src/libide/buildsystem/OVERVIEW.md                 |   54 -
 src/libide/buildsystem/ide-build-log-private.h     |   44 -
 src/libide/buildsystem/ide-build-log.c             |  245 --
 src/libide/buildsystem/ide-build-log.h             |   36 -
 src/libide/buildsystem/ide-build-manager.c         | 1773 ---------
 src/libide/buildsystem/ide-build-manager.h         |   81 -
 src/libide/buildsystem/ide-build-pipeline-addin.c  |  105 -
 src/libide/buildsystem/ide-build-pipeline-addin.h  |   54 -
 src/libide/buildsystem/ide-build-pipeline.c        | 3912 -------------------
 src/libide/buildsystem/ide-build-pipeline.h        |  192 -
 src/libide/buildsystem/ide-build-private.h         |   43 -
 src/libide/buildsystem/ide-build-stage-launcher.c  |  626 ---
 src/libide/buildsystem/ide-build-stage-launcher.h  |   72 -
 src/libide/buildsystem/ide-build-stage-mkdirs.c    |  222 --
 src/libide/buildsystem/ide-build-stage-mkdirs.h    |   52 -
 src/libide/buildsystem/ide-build-stage-private.h   |   39 -
 src/libide/buildsystem/ide-build-stage-transfer.c  |  248 --
 src/libide/buildsystem/ide-build-stage-transfer.h  |   36 -
 src/libide/buildsystem/ide-build-stage.c           | 1145 ------
 src/libide/buildsystem/ide-build-stage.h           |  255 --
 .../buildsystem/ide-build-system-discovery.c       |   71 -
 .../buildsystem/ide-build-system-discovery.h       |   52 -
 src/libide/buildsystem/ide-build-system.c          |  752 ----
 src/libide/buildsystem/ide-build-system.h          |  118 -
 src/libide/buildsystem/ide-build-target-provider.c |  119 -
 src/libide/buildsystem/ide-build-target-provider.h |   54 -
 src/libide/buildsystem/ide-build-target.c          |  203 -
 src/libide/buildsystem/ide-build-target.h          |   62 -
 src/libide/buildsystem/ide-build-utils.c           |   81 -
 src/libide/buildsystem/ide-build-utils.h           |   29 -
 src/libide/buildsystem/ide-compile-commands.c      |  738 ----
 src/libide/buildsystem/ide-compile-commands.h      |   56 -
 src/libide/buildsystem/ide-dependency-updater.c    |   81 -
 src/libide/buildsystem/ide-dependency-updater.h    |   54 -
 src/libide/buildsystem/ide-environment-variable.c  |  183 -
 src/libide/buildsystem/ide-environment-variable.h  |   46 -
 src/libide/buildsystem/ide-environment.c           |  330 --
 src/libide/buildsystem/ide-environment.h           |   58 -
 src/libide/buildsystem/ide-simple-build-target.c   |  219 --
 src/libide/buildsystem/ide-simple-build-target.h   |   60 -
 src/libide/buildsystem/meson.build                 |   58 -
 src/libide/buildui/OVERVIEW.md                     |    8 -
 src/libide/buildui/buildui.plugin                  |   10 -
 src/libide/buildui/ide-build-configuration-row.c   |  182 -
 src/libide/buildui/ide-build-configuration-row.h   |   33 -
 src/libide/buildui/ide-build-configuration-row.ui  |   82 -
 src/libide/buildui/ide-build-configuration-view.c  |  481 ---
 src/libide/buildui/ide-build-configuration-view.h  |   34 -
 src/libide/buildui/ide-build-configuration-view.ui |  282 --
 src/libide/buildui/ide-build-log-panel.c           |  375 --
 src/libide/buildui/ide-build-log-panel.h           |   32 -
 src/libide/buildui/ide-build-log-panel.ui          |   84 -
 src/libide/buildui/ide-build-panel.c               |  713 ----
 src/libide/buildui/ide-build-panel.h               |   33 -
 src/libide/buildui/ide-build-panel.ui              |  174 -
 src/libide/buildui/ide-build-perspective.c         |  476 ---
 src/libide/buildui/ide-build-perspective.h         |   34 -
 src/libide/buildui/ide-build-perspective.ui        |   46 -
 src/libide/buildui/ide-build-plugin.c              |   34 -
 src/libide/buildui/ide-build-stage-row.c           |  194 -
 src/libide/buildui/ide-build-stage-row.h           |   32 -
 src/libide/buildui/ide-build-stage-row.ui          |   17 -
 src/libide/buildui/ide-build-tool.h                |   29 -
 src/libide/buildui/ide-build-workbench-addin.c     |  289 --
 src/libide/buildui/ide-build-workbench-addin.h     |   29 -
 src/libide/buildui/ide-environment-editor-row.c    |  276 --
 src/libide/buildui/ide-environment-editor-row.h    |   35 -
 src/libide/buildui/ide-environment-editor.c        |  315 --
 src/libide/buildui/ide-environment-editor.h        |   35 -
 src/libide/buildui/meson.build                     |   24 -
 src/libide/{files => code}/defaults.ini            |    0
 src/libide/code/ide-buffer-addin-private.h         |   82 +
 src/libide/code/ide-buffer-addin.c                 |  411 ++
 src/libide/code/ide-buffer-addin.h                 |   96 +
 src/libide/code/ide-buffer-change-monitor.c        |  233 ++
 src/libide/code/ide-buffer-change-monitor.h        |   86 +
 src/libide/code/ide-buffer-manager.c               | 1309 +++++++
 src/libide/code/ide-buffer-manager.h               |  119 +
 src/libide/code/ide-buffer-private.h               |   64 +
 src/libide/code/ide-buffer.c                       | 3675 +++++++++++++++++
 src/libide/code/ide-buffer.h                       |  178 +
 src/libide/code/ide-code-global.c                  |   44 +
 src/libide/code/ide-code-index-entries.c           |  178 +
 src/libide/code/ide-code-index-entries.h           |   68 +
 src/libide/code/ide-code-index-entry.c             |  271 ++
 src/libide/code/ide-code-index-entry.h             |   92 +
 src/libide/code/ide-code-indexer.c                 |  234 ++
 src/libide/code/ide-code-indexer.h                 |   85 +
 src/libide/code/ide-code-types.h                   |   60 +
 src/libide/code/ide-diagnostic-provider.c          |  156 +
 src/libide/code/ide-diagnostic-provider.h          |   76 +
 src/libide/code/ide-diagnostic.c                   |  748 ++++
 src/libide/code/ide-diagnostic.h                   |  104 +
 src/libide/code/ide-diagnostics-manager-private.h  |   41 +
 src/libide/code/ide-diagnostics-manager.c          | 1177 ++++++
 src/libide/code/ide-diagnostics-manager.h          |   48 +
 src/libide/code/ide-diagnostics.c                  |  506 +++
 src/libide/code/ide-diagnostics.h                  |   97 +
 src/libide/code/ide-doc-seq-private.h              |   30 +
 src/libide/code/ide-doc-seq.c                      |   57 +
 src/libide/code/ide-file-settings.c                |  540 +++
 src/libide/{files => code}/ide-file-settings.defs  |    0
 src/libide/code/ide-file-settings.h                |   83 +
 src/libide/code/ide-formatter-options.c            |  170 +
 src/libide/code/ide-formatter-options.h            |   49 +
 src/libide/code/ide-formatter.c                    |  175 +
 src/libide/code/ide-formatter.h                    |   93 +
 src/libide/code/ide-gsettings-file-settings.c      |  187 +
 src/libide/code/ide-gsettings-file-settings.h      |   31 +
 src/libide/code/ide-highlight-engine.c             | 1189 ++++++
 src/libide/code/ide-highlight-engine.h             |   62 +
 src/libide/code/ide-highlight-index.c              |  244 ++
 src/libide/code/ide-highlight-index.h              |   61 +
 src/libide/code/ide-highlighter.c                  |   93 +
 src/libide/code/ide-highlighter.h                  |   91 +
 src/libide/code/ide-indent-style.h                 |   37 +
 src/libide/code/ide-language-defaults.c            |  461 +++
 src/libide/code/ide-language-defaults.h            |   33 +
 src/libide/code/ide-language.c                     |  109 +
 src/libide/code/ide-language.h                     |   36 +
 src/libide/code/ide-location.c                     |  503 +++
 src/libide/code/ide-location.h                     |   75 +
 src/libide/code/ide-range.c                        |  290 ++
 src/libide/code/ide-range.h                        |   58 +
 src/libide/code/ide-rename-provider.c              |  162 +
 src/libide/code/ide-rename-provider.h              |   73 +
 src/libide/code/ide-source-iter.c                  |  626 +++
 src/libide/code/ide-source-iter.h                  |   68 +
 src/libide/code/ide-source-style-scheme.c          |  117 +
 src/libide/code/ide-source-style-scheme.h          |   37 +
 src/libide/code/ide-spaces-style.h                 |   43 +
 src/libide/code/ide-symbol-node.c                  |  272 ++
 src/libide/code/ide-symbol-node.h                  |   73 +
 src/libide/code/ide-symbol-resolver.c              |  361 ++
 src/libide/code/ide-symbol-resolver.h              |  127 +
 src/libide/code/ide-symbol-tree.c                  |   78 +
 src/libide/code/ide-symbol-tree.h                  |   57 +
 src/libide/code/ide-symbol.c                       |  533 +++
 src/libide/code/ide-symbol.h                       |  129 +
 src/libide/code/ide-text-edit-private.h            |   32 +
 src/libide/code/ide-text-edit.c                    |  347 ++
 src/libide/code/ide-text-edit.h                    |   64 +
 src/libide/code/ide-text-iter.c                    | 1001 +++++
 src/libide/code/ide-text-iter.h                    |  102 +
 src/libide/code/ide-unsaved-file-private.h         |   32 +
 src/libide/code/ide-unsaved-file.c                 |  178 +
 src/libide/code/ide-unsaved-file.h                 |   54 +
 src/libide/code/ide-unsaved-files.c                | 1022 +++++
 src/libide/code/ide-unsaved-files.h                |   87 +
 src/libide/code/libide-code.gresource.xml          |    6 +
 src/libide/code/libide-code.h                      |   70 +
 src/libide/code/meson.build                        |  189 +
 src/libide/completion/ide-completion-context.c     | 1084 ------
 src/libide/completion/ide-completion-context.h     |   72 -
 src/libide/completion/ide-completion-display.c     |   94 -
 src/libide/completion/ide-completion-display.h     |   70 -
 .../completion/ide-completion-list-box-row.c       |  355 --
 .../completion/ide-completion-list-box-row.h       |   59 -
 src/libide/completion/ide-completion-list-box.c    |  934 -----
 src/libide/completion/ide-completion-list-box.h    |   58 -
 src/libide/completion/ide-completion-overlay.c     |  328 --
 src/libide/completion/ide-completion-overlay.h     |   34 -
 src/libide/completion/ide-completion-private.h     |   88 -
 src/libide/completion/ide-completion-proposal.c    |   30 -
 src/libide/completion/ide-completion-proposal.h    |   37 -
 src/libide/completion/ide-completion-provider.c    |  342 --
 src/libide/completion/ide-completion-provider.h    |  119 -
 src/libide/completion/ide-completion-types.h       |   40 -
 src/libide/completion/ide-completion-view.c        |  441 ---
 src/libide/completion/ide-completion-view.h        |   40 -
 src/libide/completion/ide-completion-window.c      |  385 --
 src/libide/completion/ide-completion-window.h      |   38 -
 src/libide/completion/ide-completion.c             | 1784 ---------
 src/libide/completion/ide-completion.h             |   76 -
 src/libide/completion/meson.build                  |   42 -
 src/libide/config/ide-configuration-manager.c      | 1014 -----
 src/libide/config/ide-configuration-manager.h      |   61 -
 src/libide/config/ide-configuration-private.h      |   27 -
 src/libide/config/ide-configuration-provider.c     |  388 --
 src/libide/config/ide-configuration-provider.h     |   95 -
 src/libide/config/ide-configuration.c              | 1689 --------
 src/libide/config/ide-configuration.h              |  210 -
 src/libide/config/meson.build                      |   21 -
 src/libide/{ => core}/ide-build-ident.h.in         |    0
 src/libide/core/ide-context-addin.c                |  207 +
 src/libide/core/ide-context-addin.h                |   73 +
 src/libide/core/ide-context-private.h              |   29 +
 src/libide/core/ide-context.c                      |  855 ++++
 src/libide/core/ide-context.h                      |   91 +
 src/libide/{ => core}/ide-debug.h.in               |    0
 src/libide/core/ide-global.c                       |  234 ++
 src/libide/core/ide-global.h                       |   66 +
 src/libide/core/ide-log.c                          |  380 ++
 src/libide/core/ide-log.h                          |   45 +
 src/libide/core/ide-macros.h                       |  249 ++
 src/libide/core/ide-notification.c                 | 1187 ++++++
 src/libide/core/ide-notification.h                 |  143 +
 src/libide/core/ide-notifications.c                |  516 +++
 src/libide/core/ide-notifications.h                |   48 +
 src/libide/core/ide-object-box.c                   |  289 ++
 src/libide/core/ide-object-box.h                   |   46 +
 src/libide/core/ide-object-notify.c                |  114 +
 src/libide/core/ide-object.c                       | 1367 +++++++
 src/libide/core/ide-object.h                       |  156 +
 src/libide/core/ide-settings.c                     |  589 +++
 src/libide/core/ide-settings.h                     |  111 +
 src/libide/core/ide-transfer-manager.c             |  493 +++
 src/libide/core/ide-transfer-manager.h             |   58 +
 src/libide/core/ide-transfer.c                     |  522 +++
 src/libide/core/ide-transfer.h                     |  101 +
 src/libide/core/ide-version-macros.h               |  160 +
 src/libide/{ => core}/ide-version.h.in             |    0
 src/libide/core/libide-core.h                      |   43 +
 src/libide/core/meson.build                        |  124 +
 src/libide/debugger/debugger.plugin                |   10 -
 src/libide/debugger/gtk/menus.ui                   |   16 -
 src/libide/debugger/ide-debug-manager.c            |  180 +-
 src/libide/debugger/ide-debug-manager.h            |   31 +-
 src/libide/debugger/ide-debugger-actions.c         |    6 +-
 .../debugger/ide-debugger-address-map-private.h    |   57 +
 src/libide/debugger/ide-debugger-address-map.c     |   16 +-
 src/libide/debugger/ide-debugger-address-map.h     |   55 -
 src/libide/debugger/ide-debugger-breakpoint.c      |   76 +-
 src/libide/debugger/ide-debugger-breakpoint.h      |   60 +-
 .../debugger/ide-debugger-breakpoints-view.c       |  606 ---
 .../debugger/ide-debugger-breakpoints-view.h       |   36 -
 src/libide/debugger/ide-debugger-breakpoints.c     |   16 +-
 src/libide/debugger/ide-debugger-breakpoints.h     |   22 +-
 src/libide/debugger/ide-debugger-controls.c        |   41 -
 src/libide/debugger/ide-debugger-controls.h        |   37 -
 .../debugger/ide-debugger-disassembly-view.c       |  134 -
 .../debugger/ide-debugger-disassembly-view.h       |   37 -
 .../debugger/ide-debugger-disassembly-view.ui      |   24 -
 src/libide/debugger/ide-debugger-editor-addin.c    |  668 ----
 src/libide/debugger/ide-debugger-editor-addin.h    |   38 -
 src/libide/debugger/ide-debugger-fallbacks.c       |    8 +-
 src/libide/debugger/ide-debugger-frame.c           |    6 +-
 src/libide/debugger/ide-debugger-frame.h           |   40 +-
 src/libide/debugger/ide-debugger-hover-controls.c  |  199 -
 src/libide/debugger/ide-debugger-hover-controls.h  |   35 -
 src/libide/debugger/ide-debugger-hover-provider.c  |  123 -
 src/libide/debugger/ide-debugger-hover-provider.h  |   29 -
 src/libide/debugger/ide-debugger-instruction.c     |    6 +-
 src/libide/debugger/ide-debugger-instruction.h     |   22 +-
 src/libide/debugger/ide-debugger-libraries-view.c  |  365 --
 src/libide/debugger/ide-debugger-libraries-view.h  |   36 -
 src/libide/debugger/ide-debugger-library.c         |   10 +-
 src/libide/debugger/ide-debugger-library.h         |   30 +-
 src/libide/debugger/ide-debugger-locals-view.c     |  443 ---
 src/libide/debugger/ide-debugger-locals-view.h     |   45 -
 src/libide/debugger/ide-debugger-plugin.c          |   40 -
 src/libide/debugger/ide-debugger-private.h         |   10 +-
 src/libide/debugger/ide-debugger-register.c        |    6 +-
 src/libide/debugger/ide-debugger-register.h        |   33 +-
 src/libide/debugger/ide-debugger-registers-view.c  |  330 --
 src/libide/debugger/ide-debugger-registers-view.h  |   36 -
 src/libide/debugger/ide-debugger-thread-group.c    |    6 +-
 src/libide/debugger/ide-debugger-thread-group.h    |   29 +-
 src/libide/debugger/ide-debugger-thread.c          |    6 +-
 src/libide/debugger/ide-debugger-thread.h          |   20 +-
 src/libide/debugger/ide-debugger-threads-view.c    |  826 ----
 src/libide/debugger/ide-debugger-threads-view.h    |   35 -
 src/libide/debugger/ide-debugger-types.c           |    6 +-
 src/libide/debugger/ide-debugger-types.h           |   40 +-
 src/libide/debugger/ide-debugger-variable.c        |    6 +-
 src/libide/debugger/ide-debugger-variable.h        |   26 +-
 src/libide/debugger/ide-debugger.c                 |  136 +-
 src/libide/debugger/ide-debugger.h                 |  133 +-
 src/libide/debugger/libide-debugger.h              |   44 +
 src/libide/debugger/meson.build                    |   99 +-
 src/libide/devices/OVERVIEW.md                     |   18 -
 src/libide/devices/ide-deploy-strategy.c           |  248 --
 src/libide/devices/ide-deploy-strategy.h           |   84 -
 src/libide/devices/ide-device-info.c               |  219 --
 src/libide/devices/ide-device-info.h               |   58 -
 src/libide/devices/ide-device-manager.c            | 1018 -----
 src/libide/devices/ide-device-manager.h            |   52 -
 src/libide/devices/ide-device-private.h            |   27 -
 src/libide/devices/ide-device-provider.c           |  299 --
 src/libide/devices/ide-device-provider.h           |   68 -
 src/libide/devices/ide-device.c                    |  386 --
 src/libide/devices/ide-device.h                    |   87 -
 src/libide/devices/meson.build                     |   26 -
 src/libide/diagnostics/ide-diagnostic-provider.c   |  127 -
 src/libide/diagnostics/ide-diagnostic-provider.h   |   64 -
 src/libide/diagnostics/ide-diagnostic.c            |  584 ---
 src/libide/diagnostics/ide-diagnostic.h            |   96 -
 src/libide/diagnostics/ide-diagnostics-manager.c   | 1373 -------
 src/libide/diagnostics/ide-diagnostics-manager.h   |   50 -
 src/libide/diagnostics/ide-diagnostics.c           |  196 -
 src/libide/diagnostics/ide-diagnostics.h           |   54 -
 src/libide/diagnostics/ide-fixit.c                 |  193 -
 src/libide/diagnostics/ide-fixit.h                 |   50 -
 src/libide/diagnostics/ide-source-location.c       |  357 --
 src/libide/diagnostics/ide-source-location.h       |   71 -
 src/libide/diagnostics/ide-source-range.c          |  204 -
 src/libide/diagnostics/ide-source-range.h          |   49 -
 src/libide/diagnostics/meson.build                 |   29 -
 src/libide/directory/OVERVIEW.md                   |   20 -
 src/libide/directory/directory.plugin              |   13 -
 src/libide/directory/ide-directory-build-system.c  |  190 -
 src/libide/directory/ide-directory-build-system.h  |   32 -
 src/libide/directory/ide-directory-plugin.c        |   38 -
 src/libide/directory/ide-directory-vcs.c           |  259 --
 src/libide/directory/ide-directory-vcs.h           |   32 -
 src/libide/directory/meson.build                   |   15 -
 src/libide/doap/OVERVIEW.md                        |   13 -
 src/libide/doap/ide-doap-person.c                  |  182 -
 src/libide/doap/ide-doap-person.h                  |   45 -
 src/libide/doap/ide-doap.c                         |  636 ---
 src/libide/doap/ide-doap.h                         |   73 -
 src/libide/doap/meson.build                        |   25 -
 src/libide/doap/xml-reader.c                       |  597 ---
 src/libide/doap/xml-reader.h                       |   97 -
 src/libide/editor/editor.plugin                    |    9 -
 src/libide/editor/gtk/menus.ui                     |  135 -
 src/libide/editor/ide-editor-addin.c               |   87 +-
 src/libide/editor/ide-editor-addin.h               |   54 +-
 src/libide/editor/ide-editor-hover-provider.c      |  114 -
 src/libide/editor/ide-editor-hover-provider.h      |   29 -
 src/libide/editor/ide-editor-layout-stack-addin.c  |  113 -
 src/libide/editor/ide-editor-layout-stack-addin.h  |   29 -
 .../editor/ide-editor-layout-stack-controls.c      |  350 --
 .../editor/ide-editor-layout-stack-controls.h      |   53 -
 .../editor/ide-editor-layout-stack-controls.ui     |   85 -
 src/libide/editor/ide-editor-page-actions.c        |  599 +++
 src/libide/editor/ide-editor-page-addin.c          |  113 +
 src/libide/editor/ide-editor-page-addin.h          |   70 +
 src/libide/editor/ide-editor-page-settings.c       |  233 ++
 src/libide/editor/ide-editor-page-shortcuts.c      |  141 +
 src/libide/editor/ide-editor-page.c                | 1407 +++++++
 src/libide/editor/ide-editor-page.h                |   82 +
 src/libide/editor/ide-editor-page.ui               |  124 +
 src/libide/editor/ide-editor-perspective-actions.c |  165 -
 .../editor/ide-editor-perspective-shortcuts.c      |  105 -
 src/libide/editor/ide-editor-perspective.c         |  961 -----
 src/libide/editor/ide-editor-perspective.h         |   59 -
 src/libide/editor/ide-editor-perspective.ui        |   47 -
 src/libide/editor/ide-editor-plugin-private.h      |   27 +
 src/libide/editor/ide-editor-plugin.c              |   45 -
 src/libide/editor/ide-editor-print-operation.c     |    6 +-
 src/libide/editor/ide-editor-print-operation.h     |    6 +-
 src/libide/editor/ide-editor-private.h             |   63 +-
 src/libide/editor/ide-editor-properties.c          |  443 ---
 src/libide/editor/ide-editor-properties.h          |   35 -
 src/libide/editor/ide-editor-properties.ui         |  334 --
 .../editor/ide-editor-search-bar-shortcuts.c       |    8 +-
 src/libide/editor/ide-editor-search-bar.c          |   15 +-
 src/libide/editor/ide-editor-search-bar.h          |    6 +-
 src/libide/editor/ide-editor-search.c              |  118 +-
 src/libide/editor/ide-editor-search.h              |   77 +-
 src/libide/editor/ide-editor-session-addin.c       |  552 ---
 src/libide/editor/ide-editor-session-addin.h       |   29 -
 src/libide/editor/ide-editor-settings-dialog.c     |  331 ++
 src/libide/editor/ide-editor-settings-dialog.h     |   34 +
 src/libide/editor/ide-editor-settings-dialog.ui    |  288 ++
 src/libide/editor/ide-editor-sidebar.c             |   64 +-
 src/libide/editor/ide-editor-sidebar.h             |   23 +-
 src/libide/editor/ide-editor-sidebar.ui            |    2 +-
 src/libide/editor/ide-editor-surface-actions.c     |  164 +
 src/libide/editor/ide-editor-surface-shortcuts.c   |  107 +
 src/libide/editor/ide-editor-surface.c             |  919 +++++
 src/libide/editor/ide-editor-surface.h             |   65 +
 src/libide/editor/ide-editor-surface.ui            |   36 +
 src/libide/editor/ide-editor-utilities.c           |   12 +-
 src/libide/editor/ide-editor-utilities.h           |   15 +-
 src/libide/editor/ide-editor-view-actions.c        |  622 ---
 src/libide/editor/ide-editor-view-addin.c          |  111 -
 src/libide/editor/ide-editor-view-addin.h          |   63 -
 src/libide/editor/ide-editor-view-settings.c       |  231 --
 src/libide/editor/ide-editor-view-shortcuts.c      |  139 -
 src/libide/editor/ide-editor-view.c                | 1383 -------
 src/libide/editor/ide-editor-view.h                |   76 -
 src/libide/editor/ide-editor-view.ui               |  124 -
 src/libide/editor/ide-editor-workbench-addin.c     |  485 ---
 src/libide/editor/ide-editor-workbench-addin.h     |   29 -
 src/libide/editor/ide-editor-workspace.c           |  110 +
 src/libide/editor/ide-editor-workspace.h           |   39 +
 src/libide/editor/ide-editor-workspace.ui          |   55 +
 src/libide/editor/libide-editor.gresource.xml      |   11 +
 src/libide/editor/libide-editor.h                  |   41 +
 src/libide/editor/meson.build                      |  137 +-
 src/libide/editorconfig/OVERVIEW.md                |    9 -
 src/libide/editorconfig/editorconfig-glib.c        |  119 -
 src/libide/editorconfig/editorconfig-glib.h        |   17 -
 .../editorconfig/ide-editorconfig-file-settings.c  |  193 -
 .../editorconfig/ide-editorconfig-file-settings.h  |   32 -
 src/libide/files/ide-file-settings.c               |  463 ---
 src/libide/files/ide-file-settings.h               |   72 -
 src/libide/files/ide-file.c                        |  810 ----
 src/libide/files/ide-file.h                        |   88 -
 src/libide/files/ide-indent-style.h                |   31 -
 src/libide/files/ide-spaces-style.h                |   37 -
 src/libide/files/meson.build                       |   23 -
 src/libide/formatting/ide-formatter-options.c      |  168 -
 src/libide/formatting/ide-formatter-options.h      |   45 -
 src/libide/formatting/ide-formatter.c              |  172 -
 src/libide/formatting/ide-formatter.h              |   89 -
 src/libide/formatting/meson.build                  |   14 -
 src/libide/foundry/ide-build-log-private.h         |   46 +
 src/libide/foundry/ide-build-log.c                 |  247 ++
 src/libide/foundry/ide-build-log.h                 |   42 +
 src/libide/foundry/ide-build-manager.c             | 1848 +++++++++
 src/libide/foundry/ide-build-manager.h             |   97 +
 src/libide/foundry/ide-build-pipeline-addin.c      |  108 +
 src/libide/foundry/ide-build-pipeline-addin.h      |   58 +
 src/libide/foundry/ide-build-pipeline.c            | 4119 ++++++++++++++++++++
 src/libide/foundry/ide-build-pipeline.h            |  223 ++
 src/libide/foundry/ide-build-private.h             |   46 +
 src/libide/foundry/ide-build-stage-launcher.c      |  635 +++
 src/libide/foundry/ide-build-stage-launcher.h      |   71 +
 src/libide/foundry/ide-build-stage-mkdirs.c        |  222 ++
 src/libide/foundry/ide-build-stage-mkdirs.h        |   55 +
 src/libide/foundry/ide-build-stage-private.h       |   41 +
 src/libide/foundry/ide-build-stage-transfer.c      |  270 ++
 src/libide/foundry/ide-build-stage-transfer.h      |   42 +
 src/libide/foundry/ide-build-stage.c               | 1220 ++++++
 src/libide/foundry/ide-build-stage.h               |  215 +
 src/libide/foundry/ide-build-system-discovery.c    |   75 +
 src/libide/foundry/ide-build-system-discovery.h    |   56 +
 src/libide/foundry/ide-build-system.c              |  674 ++++
 src/libide/foundry/ide-build-system.h              |  115 +
 src/libide/foundry/ide-build-target-provider.c     |  115 +
 src/libide/foundry/ide-build-target-provider.h     |   59 +
 src/libide/foundry/ide-build-target.c              |  253 ++
 src/libide/foundry/ide-build-target.h              |   80 +
 src/libide/foundry/ide-build-utils.c               |   87 +
 src/libide/foundry/ide-compile-commands.c          |  738 ++++
 src/libide/foundry/ide-compile-commands.h          |   60 +
 src/libide/foundry/ide-configuration-manager.c     | 1149 ++++++
 src/libide/foundry/ide-configuration-manager.h     |   70 +
 src/libide/foundry/ide-configuration-private.h     |   29 +
 src/libide/foundry/ide-configuration-provider.c    |  397 ++
 src/libide/foundry/ide-configuration-provider.h    |  100 +
 src/libide/foundry/ide-configuration.c             | 1724 ++++++++
 src/libide/foundry/ide-configuration.h             |  214 +
 src/libide/foundry/ide-dependency-updater.c        |   83 +
 src/libide/foundry/ide-dependency-updater.h        |   59 +
 src/libide/foundry/ide-deploy-strategy.c           |  248 ++
 src/libide/foundry/ide-deploy-strategy.h           |   87 +
 src/libide/foundry/ide-device-info.c               |  222 ++
 src/libide/foundry/ide-device-info.h               |   59 +
 src/libide/foundry/ide-device-manager.c            | 1137 ++++++
 src/libide/foundry/ide-device-manager.h            |   61 +
 src/libide/foundry/ide-device-private.h            |   29 +
 src/libide/foundry/ide-device-provider.c           |  302 ++
 src/libide/foundry/ide-device-provider.h           |   73 +
 src/libide/foundry/ide-device.c                    |  392 ++
 src/libide/foundry/ide-device.h                    |   92 +
 src/libide/foundry/ide-fallback-build-system.c     |  169 +
 src/libide/foundry/ide-fallback-build-system.h     |   35 +
 src/libide/foundry/ide-foundry-compat.c            |  227 ++
 src/libide/foundry/ide-foundry-compat.h            |   36 +
 src/libide/foundry/ide-foundry-init.c              |  161 +
 src/libide/foundry/ide-foundry-init.h              |   34 +
 src/libide/foundry/ide-foundry-types.h             |   71 +
 src/libide/foundry/ide-local-device.c              |  196 +
 src/libide/foundry/ide-local-device.h              |   46 +
 src/libide/foundry/ide-run-manager-private.h       |   41 +
 src/libide/foundry/ide-run-manager.c               | 1178 ++++++
 src/libide/foundry/ide-run-manager.h               |   90 +
 src/libide/foundry/ide-runner-addin.c              |  148 +
 src/libide/foundry/ide-runner-addin.h              |   87 +
 src/libide/foundry/ide-runner.c                    | 1442 +++++++
 src/libide/foundry/ide-runner.h                    |  143 +
 src/libide/foundry/ide-runtime-manager.c           |  442 +++
 src/libide/foundry/ide-runtime-manager.h           |   50 +
 src/libide/foundry/ide-runtime-private.h           |   37 +
 src/libide/foundry/ide-runtime-provider.c          |  299 ++
 src/libide/foundry/ide-runtime-provider.h          |   96 +
 src/libide/foundry/ide-runtime.c                   |  712 ++++
 src/libide/foundry/ide-runtime.h                   |  118 +
 .../foundry/ide-simple-build-system-discovery.c    |  374 ++
 .../foundry/ide-simple-build-system-discovery.h    |   62 +
 src/libide/foundry/ide-simple-build-target.c       |  220 ++
 src/libide/foundry/ide-simple-build-target.h       |   67 +
 src/libide/foundry/ide-simple-toolchain.c          |  168 +
 src/libide/foundry/ide-simple-toolchain.h          |   57 +
 src/libide/foundry/ide-test-manager.c              | 1021 +++++
 src/libide/foundry/ide-test-manager.h              |   77 +
 src/libide/foundry/ide-test-private.h              |   43 +
 src/libide/foundry/ide-test-provider.c             |  340 ++
 src/libide/foundry/ide-test-provider.h             |   84 +
 src/libide/foundry/ide-test.c                      |  429 ++
 src/libide/foundry/ide-test.h                      |   77 +
 src/libide/foundry/ide-toolchain-manager.c         |  590 +++
 src/libide/foundry/ide-toolchain-manager.h         |   46 +
 src/libide/foundry/ide-toolchain-private.h         |   38 +
 src/libide/foundry/ide-toolchain-provider.c        |  233 ++
 src/libide/foundry/ide-toolchain-provider.h        |   78 +
 src/libide/foundry/ide-toolchain.c                 |  354 ++
 src/libide/foundry/ide-toolchain.h                 |   94 +
 src/libide/foundry/ide-triplet.c                   |  389 ++
 src/libide/foundry/ide-triplet.h                   |   70 +
 src/libide/foundry/libide-foundry.h                |   75 +
 src/libide/foundry/meson.build                     |  192 +
 src/libide/genesis/ide-genesis-addin.c             |  144 -
 src/libide/genesis/ide-genesis-addin.h             |   80 -
 src/libide/genesis/meson.build                     |   12 -
 src/libide/greeter/ide-clone-surface.c             |  564 +++
 src/libide/greeter/ide-clone-surface.h             |   42 +
 src/libide/greeter/ide-clone-surface.ui            |  383 ++
 src/libide/greeter/ide-greeter-perspective.c       | 1383 -------
 src/libide/greeter/ide-greeter-perspective.h       |   35 -
 src/libide/greeter/ide-greeter-perspective.ui      |  427 --
 src/libide/greeter/ide-greeter-private.h           |   32 +
 src/libide/greeter/ide-greeter-section.c           |   14 +-
 src/libide/greeter/ide-greeter-section.h           |   26 +-
 src/libide/greeter/ide-greeter-workspace-actions.c |  223 ++
 .../greeter/ide-greeter-workspace-shortcuts.c      |   44 +
 src/libide/greeter/ide-greeter-workspace.c         |  808 ++++
 src/libide/greeter/ide-greeter-workspace.h         |   61 +
 src/libide/greeter/ide-greeter-workspace.ui        |  177 +
 src/libide/greeter/libide-greeter.gresource.xml    |    7 +
 src/libide/greeter/libide-greeter.h                |   34 +
 src/libide/greeter/meson.build                     |   88 +-
 src/libide/gsettings/ide-gsettings-file-settings.c |  207 -
 src/libide/gsettings/ide-gsettings-file-settings.h |   30 -
 src/libide/gsettings/ide-language-defaults.c       |  459 ---
 src/libide/gsettings/ide-language-defaults.h       |   31 -
 src/libide/gsettings/meson.build                   |    8 -
 src/libide/gtk/menus.ui                            |  266 --
 src/libide/gui/gs-markdown-private.h               |   58 +
 src/libide/gui/gs-markdown.c                       |  872 +++++
 src/libide/gui/gtk/menus.ui                        |   86 +
 src/libide/gui/ide-application-actions.c           |  441 +++
 src/libide/gui/ide-application-addin.c             |  189 +
 src/libide/gui/ide-application-addin.h             |   87 +
 src/libide/gui/ide-application-color.c             |  232 ++
 src/libide/gui/ide-application-command-line.c      |  241 ++
 src/libide/gui/ide-application-credits.h           |  599 +++
 src/libide/gui/ide-application-open.c              |  169 +
 src/libide/gui/ide-application-plugins.c           |  471 +++
 src/libide/gui/ide-application-private.h           |  122 +
 src/libide/gui/ide-application-shortcuts.c         |   75 +
 src/libide/gui/ide-application.c                   |  617 +++
 src/libide/gui/ide-application.h                   |   83 +
 src/libide/gui/ide-cell-renderer-fancy.c           |  393 ++
 src/libide/gui/ide-cell-renderer-fancy.h           |   53 +
 src/libide/gui/ide-command-provider.c              |  103 +
 src/libide/gui/ide-command-provider.h              |   66 +
 src/libide/gui/ide-command.c                       |  153 +
 src/libide/gui/ide-command.h                       |   66 +
 src/libide/gui/ide-config-view-addin.c             |   46 +
 src/libide/gui/ide-config-view-addin.h             |   48 +
 src/libide/gui/ide-environment-editor-row.c        |  278 ++
 src/libide/gui/ide-environment-editor-row.h        |   37 +
 .../{buildui => gui}/ide-environment-editor-row.ui |    0
 src/libide/gui/ide-environment-editor.c            |  317 ++
 src/libide/gui/ide-environment-editor.h            |   42 +
 src/libide/gui/ide-fancy-tree-view.c               |  201 +
 src/libide/gui/ide-fancy-tree-view.h               |   53 +
 src/libide/gui/ide-frame-actions.c                 |  429 ++
 src/libide/gui/ide-frame-addin.c                   |  111 +
 src/libide/gui/ide-frame-addin.h                   |   65 +
 src/libide/gui/ide-frame-header.c                  |  767 ++++
 src/libide/gui/ide-frame-header.h                  |   44 +
 src/libide/gui/ide-frame-header.ui                 |  183 +
 src/libide/gui/ide-frame-shortcuts.c               |  113 +
 src/libide/gui/ide-frame-wrapper.c                 |  124 +
 src/libide/gui/ide-frame-wrapper.h                 |   31 +
 src/libide/gui/ide-frame.c                         | 1413 +++++++
 src/libide/gui/ide-frame.h                         |   84 +
 src/libide/gui/ide-frame.ui                        |  142 +
 src/libide/gui/ide-grid-actions.c                  |   73 +
 src/libide/gui/ide-grid-column-actions.c           |   81 +
 src/libide/gui/ide-grid-column.c                   |  394 ++
 src/libide/gui/ide-grid-column.h                   |   47 +
 src/libide/gui/ide-grid.c                          | 1533 ++++++++
 src/libide/gui/ide-grid.h                          |   77 +
 src/libide/gui/ide-gui-global.c                    |  358 ++
 src/libide/gui/ide-gui-global.h                    |   58 +
 src/libide/gui/ide-gui-private.h                   |  103 +
 src/libide/gui/ide-header-bar-shortcuts.c          |   68 +
 src/libide/gui/ide-header-bar.c                    |  469 +++
 src/libide/gui/ide-header-bar.h                    |   67 +
 src/libide/gui/ide-header-bar.ui                   |   76 +
 src/libide/gui/ide-keybindings.c                   |  366 ++
 src/libide/gui/ide-keybindings.h                   |   36 +
 src/libide/gui/ide-marked-view.c                   |  112 +
 src/libide/gui/ide-marked-view.h                   |   37 +
 .../gui/ide-notification-list-box-row-private.h    |   38 +
 src/libide/gui/ide-notification-list-box-row.c     |  377 ++
 src/libide/gui/ide-notification-list-box-row.ui    |  112 +
 src/libide/gui/ide-notification-stack-private.h    |   44 +
 src/libide/gui/ide-notification-stack.c            |  405 ++
 src/libide/gui/ide-notification-view-private.h     |   37 +
 src/libide/gui/ide-notification-view.c             |  291 ++
 src/libide/gui/ide-notification-view.ui            |   63 +
 .../gui/ide-notifications-button-popover-private.h |   31 +
 src/libide/gui/ide-notifications-button-popover.c  |   51 +
 src/libide/gui/ide-notifications-button.c          |  217 ++
 src/libide/gui/ide-notifications-button.h          |   40 +
 src/libide/gui/ide-notifications-button.ui         |   32 +
 src/libide/gui/ide-omni-bar-addin.c                |   89 +
 src/libide/gui/ide-omni-bar-addin.h                |   55 +
 src/libide/gui/ide-omni-bar.c                      |  619 +++
 src/libide/gui/ide-omni-bar.h                      |   56 +
 src/libide/gui/ide-omni-bar.ui                     |  128 +
 src/libide/gui/ide-page.c                          |  872 +++++
 src/libide/gui/ide-page.h                          |  119 +
 src/libide/gui/ide-pane.c                          |   54 +
 src/libide/gui/ide-pane.h                          |   48 +
 src/libide/gui/ide-panel.c                         |   85 +
 src/libide/gui/ide-panel.h                         |   48 +
 src/libide/gui/ide-panel.ui                        |   13 +
 src/libide/gui/ide-preferences-addin.c             |   80 +
 src/libide/gui/ide-preferences-addin.h             |   51 +
 src/libide/gui/ide-preferences-builtin-private.h   |   29 +
 src/libide/gui/ide-preferences-builtin.c           |  571 +++
 .../gui/ide-preferences-language-row-private.h     |   31 +
 src/libide/gui/ide-preferences-language-row.c      |  171 +
 .../ide-preferences-language-row.ui                |    0
 src/libide/gui/ide-preferences-surface.c           |  136 +
 src/libide/gui/ide-preferences-surface.h           |   36 +
 src/libide/gui/ide-preferences-window.c            |   46 +
 src/libide/gui/ide-preferences-window.h            |   33 +
 src/libide/gui/ide-preferences-window.ui           |   17 +
 src/libide/gui/ide-primary-workspace-actions.c     |  109 +
 src/libide/gui/ide-primary-workspace.c             |  141 +
 src/libide/gui/ide-primary-workspace.h             |   38 +
 src/libide/gui/ide-primary-workspace.ui            |   62 +
 src/libide/gui/ide-run-button.c                    |  200 +
 src/libide/gui/ide-run-button.h                    |   33 +
 src/libide/{runner => gui}/ide-run-button.ui       |    0
 src/libide/gui/ide-search-entry.c                  |  294 ++
 src/libide/gui/ide-search-entry.h                  |   39 +
 src/libide/gui/ide-search-entry.ui                 |   11 +
 src/libide/gui/ide-session-addin.c                 |  172 +
 src/libide/gui/ide-session-addin.h                 |   83 +
 src/libide/gui/ide-session-private.h               |   51 +
 src/libide/gui/ide-session.c                       |  518 +++
 src/libide/gui/ide-shortcut-label-private.h        |   45 +
 src/libide/gui/ide-shortcut-label.c                |  271 ++
 src/libide/gui/ide-shortcuts-window-private.h      |   31 +
 src/libide/gui/ide-shortcuts-window.c              |   48 +
 .../{keybindings => gui}/ide-shortcuts-window.ui   |    0
 src/libide/gui/ide-surface.c                       |  259 ++
 src/libide/gui/ide-surface.h                       |   67 +
 src/libide/gui/ide-surfaces-button.c               |  107 +
 src/libide/gui/ide-surfaces-button.h               |   37 +
 src/libide/gui/ide-tagged-entry.c                  | 1244 ++++++
 src/libide/gui/ide-tagged-entry.h                  |  134 +
 src/libide/gui/ide-transfer-button.c               |  247 ++
 src/libide/gui/ide-transfer-button.h               |   48 +
 src/libide/gui/ide-transient-sidebar.c             |  355 ++
 src/libide/gui/ide-transient-sidebar.h             |   58 +
 src/libide/gui/ide-window-settings-private.h       |   29 +
 src/libide/gui/ide-window-settings.c               |  165 +
 src/libide/gui/ide-workbench-addin.c               |  402 ++
 src/libide/gui/ide-workbench-addin.h               |  159 +
 src/libide/gui/ide-workbench.c                     | 2299 +++++++++++
 src/libide/gui/ide-workbench.h                     |  144 +
 src/libide/gui/ide-worker-manager.c                |  299 ++
 src/libide/gui/ide-worker-manager.h                |   42 +
 src/libide/gui/ide-worker-process.c                |  475 +++
 src/libide/gui/ide-worker-process.h                |   50 +
 src/libide/gui/ide-worker.c                        |   68 +
 src/libide/gui/ide-worker.h                        |   51 +
 src/libide/gui/ide-workspace-actions.c             |   92 +
 src/libide/gui/ide-workspace-addin.c               |  118 +
 src/libide/gui/ide-workspace-addin.h               |   54 +
 src/libide/gui/ide-workspace.c                     |  971 +++++
 src/libide/gui/ide-workspace.h                     |   96 +
 src/libide/gui/ide-workspace.ui                    |   23 +
 src/libide/gui/libide-gui.gresource.xml            |   24 +
 src/libide/gui/libide-gui.h                        |   70 +
 src/libide/gui/meson.build                         |  212 +
 src/libide/highlighting/ide-highlight-engine.c     | 1173 ------
 src/libide/highlighting/ide-highlight-engine.h     |   58 -
 src/libide/highlighting/ide-highlight-index.c      |  247 --
 src/libide/highlighting/ide-highlight-index.h      |   55 -
 src/libide/highlighting/ide-highlighter.c          |   90 -
 src/libide/highlighting/ide-highlighter.h          |   87 -
 src/libide/highlighting/meson.build                |   21 -
 src/libide/hover/ide-hover-context-private.h       |   48 -
 src/libide/hover/ide-hover-context.c               |  271 --
 src/libide/hover/ide-hover-context.h               |   50 -
 src/libide/hover/ide-hover-popover-private.h       |   40 -
 src/libide/hover/ide-hover-popover.c               |  348 --
 src/libide/hover/ide-hover-private.h               |   39 -
 src/libide/hover/ide-hover-provider.c              |  148 -
 src/libide/hover/ide-hover-provider.h              |   74 -
 src/libide/hover/ide-hover.c                       |  794 ----
 src/libide/hover/meson.build                       |   22 -
 src/libide/ide-context.c                           | 3001 --------------
 src/libide/ide-context.h                           |  156 -
 src/libide/ide-enums.c.in                          |   64 -
 src/libide/ide-enums.h.in                          |   26 -
 src/libide/ide-global.h                            |   30 -
 src/libide/ide-object.c                            |  875 -----
 src/libide/ide-object.h                            |   88 -
 src/libide/ide-pausable.c                          |  253 --
 src/libide/ide-pausable.h                          |   54 -
 src/libide/ide-service.c                           |  150 -
 src/libide/ide-service.h                           |   65 -
 src/libide/ide-types.h                             |  148 -
 src/libide/ide-version-macros.h                    |  161 -
 src/libide/ide.c                                   |   87 -
 src/libide/ide.h                                   |  230 --
 src/libide/io/ide-content-type.c                   |  117 +
 src/libide/io/ide-content-type.h                   |   31 +
 src/libide/io/ide-gfile.c                          |  691 ++++
 src/libide/io/ide-gfile.h                          |   75 +
 src/libide/io/ide-line-reader.c                    |  100 +
 src/libide/io/ide-line-reader.h                    |   42 +
 src/libide/io/ide-marked-content.c                 |  236 ++
 src/libide/io/ide-marked-content.h                 |   67 +
 src/libide/io/ide-path.c                           |   97 +
 src/libide/io/ide-path.h                           |   36 +
 src/libide/io/ide-persistent-map-builder.c         |  361 ++
 src/libide/io/ide-persistent-map-builder.h         |   62 +
 src/libide/io/ide-persistent-map.c                 |  360 ++
 src/libide/io/ide-persistent-map.h                 |   53 +
 src/libide/io/ide-pkcon-transfer.c                 |  279 ++
 src/libide/io/ide-pkcon-transfer.h                 |   39 +
 src/libide/io/ide-pty-intercept.c                  |  639 +++
 src/libide/io/ide-pty-intercept.h                  |  108 +
 src/libide/io/libide-io.h                          |   42 +
 src/libide/io/meson.build                          |   69 +
 src/libide/keybindings/default.css                 |   60 -
 src/libide/keybindings/emacs.css                   |  232 --
 src/libide/keybindings/ide-keybindings.c           |  357 --
 src/libide/keybindings/ide-keybindings.h           |   34 -
 src/libide/keybindings/ide-shortcuts-window.c      |   42 -
 src/libide/keybindings/ide-shortcuts-window.h      |   29 -
 src/libide/keybindings/meson.build                 |    8 -
 src/libide/keybindings/sublime.css                 |  314 --
 src/libide/keybindings/vim.css                     | 2891 --------------
 src/libide/langserv/ide-langserv-client.c          | 1338 -------
 src/libide/langserv/ide-langserv-client.h          |  101 -
 src/libide/langserv/ide-langserv-completion-item.c |  152 -
 src/libide/langserv/ide-langserv-completion-item.h |   47 -
 .../langserv/ide-langserv-completion-provider.c    |  378 --
 .../langserv/ide-langserv-completion-provider.h    |   51 -
 .../langserv/ide-langserv-completion-results.c     |  205 -
 .../langserv/ide-langserv-completion-results.h     |   36 -
 .../langserv/ide-langserv-diagnostic-provider.c    |  253 --
 .../langserv/ide-langserv-diagnostic-provider.h    |   52 -
 src/libide/langserv/ide-langserv-formatter.c       |  441 ---
 src/libide/langserv/ide-langserv-formatter.h       |   48 -
 src/libide/langserv/ide-langserv-highlighter.c     |  519 ---
 src/libide/langserv/ide-langserv-highlighter.h     |   51 -
 src/libide/langserv/ide-langserv-hover-provider.c  |  484 ---
 src/libide/langserv/ide-langserv-hover-provider.h  |   50 -
 src/libide/langserv/ide-langserv-rename-provider.c |  382 --
 src/libide/langserv/ide-langserv-rename-provider.h |   54 -
 .../langserv/ide-langserv-symbol-node-private.h    |   41 -
 src/libide/langserv/ide-langserv-symbol-node.c     |  192 -
 src/libide/langserv/ide-langserv-symbol-node.h     |   38 -
 src/libide/langserv/ide-langserv-symbol-resolver.c |  679 ----
 src/libide/langserv/ide-langserv-symbol-resolver.h |   56 -
 .../langserv/ide-langserv-symbol-tree-private.h    |   27 -
 src/libide/langserv/ide-langserv-symbol-tree.c     |  189 -
 src/libide/langserv/ide-langserv-symbol-tree.h     |   32 -
 src/libide/langserv/ide-langserv-types.h           |   52 -
 src/libide/langserv/ide-langserv-util.c            |   78 -
 src/libide/langserv/ide-langserv-util.h            |   32 -
 src/libide/langserv/meson.build                    |   44 -
 src/libide/layout/ide-layout-grid-actions.c        |   71 -
 src/libide/layout/ide-layout-grid-column-actions.c |   79 -
 src/libide/layout/ide-layout-grid-column.c         |  388 --
 src/libide/layout/ide-layout-grid-column.h         |   42 -
 src/libide/layout/ide-layout-grid.c                | 1517 -------
 src/libide/layout/ide-layout-grid.h                |   78 -
 src/libide/layout/ide-layout-pane.c                |   66 -
 src/libide/layout/ide-layout-pane.h                |   40 -
 src/libide/layout/ide-layout-pane.ui               |   12 -
 src/libide/layout/ide-layout-private.h             |   69 -
 src/libide/layout/ide-layout-stack-actions.c       |  416 --
 src/libide/layout/ide-layout-stack-addin.c         |  122 -
 src/libide/layout/ide-layout-stack-addin.h         |   60 -
 src/libide/layout/ide-layout-stack-header.c        |  764 ----
 src/libide/layout/ide-layout-stack-header.h        |   39 -
 src/libide/layout/ide-layout-stack-header.ui       |  183 -
 src/libide/layout/ide-layout-stack-shortcuts.c     |  110 -
 src/libide/layout/ide-layout-stack-wrapper.c       |  124 -
 src/libide/layout/ide-layout-stack-wrapper.h       |   31 -
 src/libide/layout/ide-layout-stack.c               | 1408 -------
 src/libide/layout/ide-layout-stack.h               |   86 -
 src/libide/layout/ide-layout-stack.ui              |  142 -
 src/libide/layout/ide-layout-transient-sidebar.c   |  355 --
 src/libide/layout/ide-layout-transient-sidebar.h   |   59 -
 src/libide/layout/ide-layout-view.c                |  814 ----
 src/libide/layout/ide-layout-view.h                |  123 -
 src/libide/layout/ide-layout.c                     |   53 -
 src/libide/layout/ide-layout.h                     |   40 -
 src/libide/layout/ide-shortcut-label.c             |  269 --
 src/libide/layout/ide-shortcut-label.h             |   43 -
 src/libide/layout/meson.build                      |   41 -
 src/libide/libide-1.0.deps                         |    7 -
 src/libide/libide.gresource.xml                    |  143 -
 src/libide/local/ide-local-device.c                |  193 -
 src/libide/local/ide-local-device.h                |   40 -
 src/libide/local/meson.build                       |   12 -
 src/libide/logging/ide-log.c                       |  372 --
 src/libide/logging/ide-log.h                       |   39 -
 src/libide/logging/meson.build                     |   12 -
 src/libide/lsp/ide-lsp-client.c                    | 1332 +++++++
 src/libide/lsp/ide-lsp-client.h                    |   99 +
 src/libide/lsp/ide-lsp-completion-item.c           |  150 +
 src/libide/lsp/ide-lsp-completion-item.h           |   50 +
 src/libide/lsp/ide-lsp-completion-provider.c       |  373 ++
 src/libide/lsp/ide-lsp-completion-provider.h       |   54 +
 src/libide/lsp/ide-lsp-completion-results.c        |  206 +
 src/libide/lsp/ide-lsp-completion-results.h        |   42 +
 src/libide/lsp/ide-lsp-diagnostic-provider.c       |  253 ++
 src/libide/lsp/ide-lsp-diagnostic-provider.h       |   52 +
 src/libide/lsp/ide-lsp-formatter.c                 |  430 ++
 src/libide/lsp/ide-lsp-formatter.h                 |   52 +
 src/libide/lsp/ide-lsp-highlighter.c               |  518 +++
 src/libide/lsp/ide-lsp-highlighter.h               |   52 +
 src/libide/lsp/ide-lsp-hover-provider.c            |  479 +++
 src/libide/lsp/ide-lsp-hover-provider.h            |   52 +
 src/libide/lsp/ide-lsp-rename-provider.c           |  367 ++
 src/libide/lsp/ide-lsp-rename-provider.h           |   52 +
 src/libide/lsp/ide-lsp-symbol-node-private.h       |   42 +
 src/libide/lsp/ide-lsp-symbol-node.c               |  188 +
 src/libide/lsp/ide-lsp-symbol-node.h               |   42 +
 src/libide/lsp/ide-lsp-symbol-resolver.c           |  665 ++++
 src/libide/lsp/ide-lsp-symbol-resolver.h           |   52 +
 src/libide/lsp/ide-lsp-symbol-tree-private.h       |   29 +
 src/libide/lsp/ide-lsp-symbol-tree.c               |  190 +
 src/libide/lsp/ide-lsp-symbol-tree.h               |   36 +
 src/libide/lsp/ide-lsp-types.h                     |   58 +
 src/libide/lsp/ide-lsp-util.c                      |   80 +
 src/libide/lsp/ide-lsp-util.h                      |   36 +
 src/libide/lsp/libide-lsp.h                        |   42 +
 src/libide/lsp/meson.build                         |   94 +
 src/libide/meson.build                             |  300 +-
 src/libide/modelines/ide-modelines-file-settings.c |  115 -
 src/libide/modelines/ide-modelines-file-settings.h |   30 -
 src/libide/modelines/meson.build                   |    8 -
 src/libide/modelines/modeline-parser.c             |  814 ----
 src/libide/modelines/modeline-parser.h             |   39 -
 src/libide/object-modules.h                        |   40 -
 src/libide/plugins/ide-extension-adapter.c         |  115 +-
 src/libide/plugins/ide-extension-adapter.h         |   32 +-
 src/libide/plugins/ide-extension-set-adapter.c     |  216 +-
 src/libide/plugins/ide-extension-set-adapter.h     |   38 +-
 src/libide/plugins/ide-extension-util-private.h    |   43 +
 src/libide/plugins/ide-extension-util.c            |   32 +-
 src/libide/plugins/ide-extension-util.h            |   41 -
 src/libide/plugins/libide-plugins.h                |   34 +
 src/libide/plugins/meson.build                     |   57 +-
 src/libide/preferences/ide-preferences-addin.c     |   85 -
 src/libide/preferences/ide-preferences-addin.h     |   50 -
 src/libide/preferences/ide-preferences-builtin.c   |  575 ---
 src/libide/preferences/ide-preferences-builtin.h   |   27 -
 .../preferences/ide-preferences-language-row.c     |  169 -
 .../preferences/ide-preferences-language-row.h     |   29 -
 .../preferences/ide-preferences-perspective.c      |  161 -
 .../preferences/ide-preferences-perspective.h      |   33 -
 src/libide/preferences/ide-preferences-window.c    |   44 -
 src/libide/preferences/ide-preferences-window.h    |   32 -
 src/libide/preferences/ide-preferences-window.ui   |   26 -
 src/libide/preferences/meson.build                 |   24 -
 src/libide/projects/ide-doap-person.c              |  184 +
 src/libide/projects/ide-doap-person.h              |   49 +
 src/libide/projects/ide-doap.c                     |  639 +++
 src/libide/projects/ide-doap.h                     |   77 +
 src/libide/projects/ide-project-edit-private.h     |   31 -
 src/libide/projects/ide-project-edit.c             |  249 --
 src/libide/projects/ide-project-edit.h             |   58 -
 src/libide/projects/ide-project-file.c             |  617 +++
 src/libide/projects/ide-project-file.h             |  103 +
 src/libide/projects/ide-project-info.c             |  196 +-
 src/libide/projects/ide-project-info.h             |  144 +-
 src/libide/projects/ide-project-item.c             |  223 --
 src/libide/projects/ide-project-item.h             |   50 -
 src/libide/projects/ide-project-template.c         |  188 +
 src/libide/projects/ide-project-template.h         |   86 +
 src/libide/projects/ide-project-tree-addin.c       |    8 +-
 src/libide/projects/ide-project-tree-addin.h       |    5 +-
 src/libide/projects/ide-project.c                  |  334 +-
 src/libide/projects/ide-project.h                  |   38 +-
 src/libide/projects/ide-projects-global.c          |  132 +
 src/libide/projects/ide-projects-global.h          |   36 +
 src/libide/projects/ide-recent-projects.c          |   79 +-
 src/libide/projects/ide-recent-projects.h          |   20 +-
 src/libide/projects/ide-template-base.c            |  724 ++++
 src/libide/projects/ide-template-base.h            |   71 +
 src/libide/projects/ide-template-provider.c        |   61 +
 src/libide/projects/ide-template-provider.h        |   48 +
 src/libide/projects/libide-projects.h              |   40 +
 src/libide/projects/meson.build                    |   86 +-
 src/libide/projects/xml-reader-private.h           |   99 +
 src/libide/projects/xml-reader.c                   |  599 +++
 src/libide/rename/ide-rename-provider.c            |  156 -
 src/libide/rename/ide-rename-provider.h            |   66 -
 src/libide/rename/meson.build                      |   12 -
 src/libide/runner/OVERVIEW.md                      |  100 -
 src/libide/runner/ide-run-button.c                 |  200 -
 src/libide/runner/ide-run-button.h                 |   31 -
 src/libide/runner/ide-run-manager-private.h        |   39 -
 src/libide/runner/ide-run-manager.c                | 1124 ------
 src/libide/runner/ide-run-manager.h                |   85 -
 src/libide/runner/ide-runner-addin.c               |  144 -
 src/libide/runner/ide-runner-addin.h               |   84 -
 src/libide/runner/ide-runner.c                     | 1426 -------
 src/libide/runner/ide-runner.h                     |  145 -
 src/libide/runner/meson.build                      |   23 -
 src/libide/runtimes/ide-runtime-manager.c          |  436 ---
 src/libide/runtimes/ide-runtime-manager.h          |   41 -
 src/libide/runtimes/ide-runtime-private.h          |   36 -
 src/libide/runtimes/ide-runtime-provider.c         |  298 --
 src/libide/runtimes/ide-runtime-provider.h         |   92 -
 src/libide/runtimes/ide-runtime.c                  |  644 ---
 src/libide/runtimes/ide-runtime.h                  |  111 -
 src/libide/runtimes/meson.build                    |   21 -
 src/libide/search/ide-search-engine.c              |   80 +-
 src/libide/search/ide-search-engine.h              |   20 +-
 src/libide/search/ide-search-entry.c               |  289 --
 src/libide/search/ide-search-entry.h               |   35 -
 src/libide/search/ide-search-entry.ui              |   12 -
 src/libide/search/ide-search-provider.c            |   13 +-
 src/libide/search/ide-search-provider.h            |   16 +-
 src/libide/search/ide-search-reducer.c             |   24 +-
 src/libide/search/ide-search-reducer.h             |   24 +-
 src/libide/search/ide-search-result.c              |   31 +-
 src/libide/search/ide-search-result.h              |   47 +-
 src/libide/search/ide-tagged-entry.c               | 1242 ------
 src/libide/search/ide-tagged-entry.h               |  133 -
 src/libide/search/libide-search.h                  |   34 +
 src/libide/search/meson.build                      |   61 +-
 src/libide/session/ide-session-addin.c             |  164 -
 src/libide/session/ide-session-addin.h             |   74 -
 src/libide/session/ide-session.c                   |  497 ---
 src/libide/session/ide-session.h                   |   53 -
 src/libide/session/meson.build                     |   14 -
 src/libide/snippets/ide-snippet-chunk.c            |  374 --
 src/libide/snippets/ide-snippet-chunk.h            |   64 -
 src/libide/snippets/ide-snippet-context.c          |  765 ----
 src/libide/snippets/ide-snippet-context.h          |   64 -
 src/libide/snippets/ide-snippet-parser.c           |  721 ----
 src/libide/snippets/ide-snippet-parser.h           |   47 -
 src/libide/snippets/ide-snippet-private.h          |   59 -
 src/libide/snippets/ide-snippet-storage.c          |  464 ---
 src/libide/snippets/ide-snippet-storage.h          |   75 -
 src/libide/snippets/ide-snippet.c                  | 1328 -------
 src/libide/snippets/ide-snippet.h                  |   71 -
 src/libide/snippets/meson.build                    |   25 -
 src/libide/sourceview/gtk/menus.ui                 |  117 +
 src/libide/sourceview/ide-completion-context.c     | 1092 ++++++
 src/libide/sourceview/ide-completion-context.h     |   76 +
 src/libide/sourceview/ide-completion-display.c     |   96 +
 src/libide/sourceview/ide-completion-display.h     |   74 +
 .../sourceview/ide-completion-list-box-row.c       |  369 ++
 .../sourceview/ide-completion-list-box-row.h       |   64 +
 .../ide-completion-list-box-row.ui                 |    0
 src/libide/sourceview/ide-completion-list-box.c    |  938 +++++
 src/libide/sourceview/ide-completion-list-box.h    |   56 +
 src/libide/sourceview/ide-completion-overlay.c     |  330 ++
 src/libide/sourceview/ide-completion-overlay.h     |   37 +
 .../ide-completion-overlay.ui                      |    0
 src/libide/sourceview/ide-completion-private.h     |   96 +
 src/libide/sourceview/ide-completion-proposal.c    |   32 +
 src/libide/sourceview/ide-completion-proposal.h    |   41 +
 src/libide/sourceview/ide-completion-provider.c    |  350 ++
 src/libide/sourceview/ide-completion-provider.h    |  122 +
 src/libide/sourceview/ide-completion-types.h       |   52 +
 src/libide/sourceview/ide-completion-view.c        |  443 +++
 src/libide/sourceview/ide-completion-view.h        |   41 +
 .../ide-completion-view.ui                         |    0
 src/libide/sourceview/ide-completion-window.c      |  361 ++
 src/libide/sourceview/ide-completion-window.h      |   40 +
 .../ide-completion-window.ui                       |    0
 src/libide/sourceview/ide-completion.c             | 1787 +++++++++
 src/libide/sourceview/ide-completion.h             |   81 +
 src/libide/sourceview/ide-cursor.c                 |    8 +-
 src/libide/sourceview/ide-cursor.h                 |    2 +
 src/libide/sourceview/ide-gutter.c                 |  128 +
 src/libide/sourceview/ide-gutter.h                 |   58 +
 src/libide/sourceview/ide-hover-context-private.h  |   50 +
 src/libide/sourceview/ide-hover-context.c          |  272 ++
 src/libide/sourceview/ide-hover-context.h          |   51 +
 src/libide/sourceview/ide-hover-popover-private.h  |   42 +
 src/libide/sourceview/ide-hover-popover.c          |  351 ++
 src/libide/sourceview/ide-hover-private.h          |   39 +
 src/libide/sourceview/ide-hover-provider.c         |  148 +
 src/libide/sourceview/ide-hover-provider.h         |   76 +
 src/libide/sourceview/ide-hover.c                  |  798 ++++
 src/libide/sourceview/ide-indenter.c               |   13 +-
 src/libide/sourceview/ide-indenter.h               |   19 +-
 src/libide/sourceview/ide-language.c               |  107 -
 src/libide/sourceview/ide-language.h               |   31 -
 .../sourceview/ide-line-change-gutter-renderer.c   |  470 ++-
 .../sourceview/ide-line-change-gutter-renderer.h   |    9 +-
 .../sourceview/ide-omni-gutter-renderer-private.h  |   27 -
 src/libide/sourceview/ide-omni-gutter-renderer.c   | 1698 --------
 src/libide/sourceview/ide-omni-gutter-renderer.h   |   40 -
 src/libide/sourceview/ide-snippet-chunk.c          |  380 ++
 src/libide/sourceview/ide-snippet-chunk.h          |   68 +
 src/libide/sourceview/ide-snippet-context.c        |  767 ++++
 src/libide/sourceview/ide-snippet-context.h        |   68 +
 src/libide/sourceview/ide-snippet-parser.c         |  725 ++++
 src/libide/sourceview/ide-snippet-parser.h         |   53 +
 src/libide/sourceview/ide-snippet-private.h        |   61 +
 src/libide/sourceview/ide-snippet-storage.c        |  503 +++
 src/libide/sourceview/ide-snippet-storage.h        |   73 +
 src/libide/sourceview/ide-snippet-types.h          |   37 +
 src/libide/sourceview/ide-snippet.c                | 1359 +++++++
 src/libide/sourceview/ide-snippet.h                |   77 +
 src/libide/sourceview/ide-source-iter.c            |  630 ---
 src/libide/sourceview/ide-source-iter.h            |   87 -
 src/libide/sourceview/ide-source-search-context.c  |   15 +-
 src/libide/sourceview/ide-source-search-context.h  |   15 +-
 src/libide/sourceview/ide-source-style-scheme.c    |  115 -
 src/libide/sourceview/ide-source-style-scheme.h    |   32 -
 src/libide/sourceview/ide-source-view-capture.c    |    7 +-
 src/libide/sourceview/ide-source-view-capture.h    |   11 +-
 src/libide/sourceview/ide-source-view-mode.c       |   11 +-
 src/libide/sourceview/ide-source-view-mode.h       |   10 +-
 src/libide/sourceview/ide-source-view-movements.c  |   80 +-
 src/libide/sourceview/ide-source-view-movements.h  |    8 +-
 src/libide/sourceview/ide-source-view-private.h    |   14 +-
 src/libide/sourceview/ide-source-view-shortcuts.c  |   17 +-
 src/libide/sourceview/ide-source-view.c            |  672 ++--
 src/libide/sourceview/ide-source-view.h            |  166 +-
 src/libide/sourceview/ide-text-iter.c              |  978 -----
 src/libide/sourceview/ide-text-iter.h              |   97 -
 src/libide/sourceview/ide-text-util.c              |    4 +-
 src/libide/sourceview/ide-text-util.h              |    4 +-
 .../sourceview/libide-sourceview.gresource.xml     |   12 +
 src/libide/sourceview/libide-sourceview.h          |   53 +
 src/libide/sourceview/meson.build                  |  182 +-
 src/libide/storage/ide-persistent-map-builder.c    |  357 --
 src/libide/storage/ide-persistent-map-builder.h    |   61 -
 src/libide/storage/ide-persistent-map.c            |  353 --
 src/libide/storage/ide-persistent-map.h            |   52 -
 src/libide/storage/meson.build                     |   14 -
 .../subprocess/ide-breakout-subprocess-private.h   |   44 -
 src/libide/subprocess/ide-breakout-subprocess.c    | 1784 ---------
 src/libide/subprocess/ide-breakout-subprocess.h    |   26 -
 src/libide/subprocess/ide-simple-subprocess.c      |  437 ---
 src/libide/subprocess/ide-simple-subprocess.h      |   31 -
 src/libide/subprocess/ide-subprocess-launcher.c    | 1051 -----
 src/libide/subprocess/ide-subprocess-launcher.h    |  136 -
 src/libide/subprocess/ide-subprocess-supervisor.c  |  412 --
 src/libide/subprocess/ide-subprocess-supervisor.h  |   68 -
 src/libide/subprocess/ide-subprocess.c             |  424 --
 src/libide/subprocess/ide-subprocess.h             |  186 -
 src/libide/subprocess/meson.build                  |   25 -
 src/libide/symbols/ide-code-index-entries.c        |  173 -
 src/libide/symbols/ide-code-index-entries.h        |   63 -
 src/libide/symbols/ide-code-index-entry.c          |  266 --
 src/libide/symbols/ide-code-index-entry.h          |   86 -
 src/libide/symbols/ide-code-indexer.c              |  234 --
 src/libide/symbols/ide-code-indexer.h              |   80 -
 src/libide/symbols/ide-symbol-node.c               |  268 --
 src/libide/symbols/ide-symbol-node.h               |   68 -
 src/libide/symbols/ide-symbol-resolver.c           |  338 --
 src/libide/symbols/ide-symbol-resolver.h           |  120 -
 src/libide/symbols/ide-symbol-tree.c               |   71 -
 src/libide/symbols/ide-symbol-tree.h               |   53 -
 src/libide/symbols/ide-symbol.c                    |  441 ---
 src/libide/symbols/ide-symbol.h                    |  123 -
 src/libide/symbols/ide-tags-builder.c              |   56 -
 src/libide/symbols/ide-tags-builder.h              |   59 -
 src/libide/symbols/meson.build                     |   31 -
 src/libide/template/ide-project-template.c         |  180 -
 src/libide/template/ide-project-template.h         |   81 -
 src/libide/template/ide-template-base.c            |  723 ----
 src/libide/template/ide-template-base.h            |   66 -
 src/libide/template/ide-template-provider.c        |   57 -
 src/libide/template/ide-template-provider.h        |   42 -
 src/libide/template/meson.build                    |   16 -
 src/libide/terminal/gtk/menus.ui                   |   12 +
 src/libide/terminal/ide-terminal-page-actions.c    |  335 ++
 src/libide/terminal/ide-terminal-page-actions.h    |   29 +
 src/libide/terminal/ide-terminal-page-private.h    |   66 +
 src/libide/terminal/ide-terminal-page.c            |  765 ++++
 src/libide/terminal/ide-terminal-page.h            |   45 +
 src/libide/terminal/ide-terminal-page.ui           |   41 +
 src/libide/terminal/ide-terminal-private.h         |    4 +-
 src/libide/terminal/ide-terminal-search-private.h  |    7 +-
 src/libide/terminal/ide-terminal-search.c          |   19 +-
 src/libide/terminal/ide-terminal-search.h          |   21 +-
 src/libide/terminal/ide-terminal-surface.c         |   84 +
 src/libide/terminal/ide-terminal-surface.h         |   39 +
 src/libide/terminal/ide-terminal-surface.ui        |   10 +
 src/libide/terminal/ide-terminal-util.c            |   26 +-
 src/libide/terminal/ide-terminal-util.h            |   15 +-
 src/libide/terminal/ide-terminal-workspace.c       |   52 +
 src/libide/terminal/ide-terminal-workspace.h       |   37 +
 src/libide/terminal/ide-terminal-workspace.ui      |   33 +
 src/libide/terminal/ide-terminal.c                 |   12 +-
 src/libide/terminal/ide-terminal.h                 |   16 +-
 src/libide/terminal/libide-terminal.gresource.xml  |   12 +
 src/libide/terminal/libide-terminal.h              |   38 +
 src/libide/terminal/meson.build                    |   94 +-
 src/libide/testing/gtk/menus.ui                    |   17 -
 src/libide/testing/ide-test-editor-addin.c         |  119 -
 src/libide/testing/ide-test-editor-addin.h         |   29 -
 src/libide/testing/ide-test-manager.c              |  839 ----
 src/libide/testing/ide-test-manager.h              |   53 -
 src/libide/testing/ide-test-panel.c                |  362 --
 src/libide/testing/ide-test-panel.h                |   29 -
 src/libide/testing/ide-test-panel.ui               |   51 -
 src/libide/testing/ide-test-private.h              |   41 -
 src/libide/testing/ide-test-provider.c             |  337 --
 src/libide/testing/ide-test-provider.h             |   88 -
 src/libide/testing/ide-test.c                      |  427 --
 src/libide/testing/ide-test.h                      |   73 -
 src/libide/testing/meson.build                     |   30 -
 src/libide/testing/testing-plugin.c                |   34 -
 src/libide/testing/testing.plugin                  |    9 -
 src/libide/themes/libide-themes.c                  |   32 +
 src/libide/themes/libide-themes.gresource.xml      |   30 +
 src/libide/themes/libide-themes.h                  |   29 +
 src/libide/themes/meson.build                      |   53 +
 src/libide/themes/themes/Adwaita-dark.css          |   26 +
 src/libide/themes/themes/Adwaita-shared.css        |   96 +
 {data => src/libide/themes}/themes/Adwaita.css     |    0
 {data => src/libide/themes}/themes/Arc-Dark.css    |    0
 src/libide/themes/themes/Arc-Darker.css            |    7 +
 src/libide/themes/themes/Arc-shared.css            |   83 +
 {data => src/libide/themes}/themes/Arc.css         |    0
 {data => src/libide/themes}/themes/elementary.css  |    0
 src/libide/themes/themes/shared.css                |  144 +
 .../themes}/themes/shared/shared-buildui.css       |    0
 .../themes}/themes/shared/shared-completion.css    |    0
 .../themes}/themes/shared/shared-debugger.css      |    0
 src/libide/themes/themes/shared/shared-editor.css  |  124 +
 src/libide/themes/themes/shared/shared-greeter.css |   32 +
 .../themes}/themes/shared/shared-hoverer.css       |    0
 src/libide/themes/themes/shared/shared-layout.css  |   83 +
 src/libide/themes/themes/shared/shared-omnibar.css |   46 +
 .../libide/themes}/themes/shared/shared-search.css |    0
 .../themes}/themes/shared/shared-treeview.css      |    0
 src/libide/threading/ide-environment-variable.c    |  185 +
 src/libide/threading/ide-environment-variable.h    |   50 +
 src/libide/threading/ide-environment.c             |  379 ++
 src/libide/threading/ide-environment.h             |   67 +
 .../threading/ide-flatpak-subprocess-private.h     |   50 +
 src/libide/threading/ide-flatpak-subprocess.c      | 1776 +++++++++
 src/libide/threading/ide-gtask-private.h           |   37 +
 src/libide/threading/ide-gtask.c                   |  180 +
 .../threading/ide-simple-subprocess-private.h      |   39 +
 src/libide/threading/ide-simple-subprocess.c       |  435 +++
 src/libide/threading/ide-subprocess-launcher.c     | 1073 +++++
 src/libide/threading/ide-subprocess-launcher.h     |  135 +
 src/libide/threading/ide-subprocess-supervisor.c   |  418 ++
 src/libide/threading/ide-subprocess-supervisor.h   |   74 +
 src/libide/threading/ide-subprocess.c              |  441 +++
 src/libide/threading/ide-subprocess.h              |  191 +
 src/libide/threading/ide-task.c                    |   99 +-
 src/libide/threading/ide-task.h                    |   88 +-
 src/libide/threading/ide-thread-pool.c             |   31 +-
 src/libide/threading/ide-thread-pool.h             |   19 +-
 src/libide/threading/ide-thread-private.h          |    5 +-
 src/libide/threading/libide-threading.h            |   35 +
 src/libide/threading/meson.build                   |   80 +-
 src/libide/toolchain/ide-simple-toolchain.c        |  171 -
 src/libide/toolchain/ide-simple-toolchain.h        |   52 -
 src/libide/toolchain/ide-toolchain-manager.c       |  589 ---
 src/libide/toolchain/ide-toolchain-manager.h       |   41 -
 src/libide/toolchain/ide-toolchain-private.h       |   37 -
 src/libide/toolchain/ide-toolchain-provider.c      |  232 --
 src/libide/toolchain/ide-toolchain-provider.h      |   73 -
 src/libide/toolchain/ide-toolchain.c               |  356 --
 src/libide/toolchain/ide-toolchain.h               |   88 -
 src/libide/toolchain/meson.build                   |   18 -
 src/libide/transfers/ide-pkcon-transfer.c          |  281 --
 src/libide/transfers/ide-pkcon-transfer.h          |   35 -
 src/libide/transfers/ide-transfer-button.c         |  249 --
 src/libide/transfers/ide-transfer-button.h         |   48 -
 src/libide/transfers/ide-transfer-manager.c        |  449 ---
 src/libide/transfers/ide-transfer-manager.h        |   53 -
 src/libide/transfers/ide-transfer-row.c            |  218 --
 src/libide/transfers/ide-transfer-row.h            |   40 -
 src/libide/transfers/ide-transfer-row.ui           |   86 -
 src/libide/transfers/ide-transfer.c                |  468 ---
 src/libide/transfers/ide-transfer.h                |  100 -
 src/libide/transfers/ide-transfers-button.c        |  186 -
 src/libide/transfers/ide-transfers-button.h        |   35 -
 src/libide/transfers/ide-transfers-button.ui       |   54 -
 src/libide/transfers/ide-transfers-progress-icon.c |  186 -
 src/libide/transfers/ide-transfers-progress-icon.h |   40 -
 src/libide/transfers/meson.build                   |   24 -
 src/libide/tree/ide-tree-addin.c                   |  366 ++
 src/libide/tree/ide-tree-addin.h                   |  149 +
 src/libide/tree/ide-tree-model.c                   | 1626 ++++++++
 src/libide/tree/ide-tree-model.h                   |   72 +
 src/libide/tree/ide-tree-node.c                    | 1863 +++++++++
 src/libide/tree/ide-tree-node.h                    |  172 +
 src/libide/tree/ide-tree-private.h                 |   70 +
 src/libide/tree/ide-tree.c                         |  764 ++++
 src/libide/tree/ide-tree.h                         |   67 +
 src/libide/tree/libide-tree.h                      |   36 +
 src/libide/tree/meson.build                        |   62 +
 src/libide/util/gs-markdown.c                      |  870 -----
 src/libide/util/gs-markdown.h                      |   58 -
 src/libide/util/ide-async-helper.c                 |   99 -
 src/libide/util/ide-async-helper.h                 |   37 -
 src/libide/util/ide-backoff.c                      |  132 -
 src/libide/util/ide-backoff.h                      |   47 -
 src/libide/util/ide-battery-monitor.c              |  183 -
 src/libide/util/ide-battery-monitor.h              |   31 -
 src/libide/util/ide-cell-renderer-fancy.c          |  391 --
 src/libide/util/ide-cell-renderer-fancy.h          |   48 -
 src/libide/util/ide-dnd.c                          |   42 -
 src/libide/util/ide-dnd.h                          |   27 -
 src/libide/util/ide-doc-seq.c                      |   51 -
 src/libide/util/ide-doc-seq.h                      |   28 -
 src/libide/util/ide-fancy-tree-view.c              |  199 -
 src/libide/util/ide-fancy-tree-view.h              |   51 -
 src/libide/util/ide-flatpak.c                      |   70 -
 src/libide/util/ide-flatpak.h                      |   32 -
 src/libide/util/ide-glib.c                         |  806 ----
 src/libide/util/ide-glib.h                         |  122 -
 src/libide/util/ide-gtk.c                          |  270 --
 src/libide/util/ide-gtk.h                          |   55 -
 src/libide/util/ide-line-reader.c                  |   96 -
 src/libide/util/ide-line-reader.h                  |   42 -
 src/libide/util/ide-list-inline.h                  |  108 -
 src/libide/util/ide-marked-content.c               |  230 --
 src/libide/util/ide-marked-content.h               |   65 -
 src/libide/util/ide-marked-view.c                  |  114 -
 src/libide/util/ide-marked-view.h                  |   37 -
 src/libide/util/ide-posix.c                        |  164 -
 src/libide/util/ide-posix.h                        |   42 -
 src/libide/util/ide-progress.c                     |  289 --
 src/libide/util/ide-progress.h                     |   56 -
 src/libide/util/ide-ref-ptr.c                      |   89 -
 src/libide/util/ide-ref-ptr.h                      |   45 -
 src/libide/util/ide-settings.c                     |  575 ---
 src/libide/util/ide-settings.h                     |  109 -
 src/libide/util/ide-triplet.c                      |  386 --
 src/libide/util/ide-triplet.h                      |   66 -
 src/libide/util/ide-uri.c                          | 1598 --------
 src/libide/util/ide-uri.h                          |  193 -
 src/libide/util/ide-window-settings.c              |  159 -
 src/libide/util/ide-window-settings.h              |   27 -
 src/libide/util/meson.build                        |   63 -
 src/libide/util/ptyintercept.c                     |  596 ---
 src/libide/util/ptyintercept.h                     |   95 -
 src/libide/vcs/ide-directory-vcs.c                 |  180 +
 src/libide/vcs/ide-directory-vcs.h                 |   36 +
 src/libide/vcs/ide-vcs-cloner.c                    |  148 +
 src/libide/vcs/ide-vcs-cloner.h                    |   73 +
 src/libide/vcs/ide-vcs-config.c                    |    5 +-
 src/libide/vcs/ide-vcs-config.h                    |   14 +-
 src/libide/vcs/ide-vcs-file-info.c                 |   13 +-
 src/libide/vcs/ide-vcs-file-info.h                 |   20 +-
 src/libide/vcs/ide-vcs-initializer.c               |    6 +-
 src/libide/vcs/ide-vcs-initializer.h               |   18 +-
 src/libide/vcs/ide-vcs-monitor.c                   |  398 +-
 src/libide/vcs/ide-vcs-monitor.h                   |   30 +-
 src/libide/vcs/ide-vcs-uri.c                       |   63 +-
 src/libide/vcs/ide-vcs-uri.h                       |   86 +-
 src/libide/vcs/ide-vcs.c                           |  276 +-
 src/libide/vcs/ide-vcs.h                           |   57 +-
 src/libide/vcs/libide-vcs.h                        |   38 +
 src/libide/vcs/meson.build                         |   83 +-
 src/libide/webkit/ide-webkit-plugin.c              |   36 +
 src/libide/webkit/ide-webkit.c                     |   29 -
 src/libide/webkit/libide-webkit.gresource.xml      |    6 +
 src/libide/webkit/meson.build                      |   45 +
 src/libide/webkit/webkit.plugin                    |    2 +-
 src/libide/workbench/ide-omni-bar.c                |  879 -----
 src/libide/workbench/ide-omni-bar.h                |   36 -
 src/libide/workbench/ide-omni-bar.ui               |  613 ---
 src/libide/workbench/ide-omni-pausable-row.c       |  183 -
 src/libide/workbench/ide-omni-pausable-row.h       |   34 -
 src/libide/workbench/ide-omni-pausable-row.ui      |   57 -
 src/libide/workbench/ide-perspective.c             |  292 --
 src/libide/workbench/ide-perspective.h             |   78 -
 src/libide/workbench/ide-workbench-actions.c       |  360 --
 src/libide/workbench/ide-workbench-addin.c         |  250 --
 src/libide/workbench/ide-workbench-addin.h         |   94 -
 src/libide/workbench/ide-workbench-header-bar.c    |  333 --
 src/libide/workbench/ide-workbench-header-bar.h    |   71 -
 src/libide/workbench/ide-workbench-header-bar.ui   |  119 -
 src/libide/workbench/ide-workbench-message.c       |  224 --
 src/libide/workbench/ide-workbench-message.h       |   54 -
 src/libide/workbench/ide-workbench-message.ui      |   43 -
 src/libide/workbench/ide-workbench-open.c          |  535 ---
 src/libide/workbench/ide-workbench-private.h       |   66 -
 src/libide/workbench/ide-workbench-shortcuts.c     |  145 -
 src/libide/workbench/ide-workbench.c               | 1121 ------
 src/libide/workbench/ide-workbench.h               |  149 -
 src/libide/workbench/ide-workbench.ui              |   64 -
 src/libide/workbench/meson.build                   |   32 -
 src/libide/workers/ide-worker-manager.c            |  299 --
 src/libide/workers/ide-worker-manager.h            |   40 -
 src/libide/workers/ide-worker-process.c            |  476 ---
 src/libide/workers/ide-worker-process.h            |   48 -
 src/libide/workers/ide-worker.c                    |   62 -
 src/libide/workers/ide-worker.h                    |   51 -
 src/libide/workers/meson.build                     |   20 -
 src/main.c                                         |   97 +-
 src/meson.build                                    |  123 +-
 src/plugins/auto-save/auto-save-plugin.c           |   36 +
 src/plugins/auto-save/auto-save.gresource.xml      |    6 +
 src/plugins/auto-save/auto-save.plugin             |   10 +
 src/plugins/auto-save/gbp-auto-save-buffer-addin.c |  237 ++
 src/plugins/auto-save/gbp-auto-save-buffer-addin.h |   31 +
 src/plugins/auto-save/meson.build                  |   12 +
 src/plugins/autotools/autotools-plugin.c           |   28 +-
 src/plugins/autotools/autotools.gresource.xml      |    2 +-
 src/plugins/autotools/autotools.plugin             |   13 +-
 .../gbp-autotools-build-system-discovery.c         |   49 +
 .../gbp-autotools-build-system-discovery.h         |   31 +
 .../autotools/ide-autotools-autogen-stage.c        |    4 +-
 .../autotools/ide-autotools-autogen-stage.h        |    6 +-
 src/plugins/autotools/ide-autotools-build-system.c |   98 +-
 src/plugins/autotools/ide-autotools-build-system.h |    6 +-
 .../ide-autotools-build-target-provider.c          |    8 +-
 .../ide-autotools-build-target-provider.h          |    6 +-
 src/plugins/autotools/ide-autotools-build-target.c |    4 +-
 src/plugins/autotools/ide-autotools-build-target.h |    6 +-
 src/plugins/autotools/ide-autotools-make-stage.c   |    5 +-
 src/plugins/autotools/ide-autotools-make-stage.h   |    6 +-
 .../autotools/ide-autotools-makecache-stage.c      |   11 +-
 .../autotools/ide-autotools-makecache-stage.h      |    6 +-
 .../autotools/ide-autotools-pipeline-addin.c       |   30 +-
 .../autotools/ide-autotools-pipeline-addin.h       |    6 +-
 src/plugins/autotools/ide-makecache-target.c       |    4 +-
 src/plugins/autotools/ide-makecache-target.h       |    4 +-
 src/plugins/autotools/ide-makecache.c              |   22 +-
 src/plugins/autotools/ide-makecache.h              |    6 +-
 src/plugins/autotools/meson.build                  |   35 +-
 src/plugins/beautifier/beautifier-plugin.c         |   34 +
 src/plugins/beautifier/beautifier.gresource.xml    |   33 +
 src/plugins/beautifier/beautifier.plugin           |   12 +-
 src/plugins/beautifier/gb-beautifier-config.c      |   57 +-
 src/plugins/beautifier/gb-beautifier-config.h      |    2 +
 .../beautifier/gb-beautifier-editor-addin.c        |   44 +-
 .../beautifier/gb-beautifier-editor-addin.h        |    2 +
 src/plugins/beautifier/gb-beautifier-helper.c      |    4 +-
 src/plugins/beautifier/gb-beautifier-helper.h      |    4 +-
 src/plugins/beautifier/gb-beautifier-plugin.c      |   30 -
 src/plugins/beautifier/gb-beautifier-private.h     |   21 +-
 src/plugins/beautifier/gb-beautifier-process.c     |    4 +-
 src/plugins/beautifier/gb-beautifier-process.h     |    2 +
 src/plugins/beautifier/gb-beautifier.gresource.xml |   37 -
 src/plugins/beautifier/meson.build                 |   28 +-
 src/plugins/buffer-monitor/buffer-monitor-plugin.c |   36 +
 .../buffer-monitor/buffer-monitor.gresource.xml    |    6 +
 src/plugins/buffer-monitor/buffer-monitor.plugin   |   10 +
 .../gbp-buffer-monitor-buffer-addin.c              |  277 ++
 .../gbp-buffer-monitor-buffer-addin.h              |   31 +
 src/plugins/buffer-monitor/meson.build             |   12 +
 src/plugins/buildconfig/buildconfig-plugin.c       |   40 +
 src/plugins/buildconfig/buildconfig.gresource.xml  |    6 +
 src/plugins/buildconfig/buildconfig.plugin         |    9 +
 .../ide-buildconfig-configuration-provider.c       |  768 ++++
 .../ide-buildconfig-configuration-provider.h       |   31 +
 .../buildconfig/ide-buildconfig-configuration.c    |  172 +
 .../buildconfig/ide-buildconfig-configuration.h    |   38 +
 .../buildconfig/ide-buildconfig-pipeline-addin.c   |  117 +
 .../buildconfig/ide-buildconfig-pipeline-addin.h   |   31 +
 src/plugins/buildconfig/meson.build                |   14 +
 src/plugins/buildsystem/buildsystem-plugin.c       |   37 +
 src/plugins/buildsystem/buildsystem.gresource.xml  |    6 +
 src/plugins/buildsystem/buildsystem.plugin         |    9 +
 .../buildsystem/gbp-buildsystem-workbench-addin.c  |  298 ++
 .../buildsystem/gbp-buildsystem-workbench-addin.h  |   31 +
 src/plugins/buildsystem/meson.build                |   12 +
 src/plugins/buildui/buildui-plugin.c               |   45 +
 src/plugins/buildui/buildui.gresource.xml          |   13 +
 src/plugins/buildui/buildui.plugin                 |   11 +
 src/plugins/buildui/gbp-buildui-config-surface.c   |  335 ++
 src/plugins/buildui/gbp-buildui-config-surface.h   |   35 +
 src/plugins/buildui/gbp-buildui-config-surface.ui  |   28 +
 .../buildui/gbp-buildui-config-view-addin.c        |  517 +++
 .../buildui/gbp-buildui-config-view-addin.h        |   31 +
 src/plugins/buildui/gbp-buildui-log-pane.c         |  378 ++
 src/plugins/buildui/gbp-buildui-log-pane.h         |   36 +
 src/plugins/buildui/gbp-buildui-log-pane.ui        |   85 +
 src/plugins/buildui/gbp-buildui-omni-bar-section.c |  367 ++
 src/plugins/buildui/gbp-buildui-omni-bar-section.h |   35 +
 .../buildui/gbp-buildui-omni-bar-section.ui        |  530 +++
 src/plugins/buildui/gbp-buildui-pane.c             |  702 ++++
 src/plugins/buildui/gbp-buildui-pane.h             |   35 +
 src/plugins/buildui/gbp-buildui-pane.ui            |  175 +
 .../buildui/gbp-buildui-runtime-categories.c       |  251 ++
 .../buildui/gbp-buildui-runtime-categories.h       |   38 +
 src/plugins/buildui/gbp-buildui-runtime-row.c      |  137 +
 src/plugins/buildui/gbp-buildui-runtime-row.h      |   36 +
 src/plugins/buildui/gbp-buildui-stage-row.c        |  198 +
 src/plugins/buildui/gbp-buildui-stage-row.h        |   35 +
 src/plugins/buildui/gbp-buildui-stage-row.ui       |   17 +
 src/plugins/buildui/gbp-buildui-tree-addin.c       |  381 ++
 src/plugins/buildui/gbp-buildui-tree-addin.h       |   31 +
 src/plugins/buildui/gbp-buildui-workspace-addin.c  |  428 ++
 src/plugins/buildui/gbp-buildui-workspace-addin.h  |   31 +
 src/plugins/buildui/gtk/menus.ui                   |   41 +
 src/plugins/buildui/meson.build                    |   21 +
 src/plugins/buildui/themes/shared.css              |    9 +
 src/plugins/c-pack/c-pack-plugin.c                 |   26 +-
 src/plugins/c-pack/c-pack.gresource.xml            |    2 +-
 src/plugins/c-pack/c-pack.plugin                   |   15 +-
 src/plugins/c-pack/c-parse-helper.c                |    4 +-
 src/plugins/c-pack/c-parse-helper.h                |    4 +-
 src/plugins/c-pack/cpack-completion-item.c         |   10 +-
 src/plugins/c-pack/cpack-completion-item.h         |    6 +-
 src/plugins/c-pack/cpack-completion-provider.c     |   17 +-
 src/plugins/c-pack/cpack-completion-provider.h     |    6 +-
 src/plugins/c-pack/cpack-completion-results.c      |    6 +-
 src/plugins/c-pack/cpack-completion-results.h      |    6 +-
 src/plugins/c-pack/cpack-editor-page-addin.c       |  115 +
 src/plugins/c-pack/cpack-editor-page-addin.h       |   31 +
 src/plugins/c-pack/cpack-editor-view-addin.c       |  109 -
 src/plugins/c-pack/cpack-editor-view-addin.h       |   29 -
 src/plugins/c-pack/hdr-format.c                    |   15 +-
 src/plugins/c-pack/hdr-format.h                    |    4 +-
 src/plugins/c-pack/ide-c-indenter.c                |    7 +-
 src/plugins/c-pack/ide-c-indenter.h                |    6 +-
 src/plugins/c-pack/meson.build                     |   40 +-
 src/plugins/c-pack/test-cpack.c                    |   80 +
 src/plugins/c-pack/test-hdr-format.c               |   47 +
 src/plugins/cargo/cargo.plugin                     |   14 +-
 src/plugins/cargo/cargo_plugin.py                  |   53 +-
 src/plugins/cargo/meson.build                      |    4 +-
 src/plugins/clang/clang-plugin.c                   |   19 +-
 src/plugins/clang/clang.gresource.xml              |    2 +-
 src/plugins/clang/clang.plugin                     |   24 +-
 src/plugins/clang/gnome-builder-clang.c            |    8 +-
 src/plugins/clang/ide-clang-autocleanups.h         |    4 +-
 src/plugins/clang/ide-clang-client.c               |   79 +-
 src/plugins/clang/ide-clang-client.h               |    6 +-
 src/plugins/clang/ide-clang-code-index-entries.c   |    6 +-
 src/plugins/clang/ide-clang-code-index-entries.h   |    6 +-
 src/plugins/clang/ide-clang-code-indexer.c         |   20 +-
 src/plugins/clang/ide-clang-code-indexer.h         |    4 +-
 src/plugins/clang/ide-clang-completion-item.c      |   33 +-
 src/plugins/clang/ide-clang-completion-item.h      |    7 +-
 src/plugins/clang/ide-clang-completion-provider.c  |   21 +-
 src/plugins/clang/ide-clang-completion-provider.h  |    6 +-
 src/plugins/clang/ide-clang-diagnostic-provider.c  |   24 +-
 src/plugins/clang/ide-clang-diagnostic-provider.h  |    6 +-
 src/plugins/clang/ide-clang-highlighter.c          |   25 +-
 src/plugins/clang/ide-clang-highlighter.h          |    6 +-
 src/plugins/clang/ide-clang-preferences-addin.c    |    7 +-
 src/plugins/clang/ide-clang-preferences-addin.h    |    4 +-
 src/plugins/clang/ide-clang-proposals.c            |   31 +-
 src/plugins/clang/ide-clang-proposals.h            |    6 +-
 src/plugins/clang/ide-clang-rename-provider.c      |   49 +-
 src/plugins/clang/ide-clang-rename-provider.h      |    6 +-
 src/plugins/clang/ide-clang-symbol-node.c          |   30 +-
 src/plugins/clang/ide-clang-symbol-node.h          |    9 +-
 src/plugins/clang/ide-clang-symbol-resolver.c      |   85 +-
 src/plugins/clang/ide-clang-symbol-resolver.h      |    6 +-
 src/plugins/clang/ide-clang-symbol-tree.c          |   16 +-
 src/plugins/clang/ide-clang-symbol-tree.h          |   11 +-
 src/plugins/clang/ide-clang-util.h                 |   34 +-
 src/plugins/clang/ide-clang.c                      |  143 +-
 src/plugins/clang/ide-clang.h                      |    6 +-
 src/plugins/clang/meson.build                      |   31 +-
 src/plugins/cmake/cmake-plugin.c                   |   26 +-
 src/plugins/cmake/cmake.gresource.xml              |    4 +-
 src/plugins/cmake/cmake.plugin                     |   13 +-
 .../cmake/gbp-cmake-build-stage-cross-file.c       |    4 +-
 .../cmake/gbp-cmake-build-stage-cross-file.h       |    4 +-
 .../cmake/gbp-cmake-build-system-discovery.c       |   49 +
 .../cmake/gbp-cmake-build-system-discovery.h       |   31 +
 src/plugins/cmake/gbp-cmake-build-system.c         |   28 +-
 src/plugins/cmake/gbp-cmake-build-system.h         |    6 +-
 src/plugins/cmake/gbp-cmake-build-target.c         |    4 +-
 src/plugins/cmake/gbp-cmake-build-target.h         |    6 +-
 src/plugins/cmake/gbp-cmake-pipeline-addin.c       |   19 +-
 src/plugins/cmake/gbp-cmake-pipeline-addin.h       |    6 +-
 src/plugins/cmake/gbp-cmake-toolchain-provider.c   |   10 +-
 src/plugins/cmake/gbp-cmake-toolchain-provider.h   |    4 +-
 src/plugins/cmake/gbp-cmake-toolchain.c            |    2 +
 src/plugins/cmake/gbp-cmake-toolchain.h            |    4 +-
 src/plugins/cmake/meson.build                      |   28 +-
 src/plugins/code-index/code-index-plugin.c         |   23 +-
 src/plugins/code-index/code-index.gresource.xml    |    2 +-
 src/plugins/code-index/code-index.plugin           |   12 +-
 .../code-index/gbp-code-index-workbench-addin.c    |  759 ++++
 .../code-index/gbp-code-index-workbench-addin.h    |   40 +
 src/plugins/code-index/ide-code-index-builder.c    |   61 +-
 src/plugins/code-index/ide-code-index-builder.h    |   10 +-
 src/plugins/code-index/ide-code-index-index.c      |   38 +-
 src/plugins/code-index/ide-code-index-index.h      |    6 +-
 .../code-index/ide-code-index-search-provider.c    |   24 +-
 .../code-index/ide-code-index-search-provider.h    |    4 +-
 .../code-index/ide-code-index-search-result.c      |   56 +-
 .../code-index/ide-code-index-search-result.h      |   15 +-
 src/plugins/code-index/ide-code-index-service.c    |  701 ----
 src/plugins/code-index/ide-code-index-service.h    |   35 -
 .../code-index/ide-code-index-symbol-resolver.c    |   38 +-
 .../code-index/ide-code-index-symbol-resolver.h    |    4 +-
 src/plugins/code-index/meson.build                 |   29 +-
 src/plugins/codeui/codeui-plugin.c                 |   35 +
 src/plugins/codeui/codeui.gresource.xml            |    6 +
 src/plugins/codeui/codeui.plugin                   |   10 +
 src/plugins/codeui/gbp-codeui-buffer-addin.c       |  203 +
 src/plugins/codeui/gbp-codeui-buffer-addin.h       |   31 +
 src/plugins/codeui/meson.build                     |   12 +
 .../color-picker/color-picker.gresource.xml        |   26 +
 src/plugins/color-picker/color-picker.plugin       |   10 +-
 .../gb-color-picker-document-monitor.c             |    2 +
 .../gb-color-picker-document-monitor.h             |    4 +-
 .../color-picker/gb-color-picker-editor-addin.c    |   82 +-
 .../color-picker/gb-color-picker-editor-addin.h    |    6 +-
 .../gb-color-picker-editor-page-addin.c            |  237 ++
 .../gb-color-picker-editor-page-addin.h            |   37 +
 .../gb-color-picker-editor-view-addin.c            |  235 --
 .../gb-color-picker-editor-view-addin.h            |   35 -
 src/plugins/color-picker/gb-color-picker-helper.c  |    4 +-
 src/plugins/color-picker/gb-color-picker-helper.h  |    2 +
 src/plugins/color-picker/gb-color-picker-plugin.c  |   16 +-
 .../color-picker/gb-color-picker-prefs-list.c      |    2 +
 .../color-picker/gb-color-picker-prefs-list.h      |    2 +
 .../gb-color-picker-prefs-palette-list.c           |    2 +
 .../gb-color-picker-prefs-palette-list.h           |    2 +
 .../gb-color-picker-prefs-palette-row.c            |    8 +-
 .../gb-color-picker-prefs-palette-row.h            |    2 +
 src/plugins/color-picker/gb-color-picker-prefs.c   |   12 +-
 src/plugins/color-picker/gb-color-picker-prefs.h   |    2 +
 src/plugins/color-picker/gb-color-picker-private.h |    2 +
 .../color-picker/gb-color-picker.gresource.xml     |   28 -
 src/plugins/color-picker/meson.build               |   41 +-
 src/plugins/color-picker/themes/Adwaita-dark.css   |    2 +-
 src/plugins/color-picker/themes/Adwaita.css        |    2 +-
 src/plugins/command-bar/command-bar-plugin.c       |   40 +
 src/plugins/command-bar/command-bar.gresource.xml  |    8 +
 src/plugins/command-bar/command-bar.plugin         |   12 +-
 src/plugins/command-bar/gb-command-bar.c           |  764 ----
 .../command-bar/gb-command-bar.gresource.xml       |   11 -
 src/plugins/command-bar/gb-command-bar.h           |   33 -
 src/plugins/command-bar/gb-command-bar.ui          |   87 -
 .../command-bar/gb-command-gaction-provider.c      |  473 ---
 .../command-bar/gb-command-gaction-provider.h      |   32 -
 src/plugins/command-bar/gb-command-gaction.c       |  208 -
 src/plugins/command-bar/gb-command-gaction.h       |   29 -
 src/plugins/command-bar/gb-command-manager.c       |  162 -
 src/plugins/command-bar/gb-command-manager.h       |   40 -
 src/plugins/command-bar/gb-command-provider.c      |  411 --
 src/plugins/command-bar/gb-command-provider.h      |   55 -
 src/plugins/command-bar/gb-command-result.c        |  262 --
 src/plugins/command-bar/gb-command-result.h        |   43 -
 src/plugins/command-bar/gb-command-vim-provider.c  |  104 -
 src/plugins/command-bar/gb-command-vim-provider.h  |   30 -
 src/plugins/command-bar/gb-command-vim.c           |  200 -
 src/plugins/command-bar/gb-command-vim.h           |   29 -
 src/plugins/command-bar/gb-command.c               |   70 -
 src/plugins/command-bar/gb-command.h               |   41 -
 src/plugins/command-bar/gb-vim.c                   | 1661 --------
 src/plugins/command-bar/gb-vim.h                   |   44 -
 .../command-bar/gbp-command-bar-command-provider.c |  198 +
 .../command-bar/gbp-command-bar-command-provider.h |   31 +
 src/plugins/command-bar/gbp-command-bar-model.c    |  236 ++
 src/plugins/command-bar/gbp-command-bar-model.h    |   42 +
 src/plugins/command-bar/gbp-command-bar-private.h  |   29 +
 .../command-bar/gbp-command-bar-shortcuts.c        |   64 +
 .../command-bar/gbp-command-bar-suggestion.c       |  156 +
 .../command-bar/gbp-command-bar-suggestion.h       |   35 +
 .../command-bar/gbp-command-bar-workspace-addin.c  |  165 +
 .../command-bar/gbp-command-bar-workspace-addin.h  |   31 +
 src/plugins/command-bar/gbp-command-bar.c          |  294 ++
 src/plugins/command-bar/gbp-command-bar.h          |   35 +
 src/plugins/command-bar/gbp-command-bar.ui         |   18 +
 src/plugins/command-bar/gbp-gaction-command.c      |  160 +
 src/plugins/command-bar/gbp-gaction-command.h      |   40 +
 src/plugins/command-bar/meson.build                |   47 +-
 src/plugins/command-bar/themes/shared.css          |   33 +-
 src/plugins/comment-code/comment-code-plugin.c     |   34 +
 .../comment-code/comment-code.gresource.xml        |    7 +
 src/plugins/comment-code/comment-code.plugin       |   15 +-
 .../gbp-comment-code-editor-page-addin.c           |  450 +++
 .../gbp-comment-code-editor-page-addin.h           |   31 +
 src/plugins/comment-code/gbp-comment-code-plugin.c |   30 -
 .../comment-code/gbp-comment-code-view-addin.c     |  449 ---
 .../comment-code/gbp-comment-code-view-addin.h     |   27 -
 .../comment-code/gbp-comment-code.gresource.xml    |    9 -
 src/plugins/comment-code/gtk/menus.ui              |    4 +-
 src/plugins/comment-code/meson.build               |   20 +-
 src/plugins/create-project/create-project-plugin.c |   40 +
 .../create-project/create-project.gresource.xml    |   29 +
 src/plugins/create-project/create-project.plugin   |   15 +-
 .../gbp-create-project-application-addin.c         |  107 +
 .../gbp-create-project-application-addin.h         |   31 +
 .../gbp-create-project-genesis-addin.c             |  244 --
 .../gbp-create-project-genesis-addin.h             |   29 -
 .../create-project/gbp-create-project-plugin.c     |   34 -
 .../create-project/gbp-create-project-surface.c    |  880 +++++
 .../create-project/gbp-create-project-surface.h    |   39 +
 .../create-project/gbp-create-project-surface.ui   |  396 ++
 .../gbp-create-project-template-icon.c             |    8 +-
 .../gbp-create-project-template-icon.h             |    5 +-
 .../create-project/gbp-create-project-tool.c       |  444 ---
 .../create-project/gbp-create-project-tool.h       |   29 -
 .../create-project/gbp-create-project-widget.c     |  766 ----
 .../create-project/gbp-create-project-widget.h     |   38 -
 .../create-project/gbp-create-project-widget.ui    |  328 --
 .../gbp-create-project-workspace-addin.c           |   92 +
 .../gbp-create-project-workspace-addin.h           |   31 +
 .../gbp-create-project.gresource.xml               |   31 -
 src/plugins/create-project/gtk/menus.ui            |   36 +
 src/plugins/create-project/meson.build             |   29 +-
 src/plugins/ctags/ctags-plugin.c                   |   38 +-
 src/plugins/ctags/ctags.gresource.xml              |    2 +-
 src/plugins/ctags/ctags.plugin                     |   15 +-
 src/plugins/ctags/gbp-ctags-workbench-addin.c      |  183 +
 src/plugins/ctags/gbp-ctags-workbench-addin.h      |   31 +
 src/plugins/ctags/ide-ctags-builder.c              |   38 +-
 src/plugins/ctags/ide-ctags-builder.h              |   10 +-
 src/plugins/ctags/ide-ctags-completion-item.c      |    4 +-
 src/plugins/ctags/ide-ctags-completion-item.h      |    7 +-
 .../ctags/ide-ctags-completion-provider-private.h  |    6 +-
 src/plugins/ctags/ide-ctags-completion-provider.c  |   25 +-
 src/plugins/ctags/ide-ctags-completion-provider.h  |    6 +-
 src/plugins/ctags/ide-ctags-highlighter.c          |   39 +-
 src/plugins/ctags/ide-ctags-highlighter.h          |    4 +-
 src/plugins/ctags/ide-ctags-index.c                |    9 +-
 src/plugins/ctags/ide-ctags-index.h                |   28 +-
 src/plugins/ctags/ide-ctags-preferences-addin.c    |   10 +-
 src/plugins/ctags/ide-ctags-preferences-addin.h    |    4 +-
 src/plugins/ctags/ide-ctags-results.c              |    4 +-
 src/plugins/ctags/ide-ctags-results.h              |    6 +-
 src/plugins/ctags/ide-ctags-service.c              |  317 +-
 src/plugins/ctags/ide-ctags-service.h              |   28 +-
 src/plugins/ctags/ide-ctags-symbol-node.c          |   10 +-
 src/plugins/ctags/ide-ctags-symbol-node.h          |    6 +-
 src/plugins/ctags/ide-ctags-symbol-resolver.c      |   77 +-
 src/plugins/ctags/ide-ctags-symbol-resolver.h      |    9 +-
 src/plugins/ctags/ide-ctags-symbol-tree.c          |   10 +-
 src/plugins/ctags/ide-ctags-symbol-tree.h          |    6 +-
 src/plugins/ctags/ide-ctags-util.c                 |   18 +-
 src/plugins/ctags/ide-ctags-util.h                 |    4 +-
 src/plugins/ctags/ide-tags-builder.c               |   58 +
 src/plugins/ctags/ide-tags-builder.h               |   56 +
 src/plugins/ctags/meson.build                      |   34 +-
 src/plugins/ctags/test-ctags.c                     |  110 +
 src/plugins/ctags/test-tags                        |   28 +
 src/plugins/debuggerui/debuggerui-plugin.c         |   42 +
 src/plugins/debuggerui/debuggerui.gresource.xml    |   15 +
 src/plugins/debuggerui/debuggerui.plugin           |   11 +
 src/plugins/debuggerui/gtk/menus.ui                |   26 +
 .../debuggerui/ide-debugger-breakpoints-view.c     |  608 +++
 .../debuggerui/ide-debugger-breakpoints-view.h     |   38 +
 .../debuggerui}/ide-debugger-breakpoints-view.ui   |    0
 src/plugins/debuggerui/ide-debugger-controls.c     |   43 +
 src/plugins/debuggerui/ide-debugger-controls.h     |   37 +
 .../debuggerui}/ide-debugger-controls.ui           |    0
 .../debuggerui/ide-debugger-disassembly-view.c     |  138 +
 .../debuggerui/ide-debugger-disassembly-view.h     |   38 +
 .../debuggerui/ide-debugger-disassembly-view.ui    |   24 +
 src/plugins/debuggerui/ide-debugger-editor-addin.c |  691 ++++
 src/plugins/debuggerui/ide-debugger-editor-addin.h |   39 +
 .../debuggerui/ide-debugger-hover-controls.c       |  201 +
 .../debuggerui/ide-debugger-hover-controls.h       |   37 +
 .../debuggerui}/ide-debugger-hover-controls.ui     |    0
 .../debuggerui/ide-debugger-hover-provider.c       |  121 +
 .../debuggerui/ide-debugger-hover-provider.h       |   31 +
 .../debuggerui/ide-debugger-libraries-view.c       |  369 ++
 .../debuggerui/ide-debugger-libraries-view.h       |   38 +
 .../debuggerui}/ide-debugger-libraries-view.ui     |    0
 src/plugins/debuggerui/ide-debugger-locals-view.c  |  445 +++
 src/plugins/debuggerui/ide-debugger-locals-view.h  |   47 +
 .../debuggerui}/ide-debugger-locals-view.ui        |    0
 .../debuggerui/ide-debugger-registers-view.c       |  334 ++
 .../debuggerui/ide-debugger-registers-view.h       |   38 +
 .../debuggerui}/ide-debugger-registers-view.ui     |    0
 src/plugins/debuggerui/ide-debugger-threads-view.c |  831 ++++
 src/plugins/debuggerui/ide-debugger-threads-view.h |   37 +
 .../debuggerui}/ide-debugger-threads-view.ui       |    0
 src/plugins/debuggerui/meson.build                 |   21 +
 src/plugins/devhelp/devhelp-plugin.c               |   42 +
 src/plugins/devhelp/devhelp.gresource.xml          |    6 +-
 src/plugins/devhelp/devhelp.plugin                 |   13 +-
 src/plugins/devhelp/gbp-devhelp-editor-addin.c     |   47 +-
 src/plugins/devhelp/gbp-devhelp-editor-addin.h     |    6 +-
 src/plugins/devhelp/gbp-devhelp-frame-addin.c      |  210 +
 src/plugins/devhelp/gbp-devhelp-frame-addin.h      |   31 +
 src/plugins/devhelp/gbp-devhelp-hover-provider.c   |   14 +-
 src/plugins/devhelp/gbp-devhelp-hover-provider.h   |    6 +-
 .../devhelp/gbp-devhelp-layout-stack-addin.c       |  208 -
 .../devhelp/gbp-devhelp-layout-stack-addin.h       |   29 -
 src/plugins/devhelp/gbp-devhelp-menu-button.c      |   12 +-
 src/plugins/devhelp/gbp-devhelp-menu-button.h      |    4 +-
 src/plugins/devhelp/gbp-devhelp-page.c             |  244 ++
 src/plugins/devhelp/gbp-devhelp-page.h             |   34 +
 src/plugins/devhelp/gbp-devhelp-page.ui            |   25 +
 src/plugins/devhelp/gbp-devhelp-plugin.c           |   38 -
 src/plugins/devhelp/gbp-devhelp-search-private.h   |    4 +-
 src/plugins/devhelp/gbp-devhelp-search.c           |    6 +-
 src/plugins/devhelp/gbp-devhelp-search.h           |    4 +-
 src/plugins/devhelp/gbp-devhelp-view.c             |  242 --
 src/plugins/devhelp/gbp-devhelp-view.h             |   32 -
 src/plugins/devhelp/gbp-devhelp-view.ui            |   25 -
 src/plugins/devhelp/gtk/menus.ui                   |    2 +-
 src/plugins/devhelp/meson.build                    |   35 +-
 src/plugins/deviced/deviced-plugin.c               |   40 +
 src/plugins/deviced/deviced.gresource.xml          |    4 +-
 src/plugins/deviced/deviced.plugin                 |   13 +-
 src/plugins/deviced/gbp-deviced-deploy-strategy.c  |    4 +-
 src/plugins/deviced/gbp-deviced-deploy-strategy.h  |    4 +-
 src/plugins/deviced/gbp-deviced-device-provider.c  |    7 +-
 src/plugins/deviced/gbp-deviced-device-provider.h  |    4 +-
 src/plugins/deviced/gbp-deviced-device.c           |   10 +-
 src/plugins/deviced/gbp-deviced-device.h           |    9 +-
 src/plugins/deviced/gbp-deviced-plugin.c           |   32 -
 src/plugins/deviced/meson.build                    |   27 +-
 src/plugins/deviceui/deviceui-plugin.c             |   36 +
 src/plugins/deviceui/deviceui.gresource.xml        |    6 +
 src/plugins/deviceui/deviceui.plugin               |   11 +
 .../deviceui/gbp-deviceui-workspace-addin.c        |  128 +
 .../deviceui/gbp-deviceui-workspace-addin.h        |   31 +
 src/plugins/deviceui/meson.build                   |   12 +
 src/plugins/doap/doap-plugin.c                     |   37 +
 src/plugins/doap/doap.gresource.xml                |    6 +
 src/plugins/doap/doap.plugin                       |    9 +
 src/plugins/doap/gbp-doap-workbench-addin.c        |  176 +
 src/plugins/doap/gbp-doap-workbench-addin.h        |   31 +
 src/plugins/doap/meson.build                       |   12 +
 src/plugins/editor/default.css                     |   60 +
 src/plugins/editor/editor-plugin.c                 |   56 +
 src/plugins/editor/editor.gresource.xml            |   12 +
 src/plugins/editor/editor.plugin                   |   11 +
 src/plugins/editor/gbp-editor-application-addin.c  |  199 +
 src/plugins/editor/gbp-editor-application-addin.h  |   31 +
 src/plugins/editor/gbp-editor-frame-addin.c        |  115 +
 src/plugins/editor/gbp-editor-frame-addin.h        |   31 +
 src/plugins/editor/gbp-editor-frame-controls.c     |  356 ++
 src/plugins/editor/gbp-editor-frame-controls.h     |   57 +
 src/plugins/editor/gbp-editor-frame-controls.ui    |   85 +
 src/plugins/editor/gbp-editor-hover-provider.c     |  118 +
 src/plugins/editor/gbp-editor-hover-provider.h     |   31 +
 src/plugins/editor/gbp-editor-session-addin.c      |  564 +++
 src/plugins/editor/gbp-editor-session-addin.h      |   31 +
 src/plugins/editor/gbp-editor-workbench-addin.c    |  341 ++
 src/plugins/editor/gbp-editor-workbench-addin.h    |   31 +
 src/plugins/editor/gbp-editor-workspace-addin.c    |  317 ++
 src/plugins/editor/gbp-editor-workspace-addin.h    |   31 +
 src/plugins/editor/gtk/menus.ui                    |  171 +
 src/plugins/editor/meson.build                     |   18 +
 .../keybindings => plugins/editor}/shared.css      |    0
 src/plugins/editorconfig/editorconfig-glib.c       |  125 +
 src/plugins/editorconfig/editorconfig-glib.h       |   31 +
 src/plugins/editorconfig/editorconfig-plugin.c     |   37 +
 .../editorconfig/editorconfig.gresource.xml        |    6 +
 src/plugins/editorconfig/editorconfig.plugin       |    9 +
 .../editorconfig/gbp-editorconfig-file-settings.c  |  188 +
 .../editorconfig/gbp-editorconfig-file-settings.h  |   31 +
 src/plugins/editorconfig/libeditorconfig/ec_glob.c |  371 ++
 src/plugins/editorconfig/libeditorconfig/ec_glob.h |   43 +
 .../editorconfig/libeditorconfig/editorconfig.c    |  547 +++
 .../editorconfig/libeditorconfig/editorconfig.h    |   37 +
 .../libeditorconfig/editorconfig/editorconfig.h    |  309 ++
 .../editorconfig/editorconfig_handle.h             |  193 +
 .../libeditorconfig/editorconfig_handle.c          |  155 +
 .../libeditorconfig/editorconfig_handle.h          |   89 +
 src/plugins/editorconfig/libeditorconfig/global.h  |   80 +
 src/plugins/editorconfig/libeditorconfig/ini.c     |  200 +
 src/plugins/editorconfig/libeditorconfig/ini.h     |   93 +
 .../editorconfig/libeditorconfig/meson.build       |   45 +
 src/plugins/editorconfig/libeditorconfig/misc.c    |  250 ++
 src/plugins/editorconfig/libeditorconfig/misc.h    |   62 +
 src/plugins/editorconfig/libeditorconfig/utarray.h |  232 ++
 src/plugins/editorconfig/meson.build               |   20 +
 src/plugins/emacs/emacs-plugin.c                   |   36 +
 src/plugins/emacs/emacs.gresource.xml              |    7 +
 src/plugins/emacs/emacs.plugin                     |    9 +
 src/plugins/emacs/gbp-emacs-preferences-addin.c    |   89 +
 src/plugins/emacs/gbp-emacs-preferences-addin.h    |   31 +
 src/plugins/emacs/keybindings/emacs.css            |  232 ++
 src/plugins/emacs/meson.build                      |   12 +
 src/plugins/eslint/eslint.plugin                   |   12 +-
 src/plugins/eslint/eslint_plugin.py                |   66 +-
 src/plugins/eslint/meson.build                     |    4 +-
 src/plugins/file-search/file-search-plugin.c       |   36 +
 src/plugins/file-search/file-search.gresource.xml  |    2 +-
 src/plugins/file-search/file-search.plugin         |   10 +-
 src/plugins/file-search/gb-file-search-index.c     |  416 --
 src/plugins/file-search/gb-file-search-index.h     |   46 -
 src/plugins/file-search/gb-file-search-provider.c  |  351 --
 src/plugins/file-search/gb-file-search-provider.h  |   30 -
 src/plugins/file-search/gb-file-search-result.c    |  146 -
 src/plugins/file-search/gb-file-search-result.h    |   29 -
 src/plugins/file-search/gbp-file-search-index.c    |  420 ++
 src/plugins/file-search/gbp-file-search-index.h    |   48 +
 src/plugins/file-search/gbp-file-search-provider.c |  361 ++
 src/plugins/file-search/gbp-file-search-provider.h |   31 +
 src/plugins/file-search/gbp-file-search-result.c   |  156 +
 src/plugins/file-search/gbp-file-search-result.h   |   31 +
 src/plugins/file-search/meson.build                |   25 +-
 src/plugins/find-other-file/find-other-file.plugin |   15 +-
 src/plugins/find-other-file/find_other_file.py     |   45 +-
 src/plugins/find-other-file/meson.build            |    6 +-
 src/plugins/flatpak/flatpak-plugin.c               |   72 +
 src/plugins/flatpak/flatpak.gresource.xml          |    6 +-
 src/plugins/flatpak/flatpak.plugin                 |   14 +-
 .../flatpak/gbp-flatpak-application-addin.c        |  124 +-
 .../flatpak/gbp-flatpak-application-addin.h        |    8 +-
 .../flatpak/gbp-flatpak-build-system-discovery.c   |    4 +-
 .../flatpak/gbp-flatpak-build-system-discovery.h   |    6 +-
 .../flatpak/gbp-flatpak-build-target-provider.c    |    7 +-
 .../flatpak/gbp-flatpak-build-target-provider.h    |    6 +-
 src/plugins/flatpak/gbp-flatpak-build-target.c     |    4 +-
 src/plugins/flatpak/gbp-flatpak-build-target.h     |    6 +-
 src/plugins/flatpak/gbp-flatpak-clone-widget.c     |   82 +-
 src/plugins/flatpak/gbp-flatpak-clone-widget.h     |    4 +-
 src/plugins/flatpak/gbp-flatpak-clone-widget.ui    |   61 +-
 .../flatpak/gbp-flatpak-configuration-provider.c   |   31 +-
 .../flatpak/gbp-flatpak-configuration-provider.h   |    4 +-
 .../flatpak/gbp-flatpak-dependency-updater.c       |    7 +-
 .../flatpak/gbp-flatpak-dependency-updater.h       |    6 +-
 src/plugins/flatpak/gbp-flatpak-download-stage.c   |    6 +-
 src/plugins/flatpak/gbp-flatpak-download-stage.h   |   12 +-
 src/plugins/flatpak/gbp-flatpak-genesis-addin.c    |  204 -
 src/plugins/flatpak/gbp-flatpak-genesis-addin.h    |   29 -
 src/plugins/flatpak/gbp-flatpak-manifest.c         |   21 +-
 src/plugins/flatpak/gbp-flatpak-manifest.h         |    9 +-
 src/plugins/flatpak/gbp-flatpak-pipeline-addin.c   |   37 +-
 src/plugins/flatpak/gbp-flatpak-pipeline-addin.h   |    6 +-
 src/plugins/flatpak/gbp-flatpak-plugin.c           |   68 -
 .../flatpak/gbp-flatpak-preferences-addin.c        |    5 +-
 .../flatpak/gbp-flatpak-preferences-addin.h        |    6 +-
 src/plugins/flatpak/gbp-flatpak-runner.c           |   10 +-
 src/plugins/flatpak/gbp-flatpak-runner.h           |    6 +-
 src/plugins/flatpak/gbp-flatpak-runtime-provider.c |   46 +-
 src/plugins/flatpak/gbp-flatpak-runtime-provider.h |    6 +-
 src/plugins/flatpak/gbp-flatpak-runtime.c          |   93 +-
 src/plugins/flatpak/gbp-flatpak-runtime.h          |    9 +-
 src/plugins/flatpak/gbp-flatpak-sources.c          |    4 +-
 src/plugins/flatpak/gbp-flatpak-sources.h          |    2 +
 .../flatpak/gbp-flatpak-subprocess-launcher.c      |    4 +-
 .../flatpak/gbp-flatpak-subprocess-launcher.h      |    6 +-
 src/plugins/flatpak/gbp-flatpak-transfer.c         |   22 +-
 src/plugins/flatpak/gbp-flatpak-transfer.h         |   14 +-
 src/plugins/flatpak/gbp-flatpak-util.c             |   18 +-
 src/plugins/flatpak/gbp-flatpak-util.h             |    6 +-
 src/plugins/flatpak/gbp-flatpak-workbench-addin.c  |  101 +-
 src/plugins/flatpak/gbp-flatpak-workbench-addin.h  |    6 +-
 src/plugins/flatpak/meson.build                    |   51 +-
 src/plugins/gcc/gbp-gcc-pipeline-addin.c           |    8 +-
 src/plugins/gcc/gbp-gcc-pipeline-addin.h           |    6 +-
 src/plugins/gcc/gbp-gcc-plugin.c                   |   30 -
 src/plugins/gcc/gbp-gcc-toolchain-provider.c       |    7 +-
 src/plugins/gcc/gbp-gcc-toolchain-provider.h       |    4 +-
 src/plugins/gcc/gcc-plugin.c                       |   38 +
 src/plugins/gcc/gcc.gresource.xml                  |    2 +-
 src/plugins/gcc/gcc.plugin                         |   11 +-
 src/plugins/gcc/meson.build                        |   23 +-
 src/plugins/gdb/gbp-gdb-debugger.c                 |   52 +-
 src/plugins/gdb/gbp-gdb-debugger.h                 |    6 +-
 src/plugins/gdb/gbp-gdb-plugin.c                   |   27 -
 src/plugins/gdb/gdb-plugin.c                       |   34 +
 src/plugins/gdb/gdb.gresource.xml                  |    2 +-
 src/plugins/gdb/gdb.plugin                         |   13 +-
 src/plugins/gdb/gdbwire.c                          |    2 +
 src/plugins/gdb/gdbwire.h                          |    2 +
 src/plugins/gdb/meson.build                        |   19 +-
 src/plugins/gettext/gettext-plugin.c               |   10 +-
 src/plugins/gettext/gettext.gresource.xml          |    2 +-
 src/plugins/gettext/gettext.plugin                 |   13 +-
 .../gettext/ide-gettext-diagnostic-provider.c      |   38 +-
 .../gettext/ide-gettext-diagnostic-provider.h      |    6 +-
 src/plugins/gettext/meson.build                    |   20 +-
 src/plugins/git/gbp-git-buffer-addin.c             |  123 +
 src/plugins/git/gbp-git-buffer-addin.h             |   31 +
 src/plugins/git/gbp-git-buffer-change-monitor.c    |  984 +++++
 src/plugins/git/gbp-git-buffer-change-monitor.h    |   35 +
 src/plugins/git/gbp-git-dependency-updater.c       |  167 +
 src/plugins/git/gbp-git-dependency-updater.h       |   31 +
 src/plugins/git/gbp-git-index-monitor.c            |  140 +
 src/plugins/git/gbp-git-index-monitor.h            |   33 +
 src/plugins/git/gbp-git-pipeline-addin.c           |   82 +
 src/plugins/git/gbp-git-pipeline-addin.h           |   31 +
 src/plugins/git/gbp-git-remote-callbacks.c         |  265 ++
 src/plugins/git/gbp-git-remote-callbacks.h         |   37 +
 src/plugins/git/gbp-git-submodule-stage.c          |  218 ++
 src/plugins/git/gbp-git-submodule-stage.h          |   34 +
 src/plugins/git/gbp-git-vcs-cloner.c               |  317 ++
 src/plugins/git/gbp-git-vcs-cloner.h               |   31 +
 src/plugins/git/gbp-git-vcs-config.c               |  187 +
 src/plugins/git/gbp-git-vcs-config.h               |   33 +
 src/plugins/git/gbp-git-vcs-initializer.c          |  114 +
 src/plugins/git/gbp-git-vcs-initializer.h          |   31 +
 src/plugins/git/gbp-git-vcs.c                      |  543 +++
 src/plugins/git/gbp-git-vcs.h                      |   42 +
 src/plugins/git/gbp-git-workbench-addin.c          |  386 ++
 src/plugins/git/gbp-git-workbench-addin.h          |   31 +
 src/plugins/git/git-plugin.c                       |   96 +
 src/plugins/git/git.gresource.xml                  |    6 +-
 src/plugins/git/git.plugin                         |   10 +-
 src/plugins/git/ide-git-buffer-change-monitor.c    |  936 -----
 src/plugins/git/ide-git-buffer-change-monitor.h    |   30 -
 src/plugins/git/ide-git-clone-widget.c             |  587 ---
 src/plugins/git/ide-git-clone-widget.h             |   41 -
 src/plugins/git/ide-git-clone-widget.ui            |  174 -
 src/plugins/git/ide-git-dependency-updater.c       |  166 -
 src/plugins/git/ide-git-dependency-updater.h       |   31 -
 src/plugins/git/ide-git-genesis-addin.c            |  219 --
 src/plugins/git/ide-git-genesis-addin.h            |   29 -
 src/plugins/git/ide-git-pipeline-addin.c           |   81 -
 src/plugins/git/ide-git-pipeline-addin.h           |   31 -
 src/plugins/git/ide-git-plugin.c                   |   86 -
 src/plugins/git/ide-git-remote-callbacks.c         |  280 --
 src/plugins/git/ide-git-remote-callbacks.h         |   36 -
 src/plugins/git/ide-git-submodule-stage.c          |  220 --
 src/plugins/git/ide-git-submodule-stage.h          |   34 -
 src/plugins/git/ide-git-vcs-config.c               |  180 -
 src/plugins/git/ide-git-vcs-config.h               |   31 -
 src/plugins/git/ide-git-vcs-initializer.c          |  107 -
 src/plugins/git/ide-git-vcs-initializer.h          |   29 -
 src/plugins/git/ide-git-vcs.c                      |  941 -----
 src/plugins/git/ide-git-vcs.h                      |   29 -
 src/plugins/git/meson.build                        |   53 +-
 src/plugins/git/themes/shared.css                  |    5 -
 src/plugins/gjs-symbols/gjs_symbols.plugin         |   17 +-
 src/plugins/gjs-symbols/gjs_symbols.py             |   26 +-
 src/plugins/gjs-symbols/meson.build                |    4 +-
 src/plugins/glade/gbp-glade-editor-addin.c         |  144 +-
 src/plugins/glade/gbp-glade-editor-addin.h         |    4 +-
 src/plugins/glade/gbp-glade-frame-addin.c          |  409 ++
 src/plugins/glade/gbp-glade-frame-addin.h          |   31 +
 src/plugins/glade/gbp-glade-layout-stack-addin.c   |  412 --
 src/plugins/glade/gbp-glade-layout-stack-addin.h   |   31 -
 src/plugins/glade/gbp-glade-page-actions.c         |  189 +
 src/plugins/glade/gbp-glade-page-shortcuts.c       |  120 +
 src/plugins/glade/gbp-glade-page.c                 |  756 ++++
 src/plugins/glade/gbp-glade-page.h                 |   44 +
 src/plugins/glade/gbp-glade-plugin.c               |   43 -
 src/plugins/glade/gbp-glade-private.h              |   20 +-
 src/plugins/glade/gbp-glade-properties.c           |    8 +-
 src/plugins/glade/gbp-glade-properties.h           |    4 +-
 src/plugins/glade/gbp-glade-view-actions.c         |  189 -
 src/plugins/glade/gbp-glade-view-shortcuts.c       |  120 -
 src/plugins/glade/gbp-glade-view.c                 |  759 ----
 src/plugins/glade/gbp-glade-view.h                 |   44 -
 src/plugins/glade/gbp-glade-workbench-addin.c      |  165 +-
 src/plugins/glade/gbp-glade-workbench-addin.h      |    4 +-
 src/plugins/glade/glade-plugin.c                   |   48 +
 src/plugins/glade/glade.gresource.xml              |   10 +-
 src/plugins/glade/glade.plugin                     |   10 +-
 src/plugins/glade/meson.build                      |   31 +-
 src/plugins/glade/themes/Adwaita-dark.css          |   10 +-
 src/plugins/glade/themes/Adwaita-shared.css        |    6 +-
 src/plugins/glade/themes/Adwaita.css               |    2 +-
 src/plugins/gnome-builder-plugins.c                |    8 -
 src/plugins/gnome-builder-plugins.h                |   27 -
 .../gnome-code-assistance/gca-diagnostics.c        |    2 +
 .../gnome-code-assistance/gca-diagnostics.h        |    2 +
 src/plugins/gnome-code-assistance/gca-plugin.c     |   17 +-
 src/plugins/gnome-code-assistance/gca-service.c    |    2 +
 src/plugins/gnome-code-assistance/gca-service.h    |    2 +
 src/plugins/gnome-code-assistance/gca-structs.c    |    4 +-
 src/plugins/gnome-code-assistance/gca-structs.h    |    4 +-
 .../gnome-code-assistance.gresource.xml            |    2 +-
 .../gnome-code-assistance.plugin                   |   10 +-
 .../ide-gca-diagnostic-provider.c                  |   71 +-
 .../ide-gca-diagnostic-provider.h                  |    6 +-
 .../ide-gca-preferences-addin.c                    |    6 +-
 .../ide-gca-preferences-addin.h                    |    4 +-
 .../gnome-code-assistance/ide-gca-service.c        |   19 +-
 .../gnome-code-assistance/ide-gca-service.h        |   23 +-
 src/plugins/gnome-code-assistance/meson.build      |   28 +-
 src/plugins/go-langserv/go-langserv.plugin         |   10 +-
 src/plugins/go-langserv/go_langserver_plugin.py    |   24 +-
 src/plugins/go-langserv/meson.build                |    4 +-
 src/plugins/gradle/gradle.plugin                   |   14 +-
 src/plugins/gradle/gradle_plugin.py                |   51 +-
 src/plugins/gradle/meson.build                     |    4 +-
 .../greeter/gbp-greeter-application-addin.c        |  229 ++
 .../greeter/gbp-greeter-application-addin.h        |   31 +
 src/plugins/greeter/greeter-plugin.c               |   36 +
 src/plugins/greeter/greeter.gresource.xml          |    7 +
 src/plugins/greeter/greeter.plugin                 |   13 +
 src/plugins/greeter/gtk/menus.ui                   |   97 +
 src/plugins/greeter/meson.build                    |   12 +
 src/plugins/grep/gbp-grep-model.c                  |   84 +-
 src/plugins/grep/gbp-grep-model.h                  |    4 +-
 src/plugins/grep/gbp-grep-panel.c                  |   37 +-
 src/plugins/grep/gbp-grep-panel.h                  |    4 +-
 src/plugins/grep/gbp-grep-plugin.c                 |   32 -
 src/plugins/grep/gbp-grep-popover.c                |   30 +-
 src/plugins/grep/gbp-grep-popover.h                |    2 +-
 src/plugins/grep/gbp-grep-project-tree-addin.c     |  203 -
 src/plugins/grep/gbp-grep-project-tree-addin.h     |   31 -
 src/plugins/grep/gbp-grep-tree-addin.c             |  170 +
 src/plugins/grep/gbp-grep-tree-addin.h             |   31 +
 src/plugins/grep/grep-plugin.c                     |   34 +
 src/plugins/grep/grep.gresource.xml                |    4 +-
 src/plugins/grep/grep.plugin                       |   13 +-
 src/plugins/grep/gtk/menus.ui                      |    4 +-
 src/plugins/grep/meson.build                       |   23 +-
 src/plugins/grep/themes/Adwaita-dark.css           |    2 +-
 src/plugins/grep/themes/Adwaita.css                |    2 +-
 .../history/gbp-history-editor-page-addin.c        |  332 ++
 .../history/gbp-history-editor-page-addin.h        |   31 +
 .../history/gbp-history-editor-view-addin.c        |  330 --
 .../history/gbp-history-editor-view-addin.h        |   29 -
 src/plugins/history/gbp-history-frame-addin.c      |  447 +++
 src/plugins/history/gbp-history-frame-addin.h      |   34 +
 src/plugins/history/gbp-history-item.c             |   41 +-
 src/plugins/history/gbp-history-item.h             |   20 +-
 .../history/gbp-history-layout-stack-addin.c       |  445 ---
 .../history/gbp-history-layout-stack-addin.h       |   34 -
 src/plugins/history/gbp-history-plugin.c           |   34 -
 src/plugins/history/history-plugin.c               |   37 +
 src/plugins/history/history.gresource.xml          |    2 +-
 src/plugins/history/history.plugin                 |   12 +-
 src/plugins/history/meson.build                    |   24 +-
 .../html-completion/html-completion-plugin.c       |   12 +-
 .../html-completion/html-completion.gresource.xml  |    2 +-
 src/plugins/html-completion/html-completion.plugin |   10 +-
 .../html-completion/ide-html-completion-provider.c |    4 +-
 .../html-completion/ide-html-completion-provider.h |    4 +-
 src/plugins/html-completion/ide-html-proposal.c    |    8 +-
 src/plugins/html-completion/ide-html-proposal.h    |    6 +-
 src/plugins/html-completion/ide-html-proposals.c   |    8 +-
 src/plugins/html-completion/ide-html-proposals.h   |    4 +-
 src/plugins/html-completion/meson.build            |   21 +-
 src/plugins/html-preview/gtk/menus.ui              |    4 +-
 .../html-preview/html-preview.gresource.xml        |    2 +-
 src/plugins/html-preview/html-preview.plugin       |   14 +-
 src/plugins/html-preview/html_preview.py           |  107 +-
 src/plugins/html-preview/meson.build               |    4 +-
 src/plugins/jedi/jedi.plugin                       |   11 +-
 src/plugins/jedi/jedi_plugin.py                    |    9 +-
 src/plugins/jedi/meson.build                       |    4 +-
 src/plugins/jhbuild/jhbuild.plugin                 |    9 +-
 src/plugins/jhbuild/jhbuild_plugin.py              |   18 +-
 src/plugins/jhbuild/meson.build                    |    4 +-
 src/plugins/ls/gbp-ls-model.c                      |    8 +-
 src/plugins/ls/gbp-ls-model.h                      |    2 +-
 src/plugins/ls/gbp-ls-page.c                       |  349 ++
 src/plugins/ls/gbp-ls-page.h                       |   36 +
 src/plugins/ls/gbp-ls-page.ui                      |   74 +
 src/plugins/ls/gbp-ls-plugin.c                     |   34 -
 src/plugins/ls/gbp-ls-view.c                       |  353 --
 src/plugins/ls/gbp-ls-view.h                       |   36 -
 src/plugins/ls/gbp-ls-view.ui                      |   74 -
 src/plugins/ls/gbp-ls-workbench-addin.c            |   52 +-
 src/plugins/ls/gbp-ls-workbench-addin.h            |    4 +-
 src/plugins/ls/ls-plugin.c                         |   34 +
 src/plugins/ls/ls.gresource.xml                    |    6 +-
 src/plugins/ls/ls.plugin                           |   11 +-
 src/plugins/ls/meson.build                         |   21 +-
 src/plugins/make/make.gresource.xml                |    2 +-
 src/plugins/make/make.plugin                       |   16 +-
 src/plugins/make/make_plugin.py                    |   69 +-
 src/plugins/make/meson.build                       |    4 +-
 src/plugins/maven/maven.plugin                     |   15 +-
 src/plugins/maven/maven_plugin.py                  |   53 +-
 src/plugins/maven/meson.build                      |    4 +-
 .../icons/scalable/actions/pattern-browse.svg      |   44 +
 .../icons/scalable/actions/pattern-cli.svg         |   32 +
 .../icons/scalable/actions/pattern-gnome.svg       |  187 +
 .../icons/scalable/actions/pattern-grid.svg        |   42 +
 .../icons/scalable/actions/pattern-legacy.svg      |   40 +
 .../icons/scalable/actions/pattern-library.svg     |   26 +
 .../meson-templates/meson-templates.gresource.xml  |   10 +-
 src/plugins/meson-templates/meson-templates.plugin |    7 +-
 src/plugins/meson-templates/meson.build            |    6 +-
 src/plugins/meson-templates/meson_templates.py     |   12 +-
 src/plugins/meson.build                            |  208 +-
 .../meson/gbp-meson-build-stage-cross-file.c       |    4 +-
 .../meson/gbp-meson-build-stage-cross-file.h       |    4 +-
 .../meson/gbp-meson-build-system-discovery.c       |   91 +
 .../meson/gbp-meson-build-system-discovery.h       |   32 +
 src/plugins/meson/gbp-meson-build-system.c         |   80 +-
 src/plugins/meson/gbp-meson-build-system.h         |    6 +-
 .../meson/gbp-meson-build-target-provider.c        |   41 +-
 .../meson/gbp-meson-build-target-provider.h        |    6 +-
 src/plugins/meson/gbp-meson-build-target.c         |   57 +-
 src/plugins/meson/gbp-meson-build-target.h         |   15 +-
 src/plugins/meson/gbp-meson-pipeline-addin.c       |   78 +-
 src/plugins/meson/gbp-meson-pipeline-addin.h       |    6 +-
 src/plugins/meson/gbp-meson-test-provider.c        |   23 +-
 src/plugins/meson/gbp-meson-test-provider.h        |    6 +-
 src/plugins/meson/gbp-meson-test.c                 |    4 +-
 src/plugins/meson/gbp-meson-test.h                 |    6 +-
 src/plugins/meson/gbp-meson-tool-row.c             |    8 +-
 src/plugins/meson/gbp-meson-tool-row.h             |    8 +-
 ...gbp-meson-toolchain-edition-preferences-addin.c |   13 +-
 ...gbp-meson-toolchain-edition-preferences-addin.h |    8 +-
 .../gbp-meson-toolchain-edition-preferences-row.c  |   10 +-
 .../gbp-meson-toolchain-edition-preferences-row.h  |    8 +-
 src/plugins/meson/gbp-meson-toolchain-provider.c   |    7 +-
 src/plugins/meson/gbp-meson-toolchain-provider.h   |    4 +-
 src/plugins/meson/gbp-meson-toolchain.c            |   14 +-
 src/plugins/meson/gbp-meson-toolchain.h            |    4 +-
 src/plugins/meson/gbp-meson-utils.c                |    2 +
 src/plugins/meson/gbp-meson-utils.h                |    4 +-
 src/plugins/meson/meson-plugin.c                   |   39 +-
 src/plugins/meson/meson.build                      |   35 +-
 src/plugins/meson/meson.gresource.xml              |    4 +-
 src/plugins/meson/meson.plugin                     |   13 +-
 src/plugins/messages/gbp-messages-editor-addin.c   |   18 +-
 src/plugins/messages/gbp-messages-editor-addin.h   |    4 +-
 src/plugins/messages/gbp-messages-panel.c          |   11 +-
 src/plugins/messages/gbp-messages-panel.h          |    4 +-
 src/plugins/messages/gbp-messages-plugin.c         |   30 -
 src/plugins/messages/meson.build                   |   19 +-
 src/plugins/messages/messages-plugin.c             |   34 +
 src/plugins/messages/messages.gresource.xml        |    4 +-
 src/plugins/messages/messages.plugin               |   10 +-
 .../modelines/gbp-modelines-file-settings.c        |  121 +
 .../modelines/gbp-modelines-file-settings.h        |   31 +
 .../modelines/language-mappings                    |    0
 src/plugins/modelines/meson.build                  |   17 +
 src/plugins/modelines/modeline-parser.c            |  814 ++++
 src/plugins/modelines/modeline-parser.h            |   38 +
 src/plugins/modelines/modelines-plugin.c           |   37 +
 src/plugins/modelines/modelines.gresource.xml      |    7 +
 src/plugins/modelines/modelines.plugin             |   10 +
 src/plugins/mono/meson.build                       |    4 +-
 src/plugins/mono/mono.plugin                       |   13 +-
 src/plugins/newcomers/gbp-newcomers-project.c      |    7 +-
 src/plugins/newcomers/gbp-newcomers-project.h      |    4 +-
 src/plugins/newcomers/gbp-newcomers-section.c      |  108 +-
 src/plugins/newcomers/gbp-newcomers-section.h      |    4 +-
 src/plugins/newcomers/gbp-newcomers-section.ui     |    3 +-
 src/plugins/newcomers/meson.build                  |   21 +-
 src/plugins/newcomers/newcomers-plugin.c           |   14 +-
 src/plugins/newcomers/newcomers.gresource.xml      |    5 +-
 src/plugins/newcomers/newcomers.plugin             |   11 +-
 src/plugins/notification/ide-notification-addin.c  |   70 +-
 src/plugins/notification/ide-notification-addin.h  |    4 +-
 src/plugins/notification/ide-notification-plugin.c |   28 -
 src/plugins/notification/meson.build               |   20 +-
 src/plugins/notification/notification-plugin.c     |   34 +
 .../notification/notification.gresource.xml        |    2 +-
 src/plugins/notification/notification.plugin       |   11 +-
 src/plugins/npm/meson.build                        |    4 +-
 src/plugins/npm/npm.plugin                         |   14 +-
 src/plugins/npm/npm_plugin.py                      |   57 +-
 src/plugins/omni-gutter/fast-str.c                 |   77 +
 src/plugins/omni-gutter/fast-str.h                 |   32 +
 .../gbp-omni-gutter-editor-page-addin.c            |   79 +
 .../gbp-omni-gutter-editor-page-addin.h            |   31 +
 src/plugins/omni-gutter/gbp-omni-gutter-renderer.c | 1750 +++++++++
 src/plugins/omni-gutter/gbp-omni-gutter-renderer.h |   42 +
 src/plugins/omni-gutter/int-array.h                | 1255 ++++++
 src/plugins/omni-gutter/meson.build                |   14 +
 src/plugins/omni-gutter/omni-gutter-plugin.c       |   36 +
 src/plugins/omni-gutter/omni-gutter.gresource.xml  |    6 +
 src/plugins/omni-gutter/omni-gutter.plugin         |   10 +
 src/plugins/phpize/meson.build                     |    4 +-
 src/plugins/phpize/phpize.plugin                   |   15 +-
 src/plugins/phpize/phpize_plugin.py                |   43 +-
 src/plugins/plugins.map                            |    7 -
 src/plugins/project-tree/gb-new-file-popover.c     |  384 --
 src/plugins/project-tree/gb-new-file-popover.h     |   36 -
 src/plugins/project-tree/gb-new-file-popover.ui    |   57 -
 src/plugins/project-tree/gb-project-file.c         |  298 --
 src/plugins/project-tree/gb-project-file.h         |   46 -
 src/plugins/project-tree/gb-project-tree-actions.c | 1002 -----
 src/plugins/project-tree/gb-project-tree-actions.h |   28 -
 src/plugins/project-tree/gb-project-tree-addin.c   |  129 -
 src/plugins/project-tree/gb-project-tree-addin.h   |   29 -
 src/plugins/project-tree/gb-project-tree-builder.c |  948 -----
 src/plugins/project-tree/gb-project-tree-builder.h |   31 -
 .../project-tree/gb-project-tree-editor-addin.c    |  119 -
 .../project-tree/gb-project-tree-editor-addin.h    |   29 -
 src/plugins/project-tree/gb-project-tree-private.h |   38 -
 .../project-tree/gb-project-tree-shortcuts.c       |   72 -
 src/plugins/project-tree/gb-project-tree.c         |  627 ---
 src/plugins/project-tree/gb-project-tree.h         |   44 -
 src/plugins/project-tree/gb-rename-file-popover.c  |  376 --
 src/plugins/project-tree/gb-rename-file-popover.h  |   32 -
 src/plugins/project-tree/gb-rename-file-popover.ui |   57 -
 src/plugins/project-tree/gb-vcs-tree-builder.c     |  175 -
 src/plugins/project-tree/gb-vcs-tree-builder.h     |   31 -
 src/plugins/project-tree/gbp-new-file-popover.c    |  421 ++
 src/plugins/project-tree/gbp-new-file-popover.h    |   48 +
 src/plugins/project-tree/gbp-new-file-popover.ui   |   57 +
 src/plugins/project-tree/gbp-project-tree-addin.c  |  896 +++++
 src/plugins/project-tree/gbp-project-tree-addin.h  |   31 +
 .../project-tree/gbp-project-tree-pane-actions.c   |  634 +++
 src/plugins/project-tree/gbp-project-tree-pane.c   |   62 +
 src/plugins/project-tree/gbp-project-tree-pane.h   |   31 +
 src/plugins/project-tree/gbp-project-tree-pane.ui  |   20 +
 .../project-tree/gbp-project-tree-private.h        |   40 +
 .../gbp-project-tree-workspace-addin.c             |  102 +
 .../gbp-project-tree-workspace-addin.h             |   31 +
 src/plugins/project-tree/gbp-project-tree.c        |  178 +
 src/plugins/project-tree/gbp-project-tree.h        |   31 +
 src/plugins/project-tree/gbp-rename-file-popover.c |  452 +++
 src/plugins/project-tree/gbp-rename-file-popover.h |   42 +
 .../project-tree/gbp-rename-file-popover.ui        |   57 +
 src/plugins/project-tree/gtk/menus.ui              |  108 +-
 src/plugins/project-tree/meson.build               |   49 +-
 src/plugins/project-tree/project-tree-plugin.c     |   25 +-
 .../project-tree/project-tree.gresource.xml        |   11 +-
 src/plugins/project-tree/project-tree.plugin       |   14 +-
 src/plugins/project-tree/themes/shared.css         |    8 +
 .../python-gi-imports-completion/meson.build       |    6 +-
 .../python-gi-imports-completion.plugin            |   11 +-
 .../python_gi_imports_completion.py                |    5 -
 src/plugins/python-pack/ide-python-indenter.c      |    6 +-
 src/plugins/python-pack/ide-python-indenter.h      |    6 +-
 src/plugins/python-pack/meson.build                |   22 +-
 src/plugins/python-pack/python-pack-plugin.c       |   15 +-
 src/plugins/python-pack/python-pack.gresource.xml  |    2 +-
 src/plugins/python-pack/python-pack.plugin         |   17 +-
 src/plugins/qemu/gbp-qemu-device-provider.c        |   11 +-
 src/plugins/qemu/gbp-qemu-device-provider.h        |    4 +-
 src/plugins/qemu/gbp-qemu-plugin.c                 |   30 -
 src/plugins/qemu/meson.build                       |   17 +-
 src/plugins/qemu/qemu-plugin.c                     |   34 +
 src/plugins/qemu/qemu.gresource.xml                |    4 +-
 src/plugins/qemu/qemu.plugin                       |   11 +-
 .../gbp-quick-highlight-editor-page-addin.c        |  276 ++
 .../gbp-quick-highlight-editor-page-addin.h        |   31 +
 .../gbp-quick-highlight-editor-view-addin.c        |  274 --
 .../gbp-quick-highlight-editor-view-addin.h        |   29 -
 .../quick-highlight/gbp-quick-highlight-plugin.c   |   34 -
 .../gbp-quick-highlight-preferences.c              |    4 +-
 .../gbp-quick-highlight-preferences.h              |    6 +-
 src/plugins/quick-highlight/meson.build            |   21 +-
 .../quick-highlight/quick-highlight-plugin.c       |   38 +
 .../quick-highlight/quick-highlight.gresource.xml  |    2 +-
 src/plugins/quick-highlight/quick-highlight.plugin |   11 +-
 src/plugins/recent/gbp-recent-project-row.c        |    7 +-
 src/plugins/recent/gbp-recent-project-row.h        |    6 +-
 src/plugins/recent/gbp-recent-section.c            |  123 +-
 src/plugins/recent/gbp-recent-section.h            |    4 +-
 src/plugins/recent/gbp-recent-section.ui           |    2 +
 src/plugins/recent/gbp-recent-workbench-addin.c    |  248 ++
 src/plugins/recent/gbp-recent-workbench-addin.h    |   31 +
 src/plugins/recent/meson.build                     |   20 +-
 src/plugins/recent/recent-plugin.c                 |   19 +-
 src/plugins/recent/recent.gresource.xml            |    4 +-
 src/plugins/recent/recent.plugin                   |   11 +-
 .../gbp-restore-cursor-buffer-addin.c              |  151 +
 .../gbp-restore-cursor-buffer-addin.h              |   31 +
 src/plugins/restore-cursor/meson.build             |   12 +
 src/plugins/restore-cursor/restore-cursor-plugin.c |   36 +
 .../restore-cursor/restore-cursor.gresource.xml    |    6 +
 src/plugins/restore-cursor/restore-cursor.plugin   |   10 +
 src/plugins/retab/gbp-retab-editor-page-addin.c    |  226 ++
 src/plugins/retab/gbp-retab-editor-page-addin.h    |   29 +
 src/plugins/retab/gbp-retab-plugin.c               |   30 -
 src/plugins/retab/gbp-retab-view-addin.c           |  223 --
 src/plugins/retab/gbp-retab-view-addin.h           |   27 -
 src/plugins/retab/meson.build                      |   20 +-
 src/plugins/retab/retab-plugin.c                   |   36 +
 src/plugins/retab/retab.gresource.xml              |    6 +-
 src/plugins/retab/retab.plugin                     |   11 +-
 src/plugins/rls/meson.build                        |   13 +
 src/plugins/rls/rls.plugin                         |   17 +
 src/plugins/rls/rls_plugin.py                      |  254 ++
 src/plugins/rust-langserv/meson.build              |   13 -
 src/plugins/rust-langserv/rust-langserv.plugin     |   15 -
 src/plugins/rust-langserv/rust_langserv_plugin.py  |  246 --
 src/plugins/rustup/meson.build                     |    4 +-
 src/plugins/rustup/rustup.gresource.xml            |    2 +-
 src/plugins/rustup/rustup.plugin                   |   12 +-
 src/plugins/rustup/rustup.sh                       |   39 +-
 src/plugins/rustup/rustup_plugin.py                |   46 +-
 src/plugins/snippets/ide-snippet-completion-item.c |    8 +-
 src/plugins/snippets/ide-snippet-completion-item.h |    6 +-
 .../snippets/ide-snippet-completion-provider.c     |   10 +-
 .../snippets/ide-snippet-completion-provider.h     |    6 +-
 src/plugins/snippets/ide-snippet-model.c           |    8 +-
 src/plugins/snippets/ide-snippet-model.h           |    6 +-
 .../snippets/ide-snippet-preferences-addin.c       |   10 +-
 .../snippets/ide-snippet-preferences-addin.h       |    4 +-
 src/plugins/snippets/meson.build                   |   21 +-
 src/plugins/snippets/snippets-plugin.c             |   13 +-
 src/plugins/snippets/snippets.gresource.xml        |   19 +-
 src/plugins/snippets/snippets.plugin               |   10 +-
 {data => src/plugins/snippets}/snippets/c.snippets |    0
 .../plugins/snippets}/snippets/chdr.snippets       |    0
 .../plugins/snippets}/snippets/gobject.snippets    |    0
 .../plugins/snippets}/snippets/java.snippets       |    0
 .../plugins/snippets}/snippets/js.snippets         |    0
 .../plugins/snippets}/snippets/licenses.snippets   |    0
 .../plugins/snippets}/snippets/main.snippets       |    0
 .../plugins/snippets}/snippets/python.snippets     |    0
 .../plugins/snippets}/snippets/rpmspec.snippets    |    0
 .../plugins/snippets}/snippets/rust.snippets       |    0
 .../plugins/snippets}/snippets/shebang.snippets    |    0
 .../plugins/snippets}/snippets/vala.snippets       |    0
 .../plugins/snippets}/snippets/xml.snippets        |    0
 src/plugins/spellcheck/gbp-spell-buffer-addin.c    |    4 +-
 src/plugins/spellcheck/gbp-spell-buffer-addin.h    |    6 +-
 src/plugins/spellcheck/gbp-spell-dict.c            |    4 +-
 src/plugins/spellcheck/gbp-spell-dict.h            |    2 +
 src/plugins/spellcheck/gbp-spell-editor-addin.c    |   46 +-
 src/plugins/spellcheck/gbp-spell-editor-addin.h    |    6 +-
 .../spellcheck/gbp-spell-editor-page-addin.c       |  394 ++
 .../spellcheck/gbp-spell-editor-page-addin.h       |   40 +
 .../spellcheck/gbp-spell-editor-view-addin.c       |  392 --
 .../spellcheck/gbp-spell-editor-view-addin.h       |   38 -
 .../spellcheck/gbp-spell-language-popover.c        |   10 +-
 .../spellcheck/gbp-spell-language-popover.h        |    2 +
 src/plugins/spellcheck/gbp-spell-navigator.c       |    6 +-
 src/plugins/spellcheck/gbp-spell-navigator.h       |    2 +
 src/plugins/spellcheck/gbp-spell-private.h         |   18 +-
 src/plugins/spellcheck/gbp-spell-utils.c           |    2 +
 src/plugins/spellcheck/gbp-spell-utils.h           |    2 +
 src/plugins/spellcheck/gbp-spell-widget-actions.c  |   18 +-
 src/plugins/spellcheck/gbp-spell-widget.c          |  106 +-
 src/plugins/spellcheck/gbp-spell-widget.h          |   10 +-
 src/plugins/spellcheck/meson.build                 |   37 +-
 src/plugins/spellcheck/spellcheck-plugin.c         |   26 +-
 src/plugins/spellcheck/spellcheck.gresource.xml    |   10 +-
 src/plugins/spellcheck/spellcheck.plugin           |   13 +-
 .../sublime/gbp-sublime-preferences-addin.c        |   89 +
 .../sublime/gbp-sublime-preferences-addin.h        |   31 +
 src/plugins/sublime/keybindings/sublime.css        |  314 ++
 src/plugins/sublime/meson.build                    |   12 +
 src/plugins/sublime/sublime-plugin.c               |   36 +
 src/plugins/sublime/sublime.gresource.xml          |    7 +
 src/plugins/sublime/sublime.plugin                 |    9 +
 src/plugins/support/gtk/menus.ui                   |    4 +-
 .../support/ide-support-application-addin.c        |   13 +-
 .../support/ide-support-application-addin.h        |    4 +-
 src/plugins/support/ide-support-plugin.c           |   30 -
 src/plugins/support/ide-support.c                  |    6 +-
 src/plugins/support/ide-support.h                  |    4 +-
 src/plugins/support/meson.build                    |   23 +-
 src/plugins/support/support-plugin.c               |   32 +
 src/plugins/support/support.gresource.xml          |    4 +-
 src/plugins/support/support.plugin                 |    4 +-
 src/plugins/symbol-tree/gbp-symbol-frame-addin.c   |  563 +++
 src/plugins/symbol-tree/gbp-symbol-frame-addin.h   |   31 +
 .../symbol-tree/gbp-symbol-hover-provider.c        |   71 +-
 .../symbol-tree/gbp-symbol-hover-provider.h        |    6 +-
 .../symbol-tree/gbp-symbol-layout-stack-addin.c    |  594 ---
 .../symbol-tree/gbp-symbol-layout-stack-addin.h    |   29 -
 src/plugins/symbol-tree/gbp-symbol-menu-button.c   |   11 +-
 src/plugins/symbol-tree/gbp-symbol-menu-button.h   |    6 +-
 src/plugins/symbol-tree/gbp-symbol-tree-builder.c  |   19 +-
 src/plugins/symbol-tree/gbp-symbol-tree-builder.h  |    6 +-
 src/plugins/symbol-tree/meson.build                |   26 +-
 src/plugins/symbol-tree/symbol-tree-plugin.c       |   19 +-
 src/plugins/symbol-tree/symbol-tree.gresource.xml  |    6 +-
 src/plugins/symbol-tree/symbol-tree.plugin         |   13 +-
 src/plugins/sysprof/gbp-sysprof-perspective.c      |  293 --
 src/plugins/sysprof/gbp-sysprof-perspective.h      |   37 -
 src/plugins/sysprof/gbp-sysprof-perspective.ui     |   99 -
 src/plugins/sysprof/gbp-sysprof-plugin.c           |   33 -
 src/plugins/sysprof/gbp-sysprof-surface.c          |  264 ++
 src/plugins/sysprof/gbp-sysprof-surface.h          |   39 +
 src/plugins/sysprof/gbp-sysprof-surface.ui         |   99 +
 src/plugins/sysprof/gbp-sysprof-workbench-addin.c  |  592 ---
 src/plugins/sysprof/gbp-sysprof-workbench-addin.h  |   29 -
 src/plugins/sysprof/gbp-sysprof-workspace-addin.c  |  617 +++
 src/plugins/sysprof/gbp-sysprof-workspace-addin.h  |   31 +
 src/plugins/sysprof/gtk/menus.ui                   |   20 +-
 src/plugins/sysprof/meson.build                    |   35 +-
 src/plugins/sysprof/sysprof-plugin.c               |   39 +
 src/plugins/sysprof/sysprof.gresource.xml          |    6 +-
 src/plugins/sysprof/sysprof.plugin                 |   12 +-
 src/plugins/sysroot/gbp-sysroot-manager.c          |    6 +-
 src/plugins/sysroot/gbp-sysroot-manager.h          |    8 +-
 .../sysroot/gbp-sysroot-preferences-addin.c        |    7 +-
 .../sysroot/gbp-sysroot-preferences-addin.h        |    8 +-
 src/plugins/sysroot/gbp-sysroot-preferences-row.c  |    8 +-
 src/plugins/sysroot/gbp-sysroot-preferences-row.h  |    8 +-
 src/plugins/sysroot/gbp-sysroot-runtime-provider.c |   17 +-
 src/plugins/sysroot/gbp-sysroot-runtime-provider.h |    8 +-
 src/plugins/sysroot/gbp-sysroot-runtime.c          |   21 +-
 src/plugins/sysroot/gbp-sysroot-runtime.h          |   11 +-
 .../sysroot/gbp-sysroot-subprocess-launcher.c      |    6 +-
 .../sysroot/gbp-sysroot-subprocess-launcher.h      |    8 +-
 .../sysroot/gbp-sysroot-toolchain-provider.c       |    8 +-
 .../sysroot/gbp-sysroot-toolchain-provider.h       |    4 +-
 src/plugins/sysroot/meson.build                    |   28 +-
 src/plugins/sysroot/sysroot-plugin.c               |   26 +-
 src/plugins/sysroot/sysroot.gresource.xml          |    4 +-
 src/plugins/sysroot/sysroot.plugin                 |   12 +-
 src/plugins/terminal/gb-terminal-plugin.c          |   31 -
 src/plugins/terminal/gb-terminal-private.h         |   28 -
 src/plugins/terminal/gb-terminal-view-actions.c    |  332 --
 src/plugins/terminal/gb-terminal-view-actions.h    |   27 -
 src/plugins/terminal/gb-terminal-view-private.h    |   63 -
 src/plugins/terminal/gb-terminal-view.c            |  758 ----
 src/plugins/terminal/gb-terminal-view.h            |   35 -
 src/plugins/terminal/gb-terminal-view.ui           |   41 -
 src/plugins/terminal/gb-terminal-workbench-addin.c |  435 ---
 src/plugins/terminal/gb-terminal-workbench-addin.h |   29 -
 .../terminal/gbp-terminal-application-addin.c      |   88 +
 .../terminal/gbp-terminal-application-addin.h      |   31 +
 .../terminal/gbp-terminal-workspace-addin.c        |  460 +++
 .../terminal/gbp-terminal-workspace-addin.h        |   31 +
 src/plugins/terminal/gtk/menus.ui                  |   38 +-
 src/plugins/terminal/meson.build                   |   29 +-
 src/plugins/terminal/terminal-plugin.c             |   41 +
 src/plugins/terminal/terminal.gresource.xml        |    7 +-
 src/plugins/terminal/terminal.plugin               |   15 +-
 src/plugins/testui/gbp-test-path.c                 |  181 +
 src/plugins/testui/gbp-test-path.h                 |   37 +
 src/plugins/testui/gbp-test-tree-addin.c           |  394 ++
 src/plugins/testui/gbp-test-tree-addin.h           |   31 +
 src/plugins/testui/meson.build                     |   13 +
 src/plugins/testui/testui-plugin.c                 |   36 +
 src/plugins/testui/testui.gresource.xml            |    6 +
 src/plugins/testui/testui.plugin                   |   11 +
 src/plugins/todo/gbp-todo-item.c                   |    6 +-
 src/plugins/todo/gbp-todo-item.h                   |    4 +-
 src/plugins/todo/gbp-todo-model.c                  |   15 +-
 src/plugins/todo/gbp-todo-model.h                  |    7 +-
 src/plugins/todo/gbp-todo-panel.c                  |   32 +-
 src/plugins/todo/gbp-todo-panel.h                  |    6 +-
 src/plugins/todo/gbp-todo-plugin.c                 |   30 -
 src/plugins/todo/gbp-todo-workbench-addin.c        |  211 -
 src/plugins/todo/gbp-todo-workbench-addin.h        |   29 -
 src/plugins/todo/gbp-todo-workspace-addin.c        |  213 +
 src/plugins/todo/gbp-todo-workspace-addin.h        |   31 +
 src/plugins/todo/meson.build                       |   27 +-
 src/plugins/todo/todo-plugin.c                     |   34 +
 src/plugins/todo/todo.gresource.xml                |    2 +-
 src/plugins/todo/todo.plugin                       |   13 +-
 .../trim-spaces/gbp-trim-spaces-buffer-addin.c     |   77 +
 .../trim-spaces/gbp-trim-spaces-buffer-addin.h     |   31 +
 src/plugins/trim-spaces/meson.build                |   12 +
 src/plugins/trim-spaces/trim-spaces-plugin.c       |   36 +
 src/plugins/trim-spaces/trim-spaces.gresource.xml  |    6 +
 src/plugins/trim-spaces/trim-spaces.plugin         |   10 +
 src/plugins/vala-pack/ide-vala-code-indexer.vala   |    8 +-
 .../vala-pack/ide-vala-completion-provider.vala    |   11 +-
 .../vala-pack/ide-vala-diagnostic-provider.vala    |   15 +-
 src/plugins/vala-pack/ide-vala-index.vala          |   25 +-
 src/plugins/vala-pack/ide-vala-service.vala        |   16 +-
 src/plugins/vala-pack/ide-vala-source-file.vala    |   39 +-
 .../vala-pack/ide-vala-symbol-resolver.vala        |   34 +-
 src/plugins/vala-pack/ide-vala-symbol-tree.vala    |    9 +-
 src/plugins/vala-pack/meson.build                  |   62 +-
 src/plugins/vala-pack/vala-pack-plugin.vala        |    1 -
 src/plugins/vala-pack/vala-pack.plugin             |   17 +-
 src/plugins/valgrind/gtk/menus.ui                  |   10 +
 src/plugins/valgrind/meson.build                   |   11 +-
 src/plugins/valgrind/valgrind-plugin.gresource.xml |    6 -
 src/plugins/valgrind/valgrind.gresource.xml        |    6 +
 src/plugins/valgrind/valgrind.plugin               |   13 +-
 src/plugins/valgrind/valgrind_plugin.py            |   29 +-
 src/plugins/vcsui/gbp-vcsui-editor-page-addin.c    |  137 +
 src/plugins/vcsui/gbp-vcsui-editor-page-addin.h    |   31 +
 src/plugins/vcsui/gbp-vcsui-tree-addin.c           |  209 +
 src/plugins/vcsui/gbp-vcsui-tree-addin.h           |   31 +
 src/plugins/vcsui/gtk/menus.ui                     |   20 +
 src/plugins/vcsui/meson.build                      |   13 +
 src/plugins/vcsui/vcsui-plugin.c                   |   42 +
 src/plugins/vcsui/vcsui.gresource.xml              |    7 +
 src/plugins/vcsui/vcsui.plugin                     |   11 +
 src/plugins/vim/gb-vim.c                           | 1661 ++++++++
 src/plugins/vim/gb-vim.h                           |   48 +
 src/plugins/vim/gbp-vim-command-provider.c         |  122 +
 src/plugins/vim/gbp-vim-command-provider.h         |   31 +
 src/plugins/vim/gbp-vim-command.c                  |  141 +
 src/plugins/vim/gbp-vim-command.h                  |   36 +
 src/plugins/vim/gbp-vim-preferences-addin.c        |   89 +
 src/plugins/vim/gbp-vim-preferences-addin.h        |   31 +
 src/plugins/vim/keybindings/vim.css                | 2892 ++++++++++++++
 src/plugins/vim/meson.build                        |   15 +
 src/plugins/vim/vim-plugin.c                       |   41 +
 src/plugins/vim/vim.gresource.xml                  |    7 +
 src/plugins/vim/vim.plugin                         |    9 +
 src/plugins/words/gbp-word-completion-provider.c   |   10 +-
 src/plugins/words/gbp-word-completion-provider.h   |    6 +-
 src/plugins/words/gbp-word-proposal.c              |   10 +-
 src/plugins/words/gbp-word-proposal.h              |    4 +-
 src/plugins/words/gbp-word-proposals.c             |   12 +-
 src/plugins/words/gbp-word-proposals.h             |    6 +-
 src/plugins/words/meson.build                      |   21 +-
 src/plugins/words/words-plugin.c                   |   12 +-
 src/plugins/words/words.gresource.xml              |    2 +-
 src/plugins/words/words.plugin                     |   11 +-
 src/plugins/xml-pack/ide-xml-analysis.c            |   12 +-
 src/plugins/xml-pack/ide-xml-analysis.h            |    7 +-
 .../xml-pack/ide-xml-completion-attributes.c       |    5 +-
 .../xml-pack/ide-xml-completion-attributes.h       |    4 +-
 src/plugins/xml-pack/ide-xml-completion-provider.c |   19 +-
 src/plugins/xml-pack/ide-xml-completion-provider.h |    4 +-
 src/plugins/xml-pack/ide-xml-completion-values.c   |    4 +
 src/plugins/xml-pack/ide-xml-completion-values.h   |    4 +-
 src/plugins/xml-pack/ide-xml-diagnostic-provider.c |   17 +-
 src/plugins/xml-pack/ide-xml-diagnostic-provider.h |    4 +-
 src/plugins/xml-pack/ide-xml-hash-table.c          |    5 +-
 src/plugins/xml-pack/ide-xml-hash-table.h          |    2 +
 src/plugins/xml-pack/ide-xml-highlighter.c         |    6 +-
 src/plugins/xml-pack/ide-xml-highlighter.h         |    4 +-
 src/plugins/xml-pack/ide-xml-indenter.c            |    8 +-
 src/plugins/xml-pack/ide-xml-indenter.h            |    6 +-
 src/plugins/xml-pack/ide-xml-parser-generic.c      |    8 +-
 src/plugins/xml-pack/ide-xml-parser-generic.h      |    2 +
 src/plugins/xml-pack/ide-xml-parser-private.h      |    2 +
 src/plugins/xml-pack/ide-xml-parser-ui.c           |   31 +-
 src/plugins/xml-pack/ide-xml-parser-ui.h           |    2 +
 src/plugins/xml-pack/ide-xml-parser.c              |   46 +-
 src/plugins/xml-pack/ide-xml-parser.h              |    2 +
 src/plugins/xml-pack/ide-xml-path.c                |    2 +
 src/plugins/xml-pack/ide-xml-path.h                |    2 +
 src/plugins/xml-pack/ide-xml-position.c            |    4 +
 src/plugins/xml-pack/ide-xml-position.h            |    2 +
 src/plugins/xml-pack/ide-xml-proposal.c            |   10 +-
 src/plugins/xml-pack/ide-xml-proposal.h            |    6 +-
 src/plugins/xml-pack/ide-xml-rng-define.c          |    4 +
 src/plugins/xml-pack/ide-xml-rng-define.h          |    2 +
 src/plugins/xml-pack/ide-xml-rng-grammar.c         |    2 +
 src/plugins/xml-pack/ide-xml-rng-grammar.h         |    2 +
 src/plugins/xml-pack/ide-xml-rng-parser.c          |    3 +
 src/plugins/xml-pack/ide-xml-rng-parser.h          |    4 +-
 src/plugins/xml-pack/ide-xml-sax.c                 |    2 +
 src/plugins/xml-pack/ide-xml-sax.h                 |    2 +
 src/plugins/xml-pack/ide-xml-schema-cache-entry.c  |    2 +
 src/plugins/xml-pack/ide-xml-schema-cache-entry.h  |    2 +
 src/plugins/xml-pack/ide-xml-schema.c              |    2 +
 src/plugins/xml-pack/ide-xml-schema.h              |    2 +
 src/plugins/xml-pack/ide-xml-service.c             |  303 +-
 src/plugins/xml-pack/ide-xml-service.h             |   16 +-
 src/plugins/xml-pack/ide-xml-stack.c               |    6 +-
 src/plugins/xml-pack/ide-xml-stack.h               |    2 +
 src/plugins/xml-pack/ide-xml-symbol-node.c         |   22 +-
 src/plugins/xml-pack/ide-xml-symbol-node.h         |    4 +-
 src/plugins/xml-pack/ide-xml-symbol-resolver.c     |   15 +-
 src/plugins/xml-pack/ide-xml-symbol-resolver.h     |    4 +-
 src/plugins/xml-pack/ide-xml-symbol-tree.c         |    2 +
 src/plugins/xml-pack/ide-xml-symbol-tree.h         |    4 +-
 .../xml-pack/ide-xml-tree-builder-utils-private.h  |    4 +-
 src/plugins/xml-pack/ide-xml-tree-builder-utils.c  |    3 +
 src/plugins/xml-pack/ide-xml-tree-builder.c        |   46 +-
 src/plugins/xml-pack/ide-xml-tree-builder.h        |    4 +-
 src/plugins/xml-pack/ide-xml-types.h               |    2 +
 src/plugins/xml-pack/ide-xml-utils.c               |    2 +
 src/plugins/xml-pack/ide-xml-utils.h               |    2 +
 src/plugins/xml-pack/ide-xml-validator.c           |   34 +-
 src/plugins/xml-pack/ide-xml-validator.h           |    5 +-
 src/plugins/xml-pack/meson.build                   |   21 +-
 src/plugins/xml-pack/xml-pack-plugin.c             |   34 +-
 src/plugins/xml-pack/xml-pack.gresource.xml        |    4 +-
 src/plugins/xml-pack/xml-pack.plugin               |   26 +-
 src/tests/data/project1/.editorconfig              |   10 -
 src/tests/data/project1/.gitignore                 |   19 -
 src/tests/data/project1/.you-dont-git-me           |    0
 src/tests/data/project1/autogen.sh                 |   10 -
 src/tests/data/project1/build-aux/.gitignore       |    1 -
 src/tests/data/project1/build-aux/m4/.keep         |    0
 src/tests/data/project1/configure.ac               |   11 -
 src/tests/data/project1/project1.c                 |    1 -
 src/tests/data/project1/tags                       |  821 ----
 src/tests/data/project2/.you-dont-git-me           |    0
 ...le-commands.json => test-compile-commands.json} |    0
 .../data/{project1/project1.doap => test.doap}     |    0
 src/tests/meson.build                              |  200 +-
 src/tests/samples/gnome-logo.png                   |  Bin 895 -> 0 bytes
 src/tests/samples/markdown test page 2.html        |    7 -
 src/tests/samples/markdown test.md                 |  255 --
 src/tests/test-backoff.c                           |  113 -
 src/tests/test-c-parse-helper.c                    |   80 -
 src/tests/test-compile-commands.c                  |   80 +
 src/tests/test-completion-fuzzy.c                  |    2 +-
 src/tests/test-doap.c                              |   78 +
 src/tests/test-gfile.c                             |   42 +
 src/tests/test-hdr-format.c                        |   47 -
 src/tests/test-ide-buffer-manager.c                |  194 -
 src/tests/test-ide-buffer.c                        |  122 -
 src/tests/test-ide-build-pipeline.c                |  124 -
 src/tests/test-ide-compile-commands.c              |   78 -
 src/tests/test-ide-configuration.c                 |   97 -
 src/tests/test-ide-context.c                       |  127 -
 src/tests/test-ide-ctags.c                         |  108 -
 src/tests/test-ide-doap.c                          |   76 -
 src/tests/test-ide-file-settings.c                 |  184 -
 src/tests/test-ide-glib.c                          |   44 -
 src/tests/test-ide-indenter.c                      |  183 -
 src/tests/test-ide-runtime.c                       |   96 -
 src/tests/test-ide-subprocess-launcher.c           |  184 -
 src/tests/test-ide-task.c                          |  702 ----
 src/tests/test-ide-uri.c                           |  144 -
 src/tests/test-ide-vcs-uri.c                       |   93 -
 src/tests/test-iter.c                              |   69 -
 src/tests/test-libide-core.c                       |  297 ++
 src/tests/test-line-reader.c                       |    6 +-
 src/tests/test-snippet-parser.c                    |    4 +-
 src/tests/test-subprocess-launcher.c               |  186 +
 src/tests/test-task.c                              |  704 ++++
 src/tests/test-text-iter.c                         |   67 +
 src/tests/test-vcs-uri.c                           |  113 +
 src/tests/test-vim.c                               |  196 -
 2725 files changed, 202735 insertions(+), 187951 deletions(-)
---
diff --git a/JOURNAL.md b/JOURNAL.md
new file mode 100644
index 000000000..e6c47f29c
--- /dev/null
+++ b/JOURNAL.md
@@ -0,0 +1,315 @@
+# Journal
+
+This is my notes as i go, to see if we can recolect on anything useful.
+
+## Day 1 — Starting a new (sub) project
+
+To avoid having to refactor large bits of Builder, I've started a new project
+tree where I can just bring things over incrementally as I clean them up and
+port them to the new design.
+
+The big thing I'm trying to do here is:
+
+ * Break things up into a series of smaller libraries (libide-core, libide-gui,
+   libide-threading, etc).
+ * Rename lots of objects to more closely represent what they do, which wasn't
+   as clear when they were initially designed. Sometimes things get used
+   differently than their intention.
+ * Experiment with a new design for IdeObject and IdeContext that simplifies
+   reference counting, cycles, and other hard to detect issues.
+ * Create a very clear addin story in terms of what interfaces you need to
+   extend for your particular goal.
+ * Create an abstract enough base-layer that we can create similar projects
+   such as a dedicated $EDITOR (with minimal projects interaction) and something
+   like the purple-egg prototype.
+ * Some sort of multi-monitor story.
+
+That is quite ambitious for a branch, but I think they are all somewhat related
+in terms of getting this done cleanly.
+
+It might also help us figure out how more correctly support an ABI story in the
+future, if that becomes important.
+
+Another thing on my mind, is how can this help us in a Gtk4 story. There will
+be lots of porting there, so what can we clean-up now?
+
+I've done lots of drawing in my engineering journal to ensure that I have a
+grasp of the windowing objects, and how we want to name them succinctly.
+
+Anyway, that is what I've been doing mostly today. I've gotten a bit of
+libide-core created, lots of layout stuff extracted into libide-gui, and a
+threading library built (subprocess, tasks, thread pool, environ, etc).
+
+
+## Day 2 — More work on greeter, library cleanup
+
+Lots of work on cleaning up greeter apis, which were previously quite shitty.
+
+Realized that what I want a "workbench" to be, is basically a GtkWindowGroup.
+Well that simplifies a bunch since we can subclass it.
+
+I'm starting to get the header bar refactored into something re-usable, now that
+we'll likely have multiple types of workbenches (Primary, Secondary, Greeter,
+Dedicated Editor (gedit), and Terminal (purple-egg).
+
+I did another round of cleanup for how I want library headers to work. It's
+starting to look much better.
+
+I moved PTY interceptor to libide-io, because it allows us to avoid having
+the libide-terminal (having gtk widgets) a dependency of the build library
+later on.
+
+I started on the projects library. I also am re-using project-info object
+for opening projects, since it allows us to describe more about what we
+are trying to open (like a VCS uri).
+
+One interesting thing is the idea of moving away from the omnibar design as it
+is now, where it knows what to look for to display (like the build pipeline
+type stuff). We can instead create an IdeMessage and get rid of IdePausable
+in favor of that. Then the pipeline can request a message and update it during
+the process of the build. That doesn't solve the content of the popover, but
+we can deal with that in later API.
+
+An insight while working on IdeObject is that we can sort of copy how things
+are done in Gtk (but without floating references). I'm not quite sure yet
+how to ensure you can't add new objects to the tree during destroy though.
+That could be an interesting race condition to solve.
+
+If the objects have cancellables, and those are cancelled during ::destroy,
+then the tree will just cleanup work as it is no longer necessary.
+
+
+## Day 3 — More work on objects
+
+Spent most of the day on a few iterations of the object stuff. It seems
+like I started to let it get out of hand with the requirements.
+
+So I reeled it back in. IdeObject is a main thread only object, that is
+capabile of interacting with other threads. So workers (like pipelines, etc)
+can still use threads, just do the tree stuff on the main thread only.
+
+Also, IdeObject::destroy seems to work, and since it is only valid to
+dispose them from main thread, i think we get the effect we want.
+
+It does mean that consumers need to use IdeTask though.
+
+
+## Day 4 — OmniBar, Message, HeaderBar, etc
+
+I think IdeContext can exist in an "unloaded" form and be created with the
+IdeContext. It just wont have many children other than transfer-manager,
+messages, etc.
+
+I started working on the omnibar a bit, and seeing how it will fit into the
+new header bar abstractions. To keep the OmniBar from having to know about
+the buildsystem/etc, we can make it use addins and some API to extend it.
+
+I also worked a bit on making IdeMessage support the features we want. I also
+drew some mockups for how IdeMessage would be visualized in the new OmniBar.
+
+I'm thinking about how to rotate data in the omnibar, and how addins get their
+messages in. Basically, I think we want to just have the omnibar discover the
+IdeMessages object as a root IdeObject child. Then treat that as a GListModel
+that creates widgets for the children in a GtkStack (with transitions).
+
+I think we can use IdeNotification to replace IdePausable, IdeTransfer display,
+and omnibar build status messages. We'll still have IdeTransfer/
+IdeTransferManager, but they can be child objects of the IdeContext (which is
+unloaded for preferences at startup, but can still exist).
+
+The display of messages will be split based on if it has progress. Things with
+progress go under the progress button. Thinks without go under the build
+popover. But all are represented by an IdeMessage.
+
+## Day 5 — OmniBar, Messages
+
+Starting again on this work after some fresh thought.
+
+I think IdeMessage is starting to look a lot like a Notification API, and we
+should probably use that as our metaphore (renaming to IdeNotification). That
+allows for actions (buttons), icons, status, progress, etc. We'll just have to
+be thread-safe so it can be used from threads to send status. (Like during git
+clone events).
+
+Goals for today:
+
+ x IdeMessage renamed to IdeNotification
+ o IdeNotificationView -> GtkWidget to show IdeNotification in omnibar stack
+ - IdeNotificationListBoxRow -> GtkWidget to show full notification, either in
+   the omnibar popover, or in the transfers popover. Those with progress will
+   always be in the transfers (progress?) button.
+ - IdeProgressButton -> Rename from Transfers button, use Notification API
+ - IdeOmniBarAddin (allow addins to extend omnibar)
+
+I got some more of the workbench/workspace setup, and initial context setup.
+What I like so far is that all workbenches will have an IdeContext, just that
+a project may not yet be loaded into that context. That can simplify a lot of
+our inconsistencies between the two states.
+
+I want to avoid using a Popover for the omnibar popdown, unless it is also
+transient like we did for the hoverprovider. It's just too annoying to have
+to click through the entry box. I'd like it to be a quick hover, and then
+the window is displayed, possibly even wrapping around the omnibar.
+
+## Day 6 — OmniBar, Continued
+
+We got a lot of stuff done yesterday, but today I want to push through and
+try to finish more of it. The notification stuff is working well. I want to
+get the progress button stuff done, and the listboxrows for the popover.
+
+We also should create the omnibar addin, since that is pretty easy.
+
+
+## Day 7 — Refinement, meson
+
+Spent some time today trying to refine the notification work so that we can
+move on with the workbench/workspaces.
+
+I started on some new IdeObject helpers to make compat with the old libide
+design easier in a few places.
+
+I got search ported over, albeit we're going to have to get rid of the
+source location stuff and switch to just an "activate" signal. I'd also
+like to be able to drop DzlSuggestion (so we can drop dazzle from that
+library too).
+
+I'm also starting to switch to meson, because my crappy makefile was too
+slow and it will help me think about the dependencies of each library easier.
+
+We're definitely in a slog here, but it's going to be worth it.
+
+
+## Day 8 — Loading, Initialization, etc
+
+The next big challenge to handle is how do we start loading the initial
+system components that are not available in libide-core.
+
+We can do some of that work in parallel, for example discovering the
+build system while we also discover the version control system.
+
+Some components will probably need to wait for another object to appear
+on the tree before they can do anything. For example, some things may not
+be practical until the IdeVcs has attached itself to the context.
+
+Furthermore, an IdeWorkspace window might want to load additional content
+once there is a known git vcs loaded. We probably need a way to watch for
+object attachments to simplify that.
+
+Possibly something like:
+
+```
+ide_object_wait_for_child_async (parent, IDE_TYPE_VCS, NULL, child_cb, foo);
+```
+
+Now that we have IdeContextAddin, we could allow any of the VCS systems to
+attach themselves to the VCS. For this to work, all addins would need to
+have an async vfunc pair to load the project.
+
+```
+  void (*load_project_async) (IdeContextAddin     *context,
+                              GFile               *file,
+                              GVariant            *hints,
+                              GCancellable        *cancellable,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data);
+```
+
+--
+
+I'm pretty happy with how things turned out, but I decided to use
+IdeWorkbenchAddin for everything instead of IdeContext, so that we can keep
+more objects out of libide-core. I want to keep that library small.
+
+I also added lots more vfuncs to WorkbenchAddin so that it could handle the
+project being open (when plugin is delayed on opening).
+
+Anyway, I got some basic workbench/workspace mechanics working, project loading
+and unloading, and plumbing to open files. Workbench addins can also be
+notified of workspace creation/etc.
+
+## Day 9 — Workspace tracking, surfaces
+
+Today I want to focus on ensuring we can handle "surfaces" well. Previously,
+that was called "Perspective" but that never really fit right to me. A surface
+is sort of like an area of the workspace that you can do something on. A
+worksurface seemed a bit long in the tooth.
+
+We also need to track the focus chain of lots of things (windows, frames, pages)
+so that we can "do the right thing" when actions occur for the user. For example,
+if the user focuses the search box from a secondary window, they probably still
+want the new page to display in the secondary window.
+
+-----
+
+I ended up doing a bunch more work on libide-code beyond basic surface stuff,
+so that we can start importing more code from master. Some notable changes
+
+ - IdeFixit -> IdeTextEdit will allow us to kill both IdeFixit and IdeProjectEdit
+   to some degree.
+ - Make all the diagnostic/range/location/fixit IdeObjects. I'm not sure how
+   we'll do object ownership though (for the object tree). I guess they can
+   be floating, but that doesn't help when tracking down memory issues.
+
+-----
+
+Another thing I'm looking at doing is making all our little libraries statically
+linked into the gnome-builder exec. That is much better given our "private ABI"
+that we currently have. We can still export our API symbols just fine.
+
+
+Question: What is the best way to keep libraries around after static linking
+without relying on peas? We can just do _foo_init() for libide base libs.
+
+## Day 10 — Builds Questions, Static Linking
+
+I want to focus today on figuring out the more pressing build questions that
+will give us the outcome we desire. My biggest desire with the new design is
+that we can get to a single executable, `gnome-builder`. All of the workspace
+forms (ide, editor, terminal) should still route through this executable.
+
+That means we need to use static linking for all of the libraries, and the
+GCC/LD option `--require-defined=_foo_register_types` for the plugins that
+will be discovered/registered by the libpeas engine. Also, since we need to
+export symbols to the plugins, we need to be -export-dynamic and ensure that
+we are only providing the symbols we care to export.
+
+Some symbols are used across the static linking boundary (like initing the
+themes static lib) but do not need to be exported. That works fine by just
+omitting our `_IDE_EXTERN` or `IDE_AVAILABLE_IN_*` macros. But when combined
+with GResources, we will lose our automatic constructor loading of the
+resources.
+
+What slightly complicates things is that we have peas `.plugin` files that
+need to show up automatically, or libpeas wont know to discover their types
+init function. So we need 2 functions in those libraries. One is the peas
+type registration (`_foo_register_types()`), and the other to ensure that
+the base resources are registered such as calling:
+
+```
+g_resources_register (foo_get_resource ());
+```
+
+Calling the `foo_get_resource()` should be enought to ensure the link, so
+if we do that we can avoid a secondary function and have the `get_resource`
+disappear after the link (non-exported symbol).
+
+
+-----
+
+A marathon of a day, but I managed to get libide-code mostly ported over
+and compiling. Some API had to change, but that was expected.
+
+I still need to figure out how I want the buffer-change-monitor to be
+created so libide-code doesn't need to depend on libide-vcs. It would
+be nice if we could use a peas extension for that, and let the plugins
+check the current VCS to see if they care.
+
+The big API changes include some stuff like:
+
+ - Using GObject for diagnostic(s), location, ranges
+ - getting rid of `ide_context_get_*()` pattern and inverting
+   it to `ide_*_from_context()`.
+
+I think the next big thing is to try to get an editor view visible.
+
+
diff --git a/build-aux/flatpak/org.gnome.Builder.json b/build-aux/flatpak/org.gnome.Builder.json
index 5fd39c736..2eee1eab1 100644
--- a/build-aux/flatpak/org.gnome.Builder.json
+++ b/build-aux/flatpak/org.gnome.Builder.json
@@ -9,7 +9,7 @@
     ],
     "desktop-file-name-prefix" : "(Nightly) ",
     "finish-args" : [
-        "--require-version=0.10.0",
+        "--require-version=1.0.0",
         "--allow=devel",
         "--talk-name=org.freedesktop.Flatpak",
         "--share=ipc",
@@ -518,12 +518,12 @@
                 "--buildtype=debugoptimized",
                 "-Dctags_path=/app/bin/ctags",
                 "-Dfusermount_wrapper=true",
-                "-Dwith_tcmalloc=true",
+                "-Dtcmalloc=true",
                 "-Dpython_libprefix=python3.7",
-                "-Denable_tracing=true",
-                "-Dwith_help=true",
+                "-Dtracing=true",
+                "-Dhelp=true",
                 "-Dwith_channel=flatpak-nightly",
-                "-Dwith_deviced=true"
+                "-Dplugin_deviced=true"
             ],
             "sources" : [
                 {
diff --git a/data/appdata/meson.build b/data/appdata/meson.build
new file mode 100644
index 000000000..8cb9668db
--- /dev/null
+++ b/data/appdata/meson.build
@@ -0,0 +1,17 @@
+# Appdata file.
+appdata_file = i18n.merge_file(
+  input: 'org.gnome.Builder.appdata.xml.in',
+  output: 'org.gnome.Builder.appdata.xml',
+  po_dir: '../po',
+  install: true,
+  install_dir: join_paths(get_option('datadir'), 'metainfo'),
+)
+
+appstream_util = find_program('appstream-util', required: false)
+if appstream_util.found()
+  validate_args = ['validate-relax', appdata_file]
+if not get_option('network_tests')
+  validate_args += '--nonet'
+endif
+  test('Validate appstream file', appstream_util, args: validate_args)
+endif
diff --git a/data/org.gnome.Builder.appdata.xml.in b/data/appdata/org.gnome.Builder.appdata.xml.in
similarity index 100%
rename from data/org.gnome.Builder.appdata.xml.in
rename to data/appdata/org.gnome.Builder.appdata.xml.in
diff --git a/data/meson.build b/data/meson.build
index 94e2bcb17..5afb6f81d 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -1,3 +1,8 @@
+subdir('appdata')
+subdir('fonts')
+subdir('gsettings')
+subdir('icons')
+subdir('style-schemes')
 
 # Desktop launcher and description file.
 desktop_file = i18n.merge_file(
@@ -16,24 +21,6 @@ if desktop_utils.found()
   )
 endif
 
-# Appdata file.
-appdata_file = i18n.merge_file(
-  input: 'org.gnome.Builder.appdata.xml.in',
-  output: 'org.gnome.Builder.appdata.xml',
-  po_dir: '../po',
-  install: true,
-  install_dir: join_paths(get_option('datadir'), 'metainfo'),
-)
-
-appstream_util = find_program('appstream-util', required: false)
-if appstream_util.found()
-  validate_args = ['validate-relax', appdata_file]
-if not get_option('network_tests')
-  validate_args += '--nonet'
-endif
-  test('Validate appstream file', appstream_util, args: validate_args)
-endif
-
 # D-Bus service file.
 dbusconf = configuration_data()
 dbusconf.set('bindir', join_paths(get_option('prefix'), get_option('bindir')))
@@ -44,8 +31,3 @@ configure_file(
        install: true,
        install_dir: join_paths(get_option('datadir'), 'dbus-1', 'services'),
 )
-
-subdir('fonts')
-subdir('gsettings')
-subdir('icons')
-subdir('style-schemes')
diff --git a/data/style-schemes/builder-dark-refresh.style-scheme.xml 
b/data/style-schemes/builder-dark-refresh.style-scheme.xml
new file mode 100644
index 000000000..afc11335a
--- /dev/null
+++ b/data/style-schemes/builder-dark-refresh.style-scheme.xml
@@ -0,0 +1,198 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ This file is part of GtkSourceView
+
+ Copyright (C) 2007 GtkSourceView team
+ Author: Paolo Borelli <pborelli gnome org>
+
+ GtkSourceView is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GtkSourceView 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+-->
+
+<style-scheme id="builder-dark" name="Builder Dark" version="1.0">
+
+  <author>Paolo Borelli, Christian Hergert</author>
+  <description>Dark color scheme for Builder using the Tango color palette</description>
+
+  <!-- Tango Palette -->
+  <color name="butter1"                     value="#fce94f"/>
+  <color name="butter2"                     value="#edd400"/>
+  <color name="butter3"                     value="#c4a000"/>
+  <color name="chameleon1"                  value="#8ae234"/>
+  <color name="chameleon2"                  value="#73d216"/>
+  <color name="chameleon3"                  value="#4e9a06"/>
+  <color name="orange1"                     value="#fcaf3e"/>
+  <color name="orange2"                     value="#f57900"/>
+  <color name="orange3"                     value="#ce5c00"/>
+  <color name="skyblue1"                    value="#729fcf"/>
+  <color name="skyblue2"                    value="#3465a4"/>
+  <color name="skyblue3"                    value="#204a87"/>
+  <color name="plum1"                       value="#ad7fa8"/>
+  <color name="plum2"                       value="#75507b"/>
+  <color name="plum3"                       value="#5c3566"/>
+  <color name="chocolate1"                  value="#e9b96e"/>
+  <color name="chocolate2"                  value="#c17d11"/>
+  <color name="chocolate3"                  value="#8f5902"/>
+  <color name="scarletred1"                 value="#ef2929"/>
+  <color name="scarletred2"                 value="#cc0000"/>
+  <color name="scarletred3"                 value="#a40000"/>
+  <color name="aluminium1"                  value="#eeeeec"/>
+  <color name="aluminium2"                  value="#d3d7cf"/>
+  <color name="aluminium3"                  value="#babdb6"/>
+  <color name="aluminium4"                  value="#888a85"/>
+  <color name="aluminium5"                  value="#555753"/>
+  <color name="aluminium6"                  value="#2e3436"/>
+  <color name="white"                       value="#ffffff"/>
+  <color name="pink1"                       value="#dd4a68"/>
+  <color name="red1"                        value="#ff0000"/>
+
+  <color name="dark1"                        value="#201f21"/>
+  <color name="dark2"                        value="#232224"/>
+  <color name="dark3"                        value="#535255"/>
+
+  <!-- Global Settings -->
+  <style name="text"                        foreground="aluminium3" background="dark1"/>
+  <style name="selection"                   foreground="aluminium1" background="aluminium4"/>
+  <style name="cursor"                      foreground="aluminium2"/>
+  <style name="current-line"                background="dark2"/>
+  <style name="current-line-number"         background="dark2"/>
+  <style name="line-numbers"                foreground="dark3" background="dark1"/>
+  <style name="draw-spaces"                 foreground="dark3"/>
+  <style name="background-pattern"          background="dark2"/>
+  <style name="map-overlay"                 background="#rgba(136,138,133,0.25)"/>
+
+  <!-- Diagnostics Underlining -->
+  <style name="diagnostician::deprecated"   underline="error" underline-color="aluminium3"/>
+  <style name="diagnostician::error"        underline="error" underline-color="red1"/>
+  <style name="diagnostician::note"         underline="error" underline-color="skyblue1"/>
+  <style name="diagnostician::warning"      underline="error" underline-color="orange1"/>
+
+  <!-- Snippets -->
+  <style name="snippet::tab-stop"           background="orange3" foreground="aluminium6"/>
+  <style name="snippet::area"               background="#rgba(86,114,151,.5)"/>
+
+  <!-- Debugger -->
+  <style name="debugger::current-breakpoint" foreground="#2e3436" background="#fcaf3e"/>
+  <style name="debugger::breakpoint"         foreground="#ffffff" background="#204a87"/>
+
+  <!-- Hover links -->
+  <style name="action::hover-definition"    background="#41464c" underline="true"/>
+
+  <!-- Bracket Matching -->
+  <style name="bracket-match"               foreground="chocolate2" bold="true"/>
+  <style name="bracket-mismatch"            foreground="aluminium1" background="scarletred2" bold="true"/>
+
+  <!-- Right Margin -->
+  <style name="right-margin"                foreground="#484749" background="#484749"/>
+
+  <!-- Search Matching -->
+  <style name="search-match"                foreground="aluminium1" background="chameleon3"/>
+  <style name="quick-highlight-match"       background="#rgba(78,154,6,.25)"/>
+
+  <!-- Search Shadow -->
+  <style name="search-shadow"               background="#rgba(0,0,0,0.4)"/>
+
+  <!-- Spellchecker Matching -->
+  <style name="misspelled-match"            foreground="#000000" background="#b3d4fc"/>
+
+  <!-- Comments -->
+  <style name="def:comment"                 foreground="aluminium4"/>
+  <style name="def:shebang"                 foreground="aluminium4" bold="true"/>
+  <style name="def:doc-comment-element"     italic="true"/>
+
+  <!-- Constants -->
+  <style name="def:constant"                foreground="butter2"/>
+  <style name="def:string"                  foreground="#0077aa"/>
+  <style name="def:special-char"            foreground="#dd4a68"/>
+  <style name="def:special-constant"        foreground="plum1"/>
+  <style name="def:floating-point"          foreground="orange3"/>
+  <style name="def:function"                foreground="#4186A8"/>
+
+  <!-- Identifiers -->
+  <style name="def:identifier"              foreground="skyblue1"/>
+
+  <!-- Statements -->
+  <style name="def:statement"               foreground="white" bold="true"/>
+
+  <!-- Types -->
+  <style name="def:type"                    foreground="chameleon1" bold="true"/>
+
+  <!-- Others -->
+  <style name="def:preprocessor"            foreground="#dd4a68"/>
+  <style name="def:error"                   foreground="aluminium1" background="scarletred2" bold="true"/>
+  <style name="def:warning"                 foreground="aluminium1" background="plum1"/>
+  <style name="def:note"                    background="butter1" foreground="aluminium4" bold="true"/>
+  <style name="def:underlined"              italic="true" underline="true"/>
+
+  <!-- Heading styles, uncomment to enable -->
+  <!--
+  <style name="def:heading0"                scale="5.0"/>
+  <style name="def:heading1"                scale="2.5"/>
+  <style name="def:heading2"                scale="2.0"/>
+  <style name="def:heading3"                scale="1.7"/>
+  <style name="def:heading4"                scale="1.5"/>
+  <style name="def:heading5"                scale="1.3"/>
+  <style name="def:heading6"                scale="1.2"/>
+  -->
+
+  <!-- Language specific -->
+  <style name="c:comment"                   foreground="#8b9eab"/>
+  <style name="c:preprocessor"              foreground="#8194a6" bold="false"/>
+  <style name="c:boolean"                   foreground="#0077aa"/>
+  <style name="c:keyword"                   foreground="#0077aa" bold="true"/>
+  <style name="c:string"                    foreground="#669900"/>
+  <style name="c:included-file"             foreground="orange3"/>
+  <style name="c:storage-class"             foreground="orange3" bold="true"/>
+  <style name="c:type"                      foreground="#669900" bold="true"/>
+  <style name="c:macro-name"                foreground="#677685" bold="false"/>
+  <style name="c:enum-name"                 foreground="#dd4a68" bold="false"/>
+
+  <style name="diff:added-line"             foreground="chameleon2"/>
+  <style name="diff:removed-line"           foreground="plum1"/>
+  <style name="diff:changed-line"           foreground="blue1"/>
+  <style name="diff:diff-file"              foreground="chameleon1" bold="true"/>
+  <style name="diff:location"               foreground="chameleon1"/>
+  <style name="diff:special-case"           foreground="white" bold="true"/>
+
+  <style name="gutter:added-line"           foreground="chameleon2"/>
+  <style name="gutter:changed-line"         foreground="butter3"/>
+  <style name="gutter:removed-line"         foreground="scarletred3"/>
+
+  <style name="js:object"                   foreground="chameleon3" bold="true"/>
+  <style name="js:constructors"             foreground="pink1"/>
+  <style name="js:keyword"                  foreground="#2b85aa"/>
+  <style name="js:string"                   foreground="#669900"/>
+  <style name="js:function"                 foreground="pink1"/>
+
+  <style name="latex:command"               foreground="chameleon1" bold="true"/>
+  <style name="latex:include"               use-style="def:preprocessor"/>
+
+  <style name="xml:comment"                 foreground="#8b9eab"/>
+  <style name="xml:attribute-name"          foreground="#orange3" bold="false"/>
+  <style name="xml:attribute-value"         foreground="#669900"/>
+  <style name="xml:tag-match"               background="#rgba(114,159,207,.20)"/>
+
+  <!-- Symbol-tree xml-pack coloring -->
+  <style name="symboltree::label"           foreground="#000000" background="#D5E7FC"/>
+  <style name="symboltree::id"              foreground="#000000" background="#D9E7BD"/>
+  <style name="symboltree::style-class"     foreground="#000000" background="#DFCD9B"/>
+  <style name="symboltree::type"            foreground="#000000" background="#F4DAC3"/>
+  <style name="symboltree::parent"          foreground="#000000" background="#DEBECF"/>
+  <style name="symboltree::class"           foreground="#000000" background="#FFEF98"/>
+  <style name="symboltree::attribute"       foreground="#000000" background="#F0E68C"/>
+
+</style-scheme>
+
diff --git a/data/style-schemes/builder-dark.style-scheme.xml 
b/data/style-schemes/builder-dark.style-scheme.xml
index afc11335a..4f3c6a63e 100644
--- a/data/style-schemes/builder-dark.style-scheme.xml
+++ b/data/style-schemes/builder-dark.style-scheme.xml
@@ -59,9 +59,9 @@
   <color name="pink1"                       value="#dd4a68"/>
   <color name="red1"                        value="#ff0000"/>
 
-  <color name="dark1"                        value="#201f21"/>
-  <color name="dark2"                        value="#232224"/>
-  <color name="dark3"                        value="#535255"/>
+  <color name="dark1"                        value="#1c1f20"/>
+  <color name="dark2"                        value="#212527"/>
+  <color name="dark3"                        value="#4d5558"/>
 
   <!-- Global Settings -->
   <style name="text"                        foreground="aluminium3" background="dark1"/>
@@ -96,7 +96,7 @@
   <style name="bracket-mismatch"            foreground="aluminium1" background="scarletred2" bold="true"/>
 
   <!-- Right Margin -->
-  <style name="right-margin"                foreground="#484749" background="#484749"/>
+  <style name="right-margin"                foreground="aluminium3" background="aluminium6"/>
 
   <!-- Search Matching -->
   <style name="search-match"                foreground="aluminium1" background="chameleon3"/>
diff --git a/doc/help/meson.build b/doc/help/meson.build
index e684f078c..9020fa8b5 100644
--- a/doc/help/meson.build
+++ b/doc/help/meson.build
@@ -1,4 +1,4 @@
-if get_option('with_help')
+if get_option('help')
 
 sphinx = find_program(['sphinx-build-3', 'sphinx-build'], required: true)
 
diff --git a/doc/meson.build b/doc/meson.build
index 9e18f7e89..f2a084048 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -1,6 +1,2 @@
-if get_option('with_help')
-  subdir('help')
-endif
-if get_option('with_docs')
-  subdir('sdk')
-endif
+subdir('help')
+subdir('sdk')
diff --git a/doc/sdk/libide-docs.sgml b/doc/sdk/libide-docs.sgml
index 5f693a624..21294963c 100644
--- a/doc/sdk/libide-docs.sgml
+++ b/doc/sdk/libide-docs.sgml
@@ -17,278 +17,276 @@
       </para>
     </releaseinfo>
     <copyright>
-      <year>2014-2017</year>
+      <year>2014-2019</year>
       <holder>Christian Hergert, et al.</holder>
     </copyright>
   </bookinfo>
 
   <part id="libide-api">
-    <title>Builder Core API</title>
+    <title>API Reference</title>
     <chapter>
-      <title>Overview</title>
-      <para>
-        Access to Builder's SDK is centered around a project context named
-        <link linkend="IdeContext">IdeContext</link>. You access various
-        subsystems such as the debugger or build manager via accessor methods
-        on the <link linkend="IdeContext">IdeContext</link>.
-      </para>
-      <para>
-        To simplify access to the project context, many internal objects share
-        a common base class named <link linkend="IdeObject">IdeObject</link>.
-        It provides direct access to the
-        <link linkend="IdeContext">IdeContext</link> via
-        <link linkend="ide-object-get-context">ide_object_get_context()</link>
-        as well as convenience API for lifecycle management.
-      </para>
-    </chapter>
-    <chapter>
-      <title>Core Objects</title>
-      <xi:include href="xml/ide-application.xml"/>
-      <xi:include href="xml/ide-context.xml"/>
-      <xi:include href="xml/ide-object.xml"/>
-      <xi:include href="xml/ide-service.xml"/>
-    </chapter>
-    <chapter>
-      <title>Application Extensions</title>
+      <title>Extending Builder</title>
       <xi:include href="xml/ide-application-addin.xml"/>
-      <xi:include href="xml/ide-application-tool.xml"/>
+      <xi:include href="xml/ide-buffer-addin.xml"/>
+      <xi:include href="xml/ide-build-pipeline-addin.xml"/>
+      <xi:include href="xml/ide-build-target-provider.xml"/>
+      <xi:include href="xml/ide-command-provider.xml"/>
+      <xi:include href="xml/ide-completion-provider.xml"/>
+      <xi:include href="xml/ide-config-view-addin.xml"/>
+      <xi:include href="xml/ide-configuration-provider.xml"/>
+      <xi:include href="xml/ide-context-addin.xml"/>
+      <xi:include href="xml/ide-device-provider.xml"/>
+      <xi:include href="xml/ide-diagnostic-provider.xml"/>
+      <xi:include href="xml/ide-editor-addin.xml"/>
+      <xi:include href="xml/ide-editor-page-addin.xml"/>
+      <xi:include href="xml/ide-extension-adapter.xml"/>
+      <xi:include href="xml/ide-extension-set-adapter.xml"/>
+      <xi:include href="xml/ide-extension-util-private.xml"/>
+      <xi:include href="xml/ide-frame-addin.xml"/>
+      <xi:include href="xml/ide-hover-provider.xml"/>
+      <xi:include href="xml/ide-omni-bar-addin.xml"/>
+      <xi:include href="xml/ide-preferences-addin.xml"/>
+      <xi:include href="xml/ide-rename-provider.xml"/>
+      <xi:include href="xml/ide-runner-addin.xml"/>
+      <xi:include href="xml/ide-runtime-provider.xml"/>
+      <xi:include href="xml/ide-search-provider.xml"/>
+      <xi:include href="xml/ide-session-addin.xml"/>
+      <xi:include href="xml/ide-symbol-resolver.xml"/>
+      <xi:include href="xml/ide-test-provider.xml"/>
+      <xi:include href="xml/ide-toolchain-provider.xml"/>
+      <xi:include href="xml/ide-tree-addin.xml"/>
+      <xi:include href="xml/ide-workbench-addin.xml"/>
+      <xi:include href="xml/ide-workspace-addin.xml"/>
     </chapter>
+
     <chapter>
-      <title>Logging and Tracing</title>
+      <title>Core</title>
+      <xi:include href="xml/ide-build-ident.xml"/>
+      <xi:include href="xml/ide-context.xml"/>
       <xi:include href="xml/ide-debug.xml"/>
+      <xi:include href="xml/ide-global.xml"/>
       <xi:include href="xml/ide-log.xml"/>
-    </chapter>
-    <chapter>
-      <title>Builder Versioning</title>
+      <xi:include href="xml/ide-macros.xml"/>
+      <xi:include href="xml/ide-notification.xml"/>
+      <xi:include href="xml/ide-notifications.xml"/>
+      <xi:include href="xml/ide-object-box.xml"/>
+      <xi:include href="xml/ide-object.xml"/>
+      <xi:include href="xml/ide-settings.xml"/>
+      <xi:include href="xml/ide-transfer.xml"/>
+      <xi:include href="xml/ide-transfer-manager.xml"/>
+      <xi:include href="xml/ide-version-macros.xml"/>
       <xi:include href="xml/ide-version.xml"/>
-      <xi:include href="xml/ide-build-ident.xml"/>
     </chapter>
-  </part>
 
-  <part id="libide-buffers">
-    <title>The Buffer Subsystem</title>
-    <chapter>
-      <title>Files and URIs</title>
-      <xi:include href="xml/ide-uri.xml"/>
-      <xi:include href="xml/ide-file.xml"/>
-      <xi:include href="xml/ide-file-settings.xml"/>
-    </chapter>
-    <chapter>
-      <title>Buffers</title>
-      <xi:include href="xml/ide-buffer-manager.xml"/>
-      <xi:include href="xml/ide-buffer.xml"/>
-      <xi:include href="xml/ide-buffer-addin.xml"/>
-      <xi:include href="xml/ide-buffer-change-monitor.xml"/>
-    </chapter>
     <chapter>
-      <title>Tracking Unsaved Files</title>
-      <xi:include href="xml/ide-unsaved-files.xml"/>
-      <xi:include href="xml/ide-unsaved-file.xml"/>
+      <title>IO</title>
+      <xi:include href="xml/ide-content-type.xml"/>
+      <xi:include href="xml/ide-gfile.xml"/>
+      <xi:include href="xml/ide-line-reader.xml"/>
+      <xi:include href="xml/ide-marked-content.xml"/>
+      <xi:include href="xml/ide-path.xml"/>
+      <xi:include href="xml/ide-persistent-map-builder.xml"/>
+      <xi:include href="xml/ide-persistent-map.xml"/>
+      <xi:include href="xml/ide-pkcon-transfer.xml"/>
+      <xi:include href="xml/ide-pty-intercept.xml"/>
     </chapter>
-  </part>
 
-  <part id="libide-editor">
-    <title>Source Code Editing</title>
-    <chapter>
-      <title>The Editor Perspective</title>
-      <xi:include href="xml/ide-editor-perspective.xml"/>
-      <xi:include href="xml/ide-editor-sidebar.xml"/>
-      <xi:include href="xml/ide-editor-utilities.xml"/>
-    </chapter>
     <chapter>
-      <title>The Editor View</title>
-      <xi:include href="xml/ide-editor-view.xml"/>
-      <xi:include href="xml/ide-editor-view-addin.xml"/>
-      <xi:include href="xml/ide-source-view.xml"/>
-      <xi:include href="xml/ide-source-map.xml"/>
-    </chapter>
-    <chapter>
-      <title>Search and Replace</title>
-      <xi:include href="xml/ide-editor-search.xml"/>
+      <title>Gui</title>
+      <xi:include href="xml/ide-application.xml"/>
+      <xi:include href="xml/ide-cell-renderer-fancy.xml"/>
+      <xi:include href="xml/ide-command.xml"/>
+      <xi:include href="xml/ide-environment-editor.xml"/>
+      <xi:include href="xml/ide-fancy-tree-view.xml"/>
+      <xi:include href="xml/ide-frame-header.xml"/>
+      <xi:include href="xml/ide-frame.xml"/>
+      <xi:include href="xml/ide-grid-column.xml"/>
+      <xi:include href="xml/ide-grid.xml"/>
+      <xi:include href="xml/ide-gui-global.xml"/>
+      <xi:include href="xml/ide-gutter.xml"/>
+      <xi:include href="xml/ide-header-bar.xml"/>
+      <xi:include href="xml/ide-line-change-gutter-renderer.xml"/>
+      <xi:include href="xml/ide-marked-view.xml"/>
+      <xi:include href="xml/ide-notifications-button.xml"/>
+      <xi:include href="xml/ide-omni-bar.xml"/>
+      <xi:include href="xml/ide-page.xml"/>
+      <xi:include href="xml/ide-pane.xml"/>
+      <xi:include href="xml/ide-panel.xml"/>
+      <xi:include href="xml/ide-preferences-surface.xml"/>
+      <xi:include href="xml/ide-preferences-window.xml"/>
+      <xi:include href="xml/ide-primary-workspace.xml"/>
+      <xi:include href="xml/ide-search-entry.xml"/>
+      <xi:include href="xml/ide-surface.xml"/>
+      <xi:include href="xml/ide-surfaces-button.xml"/>
+      <xi:include href="xml/ide-tagged-entry.xml"/>
+      <xi:include href="xml/ide-transfer-button.xml"/>
+      <xi:include href="xml/ide-transient-sidebar.xml"/>
+      <xi:include href="xml/ide-tree-model.xml"/>
+      <xi:include href="xml/ide-tree-node.xml"/>
+      <xi:include href="xml/ide-tree.xml"/>
+      <xi:include href="xml/ide-workbench.xml"/>
+      <xi:include href="xml/ide-worker.xml"/>
+      <xi:include href="xml/ide-workspace.xml"/>
     </chapter>
+
     <chapter>
-      <title>Auto-completion</title>
-      <xi:include href="xml/ide-completion.xml"/>
-      <xi:include href="xml/ide-completion-context.xml"/>
-      <xi:include href="xml/ide-completion-provider.xml"/>
-      <xi:include href="xml/ide-completion-list-box-row.xml"/>
+      <title>Greeter</title>
+      <xi:include href="xml/ide-clone-surface.xml"/>
+      <xi:include href="xml/ide-greeter-section.xml"/>
+      <xi:include href="xml/ide-greeter-workspace.xml"/>
     </chapter>
+
     <chapter>
-      <title>Semantic Highlighting</title>
+      <title>Code</title>
+      <xi:include href="xml/ide-buffer-change-monitor.xml"/>
+      <xi:include href="xml/ide-buffer-manager.xml"/>
+      <xi:include href="xml/ide-buffer.xml"/>
+      <xi:include href="xml/ide-code-enums.xml"/>
+      <xi:include href="xml/ide-code-index-entries.xml"/>
+      <xi:include href="xml/ide-code-index-entry.xml"/>
+      <xi:include href="xml/ide-code-indexer.xml"/>
+      <xi:include href="xml/ide-code-types.xml"/>
+      <xi:include href="xml/ide-diagnostic.xml"/>
+      <xi:include href="xml/ide-diagnostics-manager.xml"/>
+      <xi:include href="xml/ide-diagnostics.xml"/>
+      <xi:include href="xml/ide-file-settings.xml"/>
+      <xi:include href="xml/ide-formatter-options.xml"/>
+      <xi:include href="xml/ide-formatter.xml"/>
       <xi:include href="xml/ide-highlight-engine.xml"/>
-      <xi:include href="xml/ide-highlighter.xml"/>
       <xi:include href="xml/ide-highlight-index.xml"/>
-    </chapter>
-    <chapter>
-      <title>Auto-Indentation</title>
-      <xi:include href="xml/ide-indenter.xml"/>
+      <xi:include href="xml/ide-highlighter.xml"/>
       <xi:include href="xml/ide-indent-style.xml"/>
+      <xi:include href="xml/ide-indenter.xml"/>
+      <xi:include href="xml/ide-language.xml"/>
+      <xi:include href="xml/ide-location.xml"/>
+      <xi:include href="xml/ide-range.xml"/>
+      <xi:include href="xml/ide-spaces-style.xml"/>
+      <xi:include href="xml/ide-symbol-node.xml"/>
+      <xi:include href="xml/ide-symbol-tree.xml"/>
+      <xi:include href="xml/ide-symbol.xml"/>
+      <xi:include href="xml/ide-text-edit.xml"/>
+      <xi:include href="xml/ide-text-iter.xml"/>
+      <xi:include href="xml/ide-text-util.xml"/>
+      <xi:include href="xml/ide-unsaved-file.xml"/>
+      <xi:include href="xml/ide-unsaved-files.xml"/>
     </chapter>
+
     <chapter>
-      <title>Reformatting Code</title>
-      <xi:include href="xml/ide-formatter.xml"/>
-      <xi:include href="xml/ide-formatter-options.xml"/>
-    </chapter>
-    <chapter>
-      <title>Snippets</title>
-      <xi:include href="xml/ide-snippet.xml"/>
+      <title>Source View</title>
+      <xi:include href="xml/ide-completion-context.xml"/>
+      <xi:include href="xml/ide-completion-display.xml"/>
+      <xi:include href="xml/ide-completion-list-box-row.xml"/>
+      <xi:include href="xml/ide-completion-proposal.xml"/>
+      <xi:include href="xml/ide-completion-types.xml"/>
+      <xi:include href="xml/ide-completion.xml"/>
       <xi:include href="xml/ide-snippet-chunk.xml"/>
       <xi:include href="xml/ide-snippet-context.xml"/>
+      <xi:include href="xml/ide-snippet-parser.xml"/>
+      <xi:include href="xml/ide-snippet-private.xml"/>
       <xi:include href="xml/ide-snippet-storage.xml"/>
+      <xi:include href="xml/ide-snippet-types.xml"/>
+      <xi:include href="xml/ide-snippet.xml"/>
+      <xi:include href="xml/ide-source-search-context.xml"/>
+      <xi:include href="xml/ide-source-style-scheme.xml"/>
+      <xi:include href="xml/ide-source-view-enums.xml"/>
+      <xi:include href="xml/ide-source-view.xml"/>
+      <xi:include href="xml/ide-hover-context.xml"/>
     </chapter>
-  </part>
 
-  <part id="libide-building">
-    <title>The Build Subsystem</title>
     <chapter>
-      <title>Core Build API</title>
-      <xi:include href="xml/ide-build-manager.xml"/>
-      <xi:include href="xml/ide-build-system.xml"/>
-      <xi:include href="xml/ide-build-target.xml"/>
+      <title>Editor</title>
+      <xi:include href="xml/ide-editor-page.xml"/>
+      <xi:include href="xml/ide-editor-search.xml"/>
+      <xi:include href="xml/ide-editor-sidebar.xml"/>
+      <xi:include href="xml/ide-editor-surface.xml"/>
+      <xi:include href="xml/ide-editor-utilities.xml"/>
+      <xi:include href="xml/ide-editor-workspace.xml"/>
     </chapter>
+
     <chapter>
-      <title>The Build Pipeline</title>
-      <xi:include href="xml/ide-build-pipeline.xml"/>
-      <xi:include href="xml/ide-build-pipeline-addin.xml"/>
-      <xi:include href="xml/ide-build-stage.xml"/>
+      <title>Threading and Processes</title>
+      <xi:include href="xml/ide-environment-variable.xml"/>
+      <xi:include href="xml/ide-environment.xml"/>
+      <xi:include href="xml/ide-subprocess-launcher.xml"/>
+      <xi:include href="xml/ide-subprocess-supervisor.xml"/>
+      <xi:include href="xml/ide-subprocess.xml"/>
+      <xi:include href="xml/ide-task.xml"/>
+      <xi:include href="xml/ide-thread-pool.xml"/>
     </chapter>
+
     <chapter>
-      <title>Reusable Build Stages</title>
+      <title>Foundry</title>
+      <xi:include href="xml/ide-build-log.xml"/>
+      <xi:include href="xml/ide-build-manager.xml"/>
+      <xi:include href="xml/ide-build-pipeline.xml"/>
       <xi:include href="xml/ide-build-stage-launcher.xml"/>
       <xi:include href="xml/ide-build-stage-mkdirs.xml"/>
       <xi:include href="xml/ide-build-stage-transfer.xml"/>
-    </chapter>
-    <chapter>
-      <title>Build Configurations</title>
+      <xi:include href="xml/ide-build-stage.xml"/>
+      <xi:include href="xml/ide-build-system-discovery.xml"/>
+      <xi:include href="xml/ide-build-system.xml"/>
+      <xi:include href="xml/ide-build-target.xml"/>
+      <xi:include href="xml/ide-compile-commands.xml"/>
       <xi:include href="xml/ide-configuration-manager.xml"/>
-      <xi:include href="xml/ide-configuration-provider.xml"/>
       <xi:include href="xml/ide-configuration.xml"/>
-      <xi:include href="xml/ide-environment.xml"/>
-      <xi:include href="xml/ide-environment-variable.xml"/>
-    </chapter>
-    <chapter>
-      <title>Utility and Fallback API</title>
-      <xi:include href="xml/ide-compile-commands.xml"/>
-      <xi:include href="xml/ide-build-system-discovery.xml"/>
-      <xi:include href="xml/ide-directory-build-system.xml"/>
-    </chapter>
-  </part>
-
-  <part id="libide-diagnostics">
-    <title>The Diagnostics Subsystem</title>
-    <chapter>
-      <title>API Reference</title>
-      <xi:include href="xml/ide-source-location.xml"/>
-      <xi:include href="xml/ide-source-range.xml"/>
-      <xi:include href="xml/ide-diagnostic-provider.xml"/>
-      <xi:include href="xml/ide-diagnostics-manager.xml"/>
-      <xi:include href="xml/ide-diagnostics.xml"/>
-      <xi:include href="xml/ide-diagnostic.xml"/>
-      <xi:include href="xml/ide-fixit.xml"/>
-    </chapter>
-  </part>
-
-  <part id="libide-devices">
-    <title>The Device Subsystem</title>
-    <chapter>
-      <title>API Reference</title>
+      <xi:include href="xml/ide-dependency-updater.xml"/>
+      <xi:include href="xml/ide-deploy-strategy.xml"/>
+      <xi:include href="xml/ide-device-info.xml"/>
       <xi:include href="xml/ide-device-manager.xml"/>
-      <xi:include href="xml/ide-device-provider.xml"/>
       <xi:include href="xml/ide-device.xml"/>
+      <xi:include href="xml/ide-fallback-build-system.xml"/>
+      <xi:include href="xml/ide-foundry-compat.xml"/>
+      <xi:include href="xml/ide-foundry-enums.xml"/>
+      <xi:include href="xml/ide-foundry-types.xml"/>
       <xi:include href="xml/ide-local-device.xml"/>
+      <xi:include href="xml/ide-run-manager.xml"/>
+      <xi:include href="xml/ide-runner.xml"/>
+      <xi:include href="xml/ide-runtime-manager.xml"/>
+      <xi:include href="xml/ide-runtime.xml"/>
+      <xi:include href="xml/ide-simple-build-system-discovery.xml"/>
+      <xi:include href="xml/ide-simple-build-target.xml"/>
+      <xi:include href="xml/ide-simple-toolchain.xml"/>
+      <xi:include href="xml/ide-test-manager.xml"/>
+      <xi:include href="xml/ide-test-private.xml"/>
+      <xi:include href="xml/ide-test.xml"/>
+      <xi:include href="xml/ide-toolchain-manager.xml"/>
+      <xi:include href="xml/ide-toolchain.xml"/>
+      <xi:include href="xml/ide-triplet.xml"/>
     </chapter>
-  </part>
 
-  <part id="libide-search">
-    <title>Project Search</title>
     <chapter>
-      <title>Search Engine</title>
-      <xi:include href="xml/ide-search-engine.xml"/>
-      <xi:include href="xml/ide-search-provider.xml"/>
-      <xi:include href="xml/ide-search-result.xml"/>
+      <title>VCS</title>
+      <xi:include href="xml/ide-directory-vcs.xml"/>
+      <xi:include href="xml/ide-vcs-cloner.xml"/>
+      <xi:include href="xml/ide-vcs-config.xml"/>
+      <xi:include href="xml/ide-vcs-enums.xml"/>
+      <xi:include href="xml/ide-vcs-file-info.xml"/>
+      <xi:include href="xml/ide-vcs-initializer.xml"/>
+      <xi:include href="xml/ide-vcs-monitor.xml"/>
+      <xi:include href="xml/ide-vcs-uri.xml"/>
+      <xi:include href="xml/ide-vcs.xml"/>
     </chapter>
-    <chapter>
-      <title>Performance Considerations</title>
-      <xi:include href="xml/ide-search-reducer.xml"/>
-    </chapter>
-    <chapter>
-      <title>Source Code Indexing</title>
-      <xi:include href="xml/ide-code-index-entries.xml"/>
-      <xi:include href="xml/ide-code-index-entry.xml"/>
-      <xi:include href="xml/ide-code-indexer.xml"/>
-    </chapter>
-  </part>
-
-  <part id="libide-refactoring">
-    <title>Refactoring</title>
-    <xi:include href="xml/ide-rename-provider.xml"/>
-    <xi:include href="xml/ide-project-edit.xml"/>
-  </part>
 
-  <part id="libide-workbench">
-    <title>Workbench and View Layout</title>
-    <chapter>
-      <title>API Reference</title>
-      <xi:include href="xml/ide-workbench.xml"/>
-      <xi:include href="xml/ide-workbench-header-bar.xml"/>
-      <xi:include href="xml/ide-perspective.xml"/>
-      <xi:include href="xml/ide-omni-bar.xml"/>
-    </chapter>
     <chapter>
-      <title>Extending the Workbench</title>
-      <xi:include href="xml/ide-workbench-addin.xml"/>
-      <xi:include href="xml/ide-workbench-message.xml"/>
-    </chapter>
-    <chapter>
-      <title>Layout Management</title>
-      <xi:include href="xml/ide-layout-view.xml"/>
-      <xi:include href="xml/ide-layout-grid-column.xml"/>
-      <xi:include href="xml/ide-layout-grid.xml"/>
-      <xi:include href="xml/ide-layout-stack-addin.xml"/>
-      <xi:include href="xml/ide-layout-stack-header.xml"/>
-      <xi:include href="xml/ide-layout-stack.xml"/>
-      <xi:include href="xml/ide-layout.xml"/>
-      <xi:include href="xml/ide-layout-pane.xml"/>
-      <xi:include href="xml/ide-layout-transient-sidebar.xml"/>
-    </chapter>
-    <chapter>
-      <title>Keyboard Shortcuts</title>
-      <!--
-      <xi:include href="xml/ide-keybindings.xml"/>
-      -->
+      <title>Search</title>
+      <xi:include href="xml/ide-search-engine.xml"/>
+      <xi:include href="xml/ide-search-reducer.xml"/>
+      <xi:include href="xml/ide-search-result.xml"/>
     </chapter>
-  </part>
-
-  <part id="libide-vcs">
-    <title>The Version Control Subsystem</title>
-    <xi:include href="xml/ide-vcs.xml"/>
-    <xi:include href="xml/ide-vcs-uri.xml"/>
-    <xi:include href="xml/ide-vcs-config.xml"/>
-    <xi:include href="xml/ide-vcs-initializer.xml"/>
-    <xi:include href="xml/ide-directory-vcs.xml"/>
-  </part>
 
-  <part id="libide-runtimes">
-    <title>SDKs and Runtimes</title>
-    <xi:include href="xml/ide-runtime-manager.xml"/>
-    <xi:include href="xml/ide-runtime-provider.xml"/>
-    <xi:include href="xml/ide-runtime.xml"/>
-  </part>
-
-  <part id="libide-runner">
-    <title>Running Project Programs</title>
-    <xi:include href="xml/ide-run-manager.xml"/>
-    <xi:include href="xml/ide-runner.xml"/>
     <chapter>
-      <title>Extending Runners</title>
-      <xi:include href="xml/ide-runner-addin.xml"/>
+      <title>Terminal</title>
+      <xi:include href="xml/ide-terminal-page.xml"/>
+      <xi:include href="xml/ide-terminal-search.xml"/>
+      <xi:include href="xml/ide-terminal-surface.xml"/>
+      <xi:include href="xml/ide-terminal-util.xml"/>
+      <xi:include href="xml/ide-terminal-workspace.xml"/>
+      <xi:include href="xml/ide-terminal.xml"/>
     </chapter>
-  </part>
 
-  <part id="libide-debugger">
-    <title>The Debugger Subsystem</title>
     <chapter>
-      <title>API Reference</title>
+      <title>Debugging</title>
+      <xi:include href="xml/ide-debugger-address-map-private.xml"/>
       <xi:include href="xml/ide-debugger-breakpoints.xml"/>
       <xi:include href="xml/ide-debugger-breakpoint.xml"/>
       <xi:include href="xml/ide-debugger-frame.xml"/>
@@ -302,136 +300,27 @@
       <xi:include href="xml/ide-debugger.xml"/>
       <xi:include href="xml/ide-debug-manager.xml"/>
     </chapter>
-  </part>
-
-  <part id="libide-symbols">
-    <title>Symbol Extraction and Resolution</title>
-    <xi:include href="xml/ide-symbol-node.xml"/>
-    <xi:include href="xml/ide-symbol-resolver.xml"/>
-    <xi:include href="xml/ide-symbol-tree.xml"/>
-    <xi:include href="xml/ide-symbol.xml"/>
-    <xi:include href="xml/ide-tags-builder.xml"/>
-  </part>
-
-  <part id="libide-testing">
-    <title>Unit Testing</title>
-    <chapter>
-      <title>API Reference</title>
-      <xi:include href="xml/ide-test-manager.xml"/>
-      <xi:include href="xml/ide-test-provider.xml"/>
-      <xi:include href="xml/ide-test.xml"/>
-    </chapter>
-  </part>
 
-  <part id="libide-projects">
-    <title>Project Management and Templates</title>
-    <chapter>
-      <title>API Reference</title>
-      <xi:include href="xml/ide-project-info.xml"/>
-      <xi:include href="xml/ide-project-item.xml"/>
-      <xi:include href="xml/ide-project.xml"/>
-    </chapter>
     <chapter>
-      <title>Extending Project Creation Workflow</title>
-      <xi:include href="xml/ide-genesis-addin.xml"/>
-      <xi:include href="xml/ide-recent-projects.xml"/>
+      <title>Language Servers</title>
+      <xi:include href="xml/ide-lsp-client.xml"/>
+      <xi:include href="xml/ide-lsp-completion-item.xml"/>
+      <xi:include href="xml/ide-lsp-completion-provider.xml"/>
+      <xi:include href="xml/ide-lsp-completion-results.xml"/>
+      <xi:include href="xml/ide-lsp-diagnostic-provider.xml"/>
+      <xi:include href="xml/ide-lsp-formatter.xml"/>
+      <xi:include href="xml/ide-lsp-highlighter.xml"/>
+      <xi:include href="xml/ide-lsp-hover-provider.xml"/>
+      <xi:include href="xml/ide-lsp-rename-provider.xml"/>
+      <xi:include href="xml/ide-lsp-symbol-node-private.xml"/>
+      <xi:include href="xml/ide-lsp-symbol-node.xml"/>
+      <xi:include href="xml/ide-lsp-symbol-resolver.xml"/>
+      <xi:include href="xml/ide-lsp-symbol-tree-private.xml"/>
+      <xi:include href="xml/ide-lsp-symbol-tree.xml"/>
+      <xi:include href="xml/ide-lsp-types.xml"/>
+      <xi:include href="xml/ide-lsp-util.xml"/>
     </chapter>
-    <chapter>
-      <title>Templates</title>
-      <xi:include href="xml/ide-project-template.xml"/>
-      <xi:include href="xml/ide-template-provider.xml"/>
-      <xi:include href="xml/ide-template-base.xml"/>
-    </chapter>
-  </part>
 
-  <part id="libide-preferences">
-    <title>Application and Plugin Preferences</title>
-    <chapter>
-      <title>API Reference</title>
-      <xi:include href="xml/ide-preferences-addin.xml"/>
-    </chapter>
-  </part>
-
-  <part id="libide-threading">
-    <title>Processes, Threading, and Tasks</title>
-    <chapter>
-      <title>Threading</title>
-      <xi:include href="xml/ide-thread-pool.xml"/>
-    </chapter>
-    <chapter>
-      <title>Worker Processes</title>
-      <xi:include href="xml/ide-worker.xml"/>
-    </chapter>
-    <chapter>
-      <title>Subprocesses</title>
-      <xi:include href="xml/ide-subprocess-launcher.xml"/>
-      <xi:include href="xml/ide-subprocess.xml"/>
-      <xi:include href="xml/ide-subprocess-supervisor.xml"/>
-    </chapter>
-    <chapter>
-      <title>Pausable Tasks</title>
-      <xi:include href="xml/ide-pausable.xml"/>
-    </chapter>
-  </part>
-
-  <part id="libide-langserv">
-    <title>Language Server Protocol</title>
-    <chapter>
-      <title>API Reference</title>
-      <xi:include href="xml/ide-langserv-client.xml"/>
-      <xi:include href="xml/ide-langserv-completion-provider.xml"/>
-      <xi:include href="xml/ide-langserv-diagnostic-provider.xml"/>
-      <xi:include href="xml/ide-langserv-formatter.xml"/>
-      <xi:include href="xml/ide-langserv-highlighter.xml"/>
-      <xi:include href="xml/ide-langserv-rename-provider.xml"/>
-      <xi:include href="xml/ide-langserv-symbol-node.xml"/>
-      <xi:include href="xml/ide-langserv-symbol-resolver.xml"/>
-      <xi:include href="xml/ide-langserv-symbol-tree.xml"/>
-    </chapter>
-  </part>
-
-  <part id="libide-transfers">
-    <title>Downloads and Transfers</title>
-    <chapter>
-      <title>API Reference</title>
-      <xi:include href="xml/ide-transfer.xml"/>
-      <xi:include href="xml/ide-transfer-manager.xml"/>
-      <xi:include href="xml/ide-pkcon-transfer.xml"/>
-    </chapter>
-    <chapter>
-      <title>Widgets</title>
-      <xi:include href="xml/ide-transfer-button.xml"/>
-      <xi:include href="xml/ide-transfers-button.xml"/>
-    </chapter>
-  </part>
-
-  <part id="libide-misc">
-    <title>Miscellaneous and Utility API</title>
-    <chapter>
-      <title>API Reference</title>
-      <xi:include href="xml/ide-doap-person.xml"/>
-      <xi:include href="xml/ide-doap.xml"/>
-      <xi:include href="xml/ide-dnd.xml"/>
-      <xi:include href="xml/ide-flatpak.xml"/>
-      <xi:include href="xml/ide-glib.xml"/>
-      <xi:include href="xml/ide-gtk.xml"/>
-      <xi:include href="xml/ide-line-reader.xml"/>
-      <xi:include href="xml/ide-posix.xml"/>
-      <xi:include href="xml/ide-enums.xml"/>
-      <xi:include href="xml/ide-progress.xml"/>
-      <xi:include href="xml/ide-ref-ptr.xml"/>
-      <xi:include href="xml/ide-settings.xml"/>
-    </chapter>
-    <chapter>
-      <title>Internal Extension Management</title>
-      <xi:include href="xml/ide-extension-adapter.xml"/>
-      <xi:include href="xml/ide-extension-set-adapter.xml"/>
-    </chapter>
-    <chapter>
-      <title>Widgets</title>
-      <xi:include href="xml/ide-cell-renderer-fancy.xml"/>
-      <xi:include href="xml/ide-fancy-tree-view.xml"/>
-    </chapter>
   </part>
 
   <chapter id="object-tree">
diff --git a/doc/sdk/meson.build b/doc/sdk/meson.build
index 1f3327679..4bb2421b3 100644
--- a/doc/sdk/meson.build
+++ b/doc/sdk/meson.build
@@ -1,7 +1,9 @@
+if get_option('docs')
+
 subdir('xml')
 
 private_headers = ['config.h']
-foreach source : libide_private_sources
+foreach source : gnome_builder_private_sources + gnome_builder_private_headers
   private_headers += ['@0@/@1@'.format(meson.source_root(), source)]
 endforeach
 
@@ -26,20 +28,77 @@ vte_docpath = join_paths(vte_prefix, 'share', 'vte-doc', 'html')
 # Locate our directory for documentation
 docpath = join_paths(get_option('datadir'), 'gtk-doc', 'html')
 
+libide_gtk_doc = shared_library('ide-gtk-doc',
+            c_args: libide_args + release_args,
+      dependencies: gnome_builder_deps,
+)
+
+libide_gtk_doc_dep = declare_dependency(
+         dependencies: gnome_builder_deps,
+            link_with: libide_gtk_doc,
+)
+
 gnome.gtkdoc('libide',
            main_xml: 'libide-docs.sgml',
             src_dir: [
-              join_paths(meson.source_root(), 'src', 'libide'),
-              join_paths(meson.build_root(), 'src', 'libide'),
+              join_paths(meson.source_root(), 'src', 'libide', 'core'),
+              join_paths(meson.build_root(), 'src', 'libide', 'core'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'io'),
+              join_paths(meson.build_root(), 'src', 'libide', 'io'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'threading'),
+              join_paths(meson.build_root(), 'src', 'libide', 'threading'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'code'),
+              join_paths(meson.build_root(), 'src', 'libide', 'code'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'foundry'),
+              join_paths(meson.build_root(), 'src', 'libide', 'foundry'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'sourceview'),
+              join_paths(meson.build_root(), 'src', 'libide', 'sourceview'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'editor'),
+              join_paths(meson.build_root(), 'src', 'libide', 'editor'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'vcs'),
+              join_paths(meson.build_root(), 'src', 'libide', 'vcs'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'debugger'),
+              join_paths(meson.build_root(), 'src', 'libide', 'debugger'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'greeter'),
+              join_paths(meson.build_root(), 'src', 'libide', 'greeter'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'gui'),
+              join_paths(meson.build_root(), 'src', 'libide', 'gui'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'lsp'),
+              join_paths(meson.build_root(), 'src', 'libide', 'lsp'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'plugins'),
+              join_paths(meson.build_root(), 'src', 'libide', 'plugins'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'search'),
+              join_paths(meson.build_root(), 'src', 'libide', 'search'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'terminal'),
+              join_paths(meson.build_root(), 'src', 'libide', 'terminal'),
+
+              join_paths(meson.source_root(), 'src', 'libide', 'tree'),
+              join_paths(meson.build_root(), 'src', 'libide', 'tree'),
             ],
 
-       dependencies: libide_dep,
+       dependencies: [ libide_gtk_doc_dep ],
   gobject_typesfile: 'libide.types',
           scan_args: [
             '--rebuild-types',
             '--ignore-decorators=_IDE_EXTERN',
           ],
      ignore_headers: private_headers,
+      content_files: gnome_builder_public_sources + gnome_builder_public_headers,
+             c_args: libide_args,
 
        fixxref_args: [
          '--html-dir=@0@'.format(docpath),
@@ -53,3 +112,5 @@ gnome.gtkdoc('libide',
        ],
             install: true)
 
+
+endif
diff --git a/meson.build b/meson.build
index 2efc0b53c..bf94bee76 100644
--- a/meson.build
+++ b/meson.build
@@ -1,7 +1,7 @@
 project('gnome-builder', 'c',
           license: 'GPL3+',
           version: '3.31.1',
-    meson_version: '>= 0.47.2',
+    meson_version: '>= 0.48.0',
   default_options: [ 'c_std=gnu11',
                      'cpp_std=c++11',
                      'warning_level=2',
@@ -14,7 +14,7 @@ MAJOR_VERSION = version_split[0]
 MINOR_VERSION = version_split[1]
 MICRO_VERSION = version_split[2]
 
-libide_api_version = '1.0'
+libide_api_version = '@0@.@1@'.format(MAJOR_VERSION, MINOR_VERSION)
 
 pkgdocdir_abs = join_paths(get_option('prefix'), get_option('datadir'), 'doc', 'gnome-builder')
 pkglibdir_abs = join_paths(get_option('prefix'), get_option('libdir'), 'gnome-builder')
@@ -66,18 +66,20 @@ status += [
   'Libdir ................ : @0@'.format(join_paths(get_option('prefix'), get_option('libdir'))),
   'Safe PATH ............. : @0@'.format(safe_path),
   '',
-  'Tracing ............... : @0@'.format(get_option('enable_tracing')),
-  'Profiling ............. : @0@'.format(get_option('enable_profiling')),
+  'Tracing ............... : @0@'.format(get_option('tracing')),
+  'Profiling ............. : @0@'.format(get_option('profiling')),
   'fusermount ............ : @0@'.format(get_option('fusermount_wrapper')),
-  'tcmalloc_minimal ...... : @0@'.format(get_option('with_tcmalloc')),
+  'tcmalloc_minimal ...... : @0@'.format(get_option('tcmalloc')),
   '',
-  'Help Docs ............. : @0@'.format(get_option('with_help')),
-  'API Docs .............. : @0@'.format(get_option('with_docs')),
+  'Help Docs ............. : @0@'.format(get_option('help')),
+  'API Docs .............. : @0@'.format(get_option('docs')),
   '', ''
 ]
 
 config_h = configuration_data()
 config_h.set_quoted('PACKAGE_NAME', 'gnome-builder')
+config_h.set_quoted('PACKAGE_ABI_S', libide_api_version)
+config_h.set('PACKAGE_ABI', libide_api_version)
 config_h.set_quoted('PACKAGE_VERSION', meson.project_version())
 config_h.set_quoted('PACKAGE_STRING', 'gnome-builder-' + meson.project_version())
 config_h.set_quoted('PACKAGE_DATADIR', join_paths(get_option('prefix'), get_option('datadir')))
@@ -100,8 +102,6 @@ config_h.set('DEVELOPMENT_BUILD', version_split[1].to_int().is_odd())
 config_h.set_quoted('SRCDIR', meson.source_root())
 config_h.set_quoted('BUILDDIR', meson.build_root())
 
-config_h.set10('ENABLE_WEBKIT', get_option('with_webkit'))
-
 add_global_arguments([
   '-DHAVE_CONFIG_H',
   '-I' + meson.build_root(), # config.h
@@ -170,7 +170,7 @@ test_c_args = [
 if get_option('buildtype') != 'plain'
   test_c_args += '-fstack-protector-strong'
 endif
-if get_option('enable_profiling')
+if get_option('profiling')
   test_c_args += '-pg'
 endif
 
@@ -201,6 +201,8 @@ if get_option('default_library') != 'static'
   endif
 endif
 
+libide_args += hidden_visibility_args
+
 add_project_arguments(global_c_args, language: 'c')
 
 release_args = []
@@ -230,7 +232,7 @@ foreach link_arg: test_link_args
 endforeach
 add_project_link_arguments(global_link_args, language: 'c')
 
-if get_option('with_tcmalloc')
+if get_option('tcmalloc')
   tcmalloc_ldflags = [
     '-fno-builtin-malloc',
     '-fno-builtin-calloc',
@@ -262,40 +264,39 @@ libpangoft2_dep = dependency('pangoft2', version: '>= 1.38.0')
 libpeas_dep = dependency('libpeas-1.0', version: '>= 1.22.0')
 libtemplate_glib_dep = dependency('template-glib-1.0', version: '>= 3.28.0')
 libvte_dep = dependency('vte-2.91', version: '>= 0.40.2')
+libwebkit_dep = dependency('webkit2gtk-4.0', version: '>= 2.22')
 libxml2_dep = dependency('libxml-2.0', version: '>= 2.9.0')
+libgit_dep = dependency('libgit2-glib-1.0', version: '>= 0.25.0')
+
+# Make sure libgit2/libgit2-glib were compiled with proper flags
+libgit_thread_safe_check = '''
+#include <libgit2-glib/ggit.h>
+int main(int argc, const char *argv[])
+{
+ggit_init ();
+return ((ggit_get_features() & GGIT_FEATURE_THREADS) != 0) ? 0 : 1;
+}
+'''
+res = cc.run(libgit_thread_safe_check,
+dependencies: libgit_dep,
+)
+if res.returncode() != 0
+error('libgit2 was not compiled with -DTHREADSAFE:BOOL=ON')
+endif
 
-if get_option('with_flatpak') or get_option('with_git')
-  libgit_dep = dependency('libgit2-glib-1.0', version: '>= 0.25.0')
-
-  libgit_thread_safe_check = '''
-    #include <libgit2-glib/ggit.h>
-    int main(int argc, const char *argv[])
-    {
-      ggit_init ();
-      return ((ggit_get_features() & GGIT_FEATURE_THREADS) != 0) ? 0 : 1;
-    }
-  '''
-  res = cc.run(libgit_thread_safe_check,
-    dependencies: libgit_dep,
-  )
-  if res.returncode() != 0
-    error('libgit2 was not compiled with -DTHREADSAFE:BOOL=ON')
-  endif
-
-  libgit_ssh_check = '''
-    #include <libgit2-glib/ggit.h>
-    int main(int argc, const char *argv[])
-    {
-      ggit_init ();
-      return ((ggit_get_features() & GGIT_FEATURE_SSH) != 0) ? 0 : 1;
-    }
-  '''
-  res = cc.run(libgit_ssh_check,
-    dependencies: libgit_dep,
-  )
-  if res.returncode() != 0
-    error('libgit2 was not compiled with SSH support')
-  endif
+libgit_ssh_check = '''
+#include <libgit2-glib/ggit.h>
+int main(int argc, const char *argv[])
+{
+ggit_init ();
+return ((ggit_get_features() & GGIT_FEATURE_SSH) != 0) ? 0 : 1;
+}
+'''
+res = cc.run(libgit_ssh_check,
+dependencies: libgit_dep,
+)
+if res.returncode() != 0
+error('libgit2 was not compiled with SSH support')
 endif
 
 check_functions = [
@@ -314,6 +315,7 @@ configure_file(output: 'config.h', configuration: config_h)
 
 gnome = import('gnome')
 i18n = import('i18n')
+pkgconfig = import('pkgconfig')
 
 subdir('data')
 subdir('src')
diff --git a/meson_options.txt b/meson_options.txt
index 68fe4227c..128f13246 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,20 +1,18 @@
-option('enable_tracing', type: 'boolean', value: false, description: 'Enable tracing of internals for 
troubleshooting Builder')
-option('enable_profiling', type: 'boolean', value: false, description: 'Enable profiling of the Builder 
codebase')
+option('tracing', type: 'boolean', value: false, description: 'Enable tracing of internals for 
troubleshooting Builder')
+option('profiling', type: 'boolean', value: false, description: 'Enable profiling of the Builder codebase')
 option('fusermount_wrapper', type: 'boolean', value: false, description: 'Install fusermount-wrapper when 
distributing with flatpak')
-option('with_tcmalloc', type: 'boolean', value: false, description: 'Use tcmalloc for dynamic allocations')
+option('tcmalloc', type: 'boolean', value: false, description: 'Use tcmalloc for dynamic allocations')
 
 option('with_safe_path', type: 'string', value: '', description: 'PATH variable to run build commands 
(default: platform-specific)')
+
 option('with_channel',
           type: 'combo',
        choices: [ 'other', 'flatpak-stable', 'flatpak-nightly' ],
    description: 'The distribution channel for Builder',
 )
 
-option('with_editorconfig', type: 'boolean')
-option('with_webkit', type: 'boolean')
-option('with_vapi', type: 'boolean')
-option('with_help', type: 'boolean', value: false)
-option('with_docs', type: 'boolean', value: false)
+option('help', type: 'boolean', value: false)
+option('docs', type: 'boolean', value: false)
 
 option('network_tests', type: 'boolean', value: true, description: 'Allow networking in unit-tests')
 
@@ -22,68 +20,54 @@ option('ctags_path', type: 'string', value: '')
 
 option('python_libprefix', type: 'string')
 
-# Plugins
-# Ideally we want many of these to be defined in the plugin dir:
-#   https://github.com/mesonbuild/meson/issues/707
-option('with_autotools', type: 'boolean')
-option('with_beautifier', type: 'boolean')
-option('with_c_pack', type: 'boolean')
-option('with_cargo', type: 'boolean')
-option('with_clang', type: 'boolean')
-option('with_cmake', type: 'boolean')
-option('with_color_picker', type: 'boolean')
-option('with_code_index', type: 'boolean')
-option('with_command_bar', type: 'boolean')
-option('with_comment_code', type: 'boolean')
-option('with_create_project', type: 'boolean')
-option('with_ctags', type: 'boolean')
-option('with_devhelp', type: 'boolean')
-option('with_deviced', type: 'boolean', value: false)
-option('with_eslint', type: 'boolean')
-option('with_file_search', type: 'boolean')
-option('with_find_other_file', type: 'boolean')
-option('with_flatpak', type: 'boolean')
-option('with_gradle', type: 'boolean')
-option('with_gcc', type: 'boolean')
-option('with_gdb', type: 'boolean')
-option('with_gettext', type: 'boolean')
-option('with_git', type: 'boolean')
-option('with_gjs_symbols', type: 'boolean')
-option('with_glade', type: 'boolean')
-option('with_gnome_code_assistance', type: 'boolean')
-option('with_go_langserv', type: 'boolean')
-option('with_grep', type: 'boolean')
-option('with_history', type: 'boolean')
-option('with_html_completion', type: 'boolean')
-option('with_html_preview', type: 'boolean')
-option('with_jedi', type: 'boolean')
-option('with_jhbuild', type: 'boolean')
-option('with_ls', type: 'boolean')
-option('with_make', type: 'boolean')
-option('with_maven', type: 'boolean')
-option('with_meson', type: 'boolean')
-option('with_meson_templates', type: 'boolean')
-option('with_mono', type: 'boolean')
-option('with_notification', type: 'boolean')
-option('with_newcomers', type: 'boolean')
-option('with_npm', type: 'boolean')
-option('with_phpize', type: 'boolean')
-option('with_project_tree', type: 'boolean')
-option('with_python_gi_imports_completion', type: 'boolean')
-option('with_python_pack', type: 'boolean')
-option('with_qemu', type: 'boolean')
-option('with_quick_highlight', type: 'boolean')
-option('with_retab', type: 'boolean')
-option('with_rust_langserv', type: 'boolean')
-option('with_rustup', type: 'boolean')
-option('with_spellcheck', type: 'boolean')
-option('with_snippets', type: 'boolean')
-option('with_support', type: 'boolean')
-option('with_symbol_tree', type: 'boolean')
-option('with_sysprof', type: 'boolean')
-option('with_sysroot', type: 'boolean')
-option('with_todo', type: 'boolean')
-option('with_vala_pack', type: 'boolean')
-option('with_valgrind', type: 'boolean')
-option('with_words', type: 'boolean')
-option('with_xml_pack', type: 'boolean')
+option('plugin_autotools', type: 'boolean')
+option('plugin_beautifier', type: 'boolean')
+option('plugin_c_pack', type: 'boolean')
+option('plugin_cargo', type: 'boolean')
+option('plugin_clang', type: 'boolean')
+option('plugin_cmake', type: 'boolean')
+option('plugin_code_index', type: 'boolean')
+option('plugin_color_picker', type: 'boolean')
+option('plugin_ctags', type: 'boolean')
+option('plugin_devhelp', type: 'boolean')
+option('plugin_deviced', type: 'boolean', value: false)
+option('plugin_editorconfig', type: 'boolean')
+option('plugin_eslint', type: 'boolean')
+option('plugin_file_search', type: 'boolean')
+option('plugin_flatpak', type: 'boolean')
+option('plugin_gdb', type: 'boolean')
+option('plugin_gettext', type: 'boolean')
+option('plugin_git', type: 'boolean')
+option('plugin_gjs_symbols', type: 'boolean')
+option('plugin_glade', type: 'boolean')
+option('plugin_gnome_code_assistance', type: 'boolean')
+option('plugin_go_langserv', type: 'boolean')
+option('plugin_gradle', type: 'boolean')
+option('plugin_grep', type: 'boolean')
+option('plugin_html_completion', type: 'boolean')
+option('plugin_html_preview', type: 'boolean')
+option('plugin_jedi', type: 'boolean')
+option('plugin_jhbuild', type: 'boolean')
+option('plugin_make', type: 'boolean')
+option('plugin_maven', type: 'boolean')
+option('plugin_meson', type: 'boolean')
+option('plugin_modelines', type: 'boolean')
+option('plugin_mono', type: 'boolean')
+option('plugin_newcomers', type: 'boolean')
+option('plugin_notification', type: 'boolean')
+option('plugin_npm', type: 'boolean')
+option('plugin_phpize', type: 'boolean')
+option('plugin_python_pack', type: 'boolean')
+option('plugin_qemu', type: 'boolean')
+option('plugin_quick_highlight', type: 'boolean')
+option('plugin_retab', type: 'boolean')
+option('plugin_rls', type: 'boolean')
+option('plugin_rustup', type: 'boolean')
+option('plugin_spellcheck', type: 'boolean')
+option('plugin_sysprof', type: 'boolean')
+option('plugin_sysroot', type: 'boolean')
+option('plugin_todo', type: 'boolean')
+option('plugin_vala', type: 'boolean')
+option('plugin_valgrind', type: 'boolean')
+option('plugin_words', type: 'boolean')
+option('plugin_xml_pack', type: 'boolean')
diff --git a/po/POTFILES.in b/po/POTFILES.in
index e95b49d10..45a8c789f 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -1,5 +1,6 @@
 # List of source files containing translatable strings.
 # Please keep this file sorted alphabetically.
+data/appdata/org.gnome.Builder.appdata.xml.in
 data/gsettings/org.gnome.builder.build.gschema.xml
 data/gsettings/org.gnome.builder.code-insight.gschema.xml.in
 data/gsettings/org.gnome.builder.editor.gschema.xml
@@ -7,194 +8,249 @@ data/gsettings/org.gnome.builder.editor.language.gschema.xml
 data/gsettings/org.gnome.builder.extension-type.gschema.xml
 data/gsettings/org.gnome.builder.gschema.xml
 data/gsettings/org.gnome.builder.plugin.gschema.xml
-data/gsettings/org.gnome.builder.project.gschema.xml
 data/gsettings/org.gnome.builder.project-tree.gschema.xml
+data/gsettings/org.gnome.builder.project.gschema.xml
 data/gsettings/org.gnome.builder.terminal.gschema.xml
 data/gsettings/org.gnome.builder.workbench.gschema.xml
-data/org.gnome.Builder.appdata.xml.in
 data/org.gnome.Builder.desktop.in.in
 data/style-schemes/builder-dark.style-scheme.xml
 data/style-schemes/builder.style-scheme.xml
-src/gstyle/data/palettes/basic.gstyle.xml
 src/gstyle/gstyle-color-panel.c
 src/gstyle/gstyle-color-plane.c
+src/gstyle/gstyle-color-scale.c
 src/gstyle/gstyle-color-widget-actions.c
-src/gstyle/gstyle-palette.c
+src/gstyle/gstyle-color-widget.c
+src/gstyle/gstyle-color.c
 src/gstyle/gstyle-palette-widget.c
+src/gstyle/gstyle-palette.c
+src/gstyle/gstyle-slidein.c
+src/gstyle/tests/data/gstyle-color-editor.ui
 src/gstyle/ui/gstyle-color-panel.ui
 src/gstyle/ui/gstyle-color-widget.ui
 src/gstyle/ui/gstyle-rename-popover.ui
-src/libide/application/ide-application-actions.c
-src/libide/application/ide-application.c
-src/libide/application/ide-application-command-line.c
-src/libide/application/ide-application-shortcuts.c
-src/libide/buffers/ide-buffer.c
-src/libide/buffers/ide-buffer-manager.c
-src/libide/buffers/ide-unsaved-files.c
-src/libide/buildconfig/ide-buildconfig-configuration-provider.c
-src/libide/buildsystem/ide-build-manager.c
-src/libide/buildsystem/ide-build-pipeline.c
-src/libide/buildsystem/ide-build-stage-transfer.c
-src/libide/buildui/ide-build-configuration-row.ui
-src/libide/buildui/ide-build-configuration-view.ui
-src/libide/buildui/ide-build-log-panel.c
-src/libide/buildui/ide-build-log-panel.ui
-src/libide/buildui/ide-build-panel.c
-src/libide/buildui/ide-build-panel.ui
-src/libide/buildui/ide-build-perspective.c
-src/libide/buildui/ide-build-workbench-addin.c
-src/libide/buildui/ide-environment-editor.c
-src/libide/buildui/ide-environment-editor-row.ui
-src/libide/config/ide-configuration-manager.c
-src/libide/debugger/gtk/menus.ui
-src/libide/debugger/ide-debugger-breakpoints-view.ui
-src/libide/debugger/ide-debugger-controls.ui
-src/libide/debugger/ide-debugger-disassembly-view.ui
-src/libide/debugger/ide-debugger-editor-addin.c
-src/libide/debugger/ide-debugger-hover-controls.ui
-src/libide/debugger/ide-debugger-hover-provider.c
-src/libide/debugger/ide-debugger-libraries-view.ui
-src/libide/debugger/ide-debugger-locals-view.c
-src/libide/debugger/ide-debugger-locals-view.ui
-src/libide/debugger/ide-debugger-registers-view.ui
-src/libide/debugger/ide-debugger-threads-view.c
-src/libide/debugger/ide-debugger-threads-view.ui
+src/libide/code/ide-buffer-change-monitor.c
+src/libide/code/ide-buffer-manager.c
+src/libide/code/ide-buffer.c
+src/libide/code/ide-file-settings.c
+src/libide/code/ide-gsettings-file-settings.c
+src/libide/code/ide-highlight-engine.c
+src/libide/code/ide-highlighter.c
+src/libide/code/ide-language-defaults.c
+src/libide/code/ide-symbol-node.c
+src/libide/code/ide-unsaved-files.c
+src/libide/core/ide-context.c
+src/libide/core/ide-global.c
+src/libide/core/ide-settings.c
 src/libide/debugger/ide-debug-manager.c
-src/libide/devices/ide-device-manager.c
-src/libide/directory/ide-directory-vcs.c
-src/libide/doap/xml-reader.c
-src/libide/editorconfig/ide-editorconfig-file-settings.c
-src/libide/editor/gtk/menus.ui
-src/libide/editor/ide-editor-hover-provider.c
-src/libide/editor/ide-editor-layout-stack-controls.c
-src/libide/editor/ide-editor-layout-stack-controls.ui
-src/libide/editor/ide-editor-perspective-actions.c
-src/libide/editor/ide-editor-perspective.c
-src/libide/editor/ide-editor-perspective-shortcuts.c
-src/libide/editor/ide-editor-perspective.ui
-src/libide/editor/ide-editor-properties.ui
+src/libide/editor/ide-editor-page-actions.c
+src/libide/editor/ide-editor-page-shortcuts.c
+src/libide/editor/ide-editor-page.ui
+src/libide/editor/ide-editor-print-operation.c
 src/libide/editor/ide-editor-search-bar.c
 src/libide/editor/ide-editor-search-bar.ui
+src/libide/editor/ide-editor-settings-dialog.ui
 src/libide/editor/ide-editor-sidebar.ui
-src/libide/editor/ide-editor-view-actions.c
-src/libide/editor/ide-editor-view-shortcuts.c
-src/libide/editor/ide-editor-view.ui
-src/libide/editor/ide-editor-workbench-addin.c
-src/libide/greeter/ide-greeter-perspective.c
-src/libide/greeter/ide-greeter-perspective.ui
-src/libide/gsettings/ide-language-defaults.c
-src/libide/gtk/menus.ui
-src/libide/ide.c
-src/libide/ide-context.c
-src/libide/ide-object.c
-src/libide/keybindings/ide-shortcuts-window.ui
-src/libide/langserv/ide-langserv-client.c
-src/libide/layout/ide-layout-stack.c
-src/libide/layout/ide-layout-stack-header.ui
-src/libide/layout/ide-layout-stack-shortcuts.c
-src/libide/layout/ide-layout-stack.ui
-src/libide/local/ide-local-device.c
-src/libide/preferences/ide-preferences-builtin.c
-src/libide/preferences/ide-preferences-perspective.c
-src/libide/preferences/ide-preferences-window.ui
+src/libide/editor/ide-editor-surface-actions.c
+src/libide/editor/ide-editor-surface-shortcuts.c
+src/libide/editor/ide-editor-surface.c
+src/libide/editor/ide-editor-surface.ui
+src/libide/editor/ide-editor-workspace.ui
+src/libide/foundry/ide-build-manager.c
+src/libide/foundry/ide-build-pipeline.c
+src/libide/foundry/ide-build-stage-transfer.c
+src/libide/foundry/ide-configuration-manager.c
+src/libide/foundry/ide-device-manager.c
+src/libide/foundry/ide-device.c
+src/libide/foundry/ide-fallback-build-system.c
+src/libide/foundry/ide-local-device.c
+src/libide/foundry/ide-run-manager.c
+src/libide/foundry/ide-runner.c
+src/libide/foundry/ide-runtime-manager.c
+src/libide/foundry/ide-runtime.c
+src/libide/foundry/ide-toolchain-manager.c
+src/libide/greeter/ide-clone-surface.c
+src/libide/greeter/ide-clone-surface.ui
+src/libide/greeter/ide-greeter-workspace-actions.c
+src/libide/greeter/ide-greeter-workspace-shortcuts.c
+src/libide/greeter/ide-greeter-workspace.c
+src/libide/greeter/ide-greeter-workspace.ui
+src/libide/gui/gtk/menus.ui
+src/libide/gui/ide-application-actions.c
+src/libide/gui/ide-application-command-line.c
+src/libide/gui/ide-application-shortcuts.c
+src/libide/gui/ide-application.c
+src/libide/gui/ide-environment-editor-row.ui
+src/libide/gui/ide-environment-editor.c
+src/libide/gui/ide-frame-header.c
+src/libide/gui/ide-frame-header.ui
+src/libide/gui/ide-frame-shortcuts.c
+src/libide/gui/ide-frame.c
+src/libide/gui/ide-frame.ui
+src/libide/gui/ide-header-bar-shortcuts.c
+src/libide/gui/ide-keybindings.c
+src/libide/gui/ide-panel.c
+src/libide/gui/ide-preferences-builtin.c
+src/libide/gui/ide-preferences-surface.c
+src/libide/gui/ide-preferences-window.ui
+src/libide/gui/ide-primary-workspace.ui
+src/libide/gui/ide-run-button.c
+src/libide/gui/ide-run-button.ui
+src/libide/gui/ide-search-entry.c
+src/libide/gui/ide-shortcuts-window.c
+src/libide/gui/ide-shortcuts-window.ui
+src/libide/gui/ide-transfer-button.c
+src/libide/gui/ide-workbench.c
+src/libide/gui/ide-worker-manager.c
+src/libide/io/ide-pkcon-transfer.c
+src/libide/lsp/ide-lsp-client.c
+src/libide/plugins/ide-extension-adapter.c
+src/libide/plugins/ide-extension-set-adapter.c
+src/libide/projects/ide-doap-person.c
+src/libide/projects/ide-doap.c
+src/libide/projects/ide-project-info.c
 src/libide/projects/ide-project.c
+src/libide/projects/ide-projects-global.c
 src/libide/projects/ide-recent-projects.c
-src/libide/runner/ide-run-button.ui
-src/libide/runner/ide-run-manager.c
-src/libide/runner/ide-runner.c
-src/libide/runtimes/ide-runtime-manager.c
-src/libide/sourceview/ide-omni-gutter-renderer.c
+src/libide/projects/xml-reader.c
+src/libide/sourceview/gtk/menus.ui
+src/libide/sourceview/ide-snippet-chunk.c
+src/libide/sourceview/ide-snippet-context.c
+src/libide/sourceview/ide-snippet-parser.c
+src/libide/sourceview/ide-snippet.c
+src/libide/sourceview/ide-source-view-capture.c
+src/libide/sourceview/ide-source-view-mode.c
 src/libide/sourceview/ide-source-view.c
+src/libide/terminal/gtk/menus.ui
+src/libide/terminal/ide-terminal-page-actions.c
+src/libide/terminal/ide-terminal-page.c
+src/libide/terminal/ide-terminal-search.c
 src/libide/terminal/ide-terminal-search.ui
-src/libide/testing/gtk/menus.ui
-src/libide/testing/ide-test-editor-addin.c
-src/libide/testing/ide-test-panel.ui
-src/libide/toolchain/ide-toolchain-manager.c
-src/libide/transfers/ide-pkcon-transfer.c
-src/libide/transfers/ide-transfers-button.ui
-src/libide/util/ide-uri.c
-src/libide/workbench/ide-omni-bar.c
-src/libide/workbench/ide-omni-bar.ui
-src/libide/workbench/ide-workbench-actions.c
-src/libide/workbench/ide-workbench.c
-src/libide/workbench/ide-workbench-header-bar.c
-src/libide/workbench/ide-workbench-header-bar.ui
-src/libide/workbench/ide-workbench-shortcuts.c
+src/libide/terminal/ide-terminal.c
+src/libide/tree/ide-tree-model.c
+src/libide/tree/ide-tree-node.c
+src/libide/vcs/ide-directory-vcs.c
 src/main.c
 src/plugins/autotools/ide-autotools-makecache-stage.c
 src/plugins/autotools/ide-autotools-pipeline-addin.c
+src/plugins/autotools/ide-makecache.c
 src/plugins/beautifier/gb-beautifier-config.c
 src/plugins/beautifier/gb-beautifier-editor-addin.c
 src/plugins/beautifier/gb-beautifier-helper.c
 src/plugins/beautifier/gb-beautifier-process.c
 src/plugins/beautifier/gtk/menus.ui
+src/plugins/buildconfig/ide-buildconfig-configuration-provider.c
+src/plugins/buildui/gbp-buildui-config-surface.c
+src/plugins/buildui/gbp-buildui-config-view-addin.c
+src/plugins/buildui/gbp-buildui-log-pane.c
+src/plugins/buildui/gbp-buildui-log-pane.ui
+src/plugins/buildui/gbp-buildui-omni-bar-section.c
+src/plugins/buildui/gbp-buildui-omni-bar-section.ui
+src/plugins/buildui/gbp-buildui-pane.c
+src/plugins/buildui/gbp-buildui-pane.ui
+src/plugins/buildui/gbp-buildui-tree-addin.c
+src/plugins/buildui/gbp-buildui-workspace-addin.c
+src/plugins/buildui/gtk/menus.ui
+src/plugins/c-pack/ide-c-indenter.c
 src/plugins/cargo/cargo_plugin.py
+src/plugins/clang/ide-clang-completion-item.c
+src/plugins/clang/ide-clang-diagnostic-provider.c
+src/plugins/clang/ide-clang-highlighter.c
 src/plugins/clang/ide-clang-preferences-addin.c
 src/plugins/clang/ide-clang-symbol-node.c
+src/plugins/clang/ide-clang-symbol-tree.c
 src/plugins/clang/org.gnome.builder.clang.gschema.xml
 src/plugins/cmake/gbp-cmake-build-system.c
 src/plugins/cmake/gbp-cmake-pipeline-addin.c
 src/plugins/cmake/gbp-cmake-toolchain.c
 src/plugins/code-index/ide-code-index-index.c
-src/plugins/code-index/ide-code-index-service.c
-src/plugins/color-picker/data/basic.gstyle.xml
 src/plugins/color-picker/gb-color-picker-editor-addin.c
-src/plugins/color-picker/gb-color-picker-prefs.c
 src/plugins/color-picker/gb-color-picker-prefs-palette-row.c
+src/plugins/color-picker/gb-color-picker-prefs.c
 src/plugins/color-picker/gsettings/org.gnome.builder.plugins.color_picker_plugin.gschema.xml
 src/plugins/color-picker/gtk/color-picker-palette-menu.ui
 src/plugins/color-picker/gtk/color-picker-prefs.ui
 src/plugins/color-picker/gtk/color-picker-preview.ui
 src/plugins/color-picker/gtk/color-picker.ui
 src/plugins/color-picker/gtk/menus.ui
-src/plugins/command-bar/gb-command-bar.c
-src/plugins/command-bar/gb-command-vim.c
-src/plugins/command-bar/gb-vim.c
-src/plugins/comment-code/gbp-comment-code-view-addin.c
+src/plugins/command-bar/gbp-command-bar-shortcuts.c
+src/plugins/comment-code/gbp-comment-code-editor-page-addin.c
 src/plugins/comment-code/gtk/menus.ui
-src/plugins/create-project/gbp-create-project-genesis-addin.c
-src/plugins/create-project/gbp-create-project-tool.c
-src/plugins/create-project/gbp-create-project-widget.c
-src/plugins/create-project/gbp-create-project-widget.ui
+src/plugins/create-project/gbp-create-project-application-addin.c
+src/plugins/create-project/gbp-create-project-surface.c
+src/plugins/create-project/gbp-create-project-surface.ui
+src/plugins/create-project/gbp-create-project-workspace-addin.c
+src/plugins/create-project/gtk/menus.ui
+src/plugins/ctags/ide-ctags-completion-item.c
+src/plugins/ctags/ide-ctags-completion-provider.c
+src/plugins/ctags/ide-ctags-highlighter.c
+src/plugins/ctags/ide-ctags-index.c
 src/plugins/ctags/ide-ctags-preferences-addin.c
+src/plugins/ctags/ide-ctags-service.c
+src/plugins/ctags/ide-ctags-symbol-resolver.c
+src/plugins/debuggerui/gtk/menus.ui
+src/plugins/debuggerui/ide-debugger-breakpoints-view.ui
+src/plugins/debuggerui/ide-debugger-controls.ui
+src/plugins/debuggerui/ide-debugger-disassembly-view.ui
+src/plugins/debuggerui/ide-debugger-editor-addin.c
+src/plugins/debuggerui/ide-debugger-hover-controls.ui
+src/plugins/debuggerui/ide-debugger-hover-provider.c
+src/plugins/debuggerui/ide-debugger-libraries-view.ui
+src/plugins/debuggerui/ide-debugger-locals-view.c
+src/plugins/debuggerui/ide-debugger-locals-view.ui
+src/plugins/debuggerui/ide-debugger-registers-view.ui
+src/plugins/debuggerui/ide-debugger-threads-view.c
+src/plugins/debuggerui/ide-debugger-threads-view.ui
 src/plugins/devhelp/gbp-devhelp-hover-provider.c
+src/plugins/devhelp/gbp-devhelp-menu-button.c
 src/plugins/devhelp/gbp-devhelp-menu-button.ui
-src/plugins/devhelp/gbp-devhelp-view.c
+src/plugins/devhelp/gbp-devhelp-page.c
+src/plugins/devhelp/gbp-devhelp-search.c
 src/plugins/devhelp/gtk/menus.ui
+src/plugins/editor/gbp-editor-application-addin.c
+src/plugins/editor/gbp-editor-frame-controls.c
+src/plugins/editor/gbp-editor-frame-controls.ui
+src/plugins/editor/gbp-editor-hover-provider.c
+src/plugins/editor/gbp-editor-workbench-addin.c
+src/plugins/editor/gtk/menus.ui
+src/plugins/editorconfig/gbp-editorconfig-file-settings.c
+src/plugins/emacs/gbp-emacs-preferences-addin.c
 src/plugins/eslint/eslint_plugin.py
 src/plugins/eslint/org.gnome.builder.plugins.eslint.gschema.xml
-src/plugins/find-other-file/find_other_file.py
+src/plugins/file-search/gbp-file-search-index.c
+src/plugins/file-search/gbp-file-search-provider.c
+src/plugins/flatpak/gbp-flatpak-application-addin.c
+src/plugins/flatpak/gbp-flatpak-clone-widget.c
 src/plugins/flatpak/gbp-flatpak-clone-widget.ui
 src/plugins/flatpak/gbp-flatpak-configuration-provider.c
 src/plugins/flatpak/gbp-flatpak-download-stage.c
-src/plugins/flatpak/gbp-flatpak-genesis-addin.c
 src/plugins/flatpak/gbp-flatpak-pipeline-addin.c
 src/plugins/flatpak/gbp-flatpak-preferences-addin.c
+src/plugins/flatpak/gbp-flatpak-runner.c
 src/plugins/flatpak/gbp-flatpak-runtime.c
 src/plugins/flatpak/gbp-flatpak-transfer.c
 src/plugins/flatpak/gbp-flatpak-workbench-addin.c
 src/plugins/gcc/gbp-gcc-toolchain-provider.c
-src/plugins/git/ide-git-buffer-change-monitor.c
-src/plugins/git/ide-git-clone-widget.c
-src/plugins/git/ide-git-clone-widget.ui
-src/plugins/git/ide-git-genesis-addin.c
-src/plugins/git/ide-git-pipeline-addin.c
-src/plugins/git/ide-git-remote-callbacks.c
-src/plugins/git/ide-git-submodule-stage.c
-src/plugins/git/ide-git-vcs.c
+src/plugins/gettext/ide-gettext-diagnostic-provider.c
+src/plugins/git/gbp-git-buffer-change-monitor.c
+src/plugins/git/gbp-git-pipeline-addin.c
+src/plugins/git/gbp-git-remote-callbacks.c
+src/plugins/git/gbp-git-submodule-stage.c
+src/plugins/git/gbp-git-vcs-cloner.c
 src/plugins/glade/gbp-glade-editor-addin.c
-src/plugins/glade/gbp-glade-layout-stack-addin.c
+src/plugins/glade/gbp-glade-frame-addin.c
+src/plugins/glade/gbp-glade-page-actions.c
+src/plugins/glade/gbp-glade-page-shortcuts.c
+src/plugins/glade/gbp-glade-page.c
 src/plugins/glade/gbp-glade-properties.c
 src/plugins/glade/gbp-glade-properties.ui
-src/plugins/glade/gbp-glade-view-actions.c
-src/plugins/glade/gbp-glade-view.c
-src/plugins/glade/gbp-glade-view-shortcuts.c
 src/plugins/glade/gtk/menus.ui
 src/plugins/gnome-code-assistance/ide-gca-diagnostic-provider.c
 src/plugins/gnome-code-assistance/ide-gca-preferences-addin.c
 src/plugins/gnome-code-assistance/ide-gca-service.c
 src/plugins/gnome-code-assistance/org.gnome.builder.gnome-code-assistance.gschema.xml
 src/plugins/gradle/gradle_plugin.py
+src/plugins/greeter/gbp-greeter-application-addin.c
+src/plugins/greeter/gtk/menus.ui
 src/plugins/grep/gbp-grep-panel.c
 src/plugins/grep/gbp-grep-panel.ui
 src/plugins/grep/gbp-grep-popover.ui
@@ -202,70 +258,85 @@ src/plugins/grep/gtk/menus.ui
 src/plugins/html-preview/gtk/menus.ui
 src/plugins/html-preview/html_preview.py
 src/plugins/jedi/jedi_plugin.py
-src/plugins/ls/gbp-ls-view.c
-src/plugins/ls/gbp-ls-view.ui
+src/plugins/jhbuild/jhbuild_plugin.py
+src/plugins/ls/gbp-ls-page.c
+src/plugins/ls/gbp-ls-page.ui
 src/plugins/make/make_plugin.py
 src/plugins/maven/maven_plugin.py
+src/plugins/meson-templates/meson_templates.py
 src/plugins/meson/gbp-meson-build-system.c
 src/plugins/meson/gbp-meson-pipeline-addin.c
-src/plugins/meson/gbp-meson-toolchain.c
+src/plugins/meson/gbp-meson-tool-row.c
+src/plugins/meson/gbp-meson-tool-row.ui
 src/plugins/meson/gbp-meson-toolchain-edition-preferences-addin.c
 src/plugins/meson/gbp-meson-toolchain-edition-preferences-row.c
 src/plugins/meson/gbp-meson-toolchain-edition-preferences-row.ui
-src/plugins/meson/gbp-meson-tool-row.ui
+src/plugins/meson/gbp-meson-toolchain.c
 src/plugins/meson/gbp-meson-utils.c
-src/plugins/meson-templates/meson_templates.py
 src/plugins/messages/gbp-messages-panel.ui
+src/plugins/modelines/gbp-modelines-file-settings.c
 src/plugins/newcomers/gbp-newcomers-section.ui
 src/plugins/notification/ide-notification-addin.c
-src/plugins/npm/npm_plugin.py
+src/plugins/omni-gutter/gbp-omni-gutter-renderer.c
 src/plugins/phpize/phpize_plugin.py
-src/plugins/project-tree/gb-new-file-popover.c
-src/plugins/project-tree/gb-new-file-popover.ui
-src/plugins/project-tree/gb-project-tree-actions.c
-src/plugins/project-tree/gb-project-tree-addin.c
-src/plugins/project-tree/gb-project-tree-builder.c
-src/plugins/project-tree/gb-project-tree-shortcuts.c
-src/plugins/project-tree/gb-rename-file-popover.c
-src/plugins/project-tree/gb-rename-file-popover.ui
+src/plugins/project-tree/gbp-new-file-popover.c
+src/plugins/project-tree/gbp-new-file-popover.ui
+src/plugins/project-tree/gbp-project-tree-addin.c
+src/plugins/project-tree/gbp-project-tree-workspace-addin.c
+src/plugins/project-tree/gbp-rename-file-popover.c
+src/plugins/project-tree/gbp-rename-file-popover.ui
 src/plugins/project-tree/gtk/menus.ui
 src/plugins/qemu/gbp-qemu-device-provider.c
 src/plugins/quick-highlight/gbp-quick-highlight-preferences.c
+src/plugins/recent/gbp-recent-project-row.c
+src/plugins/recent/gbp-recent-section.c
 src/plugins/recent/gbp-recent-section.ui
+src/plugins/retab/gbp-retab-editor-page-addin.c
 src/plugins/retab/gtk/menus.ui
 src/plugins/rustup/rustup_plugin.py
 src/plugins/snippets/ide-snippet-completion-item.c
 src/plugins/snippets/ide-snippet-preferences-addin.c
 src/plugins/spellcheck/gbp-spell-editor-addin.c
-src/plugins/spellcheck/gbp-spell-editor-view-addin.c
+src/plugins/spellcheck/gbp-spell-editor-page-addin.c
 src/plugins/spellcheck/gbp-spell-language-popover.c
 src/plugins/spellcheck/gbp-spell-navigator.c
 src/plugins/spellcheck/gbp-spell-widget.c
 src/plugins/spellcheck/gbp-spell-widget.ui
 src/plugins/spellcheck/gtk/menus.ui
+src/plugins/sublime/gbp-sublime-preferences-addin.c
 src/plugins/support/gtk/menus.ui
 src/plugins/support/ide-support-application-addin.c
+src/plugins/symbol-tree/gbp-symbol-frame-addin.c
 src/plugins/symbol-tree/gbp-symbol-hover-provider.c
-src/plugins/symbol-tree/gbp-symbol-layout-stack-addin.c
 src/plugins/symbol-tree/gbp-symbol-menu-button.c
 src/plugins/symbol-tree/gbp-symbol-menu-button.ui
-src/plugins/sysprof/gbp-sysprof-perspective.c
-src/plugins/sysprof/gbp-sysprof-perspective.ui
-src/plugins/sysprof/gbp-sysprof-workbench-addin.c
+src/plugins/symbol-tree/gbp-symbol-tree-builder.c
+src/plugins/sysprof/gbp-sysprof-surface.c
+src/plugins/sysprof/gbp-sysprof-surface.ui
+src/plugins/sysprof/gbp-sysprof-workspace-addin.c
 src/plugins/sysprof/gtk/menus.ui
 src/plugins/sysroot/gbp-sysroot-preferences-addin.c
 src/plugins/sysroot/gbp-sysroot-preferences-row.ui
+src/plugins/sysroot/gbp-sysroot-runtime-provider.c
+src/plugins/sysroot/gbp-sysroot-subprocess-launcher.c
 src/plugins/sysroot/gbp-sysroot-toolchain-provider.c
-src/plugins/terminal/gb-terminal-view-actions.c
-src/plugins/terminal/gb-terminal-view.c
-src/plugins/terminal/gb-terminal-workbench-addin.c
+src/plugins/terminal/gbp-terminal-application-addin.c
+src/plugins/terminal/gbp-terminal-workspace-addin.c
 src/plugins/terminal/gtk/menus.ui
+src/plugins/testui/gbp-test-tree-addin.c
 src/plugins/todo/gbp-todo-panel.c
-src/plugins/todo/gbp-todo-workbench-addin.c
+src/plugins/todo/gbp-todo-workspace-addin.c
 src/plugins/vala-pack/ide-vala-completion-item.vala
 src/plugins/vala-pack/ide-vala-preferences-addin.vala
 src/plugins/valgrind/gtk/menus.ui
 src/plugins/valgrind/valgrind_plugin.py
+src/plugins/vcsui/gbp-vcsui-tree-addin.c
+src/plugins/vcsui/gtk/menus.ui
+src/plugins/vim/gb-vim.c
+src/plugins/vim/gbp-vim-command-provider.c
+src/plugins/vim/gbp-vim-preferences-addin.c
+src/plugins/xml-pack/ide-xml-highlighter.c
 src/plugins/xml-pack/ide-xml-parser.c
+src/plugins/xml-pack/ide-xml-sax.c
 src/plugins/xml-pack/ide-xml-service.c
 src/plugins/xml-pack/ide-xml-tree-builder.c
diff --git a/src/bug-buddy.c b/src/bug-buddy.c
index 290d9df5d..9c735b423 100644
--- a/src/bug-buddy.c
+++ b/src/bug-buddy.c
@@ -1,6 +1,6 @@
 /* bug-buddy.c
  *
- * Copyright © 2017 Christian Hergert <christian hergert me>
+ * Copyright 2017-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <signal.h>
@@ -35,7 +37,7 @@
 
 static gchar **gdb_argv = NULL;
 
-static void
+G_GNUC_NORETURN static void
 bug_buddy_sigsegv_handler (int signum)
 {
   int pid;
diff --git a/src/bug-buddy.h b/src/bug-buddy.h
index 64cfceec1..20505de80 100644
--- a/src/bug-buddy.h
+++ b/src/bug-buddy.h
@@ -1,6 +1,6 @@
 /* bug-buddy.h
  *
- * Copyright © 2017 Christian Hergert <christian hergert me>
+ * Copyright 2017-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/fusermount-wrapper.c b/src/fusermount-wrapper.c
index a3fded7d9..13a23af1c 100644
--- a/src/fusermount-wrapper.c
+++ b/src/fusermount-wrapper.c
@@ -1,6 +1,6 @@
 /* fusermount-wrapper.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -21,12 +21,10 @@
 #include "config.h"
 
 #include <errno.h>
-#include <ide.h>
+#include <libide-threading.h>
 #include <stdlib.h>
 #include <unistd.h>
 
-#include "threading/ide-thread-pool.h"
-
 static gint exit_code;
 
 static gboolean
diff --git a/src/libide/gconstructor.h b/src/gconstructor.h
similarity index 100%
rename from src/libide/gconstructor.h
rename to src/gconstructor.h
diff --git a/src/gstyle/gstyle-animation.c b/src/gstyle/gstyle-animation.c
index 16cb30e45..07d245c1d 100644
--- a/src/gstyle/gstyle-animation.c
+++ b/src/gstyle/gstyle-animation.c
@@ -1,6 +1,6 @@
 /* gstyle-animation.c
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <gtk/gtk.h>
diff --git a/src/gstyle/gstyle-animation.h b/src/gstyle/gstyle-animation.h
index 6ec8e1699..ba5a94754 100644
--- a/src/gstyle/gstyle-animation.h
+++ b/src/gstyle/gstyle-animation.h
@@ -1,6 +1,6 @@
 /* gstyle-animation.h
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-cielab.c b/src/gstyle/gstyle-cielab.c
index acaff84db..87fa42827 100644
--- a/src/gstyle/gstyle-cielab.c
+++ b/src/gstyle/gstyle-cielab.c
@@ -1,6 +1,6 @@
 /* gstyle-cielab.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-cielab"
diff --git a/src/gstyle/gstyle-cielab.h b/src/gstyle/gstyle-cielab.h
index b8851b164..4e5b12971 100644
--- a/src/gstyle/gstyle-cielab.h
+++ b/src/gstyle/gstyle-cielab.h
@@ -1,6 +1,6 @@
 /* gstyle-cielab.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-component.c b/src/gstyle/gstyle-color-component.c
index 3dcd6ff2c..554636821 100644
--- a/src/gstyle/gstyle-color-component.c
+++ b/src/gstyle/gstyle-color-component.c
@@ -1,6 +1,6 @@
 /* gstyle-color-component.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <gtk/gtk.h>
diff --git a/src/gstyle/gstyle-color-component.h b/src/gstyle/gstyle-color-component.h
index 80a746f11..f76298671 100644
--- a/src/gstyle/gstyle-color-component.h
+++ b/src/gstyle/gstyle-color-component.h
@@ -1,6 +1,6 @@
 /* gstyle-color-component.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-convert.c b/src/gstyle/gstyle-color-convert.c
index 339e53b3b..d65928e2b 100644
--- a/src/gstyle/gstyle-color-convert.c
+++ b/src/gstyle/gstyle-color-convert.c
@@ -1,6 +1,6 @@
 /* gstyle-color-convert.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <stdio.h>
@@ -45,7 +47,7 @@
 
 /* pow_1_24 and pow_24 are adapted version from babl, published under LGPL */
 /* babl - dynamically extendable universal pixel conversion library.
- * Copyright © 2012, Red Hat, Inc.
+ * Copyright 2012, Red Hat, Inc.
  */
 
 /* Chebychev polynomial terms for x^(5/12) expanded around x=1.5
diff --git a/src/gstyle/gstyle-color-convert.h b/src/gstyle/gstyle-color-convert.h
index a5e063f7e..d4f8dd69d 100644
--- a/src/gstyle/gstyle-color-convert.h
+++ b/src/gstyle/gstyle-color-convert.h
@@ -1,6 +1,6 @@
 /* gstyle-color-convert.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-filter.c b/src/gstyle/gstyle-color-filter.c
index 4ff2b2ae4..0a0012517 100644
--- a/src/gstyle/gstyle-color-filter.c
+++ b/src/gstyle/gstyle-color-filter.c
@@ -1,6 +1,6 @@
 /* gstyle-color-filter.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "gstyle-color-filter.h"
diff --git a/src/gstyle/gstyle-color-filter.h b/src/gstyle/gstyle-color-filter.h
index c221968c9..264da9d1d 100644
--- a/src/gstyle/gstyle-color-filter.h
+++ b/src/gstyle/gstyle-color-filter.h
@@ -1,6 +1,6 @@
 /* gstyle-color-filter.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-item.c b/src/gstyle/gstyle-color-item.c
index a207a517c..3244fda06 100644
--- a/src/gstyle/gstyle-color-item.c
+++ b/src/gstyle/gstyle-color-item.c
@@ -1,6 +1,6 @@
 /* gstyle-color-item.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-color-item"
diff --git a/src/gstyle/gstyle-color-item.h b/src/gstyle/gstyle-color-item.h
index f8e45d367..855adb47a 100644
--- a/src/gstyle/gstyle-color-item.h
+++ b/src/gstyle/gstyle-color-item.h
@@ -1,6 +1,6 @@
 /* gstyle-color-item.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-panel-actions.c b/src/gstyle/gstyle-color-panel-actions.c
index ba44303b0..6c6098098 100644
--- a/src/gstyle/gstyle-color-panel-actions.c
+++ b/src/gstyle/gstyle-color-panel-actions.c
@@ -1,6 +1,6 @@
 /* gstyle-color-panel-actions.c
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-color-panel"
diff --git a/src/gstyle/gstyle-color-panel-actions.h b/src/gstyle/gstyle-color-panel-actions.h
index 283e21919..d3858dbc5 100644
--- a/src/gstyle/gstyle-color-panel-actions.h
+++ b/src/gstyle/gstyle-color-panel-actions.h
@@ -1,6 +1,6 @@
 /* gstyle-color-panel-actions.h
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
 
diff --git a/src/gstyle/gstyle-color-panel-private.h b/src/gstyle/gstyle-color-panel-private.h
index 93f110e52..cdd0d80c9 100644
--- a/src/gstyle/gstyle-color-panel-private.h
+++ b/src/gstyle/gstyle-color-panel-private.h
@@ -1,6 +1,6 @@
 /* gstyle-color-panel-private.h
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-panel.c b/src/gstyle/gstyle-color-panel.c
index 80ddb05ed..6c91a4c1e 100644
--- a/src/gstyle/gstyle-color-panel.c
+++ b/src/gstyle/gstyle-color-panel.c
@@ -1,6 +1,6 @@
 /* gstyle-color-panel.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,12 +14,16 @@
  *
  * 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 "gstyle-color-panel"
 
 #include <glib/gi18n.h>
 
+#include "gstyle-resources.h"
+
 #include "gstyle-color-panel-private.h"
 #include "gstyle-color-panel-actions.h"
 #include "gstyle-revealer.h"
@@ -1376,6 +1380,8 @@ gstyle_color_panel_class_init (GstyleColorPanelClass *klass)
   object_class->get_property = gstyle_color_panel_get_property;
   object_class->set_property = gstyle_color_panel_set_property;
 
+  g_resources_register (gstyle_get_resource ());
+
   gtk_widget_class_set_template_from_resource (widget_class,
                                                "/org/gnome/libgstyle/ui/gstyle-color-panel.ui");
 
diff --git a/src/gstyle/gstyle-color-panel.h b/src/gstyle/gstyle-color-panel.h
index bcef3a9bc..634dbf7c9 100644
--- a/src/gstyle/gstyle-color-panel.h
+++ b/src/gstyle/gstyle-color-panel.h
@@ -1,6 +1,6 @@
 /* gstyle-color-panel.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-plane.c b/src/gstyle/gstyle-color-plane.c
index 890dc5d14..d9fec443f 100644
--- a/src/gstyle/gstyle-color-plane.c
+++ b/src/gstyle/gstyle-color-plane.c
@@ -2,9 +2,9 @@
  *
  * based on : gtk-color-plane
  *   GTK - The GIMP Toolkit
- *   Copyright © 2012 Red Hat, Inc.
+ *   Copyright 2012 Red Hat, Inc.
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -18,6 +18,8 @@
  *
  * 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 "gstyle-color-plane"
diff --git a/src/gstyle/gstyle-color-plane.h b/src/gstyle/gstyle-color-plane.h
index 48a80d086..cf4406f79 100644
--- a/src/gstyle/gstyle-color-plane.h
+++ b/src/gstyle/gstyle-color-plane.h
@@ -2,9 +2,9 @@
  *
  * based on : gtk-color-plane
  *   GTK - The GIMP Toolkit
- *   Copyright © 2012 Red Hat, Inc.
+ *   Copyright 2012 Red Hat, Inc.
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -18,6 +18,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-predefined.h b/src/gstyle/gstyle-color-predefined.h
index 98754668d..26945aa09 100644
--- a/src/gstyle/gstyle-color-predefined.h
+++ b/src/gstyle/gstyle-color-predefined.h
@@ -1,6 +1,6 @@
 /* gstyle-color-predefined.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-scale.c b/src/gstyle/gstyle-color-scale.c
index a0fd4c551..786d25864 100644
--- a/src/gstyle/gstyle-color-scale.c
+++ b/src/gstyle/gstyle-color-scale.c
@@ -1,6 +1,6 @@
 /* gstyle-color-scale.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-color-scale"
diff --git a/src/gstyle/gstyle-color-scale.h b/src/gstyle/gstyle-color-scale.h
index 40c878fbb..b6ad6262a 100644
--- a/src/gstyle/gstyle-color-scale.h
+++ b/src/gstyle/gstyle-color-scale.h
@@ -1,6 +1,6 @@
 /* gstyle-color-scale.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-widget-actions.c b/src/gstyle/gstyle-color-widget-actions.c
index 1fa48bbeb..8ee2905ba 100644
--- a/src/gstyle/gstyle-color-widget-actions.c
+++ b/src/gstyle/gstyle-color-widget-actions.c
@@ -1,6 +1,6 @@
 /* gstyle-color-widget-actions.c
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-color-widget"
diff --git a/src/gstyle/gstyle-color-widget-actions.h b/src/gstyle/gstyle-color-widget-actions.h
index 8090ba4e0..b7ab82c66 100644
--- a/src/gstyle/gstyle-color-widget-actions.h
+++ b/src/gstyle/gstyle-color-widget-actions.h
@@ -1,6 +1,6 @@
 /* gstyle-color-widget-actions.h
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color-widget.c b/src/gstyle/gstyle-color-widget.c
index a53898134..61fb01df4 100644
--- a/src/gstyle/gstyle-color-widget.c
+++ b/src/gstyle/gstyle-color-widget.c
@@ -1,6 +1,6 @@
 /* gstyle-color-widget.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-color-widget"
diff --git a/src/gstyle/gstyle-color-widget.h b/src/gstyle/gstyle-color-widget.h
index 7f56eaf6c..46db82dcb 100644
--- a/src/gstyle/gstyle-color-widget.h
+++ b/src/gstyle/gstyle-color-widget.h
@@ -1,6 +1,6 @@
 /* gstyle-color-widget.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-color.c b/src/gstyle/gstyle-color.c
index b7607f27b..fb8a0c795 100644
--- a/src/gstyle/gstyle-color.c
+++ b/src/gstyle/gstyle-color.c
@@ -1,6 +1,6 @@
 /* gstyle-color.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * This file is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-color"
diff --git a/src/gstyle/gstyle-color.h b/src/gstyle/gstyle-color.h
index 003f44377..6a1f80b24 100644
--- a/src/gstyle/gstyle-color.h
+++ b/src/gstyle/gstyle-color.h
@@ -1,6 +1,6 @@
 /* gstyle-color.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * This file is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-colorlexer.c b/src/gstyle/gstyle-colorlexer.c
index 5d4348dd6..ebbe4e9ee 100644
--- a/src/gstyle/gstyle-colorlexer.c
+++ b/src/gstyle/gstyle-colorlexer.c
@@ -1,7 +1,7 @@
 /* Generated by re2c 0.16 on Wed Jun 29 18:01:24 2016 */
 /* gstyle-colorlexer.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -15,6 +15,8 @@
  *
  * 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 "gstyle-colorlexer"
diff --git a/src/gstyle/gstyle-colorlexer.h b/src/gstyle/gstyle-colorlexer.h
index c7a84b602..427de3ef5 100644
--- a/src/gstyle/gstyle-colorlexer.h
+++ b/src/gstyle/gstyle-colorlexer.h
@@ -1,6 +1,6 @@
 /* gstyle-colorlexer.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-css-provider.c b/src/gstyle/gstyle-css-provider.c
index 2994ed565..1780695c7 100644
--- a/src/gstyle/gstyle-css-provider.c
+++ b/src/gstyle/gstyle-css-provider.c
@@ -1,6 +1,6 @@
 /* gstyle-css-provider.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-css-provider"
diff --git a/src/gstyle/gstyle-css-provider.h b/src/gstyle/gstyle-css-provider.h
index 278f0fc97..55a54a635 100644
--- a/src/gstyle/gstyle-css-provider.h
+++ b/src/gstyle/gstyle-css-provider.h
@@ -1,6 +1,6 @@
 /* gstyle-css-provider.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-eyedropper.c b/src/gstyle/gstyle-eyedropper.c
index 5362f61b9..1ce5b0097 100644
--- a/src/gstyle/gstyle-eyedropper.c
+++ b/src/gstyle/gstyle-eyedropper.c
@@ -1,6 +1,6 @@
 /* gstyle-eyedropper.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 /* This source code is first based on the now deprecated
diff --git a/src/gstyle/gstyle-eyedropper.h b/src/gstyle/gstyle-eyedropper.h
index 1b49cc7db..2c1cc99ad 100644
--- a/src/gstyle/gstyle-eyedropper.h
+++ b/src/gstyle/gstyle-eyedropper.h
@@ -1,6 +1,6 @@
 /* gstyle-eyedropper.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-hsv.c b/src/gstyle/gstyle-hsv.c
index bc7f82ed6..aaddae2e3 100644
--- a/src/gstyle/gstyle-hsv.c
+++ b/src/gstyle/gstyle-hsv.c
@@ -1,6 +1,6 @@
 /* gstyle-hsv.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-hsv"
diff --git a/src/gstyle/gstyle-hsv.h b/src/gstyle/gstyle-hsv.h
index fa44cb80b..2d4839eee 100644
--- a/src/gstyle/gstyle-hsv.h
+++ b/src/gstyle/gstyle-hsv.h
@@ -1,6 +1,6 @@
 /* gstyle-hsv.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-palette-widget.c b/src/gstyle/gstyle-palette-widget.c
index dc14fa51f..17ec5be05 100644
--- a/src/gstyle/gstyle-palette-widget.c
+++ b/src/gstyle/gstyle-palette-widget.c
@@ -1,6 +1,6 @@
 /* gstyle-palette-widget.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-palette-widget"
diff --git a/src/gstyle/gstyle-palette-widget.h b/src/gstyle/gstyle-palette-widget.h
index 6375d5ccb..621b1dd4d 100644
--- a/src/gstyle/gstyle-palette-widget.h
+++ b/src/gstyle/gstyle-palette-widget.h
@@ -1,6 +1,6 @@
 /* gstyle-palette-widget.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-palette.c b/src/gstyle/gstyle-palette.c
index 54f8b7750..a40b2a51d 100644
--- a/src/gstyle/gstyle-palette.c
+++ b/src/gstyle/gstyle-palette.c
@@ -1,6 +1,6 @@
 /* gstyle-palette.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-palette"
@@ -871,7 +873,7 @@ gstyle_palette_save_to_xml (GstylePalette  *self,
   gint succes;
 
   const gchar *header =
-                  "Copyright © 2016 GNOME Builder Team at irc.gimp.net/#gnome-builder\n"   \
+                  "Copyright 2016 GNOME Builder Team at irc.gimp.net/#gnome-builder\n"   \
                   "This program is free software: you can redistribute it and/or modify\n" \
                   "it under the terms of the GNU General Public License as published by\n" \
                   "the Free Software Foundation, either version 3 of the License, or\n"    \
diff --git a/src/gstyle/gstyle-palette.h b/src/gstyle/gstyle-palette.h
index 6cf14cd3f..347508216 100644
--- a/src/gstyle/gstyle-palette.h
+++ b/src/gstyle/gstyle-palette.h
@@ -1,6 +1,6 @@
 /* gstyle-palette.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-private.h b/src/gstyle/gstyle-private.h
index d8f8773e9..9b747f436 100644
--- a/src/gstyle/gstyle-private.h
+++ b/src/gstyle/gstyle-private.h
@@ -1,6 +1,6 @@
 /* gstyle-private.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * This file is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-rename-popover.c b/src/gstyle/gstyle-rename-popover.c
index fc987451d..a86e62366 100644
--- a/src/gstyle/gstyle-rename-popover.c
+++ b/src/gstyle/gstyle-rename-popover.c
@@ -1,6 +1,6 @@
 /* gstyle-rename-popover.c
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "gstyle-rename-popover.h"
diff --git a/src/gstyle/gstyle-rename-popover.h b/src/gstyle/gstyle-rename-popover.h
index 794dbc3ec..b7607e99c 100644
--- a/src/gstyle/gstyle-rename-popover.h
+++ b/src/gstyle/gstyle-rename-popover.h
@@ -1,6 +1,6 @@
 /* gstyle-rename-popover.h
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-revealer.c b/src/gstyle/gstyle-revealer.c
index b69fc0d4f..44dbf4209 100644
--- a/src/gstyle/gstyle-revealer.c
+++ b/src/gstyle/gstyle-revealer.c
@@ -1,6 +1,6 @@
 /* gstyle-revealer.c
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 /*
diff --git a/src/gstyle/gstyle-revealer.h b/src/gstyle/gstyle-revealer.h
index 26bd3dbe0..896d7362d 100644
--- a/src/gstyle/gstyle-revealer.h
+++ b/src/gstyle/gstyle-revealer.h
@@ -1,6 +1,6 @@
 /* gstyle-revealer.h
  *
- * Copyright © 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-slidein.c b/src/gstyle/gstyle-slidein.c
index 08af6afb6..8e0dac75a 100644
--- a/src/gstyle/gstyle-slidein.c
+++ b/src/gstyle/gstyle-slidein.c
@@ -1,6 +1,6 @@
 /* gstyle-slidein.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -16,7 +16,9 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Initial ideas based on Gnome Builder Pnl dock system :
- * Copyright © 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 Christian Hergert <chergert redhat com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gstyle-slidein"
diff --git a/src/gstyle/gstyle-slidein.h b/src/gstyle/gstyle-slidein.h
index 4e3dc2b22..eb6043c0a 100644
--- a/src/gstyle/gstyle-slidein.h
+++ b/src/gstyle/gstyle-slidein.h
@@ -1,6 +1,6 @@
 /* gstyle-slidein.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-types.h b/src/gstyle/gstyle-types.h
index b1594a7a3..53301eb5e 100644
--- a/src/gstyle/gstyle-types.h
+++ b/src/gstyle/gstyle-types.h
@@ -1,6 +1,6 @@
 /* gstyle-types.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-utils.c b/src/gstyle/gstyle-utils.c
index 4fa18ea72..e01e0712c 100644
--- a/src/gstyle/gstyle-utils.c
+++ b/src/gstyle/gstyle-utils.c
@@ -1,6 +1,6 @@
 /* gstyle-utils.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "gstyle-utils.h"
diff --git a/src/gstyle/gstyle-utils.h b/src/gstyle/gstyle-utils.h
index dd093a4e9..2975d6df4 100644
--- a/src/gstyle/gstyle-utils.h
+++ b/src/gstyle/gstyle-utils.h
@@ -1,6 +1,6 @@
 /* gstyle-utils.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/gstyle-xyz.c b/src/gstyle/gstyle-xyz.c
index 39a21c321..3eff6e1d2 100644
--- a/src/gstyle/gstyle-xyz.c
+++ b/src/gstyle/gstyle-xyz.c
@@ -1,6 +1,6 @@
 /* gstyle-xyz.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "gstyle-xyz"
diff --git a/src/gstyle/gstyle-xyz.h b/src/gstyle/gstyle-xyz.h
index 96a16192e..ada286f28 100644
--- a/src/gstyle/gstyle-xyz.h
+++ b/src/gstyle/gstyle-xyz.h
@@ -1,6 +1,6 @@
 /* gstyle-xyz.h
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/gstyle/meson.build b/src/gstyle/meson.build
index 4fe328750..4d8c310fb 100644
--- a/src/gstyle/meson.build
+++ b/src/gstyle/meson.build
@@ -66,6 +66,7 @@ libgstyle_sources = [
   'gstyle-utils.c',
   'gstyle-xyz.c',
   libgstyle_resources[0],
+  libgstyle_resources[1],
 ]
 
 libgstyle_deps = [
@@ -75,24 +76,14 @@ libgstyle_deps = [
   libxml2_dep,
 ]
 
-libgstyle_link_args = []
-if ld_supports_version_script
-libgstyle_link_args += ['-Wl,--version-script=' + join_paths(meson.current_source_dir(), 'gstyle.map')]
-endif
-
-libgstyle = shared_library('gstyle-private', libgstyle_sources,
+libgstyle = static_library('gstyle-private', libgstyle_sources,
   dependencies: libgstyle_deps,
-     link_args: libgstyle_link_args,
-        c_args: release_args,
-  link_depends: 'gstyle.map',
-       version: '0.0.0',
-       install: true,
-   install_dir: pkglibdir,
+        c_args: hidden_visibility_args + release_args,
 )
 
 libgstyle_dep = declare_dependency(
-  link_with: libgstyle,
-  dependencies: libgstyle_deps,
+           link_whole: libgstyle,
+         dependencies: libgstyle_deps,
   include_directories: include_directories('.'),
 )
 
@@ -131,28 +122,4 @@ libgstyle_introspection_sources = [
   'gstyle-xyz.c',
 ]
 
-libgstyle_gir = gnome.generate_gir(libgstyle,
-              sources: libgstyle_introspection_sources,
-            nsversion: '1.0',
-            namespace: 'Gstyle',
-        symbol_prefix: 'gstyle',
-    identifier_prefix: 'Gstyle',
-             includes: ['Gdk-3.0', 'Gio-2.0', 'Gtk-3.0', 'GtkSource-4'],
-              install: true,
-      install_dir_gir: pkggirdir,
-  install_dir_typelib: pkgtypelibdir,
-           extra_args: [ '--c-include=gstyle-private.h' ],
-)
-
-if get_option('with_vapi')
-
-  libgstyle_vapi = gnome.generate_vapi('gstyle-private',
-        sources: libgstyle_gir[0],
-       packages: ['gio-2.0', 'gtk+-3.0', 'gtksourceview-4'],
-        install: true,
-    install_dir: pkgvapidir,
-  )
-
-endif
-
 subdir('tests')
diff --git a/src/gstyle/tests/test-gstyle-color-panel.c b/src/gstyle/tests/test-gstyle-color-panel.c
index 104720b74..02410cc78 100644
--- a/src/gstyle/tests/test-gstyle-color-panel.c
+++ b/src/gstyle/tests/test-gstyle-color-panel.c
@@ -1,6 +1,6 @@
 /* test-gstyle-color-panel.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <glib.h>
diff --git a/src/gstyle/tests/test-gstyle-color-plane.c b/src/gstyle/tests/test-gstyle-color-plane.c
index cf6cfffff..61fcedde0 100644
--- a/src/gstyle/tests/test-gstyle-color-plane.c
+++ b/src/gstyle/tests/test-gstyle-color-plane.c
@@ -1,6 +1,6 @@
 /* test-gstyle-color-plane.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <glib.h>
diff --git a/src/gstyle/tests/test-gstyle-color-scale.c b/src/gstyle/tests/test-gstyle-color-scale.c
index 6fb724200..f3d7b9895 100644
--- a/src/gstyle/tests/test-gstyle-color-scale.c
+++ b/src/gstyle/tests/test-gstyle-color-scale.c
@@ -1,6 +1,6 @@
 /* test-gstyle-color-scale.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <glib.h>
diff --git a/src/gstyle/tests/test-gstyle-color-widget.c b/src/gstyle/tests/test-gstyle-color-widget.c
index 78c58b267..f4279bccb 100644
--- a/src/gstyle/tests/test-gstyle-color-widget.c
+++ b/src/gstyle/tests/test-gstyle-color-widget.c
@@ -1,6 +1,6 @@
 /* test-gstyle-color-widget.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <glib.h>
diff --git a/src/gstyle/tests/test-gstyle-color.c b/src/gstyle/tests/test-gstyle-color.c
index e640ccd48..adf342181 100644
--- a/src/gstyle/tests/test-gstyle-color.c
+++ b/src/gstyle/tests/test-gstyle-color.c
@@ -1,6 +1,6 @@
 /* test-gstyle-color.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <glib.h>
diff --git a/src/gstyle/tests/test-gstyle-filter.c b/src/gstyle/tests/test-gstyle-filter.c
index 89587275d..95c0a4984 100644
--- a/src/gstyle/tests/test-gstyle-filter.c
+++ b/src/gstyle/tests/test-gstyle-filter.c
@@ -1,6 +1,6 @@
 /* test-gstyle-filter.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 /* Photos sources:
diff --git a/src/gstyle/tests/test-gstyle-palette-widget.c b/src/gstyle/tests/test-gstyle-palette-widget.c
index 91cb291b1..f090dcd93 100644
--- a/src/gstyle/tests/test-gstyle-palette-widget.c
+++ b/src/gstyle/tests/test-gstyle-palette-widget.c
@@ -1,6 +1,6 @@
 /* test-gstyle-palette-widget.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <glib.h>
diff --git a/src/gstyle/tests/test-gstyle-palette.c b/src/gstyle/tests/test-gstyle-palette.c
index f4c65c880..222f1202f 100644
--- a/src/gstyle/tests/test-gstyle-palette.c
+++ b/src/gstyle/tests/test-gstyle-palette.c
@@ -1,6 +1,6 @@
 /* test-gstyle-palette.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <glib.h>
diff --git a/src/gstyle/tests/test-gstyle-parse.c b/src/gstyle/tests/test-gstyle-parse.c
index 7b9abe35c..91f0c34df 100644
--- a/src/gstyle/tests/test-gstyle-parse.c
+++ b/src/gstyle/tests/test-gstyle-parse.c
@@ -1,6 +1,6 @@
 /* test-gstyle-parse.c
  *
- * Copyright © 2016 sebastien lafargue <slafargue gnome org>
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <glib.h>
diff --git a/src/libide.deps b/src/libide.deps
new file mode 100644
index 000000000..cdbe37f99
--- /dev/null
+++ b/src/libide.deps
@@ -0,0 +1,8 @@
+gio-2.0
+gtk+-3.0
+gtksourceview-4
+json-glib-1.0
+libdazzle-1.0
+libpeas-1.0
+template-glib-1.0
+vte-2.91
diff --git a/src/libide/Ide.py b/src/libide/Ide.py
index 4aa565086..06a9a176a 100644
--- a/src/libide/Ide.py
+++ b/src/libide/Ide.py
@@ -3,7 +3,7 @@
 #
 # Ide.py
 #
-# Copyright 2015 Christian Hergert <christian hergert me>
+# Copyright 2015-2019 Christian Hergert <christian hergert me>
 #
 # 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
diff --git a/src/libide/files/defaults.ini b/src/libide/code/defaults.ini
similarity index 100%
rename from src/libide/files/defaults.ini
rename to src/libide/code/defaults.ini
diff --git a/src/libide/code/ide-buffer-addin-private.h b/src/libide/code/ide-buffer-addin-private.h
new file mode 100644
index 000000000..ec6f3c6b4
--- /dev/null
+++ b/src/libide/code/ide-buffer-addin-private.h
@@ -0,0 +1,82 @@
+/* ide-buffer-addin-private.h
+ *
+ * Copyright 2018-2019 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-plugins.h>
+#include <libpeas/peas.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-addin.h"
+
+G_BEGIN_DECLS
+
+typedef struct
+{
+  IdeBuffer   *buffer;
+  const gchar *language_id;
+} IdeBufferLanguageSet;
+
+typedef struct
+{
+  IdeBuffer *buffer;
+  GFile     *file;
+} IdeBufferFileSave;
+
+typedef struct
+{
+  IdeBuffer *buffer;
+  GFile     *file;
+} IdeBufferFileLoad;
+
+void _ide_buffer_addin_load_cb                 (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_unload_cb               (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_file_loaded_cb          (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_save_file_cb            (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_file_saved_cb           (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_language_set_cb         (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_change_settled_cb       (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+void _ide_buffer_addin_style_scheme_changed_cb (IdeExtensionSetAdapter *set,
+                                                PeasPluginInfo         *plugin_info,
+                                                PeasExtension          *exten,
+                                                gpointer                user_data);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-buffer-addin.c b/src/libide/code/ide-buffer-addin.c
new file mode 100644
index 000000000..7eaf484b8
--- /dev/null
+++ b/src/libide/code/ide-buffer-addin.c
@@ -0,0 +1,411 @@
+/* ide-buffer-addin.c
+ *
+ * Copyright 2017-2019 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 "ide-buffer-addin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-addin.h"
+#include "ide-buffer-addin-private.h"
+#include "ide-buffer-private.h"
+
+/**
+ * SECTION:ide-buffer-addin
+ * @title: IdeBufferAddin
+ * @short_description: addins for #IdeBuffer
+ *
+ * The #IdeBufferAddin allows a plugin to register an object that will be
+ * created with every #IdeBuffer. It can register extra features with the
+ * buffer or extend it as necessary.
+ *
+ * Once use of #IdeBufferAddin is to add a spellchecker to the buffer that
+ * may be used by views to show the misspelled words. This is preferrable
+ * to adding a spellchecker in each view because it allows for multiple
+ * views to share one spellcheker on the underlying buffer.
+ *
+ * Since: 3.32
+ */
+
+G_DEFINE_INTERFACE (IdeBufferAddin, ide_buffer_addin, G_TYPE_OBJECT)
+
+static void
+ide_buffer_addin_default_init (IdeBufferAddinInterface *iface)
+{
+}
+
+/**
+ * ide_buffer_addin_load:
+ * @self: an #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ *
+ * This calls the load virtual function of #IdeBufferAddin to request
+ * that the addin load itself.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_load (IdeBufferAddin *self,
+                       IdeBuffer      *buffer)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->load)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->load (self, buffer);
+}
+
+/**
+ * ide_buffer_addin_unload:
+ * @self: an #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ *
+ * This calls the unload virtual function of #IdeBufferAddin to request
+ * that the addin unload itself.
+ *
+ * The addin should cancel any in-flight operations and attempt to drop
+ * references to the buffer or any other machinery as soon as possible.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_unload (IdeBufferAddin *self,
+                         IdeBuffer      *buffer)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->unload)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->unload (self, buffer);
+}
+
+/**
+ * ide_buffer_addin_file_loaded:
+ * @self: a #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ * @file: a #GFile
+ *
+ * This function is called for an addin after a file has been loaded from disk.
+ *
+ * It is not guaranteed that this function will be called for addins that were
+ * loaded after the buffer already loaded a file.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_file_loaded (IdeBufferAddin *self,
+                              IdeBuffer      *buffer,
+                              GFile          *file)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (G_IS_FILE (file));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->file_loaded)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->file_loaded (self, buffer, file);
+}
+
+/**
+ * ide_buffer_addin_save_file:
+ * @self: a #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ * @file: a #GFile
+ *
+ * This function gives a chance for plugins to modify the buffer right before
+ * writing to disk.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_save_file (IdeBufferAddin *self,
+                            IdeBuffer      *buffer,
+                            GFile          *file)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (G_IS_FILE (file));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->save_file)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->save_file (self, buffer, file);
+}
+
+/**
+ * ide_buffer_addin_file_saved:
+ * @self: a #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ * @file: a #GFile
+ *
+ * This function is called for an addin after a file has been saved to disk.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_file_saved (IdeBufferAddin *self,
+                             IdeBuffer      *buffer,
+                             GFile          *file)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (G_IS_FILE (file));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->file_saved)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->file_saved (self, buffer, file);
+}
+
+/**
+ * ide_buffer_addin_language_set:
+ * @self: an #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ * @language_id: the GtkSourceView language identifier
+ *
+ * This vfunc is called when the source language in the buffer changes. This
+ * will only be delivered to addins that support multiple languages.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_language_set (IdeBufferAddin *self,
+                               IdeBuffer      *buffer,
+                               const gchar    *language_id)
+{
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->language_set)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->language_set (self, buffer, language_id);
+}
+
+/**
+ * ide_buffer_addin_change_settled:
+ * @self: an #IdeBufferAddin
+ * @buffer: an #ideBuffer
+ *
+ * This function is called when the buffer has settled after a number of
+ * changes provided by the user. It is a convenient way to know when you
+ * should perform more background work without having to coalesce work
+ * yourself.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_change_settled (IdeBufferAddin *self,
+                                 IdeBuffer      *buffer)
+{
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->change_settled)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->change_settled (self, buffer);
+}
+
+/**
+ * ide_buffer_addin_style_scheme_changed:
+ * @self: an #IdeBufferAddin
+ * @buffer: an #IdeBuffer
+ *
+ * This function is called when the #GtkSourceStyleScheme of the #IdeBuffer
+ * has changed.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_addin_style_scheme_changed (IdeBufferAddin *self,
+                                       IdeBuffer      *buffer)
+{
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (IDE_BUFFER_ADDIN_GET_IFACE (self)->style_scheme_changed)
+    IDE_BUFFER_ADDIN_GET_IFACE (self)->style_scheme_changed (self, buffer);
+}
+
+/**
+ * ide_buffer_addin_find_by_module_name:
+ * @buffer: an #IdeBuffer
+ * @module_name: the module name of the addin
+ *
+ * Locates an addin attached to the #IdeBuffer by the name of the module
+ * that provides the addin.
+ *
+ * Returns: (transfer none) (nullable): An #IdeBufferAddin or %NULL
+ *
+ * Since: 3.32
+ */
+IdeBufferAddin *
+ide_buffer_addin_find_by_module_name (IdeBuffer   *buffer,
+                                      const gchar *module_name)
+{
+  PeasPluginInfo *plugin_info;
+  IdeExtensionSetAdapter *set;
+  PeasExtension *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
+  g_return_val_if_fail (module_name != NULL, NULL);
+
+  set = _ide_buffer_get_addins (buffer);
+
+  /* Addins might not be loaded */
+  if (set == NULL)
+    return NULL;
+
+  plugin_info = peas_engine_get_plugin_info (peas_engine_get_default (), module_name);
+
+  if (plugin_info != NULL)
+    ret = ide_extension_set_adapter_get_extension (set, plugin_info);
+  else
+    g_warning ("Failed to locate addin named %s", module_name);
+
+  return ret ? IDE_BUFFER_ADDIN (ret) : NULL;
+}
+
+void
+_ide_buffer_addin_load_cb (IdeExtensionSetAdapter *set,
+                           PeasPluginInfo         *plugin_info,
+                           PeasExtension          *exten,
+                           gpointer                user_data)
+{
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (IDE_IS_BUFFER (user_data));
+
+  ide_buffer_addin_load (IDE_BUFFER_ADDIN (exten), IDE_BUFFER (user_data));
+}
+
+void
+_ide_buffer_addin_unload_cb (IdeExtensionSetAdapter *set,
+                             PeasPluginInfo         *plugin_info,
+                             PeasExtension          *exten,
+                             gpointer                user_data)
+{
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (IDE_IS_BUFFER (user_data));
+
+  ide_buffer_addin_unload (IDE_BUFFER_ADDIN (exten), IDE_BUFFER (user_data));
+}
+
+void
+_ide_buffer_addin_file_loaded_cb (IdeExtensionSetAdapter *set,
+                                  PeasPluginInfo         *plugin_info,
+                                  PeasExtension          *exten,
+                                  gpointer                user_data)
+{
+  IdeBufferFileLoad *load = user_data;
+
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (load != NULL);
+  g_return_if_fail (IDE_IS_BUFFER (load->buffer));
+  g_return_if_fail (G_IS_FILE (load->file));
+
+  ide_buffer_addin_file_loaded (IDE_BUFFER_ADDIN (exten), load->buffer, load->file);
+}
+
+void
+_ide_buffer_addin_save_file_cb (IdeExtensionSetAdapter *set,
+                                PeasPluginInfo         *plugin_info,
+                                PeasExtension          *exten,
+                                gpointer                user_data)
+{
+  IdeBufferFileSave *save = user_data;
+
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (save != NULL);
+  g_return_if_fail (IDE_IS_BUFFER (save->buffer));
+  g_return_if_fail (G_IS_FILE (save->file));
+
+  ide_buffer_addin_save_file (IDE_BUFFER_ADDIN (exten), save->buffer, save->file);
+}
+
+void
+_ide_buffer_addin_file_saved_cb (IdeExtensionSetAdapter *set,
+                                 PeasPluginInfo         *plugin_info,
+                                 PeasExtension          *exten,
+                                 gpointer                user_data)
+{
+  IdeBufferFileSave *save = user_data;
+
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (save != NULL);
+  g_return_if_fail (IDE_IS_BUFFER (save->buffer));
+  g_return_if_fail (G_IS_FILE (save->file));
+
+  ide_buffer_addin_file_saved (IDE_BUFFER_ADDIN (exten), save->buffer, save->file);
+}
+
+void
+_ide_buffer_addin_language_set_cb (IdeExtensionSetAdapter *set,
+                                   PeasPluginInfo         *plugin_info,
+                                   PeasExtension          *exten,
+                                   gpointer                user_data)
+{
+  IdeBufferLanguageSet *lang = user_data;
+
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (lang != NULL);
+  g_return_if_fail (IDE_IS_BUFFER (lang->buffer));
+
+  ide_buffer_addin_language_set (IDE_BUFFER_ADDIN (exten), lang->buffer, lang->language_id);
+}
+
+void
+_ide_buffer_addin_change_settled_cb (IdeExtensionSetAdapter *set,
+                                     PeasPluginInfo         *plugin_info,
+                                     PeasExtension          *exten,
+                                     gpointer                user_data)
+{
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (IDE_IS_BUFFER (user_data));
+
+  ide_buffer_addin_change_settled (IDE_BUFFER_ADDIN (exten), IDE_BUFFER (user_data));
+}
+
+void
+_ide_buffer_addin_style_scheme_changed_cb (IdeExtensionSetAdapter *set,
+                                           PeasPluginInfo         *plugin_info,
+                                           PeasExtension          *exten,
+                                           gpointer                user_data)
+{
+  g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_return_if_fail (plugin_info != NULL);
+  g_return_if_fail (IDE_IS_BUFFER_ADDIN (exten));
+  g_return_if_fail (IDE_IS_BUFFER (user_data));
+
+  ide_buffer_addin_style_scheme_changed (IDE_BUFFER_ADDIN (exten), IDE_BUFFER (user_data));
+}
diff --git a/src/libide/code/ide-buffer-addin.h b/src/libide/code/ide-buffer-addin.h
new file mode 100644
index 000000000..78481188e
--- /dev/null
+++ b/src/libide/code/ide-buffer-addin.h
@@ -0,0 +1,96 @@
+/* ide-buffer-addin.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUFFER_ADDIN (ide_buffer_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeBufferAddin, ide_buffer_addin, IDE, BUFFER_ADDIN, GObject)
+
+struct _IdeBufferAddinInterface
+{
+  GTypeInterface parent_iface;
+
+  void (*load)                 (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer);
+  void (*unload)               (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer);
+  void (*file_loaded)          (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer,
+                                GFile             *file);
+  void (*save_file)            (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer,
+                                GFile             *file);
+  void (*file_saved)           (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer,
+                                GFile             *file);
+  void (*language_set)         (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer,
+                                const gchar       *language_id);
+  void (*change_settled)       (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer);
+  void (*style_scheme_changed) (IdeBufferAddin    *self,
+                                IdeBuffer         *buffer);
+};
+
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_load                 (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_unload               (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_file_loaded          (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer,
+                                                       GFile          *file);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_save_file            (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer,
+                                                       GFile          *file);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_file_saved           (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer,
+                                                       GFile          *file);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_language_set         (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer,
+                                                       const gchar    *language_id);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_change_settled       (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer);
+IDE_AVAILABLE_IN_3_32
+void            ide_buffer_addin_style_scheme_changed (IdeBufferAddin *self,
+                                                       IdeBuffer      *buffer);
+IDE_AVAILABLE_IN_3_32
+IdeBufferAddin *ide_buffer_addin_find_by_module_name  (IdeBuffer      *buffer,
+                                                       const gchar    *module_name);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-buffer-change-monitor.c b/src/libide/code/ide-buffer-change-monitor.c
new file mode 100644
index 000000000..b812bfb65
--- /dev/null
+++ b/src/libide/code/ide-buffer-change-monitor.c
@@ -0,0 +1,233 @@
+/* ide-buffer-change-monitor.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-buffer-change-monitor"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-change-monitor.h"
+#include "ide-buffer-private.h"
+
+typedef struct
+{
+  IdeBuffer *buffer;
+} IdeBufferChangeMonitorPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeBufferChangeMonitor, ide_buffer_change_monitor, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_BUFFER,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+IdeBufferLineChange
+ide_buffer_change_monitor_get_change (IdeBufferChangeMonitor *self,
+                                      guint                   line)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self), IDE_BUFFER_LINE_CHANGE_NONE);
+
+  if G_LIKELY (IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->get_change)
+    return IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->get_change (self, line);
+  else
+    return IDE_BUFFER_LINE_CHANGE_NONE;
+}
+
+static void
+ide_buffer_change_monitor_set_buffer (IdeBufferChangeMonitor *self,
+                                      IdeBuffer              *buffer)
+{
+  IdeBufferChangeMonitorPrivate *priv = ide_buffer_change_monitor_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  priv->buffer = g_object_ref (buffer);
+
+  if (IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->load)
+    IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->load (self, buffer);
+}
+
+void
+ide_buffer_change_monitor_emit_changed (IdeBufferChangeMonitor *self)
+{
+  IdeBufferChangeMonitorPrivate *priv = ide_buffer_change_monitor_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+
+  g_signal_emit (self, signals [CHANGED], 0);
+
+  if (priv->buffer)
+    _ide_buffer_line_flags_changed (priv->buffer);
+}
+
+static void
+ide_buffer_change_monitor_destroy (IdeObject *object)
+{
+  IdeBufferChangeMonitor *self = (IdeBufferChangeMonitor *)object;
+  IdeBufferChangeMonitorPrivate *priv = ide_buffer_change_monitor_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+
+  g_clear_object (&priv->buffer);
+
+  IDE_OBJECT_CLASS (ide_buffer_change_monitor_parent_class)->destroy (object);
+}
+
+static void
+ide_buffer_change_monitor_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  IdeBufferChangeMonitor *self = IDE_BUFFER_CHANGE_MONITOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      g_value_set_object (value, ide_buffer_change_monitor_get_buffer (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_change_monitor_set_property (GObject      *object,
+                                        guint         prop_id,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  IdeBufferChangeMonitor *self = IDE_BUFFER_CHANGE_MONITOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      ide_buffer_change_monitor_set_buffer (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_change_monitor_class_init (IdeBufferChangeMonitorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_buffer_change_monitor_get_property;
+  object_class->set_property = ide_buffer_change_monitor_set_property;
+
+  i_object_class->destroy = ide_buffer_change_monitor_destroy;
+
+  properties [PROP_BUFFER] =
+    g_param_spec_object ("buffer",
+                         "Buffer",
+                         "The IdeBuffer to be monitored.",
+                         IDE_TYPE_BUFFER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHANGED] = g_signal_new ("changed",
+                                    G_TYPE_FROM_CLASS (klass),
+                                    G_SIGNAL_RUN_LAST,
+                                    0,
+                                    NULL, NULL, NULL,
+                                    G_TYPE_NONE,
+                                    0);
+}
+
+static void
+ide_buffer_change_monitor_init (IdeBufferChangeMonitor *self)
+{
+}
+
+void
+ide_buffer_change_monitor_reload (IdeBufferChangeMonitor *self)
+{
+  g_return_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+
+  if (IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->reload)
+    IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->reload (self);
+}
+
+/**
+ * ide_buffer_change_monitor_foreach_change:
+ * @self: a #IdeBufferChangeMonitor
+ * @line_begin: the starting line
+ * @line_end: the end line
+ * @callback: (scope call): a callback
+ * @user_data: user data for @callback
+ *
+ * Calls @callback for every line between @line_begin and @line_end that have
+ * an addition, deletion, or change.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_change_monitor_foreach_change (IdeBufferChangeMonitor            *self,
+                                          guint                              line_begin,
+                                          guint                              line_end,
+                                          IdeBufferChangeMonitorForeachFunc  callback,
+                                          gpointer                           user_data)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+  g_return_if_fail (callback != NULL);
+
+  if (IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->foreach_change)
+    IDE_BUFFER_CHANGE_MONITOR_GET_CLASS (self)->foreach_change (self, line_begin, line_end, callback, 
user_data);
+}
+
+/**
+ * ide_buffer_change_monitor_get_buffer:
+ * @self: a #IdeBufferChangeMonitor
+ *
+ * Gets the #IdeBufferChangeMonitor:buffer property.
+ *
+ * Returns: (transfer none): an #IdeBuffer
+ *
+ * Since: 3.32
+ */
+IdeBuffer *
+ide_buffer_change_monitor_get_buffer (IdeBufferChangeMonitor *self)
+{
+  IdeBufferChangeMonitorPrivate *priv = ide_buffer_change_monitor_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUFFER_CHANGE_MONITOR (self), NULL);
+
+  return priv->buffer;
+}
diff --git a/src/libide/code/ide-buffer-change-monitor.h b/src/libide/code/ide-buffer-change-monitor.h
new file mode 100644
index 000000000..3c48cf931
--- /dev/null
+++ b/src/libide/code/ide-buffer-change-monitor.h
@@ -0,0 +1,86 @@
+/* ide-buffer-change-monitor.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUFFER_CHANGE_MONITOR (ide_buffer_change_monitor_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeBufferChangeMonitor, ide_buffer_change_monitor, IDE, BUFFER_CHANGE_MONITOR, 
IdeObject)
+
+typedef enum
+{
+  IDE_BUFFER_LINE_CHANGE_NONE    = 0,
+  IDE_BUFFER_LINE_CHANGE_ADDED   = 1 << 0,
+  IDE_BUFFER_LINE_CHANGE_CHANGED = 1 << 1,
+  IDE_BUFFER_LINE_CHANGE_DELETED = 1 << 2,
+} IdeBufferLineChange;
+
+typedef void (*IdeBufferChangeMonitorForeachFunc) (guint               line,
+                                                   IdeBufferLineChange change,
+                                                   gpointer            user_data);
+
+struct _IdeBufferChangeMonitorClass
+{
+  IdeObjectClass parent_class;
+
+  void                (*load)           (IdeBufferChangeMonitor            *self,
+                                         IdeBuffer                         *buffer);
+  IdeBufferLineChange (*get_change)     (IdeBufferChangeMonitor            *self,
+                                         guint                              line);
+  void                (*reload)         (IdeBufferChangeMonitor            *self);
+  void                (*foreach_change) (IdeBufferChangeMonitor            *self,
+                                         guint                              line_begin,
+                                         guint                              line_end,
+                                         IdeBufferChangeMonitorForeachFunc  callback,
+                                         gpointer                           user_data);
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeBuffer          *ide_buffer_change_monitor_get_buffer     (IdeBufferChangeMonitor            *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_buffer_change_monitor_emit_changed   (IdeBufferChangeMonitor            *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_buffer_change_monitor_foreach_change (IdeBufferChangeMonitor            *self,
+                                                              guint                              line_begin,
+                                                              guint                              line_end,
+                                                              IdeBufferChangeMonitorForeachFunc  callback,
+                                                              gpointer                           user_data);
+IDE_AVAILABLE_IN_3_32
+IdeBufferLineChange ide_buffer_change_monitor_get_change     (IdeBufferChangeMonitor            *self,
+                                                              guint                              line);
+IDE_AVAILABLE_IN_3_32
+void                ide_buffer_change_monitor_reload         (IdeBufferChangeMonitor            *self);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-buffer-manager.c b/src/libide/code/ide-buffer-manager.c
new file mode 100644
index 000000000..76a206712
--- /dev/null
+++ b/src/libide/code/ide-buffer-manager.c
@@ -0,0 +1,1309 @@
+/* ide-buffer-manager.c
+ *
+ * Copyright 2018-2019 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 "ide-buffer-manager"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-private.h"
+#include "ide-buffer-manager.h"
+#include "ide-doc-seq-private.h"
+#include "ide-text-edit.h"
+#include "ide-text-edit-private.h"
+#include "ide-location.h"
+#include "ide-range.h"
+
+struct _IdeBufferManager
+{
+  IdeObject   parent_instance;
+  GHashTable *loading_tasks;
+  gssize      max_file_size;
+};
+
+typedef struct
+{
+  GPtrArray *buffers;
+  guint      n_active;
+  guint      had_failure : 1;
+} SaveAll;
+
+typedef struct
+{
+  GPtrArray  *edits;
+  GHashTable *buffers;
+  GHashTable *to_close;
+  guint       n_active;
+  guint       failed : 1;
+} EditState;
+
+typedef struct
+{
+  GFile *file;
+  IdeBuffer *buffer;
+} FindBuffer;
+
+typedef struct
+{
+  IdeBufferForeachFunc func;
+  gpointer user_data;
+} Foreach;
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeBufferManager, ide_buffer_manager, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_MAX_FILE_SIZE,
+  N_PROPS
+};
+
+enum {
+  BUFFER_LOADED,
+  BUFFER_SAVED,
+  BUFFER_UNLOADED,
+  LOAD_BUFFER,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+edit_state_free (EditState *state)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  if (state != NULL)
+    {
+      g_clear_pointer (&state->edits, g_ptr_array_unref);
+      g_clear_pointer (&state->buffers, g_hash_table_unref);
+      g_clear_pointer (&state->to_close, g_hash_table_unref);
+      g_slice_free (EditState, state);
+    }
+}
+
+static void
+save_all_free (SaveAll *state)
+{
+  g_assert (state->n_active == 0);
+  g_clear_pointer (&state->buffers, g_ptr_array_unref);
+  g_slice_free (SaveAll, state);
+}
+
+static IdeBuffer *
+ide_buffer_manager_create_buffer (IdeBufferManager *self,
+                                  GFile            *file,
+                                  gboolean          is_temporary)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+  g_autoptr(IdeObjectBox) box = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+
+  buffer = _ide_buffer_new (self, file, is_temporary);
+  box = ide_object_box_new (G_OBJECT (buffer));
+
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (box));
+  _ide_buffer_attach (buffer, IDE_OBJECT (box));
+
+  IDE_RETURN (g_steal_pointer (&buffer));
+}
+
+static void
+ide_buffer_manager_add (IdeObject         *object,
+                        IdeObject         *sibling,
+                        IdeObject         *child,
+                        IdeObjectLocation  location)
+{
+  IdeBufferManager *self = (IdeBufferManager *)object;
+  g_autoptr(IdeBuffer) buffer = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (IDE_IS_OBJECT (child));
+
+  if (!IDE_IS_OBJECT_BOX (child) ||
+      !IDE_IS_BUFFER ((buffer = ide_object_box_ref_object (IDE_OBJECT_BOX (child)))))
+    {
+      g_critical ("You may only add an IdeObjectBox of IdeBuffer to an IdeBufferManager");
+      return;
+    }
+
+  IDE_OBJECT_CLASS (ide_buffer_manager_parent_class)->add (object, sibling, child, location);
+  g_list_model_items_changed (G_LIST_MODEL (self), ide_object_get_position (child), 0, 1);
+}
+
+static void
+ide_buffer_manager_remove (IdeObject *object,
+                           IdeObject *child)
+{
+  IdeBufferManager *self = (IdeBufferManager *)object;
+  g_autoptr(IdeBuffer) buffer = NULL;
+  guint position;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (IDE_IS_OBJECT_BOX (child));
+
+  buffer = ide_object_box_ref_object (IDE_OBJECT_BOX (child));
+  g_signal_emit (self, signals [BUFFER_UNLOADED], 0, buffer);
+
+  position = ide_object_get_position (child);
+  IDE_OBJECT_CLASS (ide_buffer_manager_parent_class)->remove (object, child);
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 1, 0);
+}
+
+static void
+ide_buffer_manager_destroy (IdeObject *object)
+{
+  IdeBufferManager *self = (IdeBufferManager *)object;
+
+  IDE_ENTRY;
+
+  g_clear_pointer (&self->loading_tasks, g_hash_table_unref);
+
+  IDE_OBJECT_CLASS (ide_buffer_manager_parent_class)->destroy (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_manager_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeBufferManager *self = IDE_BUFFER_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_FILE_SIZE:
+      g_value_set_int64 (value, ide_buffer_manager_get_max_file_size (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_manager_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeBufferManager *self = IDE_BUFFER_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_FILE_SIZE:
+      ide_buffer_manager_set_max_file_size (self, g_value_get_int64 (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_manager_class_init (IdeBufferManagerClass *klass)
+{
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_buffer_manager_get_property;
+  object_class->set_property = ide_buffer_manager_set_property;
+
+  i_object_class->add = ide_buffer_manager_add;
+  i_object_class->remove = ide_buffer_manager_remove;
+  i_object_class->destroy = ide_buffer_manager_destroy;
+
+  /**
+   * IdeBufferManager:max-file-size:
+   *
+   * The "max-file-size" property is the largest file size in bytes that
+   * Builder will attempt to load. Larger files will fail to load to help
+   * ensure that Builder's buffer manager does not attempt to load files that
+   * will slow the buffer management beyond usefulness.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_MAX_FILE_SIZE] =
+    g_param_spec_int64 ("max-file-size",
+                        "Max File Size",
+                        "The max file size to load",
+                        -1,
+                        G_MAXINT64,
+                        10L * 1024L * 1024L,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeBufferManager:load-buffer:
+   * @self: an #IdeBufferManager
+   * @buffer: an #IdeBuffer
+   * @create_new_view: if the buffer requires a view to be created
+   *
+   * The "load-buffer" signal is emitted before a buffer is (re)loaded.
+   *
+   * Since: 3.32
+   */
+  signals [LOAD_BUFFER] =
+    g_signal_new ("load-buffer",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  NULL,
+                  G_TYPE_NONE, 2, IDE_TYPE_BUFFER, G_TYPE_BOOLEAN);
+
+  /**
+   * IdeBufferManager::buffer-loaded:
+   * @self: an #IdeBufferManager
+   * @buffer: an #IdeBuffer
+   *
+   * The "buffer-loaded" signal is emitted when an #IdeBuffer has loaded
+   * a file from storage.
+   *
+   * Since: 3.32
+   */
+  signals [BUFFER_LOADED] =
+    g_signal_new ("buffer-loaded",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_BUFFER);
+  g_signal_set_va_marshaller (signals [BUFFER_LOADED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  /**
+   * IdeBufferManager::buffer-saved:
+   * @self: an #IdeBufferManager
+   * @buffer: an #IdeBuffer
+   *
+   * The "buffer-saved" signal is emitted when an #IdeBuffer has been saved
+   * to storage.
+   *
+   * Since: 3.32
+   */
+  signals [BUFFER_SAVED] =
+    g_signal_new ("buffer-saved",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_BUFFER);
+  g_signal_set_va_marshaller (signals [BUFFER_SAVED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  /**
+   * IdeBufferManager::buffer-unloaded:
+   * @self: an #IdeBufferManager
+   * @buffer: an #IdeBuffer
+   *
+   * The "buffer-unloaded" signal is emitted when an #IdeBuffer has been
+   * unloaded from the buffer manager.
+   *
+   * Since: 3.32
+   */
+  signals [BUFFER_UNLOADED] =
+    g_signal_new ("buffer-unloaded",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_BUFFER);
+  g_signal_set_va_marshaller (signals [BUFFER_UNLOADED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+}
+
+static void
+ide_buffer_manager_init (IdeBufferManager *self)
+{
+  IDE_ENTRY;
+
+  self->loading_tasks = g_hash_table_new_full (g_file_hash,
+                                               (GEqualFunc)g_file_equal,
+                                               g_object_unref,
+                                               g_object_unref);
+
+  IDE_EXIT;
+}
+
+static GFile *
+ide_buffer_manager_next_temp_file (IdeBufferManager *self)
+{
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GFile) ret = NULL;
+  g_autofree gchar *name = NULL;
+  guint doc_id;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), NULL);
+
+  context = IDE_CONTEXT (ide_object_ref_root (IDE_OBJECT (self)));
+  workdir = ide_context_ref_workdir (context);
+  doc_id = ide_doc_seq_acquire ();
+
+  /* translators: %u is replaced with an incrementing number */
+  name = g_strdup_printf (_("unsaved file %u"), doc_id);
+
+  ret = g_file_get_child (workdir, name);
+
+  IDE_RETURN (g_steal_pointer (&ret));
+}
+
+/**
+ * ide_buffer_manager_from_context:
+ *
+ * Gets the #IdeBufferManager for the #IdeContext.
+ *
+ * Returns: (transfer none): an #IdeBufferManager
+ *
+ * Since: 3.32
+ */
+IdeBufferManager *
+ide_buffer_manager_from_context (IdeContext *context)
+{
+  IdeBufferManager *self;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  self = ide_context_peek_child_typed (context, IDE_TYPE_BUFFER_MANAGER);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), NULL);
+  return self;
+}
+
+/**
+ * ide_buffer_manager_has_file:
+ * @self: an #IdeBufferManager
+ * @file: a #GFile
+ *
+ * Checks to see if a buffer has been loaded which contains the contents
+ * of @file.
+ *
+ * Returns: %TRUE if a buffer exists for @file
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_manager_has_file (IdeBufferManager *self,
+                             GFile            *file)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+
+  return ide_buffer_manager_find_buffer (self, file) != NULL;
+}
+
+static void
+ide_buffer_manager_find_buffer_cb (IdeObject  *object,
+                                   FindBuffer *find)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+
+  g_assert (IDE_IS_OBJECT_BOX (object));
+  g_assert (find != NULL);
+  g_assert (G_IS_FILE (find->file));
+
+  if (find->buffer != NULL)
+    return;
+
+  buffer = ide_object_box_ref_object (IDE_OBJECT_BOX (object));
+
+  /* We pass back a borrowed reference */
+  if (g_file_equal (find->file, ide_buffer_get_file (buffer)))
+    find->buffer = buffer;
+}
+
+/**
+ * ide_buffer_manager_find_buffer:
+ * @self: an #IdeBufferManager
+ * @file: a #GFile
+ *
+ * Locates the #IdeBuffer that matches #GFile, if any.
+ *
+ * Returns: (transfer full): an #IdeBuffer or %NULL
+ *
+ * Since: 3.32
+ */
+IdeBuffer *
+ide_buffer_manager_find_buffer (IdeBufferManager *self,
+                                GFile            *file)
+{
+  FindBuffer find = { file, NULL };
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  ide_object_foreach (IDE_OBJECT (self),
+                      (GFunc)ide_buffer_manager_find_buffer_cb,
+                      &find);
+
+  IDE_RETURN (find.buffer);
+}
+
+/**
+ * ide_buffer_manager_get_max_file_size:
+ * @self: an #IdeBufferManager
+ *
+ * Gets the max file size that will be allowed to be loaded from disk.
+ * This is useful to protect Builder from files that would overload the
+ * various subsystems.
+ *
+ * Returns: the max file size in bytes or -1 for unlimited
+ *
+ * Since: 3.32
+ */
+gssize
+ide_buffer_manager_get_max_file_size (IdeBufferManager *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), 0);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), 0);
+
+  return self->max_file_size;
+}
+
+/**
+ * ide_buffer_manager_set_max_size:
+ * @self: an #IdeBufferManager
+ * @max_file_size: the max file size in bytes or -1 for unlimited
+ *
+ * Sets the max file size that will be allowed to be loaded from disk.
+ * This is useful to protect Builder from files that would overload the
+ * various subsystems.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_manager_set_max_file_size (IdeBufferManager *self,
+                                      gssize            max_file_size)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (max_file_size >= -1);
+
+  if (self->max_file_size != max_file_size)
+    {
+      self->max_file_size = max_file_size;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MAX_FILE_SIZE]);
+    }
+}
+
+static void
+ide_buffer_manager_load_file_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  IdeBuffer *buffer = (IdeBuffer *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeBufferManager *self;
+  GFile *file;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  file = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (G_IS_FILE (file));
+
+  g_hash_table_remove (self->loading_tasks, file);
+
+  if (!_ide_buffer_load_file_finish (buffer, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_object (task, g_object_ref (buffer));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_manager_load_file_async:
+ * @self: an #IdeBufferManager
+ * @file: (nullable): a #GFile
+ * @flags: optional flags for loading the buffer
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @notif: (out) (optional): a location for an #IdeNotification, or %NULL
+ * @callback: a callback to execute upon completion of the operation
+ * @user_data: closure data for @callback
+ *
+ * Requests that @file be loaded by the buffer manager. Depending on @flags,
+ * this may result in a new view being displayed in a Builder workspace.
+ *
+ * If @file is %NULL, then a new temporary file is created with an
+ * incrementing number to denote the document, such as "unsaved file 1".
+ *
+ * After completion, @callback will be executed and you can receive the buffer
+ * that was loaded with ide_buffer_manager_load_file_finish().
+ *
+ * If a buffer has already been loaded from @file, the operation will complete
+ * using that existing buffer.
+ *
+ * If a buffer is currently loading for @file, the operation will complete
+ * using that existing buffer after it has completed loading.
+ *
+ * If @notif is non-NULL, it will be set to a new #IdeNotification which should
+ * be freed with g_object_unref() when no longer in use. It will be kept up to
+ * date with loading progress as the file is loaded.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_manager_load_file_async (IdeBufferManager     *self,
+                                    GFile                *file,
+                                    IdeBufferOpenFlags    flags,
+                                    GCancellable         *cancellable,
+                                    IdeNotification     **notif,
+                                    GAsyncReadyCallback   callback,
+                                    gpointer              user_data)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) temp_file = NULL;
+  IdeBuffer *existing;
+  gboolean create_new_view = FALSE;
+  gboolean is_new = FALSE;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (!file || G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (notif != NULL)
+    *notif = NULL;
+
+  if (file == NULL)
+    file = temp_file = ide_buffer_manager_next_temp_file (self);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_manager_load_file_async);
+  ide_task_set_task_data (task, g_file_dup (file), g_object_unref);
+
+  /* If the file requested has already been opened, then we will return
+   * that (unless a forced reload was requested).
+   */
+  if ((existing = ide_buffer_manager_find_buffer (self, file)))
+    {
+      IdeTask *existing_task;
+
+      /* If the buffer does not need to be reloaded, just return the
+       * buffer to the user now.
+       */
+      if (!(flags & IDE_BUFFER_OPEN_FLAGS_FORCE_RELOAD))
+        {
+          ide_task_return_object (task, g_object_ref (existing));
+          IDE_EXIT;
+        }
+
+      /* If the buffer is still loading, we can just chain onto that
+       * loading operation and complete this task when that task finishes.
+       */
+      if ((existing_task = g_hash_table_lookup (self->loading_tasks, file)))
+        {
+          ide_task_chain (existing_task, task);
+          IDE_EXIT;
+        }
+    }
+  else
+    {
+      /* Create the buffer and track it so we can find it later */
+      buffer = ide_buffer_manager_create_buffer (self, file, temp_file != NULL);
+      is_new = TRUE;
+    }
+
+  /* Save this task for later in case we get in a second request to open
+   * the file while we are already opening it.
+   */
+  g_hash_table_insert (self->loading_tasks, g_file_dup (file), g_object_ref (task));
+
+  /* We might have listeners tracking new buffers. Apply some rules to
+   * determine if we need the UI to create a new view for the buffer.
+   */
+  create_new_view = !(flags & IDE_BUFFER_OPEN_FLAGS_NO_VIEW) &&
+                     (is_new || (flags & IDE_BUFFER_OPEN_FLAGS_BACKGROUND) == 0);
+  g_signal_emit (self, signals [LOAD_BUFFER], 0, buffer, create_new_view);
+
+  /* Now we can load the buffer asynchronously */
+  _ide_buffer_load_file_async (buffer,
+                               cancellable,
+                               notif,
+                               ide_buffer_manager_load_file_cb,
+                               g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_manager_load_file_finish:
+ * @self: an #IdeBufferManager
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_buffer_manager_laod_file_async().
+ *
+ * Returns: (transfer full): an #IdeBuffer
+ *
+ * Since: 3.32
+ */
+IdeBuffer *
+ide_buffer_manager_load_file_finish (IdeBufferManager  *self,
+                                     GAsyncResult      *result,
+                                     GError           **error)
+{
+  IdeBuffer *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_object (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_buffer_manager_save_all_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeBuffer *buffer = (IdeBuffer *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  SaveAll *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+
+  g_assert (state != NULL);
+  g_assert (state->buffers != NULL);
+  g_assert (state->n_active > 0);
+
+  if (!ide_buffer_save_file_finish (buffer, result, &error))
+    {
+      g_warning ("Failed to save buffer “%s”: %s",
+                 ide_buffer_dup_title (buffer),
+                 error->message);
+      state->had_failure = TRUE;
+    }
+
+  state->n_active--;
+
+  if (state->n_active == 0)
+    {
+      if (state->had_failure)
+        ide_task_return_new_error (task,
+                                   G_IO_ERROR,
+                                   G_IO_ERROR_FAILED,
+                                   "One or more buffers failed to save");
+      else
+        ide_task_return_boolean (task, TRUE);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_manager_save_all_foreach_cb (IdeObject *object,
+                                        IdeTask   *task)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+  SaveAll *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_OBJECT_BOX (object));
+  g_assert (IDE_IS_TASK (task));
+
+  buffer = ide_object_box_ref_object (IDE_OBJECT_BOX (object));
+  state = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (state != NULL);
+  g_assert (state->buffers != NULL);
+
+  /* Skip buffers that are loading or saving, as they are already in
+   * the correct form on disk (or will be soon). We somewhat risk beating
+   * an existing save, but that is probably okay to the user since they've
+   * already submitted the save request.
+   */
+  if (ide_buffer_get_state (buffer) != IDE_BUFFER_STATE_READY)
+    return;
+
+  g_ptr_array_add (state->buffers, g_object_ref (buffer));
+
+  state->n_active++;
+
+  ide_buffer_save_file_async (buffer,
+                              NULL,
+                              ide_task_get_cancellable (task),
+                              NULL,
+                              ide_buffer_manager_save_all_cb,
+                              g_object_ref (task));
+}
+
+/**
+ * ide_buffer_manager_save_all_async:
+ * @self: an #IdeBufferManager
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously requests that the #IdeBufferManager save all of the loaded
+ * buffers to disk.
+ *
+ * @callback will be executed after all the buffers have been saved.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_manager_save_all_async (IdeBufferManager    *self,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  SaveAll *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_manager_save_all_async);
+
+  state = g_slice_new0 (SaveAll);
+  state->buffers = g_ptr_array_new_full (0, g_object_unref);
+  ide_task_set_task_data (task, state, save_all_free);
+
+  ide_object_foreach (IDE_OBJECT (self),
+                      (GFunc)ide_buffer_manager_save_all_foreach_cb,
+                      task);
+
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_manager_save_all_finish:
+ * @self: an #IdeBufferManager
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NUL
+ *
+ * Completes an asynchronous request to save all buffers.
+ *
+ * Returns: %TRUE if all the buffers were saved successfully
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_manager_save_all_finish (IdeBufferManager  *self,
+                                    GAsyncResult      *result,
+                                    GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_buffer_manager_apply_edits_save_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  IdeBufferManager *self = (IdeBufferManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_buffer_manager_save_all_finish (self, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_manager_do_apply_edits (IdeBufferManager *self,
+                                   GHashTable       *buffers,
+                                   GPtrArray        *edits)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (buffers != NULL);
+  g_assert (edits != NULL);
+
+  /* Allow each project edit to stage its GtkTextMarks */
+  for (guint i = 0; i < edits->len; i++)
+    {
+      IdeTextEdit *edit = g_ptr_array_index (edits, i);
+      IdeLocation *location;
+      IdeRange *range;
+      IdeBuffer *buffer;
+      GFile *file;
+
+      if (NULL == (range = ide_text_edit_get_range (edit)) ||
+          NULL == (location = ide_range_get_begin (range)) ||
+          NULL == (file = ide_location_get_file (location)) ||
+          NULL == (buffer = g_hash_table_lookup (buffers, file)))
+        {
+          g_warning ("Implausible failure to access buffer");
+          continue;
+        }
+
+      gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+
+      _ide_text_edit_prepare (edit, buffer);
+    }
+
+  /* Now actually perform the replacement between the text marks */
+  for (guint i = 0; i < edits->len; i++)
+    {
+      IdeTextEdit *edit = g_ptr_array_index (edits, i);
+      IdeLocation *location;
+      IdeRange *range;
+      IdeBuffer *buffer;
+      GFile *file;
+
+      if (NULL == (range = ide_text_edit_get_range (edit)) ||
+          NULL == (location = ide_range_get_begin (range)) ||
+          NULL == (file = ide_location_get_file (location)) ||
+          NULL == (buffer = g_hash_table_lookup (buffers, file)))
+        {
+          g_warning ("Implausible failure to access buffer");
+          continue;
+        }
+
+      _ide_text_edit_apply (edit, buffer);
+    }
+
+  /* Complete all of our undo groups */
+  for (guint i = 0; i < edits->len; i++)
+    {
+      IdeTextEdit *edit = g_ptr_array_index (edits, i);
+      IdeLocation *location;
+      IdeRange *range;
+      IdeBuffer *buffer;
+      GFile *file;
+
+      if (NULL == (range = ide_text_edit_get_range (edit)) ||
+          NULL == (location = ide_range_get_begin (range)) ||
+          NULL == (file = ide_location_get_file (location)) ||
+          NULL == (buffer = g_hash_table_lookup (buffers, file)))
+        {
+          g_warning ("Implausible failure to access buffer");
+          continue;
+        }
+
+      gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_manager_apply_edits_buffer_loaded_cb (GObject      *object,
+                                                 GAsyncResult *result,
+                                                 gpointer      user_data)
+{
+  IdeBufferManager *self = (IdeBufferManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeBuffer) buffer = NULL;
+  GCancellable *cancellable;
+  EditState *state;
+  GFile *file;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  cancellable = ide_task_get_cancellable (task);
+  state = ide_task_get_task_data (task);
+
+  g_assert (state != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  state->n_active--;
+
+  /* Get our buffer, if we failed, we won't proceed with edits */
+  if (!(buffer = ide_buffer_manager_load_file_finish (self, result, &error)))
+    {
+      if (state->failed == FALSE)
+        {
+          state->failed = TRUE;
+          ide_task_return_error (task, g_steal_pointer (&error));
+        }
+    }
+
+  /* Nothing to do if we already failed */
+  if (state->failed)
+    IDE_EXIT;
+
+  /* Save the buffer for future use when applying edits */
+  file = ide_buffer_get_file (buffer);
+  g_hash_table_insert (state->buffers, g_object_ref (file), g_object_ref (buffer));
+  g_hash_table_insert (state->to_close, g_object_ref (file), g_object_ref (buffer));
+
+  /* If this is the last buffer to load, then we can go apply the edits. */
+  if (state->n_active == 0)
+    {
+      ide_buffer_manager_do_apply_edits (self,
+                                         state->buffers,
+                                         state->edits);
+      ide_buffer_manager_save_all_async (self,
+                                         cancellable,
+                                         ide_buffer_manager_apply_edits_save_cb,
+                                         g_steal_pointer (&task));
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_manager_apply_edits_completed_cb (IdeBufferManager *self,
+                                             GParamSpec       *pspec,
+                                             IdeTask          *task)
+{
+  GHashTableIter iter;
+  IdeBuffer *buffer;
+  EditState *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+  g_assert (state != NULL);
+  g_assert (state->to_close != NULL);
+  g_assert (state->buffers != NULL);
+
+  g_hash_table_iter_init (&iter, state->to_close);
+
+  while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&buffer))
+    {
+      IdeObjectBox *box = ide_object_box_from_object (G_OBJECT (buffer));
+
+      g_assert (IDE_IS_OBJECT_BOX (box));
+
+      ide_object_destroy (IDE_OBJECT (box));
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_manager_apply_edits_async:
+ * @self: An #IdeBufferManager
+ * @edits: (transfer full) (element-type IdeTextEdit):
+ *   An #GPtrArray of #IdeTextEdit.
+ * @cancellable: (allow-none): a #GCancellable or %NULL
+ * @callback: the callback to complete the request
+ * @user_data: user data for @callback
+ *
+ * Asynchronously requests that all of @edits are applied to the buffers
+ * in the project. If the buffer has not been loaded for a particular edit,
+ * it will be loaded.
+ *
+ * @callback should call ide_buffer_manager_apply_edits_finish() to get the
+ * result of this operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_manager_apply_edits_async (IdeBufferManager    *self,
+                                      GPtrArray           *edits,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  EditState *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (edits != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_PTR_ARRAY_SET_FREE_FUNC (edits, g_object_unref);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_manager_apply_edits_async);
+
+  state = g_slice_new0 (EditState);
+  state->buffers = g_hash_table_new_full (g_file_hash,
+                                          (GEqualFunc)g_file_equal,
+                                          g_object_unref,
+                                          _g_object_unref0);
+  state->to_close = g_hash_table_new_full (g_file_hash,
+                                           (GEqualFunc)g_file_equal,
+                                           g_object_unref,
+                                           _g_object_unref0);
+  state->edits = g_steal_pointer (&edits);
+  ide_task_set_task_data (task, state, edit_state_free);
+
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_buffer_manager_apply_edits_completed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  for (guint i = 0; i < state->edits->len; i++)
+    {
+      IdeTextEdit *edit = g_ptr_array_index (state->edits, i);
+      IdeLocation *location;
+      IdeBuffer *buffer;
+      IdeRange *range;
+      GFile *file;
+
+      if (NULL == (range = ide_text_edit_get_range (edit)) ||
+          NULL == (location = ide_range_get_begin (range)) ||
+          NULL == (file = ide_location_get_file (location)))
+        continue;
+
+      if (g_hash_table_contains (state->buffers, file))
+        continue;
+
+      if ((buffer = ide_buffer_manager_find_buffer (self, file)))
+        {
+          g_hash_table_insert (state->buffers, g_object_ref (file), g_object_ref (buffer));
+          continue;
+        }
+
+      g_hash_table_insert (state->buffers, g_object_ref (file), NULL);
+
+      state->n_active++;
+
+      /* Load buffers, but don't create views for them since we don't want to
+       * create lots of views if there are lots of files to edit.
+       */
+      ide_buffer_manager_load_file_async (self,
+                                          file,
+                                          IDE_BUFFER_OPEN_FLAGS_NO_VIEW,
+                                          cancellable,
+                                          NULL,
+                                          ide_buffer_manager_apply_edits_buffer_loaded_cb,
+                                          g_object_ref (task));
+    }
+
+  IDE_TRACE_MSG ("Waiting for %d buffers to load", state->n_active);
+
+  if (state->n_active == 0)
+    {
+      ide_buffer_manager_do_apply_edits (self, state->buffers, state->edits);
+      ide_buffer_manager_save_all_async (self,
+                                         cancellable,
+                                         ide_buffer_manager_apply_edits_save_cb,
+                                         g_steal_pointer (&task));
+    }
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_buffer_manager_apply_edits_finish (IdeBufferManager  *self,
+                                       GAsyncResult      *result,
+                                       GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+void
+_ide_buffer_manager_buffer_loaded (IdeBufferManager *self,
+                                   IdeBuffer        *buffer)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  g_signal_emit (self, signals [BUFFER_LOADED], 0, buffer);
+}
+
+void
+_ide_buffer_manager_buffer_saved (IdeBufferManager *self,
+                                  IdeBuffer        *buffer)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  g_signal_emit (self, signals [BUFFER_SAVED], 0, buffer);
+}
+
+static GType
+ide_buffer_manager_get_item_type (GListModel *model)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (model));
+
+  return IDE_TYPE_BUFFER;
+}
+
+static gpointer
+ide_buffer_manager_get_item (GListModel *model,
+                             guint       position)
+{
+  IdeBufferManager *self = (IdeBufferManager *)model;
+  g_autoptr(IdeObject) box = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (self));
+
+  if ((box = ide_object_get_nth_child (IDE_OBJECT (self), position)))
+    return ide_object_box_ref_object (IDE_OBJECT_BOX (box));
+
+  return NULL;
+}
+
+static guint
+ide_buffer_manager_get_n_items (GListModel *model)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER_MANAGER (model));
+
+  return ide_object_get_n_children (IDE_OBJECT (model));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_buffer_manager_get_item_type;
+  iface->get_item = ide_buffer_manager_get_item;
+  iface->get_n_items = ide_buffer_manager_get_n_items;
+}
+
+static void
+ide_buffer_manager_foreach_cb (IdeObject *object,
+                               gpointer   user_data)
+{
+  const Foreach *state = user_data;
+
+  g_assert (IDE_IS_OBJECT (object));
+
+  if (IDE_IS_BUFFER (object))
+    state->func (IDE_BUFFER (object), state->user_data);
+}
+
+/**
+ * ide_buffer_manager_foreach:
+ * @self: a #IdeBufferManager
+ * @foreach_func: (scope call): an #IdeBufferForeachFunc
+ * @user_data: closure data for @foreach_func
+ *
+ * Calls @foreach_func for every buffer registered.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_manager_foreach (IdeBufferManager     *self,
+                            IdeBufferForeachFunc  foreach_func,
+                            gpointer              user_data)
+{
+  Foreach state = { foreach_func, user_data };
+
+  g_return_if_fail (IDE_IS_BUFFER_MANAGER (self));
+  g_return_if_fail (foreach_func != NULL);
+
+  ide_object_foreach (IDE_OBJECT (self),
+                      (GFunc)ide_buffer_manager_foreach_cb,
+                      &state);
+}
diff --git a/src/libide/code/ide-buffer-manager.h b/src/libide/code/ide-buffer-manager.h
new file mode 100644
index 000000000..3bbc43342
--- /dev/null
+++ b/src/libide/code/ide-buffer-manager.h
@@ -0,0 +1,119 @@
+/* ide-buffer-manager.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-buffer.h"
+
+G_BEGIN_DECLS
+
+/**
+ * IdeBufferOpenFlags:
+ * @IDE_BUFFER_OPEN_FLAGS_NONE: No special processing will be performed
+ * @IDE_BUFFER_OPEN_FLAGS_BACKGROUND: Open the document in the background (behind current view)
+ * @IDE_BUFFER_OPEN_FLAGS_NO_VIEW: Open the document but do not create a new view for it
+ *
+ * The #IdeBufferOpenFlags enumeration is used to specify how a document should
+ * be opened by the workbench. Plugins may want to have a bit of control over
+ * where the document is opened, and this provides a some control over that.
+ *
+ * Since: 3.32
+ */
+typedef enum
+{
+  IDE_BUFFER_OPEN_FLAGS_NONE         = 0,
+  IDE_BUFFER_OPEN_FLAGS_BACKGROUND   = 1 << 0,
+  IDE_BUFFER_OPEN_FLAGS_NO_VIEW      = 1 << 1,
+  IDE_BUFFER_OPEN_FLAGS_FORCE_RELOAD = 1 << 2,
+} IdeBufferOpenFlags;
+
+/**
+ * IdeBufferForeachFunc:
+ * @buffer: an #IdeBuffer
+ * @user_data: closure data
+ *
+ * Callback prototype for ide_buffer_manager_foreach().
+ *
+ * Since: 3.32
+ */
+typedef void (*IdeBufferForeachFunc) (IdeBuffer *buffer,
+                                      gpointer   user_data);
+
+#define IDE_TYPE_BUFFER_MANAGER (ide_buffer_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeBufferManager, ide_buffer_manager, IDE, BUFFER_MANAGER, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeBufferManager *ide_buffer_manager_from_context       (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void              ide_buffer_manager_foreach            (IdeBufferManager     *self,
+                                                         IdeBufferForeachFunc  foreach_func,
+                                                         gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void              ide_buffer_manager_load_file_async    (IdeBufferManager     *self,
+                                                         GFile                *file,
+                                                         IdeBufferOpenFlags    flags,
+                                                         GCancellable         *cancellable,
+                                                         IdeNotification     **notif,
+                                                         GAsyncReadyCallback   callback,
+                                                         gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+IdeBuffer        *ide_buffer_manager_load_file_finish   (IdeBufferManager     *self,
+                                                         GAsyncResult         *result,
+                                                         GError              **error);
+IDE_AVAILABLE_IN_3_32
+void              ide_buffer_manager_save_all_async     (IdeBufferManager     *self,
+                                                         GCancellable         *cancellable,
+                                                         GAsyncReadyCallback   callback,
+                                                         gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_buffer_manager_save_all_finish    (IdeBufferManager     *self,
+                                                         GAsyncResult         *result,
+                                                         GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_buffer_manager_has_file           (IdeBufferManager     *self,
+                                                         GFile                *file);
+IDE_AVAILABLE_IN_3_32
+IdeBuffer        *ide_buffer_manager_find_buffer        (IdeBufferManager     *self,
+                                                         GFile                *file);
+IDE_AVAILABLE_IN_3_32
+gssize            ide_buffer_manager_get_max_file_size  (IdeBufferManager     *self);
+IDE_AVAILABLE_IN_3_32
+void              ide_buffer_manager_set_max_file_size  (IdeBufferManager     *self,
+                                                         gssize                max_file_size);
+IDE_AVAILABLE_IN_3_32
+void              ide_buffer_manager_apply_edits_async  (IdeBufferManager     *self,
+                                                         GPtrArray            *edits,
+                                                         GCancellable         *cancellable,
+                                                         GAsyncReadyCallback   callback,
+                                                         gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_buffer_manager_apply_edits_finish (IdeBufferManager     *self,
+                                                         GAsyncResult         *result,
+                                                         GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-buffer-private.h b/src/libide/code/ide-buffer-private.h
new file mode 100644
index 000000000..836fda7d7
--- /dev/null
+++ b/src/libide/code/ide-buffer-private.h
@@ -0,0 +1,64 @@
+/* ide-buffer-private.h
+ *
+ * Copyright 2018-2019 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-core.h>
+#include <libide-plugins.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-manager.h"
+#include "ide-highlight-engine.h"
+
+G_BEGIN_DECLS
+
+void                    _ide_buffer_manager_buffer_loaded (IdeBufferManager     *self,
+                                                           IdeBuffer            *buffer);
+void                    _ide_buffer_manager_buffer_saved  (IdeBufferManager     *self,
+                                                           IdeBuffer            *buffer);
+void                    _ide_buffer_cancel_cursor_restore (IdeBuffer            *self);
+gboolean                _ide_buffer_can_restore_cursor    (IdeBuffer            *self);
+IdeExtensionSetAdapter *_ide_buffer_get_addins            (IdeBuffer            *self);
+IdeBuffer              *_ide_buffer_new                   (IdeBufferManager     *self,
+                                                           GFile                *file,
+                                                           gboolean              is_temporary);
+void                    _ide_buffer_attach                (IdeBuffer            *self,
+                                                           IdeObject            *parent);
+void                    _ide_buffer_load_file_async       (IdeBuffer            *self,
+                                                           GCancellable         *cancellable,
+                                                           IdeNotification     **notif,
+                                                           GAsyncReadyCallback   callback,
+                                                           gpointer              user_data);
+gboolean                _ide_buffer_load_file_finish      (IdeBuffer            *self,
+                                                           GAsyncResult         *result,
+                                                           GError              **error);
+void                    _ide_buffer_line_flags_changed    (IdeBuffer            *self);
+void                    _ide_buffer_set_changed_on_volume (IdeBuffer            *self,
+                                                           gboolean              changed_on_volume);
+void                    _ide_buffer_set_read_only         (IdeBuffer            *self,
+                                                           gboolean              read_only);
+IdeHighlightEngine     *_ide_buffer_get_highlight_engine  (IdeBuffer            *self);
+void                    _ide_buffer_set_failure           (IdeBuffer            *self,
+                                                           const GError         *error);
+void                    _ide_buffer_sync_to_unsaved_files (IdeBuffer            *self);
+void                    _ide_buffer_set_file              (IdeBuffer            *self,
+                                                           GFile                *file);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-buffer.c b/src/libide/code/ide-buffer.c
new file mode 100644
index 000000000..6ba46952c
--- /dev/null
+++ b/src/libide/code/ide-buffer.c
@@ -0,0 +1,3675 @@
+/* ide-buffer.c
+ *
+ * Copyright 2018-2019 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 "ide-buffer"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-addin.h"
+#include "ide-buffer-addin-private.h"
+#include "ide-buffer-manager.h"
+#include "ide-buffer-private.h"
+#include "ide-code-enums.h"
+#include "ide-diagnostic.h"
+#include "ide-diagnostics.h"
+#include "ide-file-settings.h"
+#include "ide-formatter.h"
+#include "ide-formatter-options.h"
+#include "ide-location.h"
+#include "ide-highlight-engine.h"
+#include "ide-range.h"
+#include "ide-source-iter.h"
+#include "ide-source-style-scheme.h"
+#include "ide-symbol-resolver.h"
+#include "ide-unsaved-files.h"
+
+#define SETTLING_DELAY_MSEC  333
+
+#define TAG_ERROR            "diagnostician::error"
+#define TAG_WARNING          "diagnostician::warning"
+#define TAG_DEPRECATED       "diagnostician::deprecated"
+#define TAG_NOTE             "diagnostician::note"
+#define TAG_SNIPPET_TAB_STOP "snippet::tab-stop"
+#define TAG_DEFINITION       "action::hover-definition"
+#define TAG_CURRENT_BKPT     "debugger::current-breakpoint"
+
+#define DEPRECATED_COLOR     "#babdb6"
+#define ERROR_COLOR          "#ff0000"
+#define NOTE_COLOR           "#708090"
+#define WARNING_COLOR        "#fcaf3e"
+#define CURRENT_BKPT_FG      "#fffffe"
+#define CURRENT_BKPT_BG      "#fcaf3e"
+
+struct _IdeBuffer
+{
+  GtkSourceBuffer         parent_instance;
+
+  /* Owned references */
+  IdeExtensionSetAdapter *addins;
+  IdeExtensionSetAdapter *symbol_resolvers;
+  IdeExtensionAdapter    *rename_provider;
+  IdeExtensionAdapter    *formatter;
+  IdeBufferManager       *buffer_manager;
+  IdeBufferChangeMonitor *change_monitor;
+  GBytes                 *content;
+  IdeDiagnostics         *diagnostics;
+  GError                 *failure;
+  IdeFileSettings        *file_settings;
+  IdeHighlightEngine     *highlight_engine;
+  GtkSourceFile          *source_file;
+
+  /* Scalars */
+  guint                   change_count;
+  guint                   settling_source;
+  gint                    hold;
+
+  /* Bit-fields */
+  IdeBufferState          state : 3;
+  guint                   can_restore_cursor : 1;
+  guint                   is_temporary : 1;
+  guint                   changed_on_volume : 1;
+  guint                   read_only : 1;
+  guint                   highlight_diagnostics : 1;
+};
+
+typedef struct
+{
+  IdeNotification *notif;
+  GFile           *file;
+  guint            highlight_syntax : 1;
+} LoadState;
+
+typedef struct
+{
+  GFile           *file;
+  IdeNotification *notif;
+} SaveState;
+
+typedef struct
+{
+  GPtrArray   *resolvers;
+  IdeLocation *location;
+  IdeSymbol   *symbol;
+} LookUpSymbolData;
+
+G_DEFINE_TYPE (IdeBuffer, ide_buffer, GTK_SOURCE_TYPE_BUFFER)
+
+enum {
+  PROP_0,
+  PROP_BUFFER_MANAGER,
+  PROP_CHANGE_MONITOR,
+  PROP_CHANGED_ON_VOLUME,
+  PROP_DIAGNOSTICS,
+  PROP_FAILED,
+  PROP_FILE,
+  PROP_FILE_SETTINGS,
+  PROP_HAS_DIAGNOSTICS,
+  PROP_HAS_SYMBOL_RESOLVERS,
+  PROP_HIGHLIGHT_DIAGNOSTICS,
+  PROP_IS_TEMPORARY,
+  PROP_LANGUAGE_ID,
+  PROP_READ_ONLY,
+  PROP_STATE,
+  PROP_STYLE_SCHEME_NAME,
+  PROP_TITLE,
+  N_PROPS
+};
+
+enum {
+  CHANGE_SETTLED,
+  CURSOR_MOVED,
+  LINE_FLAGS_CHANGED,
+  LOADED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void     lookup_symbol_data_free            (LookUpSymbolData       *data);
+static void     apply_style                        (GtkTextTag             *tag,
+                                                    const gchar            *first_property,
+                                                    ...);
+static void     load_state_free                    (LoadState              *state);
+static void     save_state_free                    (SaveState              *state);
+static void     ide_buffer_save_file_cb            (GObject                *object,
+                                                    GAsyncResult           *result,
+                                                    gpointer                user_data);
+static void     ide_buffer_load_file_cb            (GObject                *object,
+                                                    GAsyncResult           *result,
+                                                    gpointer                user_data);
+static void     ide_buffer_progress_cb             (goffset                 current_num_bytes,
+                                                    goffset                 total_num_bytes,
+                                                    gpointer                user_data);
+static void     ide_buffer_get_property            (GObject                *object,
+                                                    guint                   prop_id,
+                                                    GValue                 *value,
+                                                    GParamSpec             *pspec);
+static void     ide_buffer_set_property            (GObject                *object,
+                                                    guint                   prop_id,
+                                                    const GValue           *value,
+                                                    GParamSpec             *pspec);
+static void     ide_buffer_constructed             (GObject                *object);
+static void     ide_buffer_dispose                 (GObject                *object);
+static void     ide_buffer_notify_language         (IdeBuffer              *self,
+                                                    GParamSpec             *pspec,
+                                                    gpointer                user_data);
+static void     ide_buffer_notify_style_scheme     (IdeBuffer              *self,
+                                                    GParamSpec             *pspec,
+                                                    gpointer                unused);
+static void     ide_buffer_reload_file_settings    (IdeBuffer              *self);
+static void     ide_buffer_set_file_settings       (IdeBuffer              *self,
+                                                    IdeFileSettings        *file_settings);
+static void     ide_buffer_emit_cursor_moved       (IdeBuffer              *self);
+static void     ide_buffer_changed                 (GtkTextBuffer          *buffer);
+static void     ide_buffer_delete_range            (GtkTextBuffer          *buffer,
+                                                    GtkTextIter            *start,
+                                                    GtkTextIter            *end);
+static void     ide_buffer_insert_text             (GtkTextBuffer          *buffer,
+                                                    GtkTextIter            *location,
+                                                    const gchar            *text,
+                                                    gint                    len);
+static void     ide_buffer_mark_set                (GtkTextBuffer          *buffer,
+                                                    const GtkTextIter      *iter,
+                                                    GtkTextMark            *mark);
+static void     ide_buffer_delay_settling          (IdeBuffer              *self);
+static gboolean ide_buffer_settled_cb              (gpointer                user_data);
+static void     ide_buffer_apply_diagnostics       (IdeBuffer              *self);
+static void     ide_buffer_clear_diagnostics       (IdeBuffer              *self);
+static void     ide_buffer_apply_diagnostic        (IdeBuffer              *self,
+                                                    IdeDiagnostic          *diagnostics);
+static void     ide_buffer_init_tags               (IdeBuffer              *self);
+static void     ide_buffer_on_tag_added            (IdeBuffer              *self,
+                                                    GtkTextTag             *tag,
+                                                    GtkTextTagTable        *table);
+static void     ide_buffer_get_symbol_resolvers_cb (IdeExtensionSetAdapter *set,
+                                                    PeasPluginInfo         *plugin_info,
+                                                    PeasExtension          *exten,
+                                                    gpointer                user_data);
+static void     ide_buffer_symbol_resolver_removed (IdeExtensionSetAdapter *adapter,
+                                                    PeasPluginInfo         *plugin_info,
+                                                    PeasExtension          *extension,
+                                                    gpointer                user_data);
+static void     ide_buffer_symbol_resolver_added   (IdeExtensionSetAdapter *adapter,
+                                                    PeasPluginInfo         *plugin_info,
+                                                    PeasExtension          *extension,
+                                                    gpointer                user_data);
+static gboolean ide_buffer_can_do_newline_hack     (IdeBuffer              *self,
+                                                    guint                   len);
+static void     ide_buffer_guess_language          (IdeBuffer              *self);
+static void     ide_buffer_real_loaded             (IdeBuffer              *self);
+
+static void
+load_state_free (LoadState *state)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (state != NULL);
+
+  g_clear_object (&state->notif);
+  g_clear_object (&state->file);
+  g_slice_free (LoadState, state);
+}
+
+static void
+save_state_free (SaveState *state)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (state != NULL);
+
+  g_clear_object (&state->notif);
+  g_clear_object (&state->file);
+  g_slice_free (SaveState, state);
+}
+
+static void
+lookup_symbol_data_free (LookUpSymbolData *data)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_pointer (&data->resolvers, g_ptr_array_unref);
+  g_clear_object (&data->location);
+  g_clear_object (&data->symbol);
+  g_slice_free (LookUpSymbolData, data);
+}
+
+IdeBuffer *
+_ide_buffer_new (IdeBufferManager *buffer_manager,
+                 GFile            *file,
+                 gboolean          is_temporary)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER_MANAGER (buffer_manager), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  return g_object_new (IDE_TYPE_BUFFER,
+                       "buffer-manager", buffer_manager,
+                       "file", file,
+                       "is-temporary", is_temporary,
+                       NULL);
+}
+
+void
+_ide_buffer_set_file (IdeBuffer *self,
+                      GFile     *file)
+{
+  GFile *location;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  location = gtk_source_file_get_location (self->source_file);
+
+  if (location == NULL || !g_file_equal (file, location))
+    {
+      gtk_source_file_set_location (self->source_file, file);
+      ide_buffer_reload_file_settings (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE]);
+    }
+}
+
+static void
+ide_buffer_set_state (IdeBuffer      *self,
+                      IdeBufferState  state)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (state == IDE_BUFFER_STATE_READY ||
+                    state == IDE_BUFFER_STATE_LOADING ||
+                    state == IDE_BUFFER_STATE_SAVING ||
+                    state == IDE_BUFFER_STATE_FAILED);
+
+  if (self->state != state)
+    {
+      self->state = state;
+      if (self->state != IDE_BUFFER_STATE_FAILED)
+        g_clear_pointer (&self->failure, g_error_free);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATE]);
+    }
+}
+
+static void
+ide_buffer_real_loaded (IdeBuffer *self)
+{
+  g_assert (IDE_IS_BUFFER (self));
+
+  if (self->buffer_manager != NULL)
+    _ide_buffer_manager_buffer_loaded (self->buffer_manager, self);
+}
+
+static void
+ide_buffer_notify_language (IdeBuffer  *self,
+                            GParamSpec *pspec,
+                            gpointer    user_data)
+{
+  const gchar *lang_id;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  ide_buffer_reload_file_settings (self);
+
+  lang_id = ide_buffer_get_language_id (self);
+
+  if (self->addins != NULL)
+    {
+      IdeBufferLanguageSet state = { self, lang_id };
+
+      ide_extension_set_adapter_set_value (self->addins, state.language_id);
+      ide_extension_set_adapter_foreach (self->addins,
+                                         _ide_buffer_addin_language_set_cb,
+                                         &state);
+    }
+
+  if (self->symbol_resolvers)
+    ide_extension_set_adapter_set_value (self->symbol_resolvers, lang_id);
+
+  if (self->rename_provider)
+    ide_extension_adapter_set_value (self->rename_provider, lang_id);
+
+  if (self->formatter)
+    ide_extension_adapter_set_value (self->formatter, lang_id);
+}
+
+static void
+ide_buffer_constructed (GObject *object)
+{
+  IdeBuffer *self = (IdeBuffer *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  G_OBJECT_CLASS (ide_buffer_parent_class)->constructed (object);
+
+  ide_buffer_init_tags (self);
+}
+
+static void
+ide_buffer_dispose (GObject *object)
+{
+  IdeBuffer *self = (IdeBuffer *)object;
+  IdeObjectBox *box;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_handle_id (&self->settling_source, g_source_remove);
+
+  /* Remove ourselves from the object-tree if necessary */
+  if ((box = ide_object_box_from_object (object)) &&
+      !ide_object_in_destruction (IDE_OBJECT (box)))
+    ide_object_destroy (IDE_OBJECT (box));
+
+  ide_clear_and_destroy_object (&self->addins);
+  ide_clear_and_destroy_object (&self->rename_provider);
+  ide_clear_and_destroy_object (&self->symbol_resolvers);
+  ide_clear_and_destroy_object (&self->formatter);
+  ide_clear_and_destroy_object (&self->highlight_engine);
+  g_clear_object (&self->buffer_manager);
+  ide_clear_and_destroy_object (&self->change_monitor);
+  g_clear_pointer (&self->content, g_bytes_unref);
+  g_clear_object (&self->diagnostics);
+  ide_clear_and_destroy_object (&self->file_settings);
+
+  G_OBJECT_CLASS (ide_buffer_parent_class)->dispose (object);
+}
+
+static void
+ide_buffer_finalize (GObject *object)
+{
+  IdeBuffer *self = (IdeBuffer *)object;
+
+  g_clear_object (&self->source_file);
+  g_clear_pointer (&self->failure, g_error_free);
+
+  G_OBJECT_CLASS (ide_buffer_parent_class)->finalize (object);
+}
+
+static void
+ide_buffer_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  IdeBuffer *self = IDE_BUFFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CHANGE_MONITOR:
+      g_value_set_object (value, ide_buffer_get_change_monitor (self));
+      break;
+
+    case PROP_CHANGED_ON_VOLUME:
+      g_value_set_boolean (value, ide_buffer_get_changed_on_volume (self));
+      break;
+
+    case PROP_DIAGNOSTICS:
+      g_value_set_object (value, ide_buffer_get_diagnostics (self));
+      break;
+
+    case PROP_FAILED:
+      g_value_set_boolean (value, ide_buffer_get_failed (self));
+      break;
+
+    case PROP_FILE:
+      g_value_set_object (value, ide_buffer_get_file (self));
+      break;
+
+    case PROP_FILE_SETTINGS:
+      g_value_set_object (value, ide_buffer_get_file_settings (self));
+      break;
+
+    case PROP_HAS_DIAGNOSTICS:
+      g_value_set_boolean (value, ide_buffer_has_diagnostics (self));
+      break;
+
+    case PROP_HAS_SYMBOL_RESOLVERS:
+      g_value_set_boolean (value, ide_buffer_has_symbol_resolvers (self));
+      break;
+
+    case PROP_HIGHLIGHT_DIAGNOSTICS:
+      g_value_set_boolean (value, ide_buffer_get_highlight_diagnostics (self));
+      break;
+
+    case PROP_LANGUAGE_ID:
+      g_value_set_string (value, ide_buffer_get_language_id (self));
+      break;
+
+    case PROP_IS_TEMPORARY:
+      g_value_set_boolean (value, ide_buffer_get_is_temporary (self));
+      break;
+
+    case PROP_READ_ONLY:
+      g_value_set_boolean (value, ide_buffer_get_read_only (self));
+      break;
+
+    case PROP_STATE:
+      g_value_set_enum (value, ide_buffer_get_state (self));
+      break;
+
+    case PROP_STYLE_SCHEME_NAME:
+      g_value_set_string (value, ide_buffer_get_style_scheme_name (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_take_string (value, ide_buffer_dup_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  IdeBuffer *self = IDE_BUFFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER_MANAGER:
+      self->buffer_manager = g_value_dup_object (value);
+      break;
+
+    case PROP_CHANGE_MONITOR:
+      ide_buffer_set_change_monitor (self, g_value_get_object (value));
+      break;
+
+    case PROP_DIAGNOSTICS:
+      ide_buffer_set_diagnostics (self, g_value_get_object (value));
+      break;
+
+    case PROP_FILE:
+      _ide_buffer_set_file (self, g_value_get_object (value));
+      break;
+
+    case PROP_HIGHLIGHT_DIAGNOSTICS:
+      ide_buffer_set_highlight_diagnostics (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_LANGUAGE_ID:
+      ide_buffer_set_language_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_IS_TEMPORARY:
+      self->is_temporary = g_value_get_boolean (value);
+      break;
+
+    case PROP_STYLE_SCHEME_NAME:
+      ide_buffer_set_style_scheme_name (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buffer_class_init (IdeBufferClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkTextBufferClass *buffer_class = GTK_TEXT_BUFFER_CLASS (klass);
+
+  object_class->constructed = ide_buffer_constructed;
+  object_class->dispose = ide_buffer_dispose;
+  object_class->finalize = ide_buffer_finalize;
+  object_class->get_property = ide_buffer_get_property;
+  object_class->set_property = ide_buffer_set_property;
+
+  buffer_class->changed = ide_buffer_changed;
+  buffer_class->delete_range = ide_buffer_delete_range;
+  buffer_class->insert_text = ide_buffer_insert_text;
+  buffer_class->mark_set = ide_buffer_mark_set;
+
+  /**
+   * IdeBuffer:buffer-manager:
+   *
+   * Sets the "buffer-manager" property, which is used by the buffer to
+   * clean-up state when the buffer is no longer in use.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_BUFFER_MANAGER] =
+    g_param_spec_object ("buffer-manager",
+                         "Buffer Manager",
+                         "The buffer manager for the context.",
+                         IDE_TYPE_BUFFER_MANAGER,
+                         (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:change-monitor:
+   *
+   * The "change-monitor" property is an #IdeBufferChangeMonitor that will be
+   * used to track changes in the #IdeBuffer. This can be used to show line
+   * changes in the editor gutter.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CHANGE_MONITOR] =
+    g_param_spec_object ("change-monitor",
+                         "Change Monitor",
+                         "Change Monitor",
+                         IDE_TYPE_BUFFER_CHANGE_MONITOR,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:changed-on-volume:
+   *
+   * The "changed-on-volume" property is set to %TRUE when it has been
+   * discovered that the file represented by the #IdeBuffer has changed
+   * externally to Builder.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CHANGED_ON_VOLUME] =
+    g_param_spec_boolean ("changed-on-volume",
+                          "Changed On Volume",
+                          "If the buffer has been modified externally",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:diagnostics:
+   *
+   * The "diagnostics" property contains an #IdeDiagnostics that represent
+   * the diagnostics found in the buffer.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_DIAGNOSTICS] =
+    g_param_spec_object ("diagnostics",
+                         "Diagnostics",
+                         "The diagnostics for the buffer",
+                         IDE_TYPE_DIAGNOSTICS,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:failed:
+   *
+   * The "failed" property is %TRUE when the buffer has entered a failed
+   * state such as when loading or saving the buffer to disk.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_FAILED] =
+    g_param_spec_boolean ("failed",
+                          "Failed",
+                          "If the buffer has entered a failed state",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:file:
+   *
+   * The "file" property is the underlying file represented by the buffer.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_FILE] =
+    g_param_spec_object ("file",
+                         "File",
+                         "The file the buffer represents",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:file-settings:
+   *
+   * The "file-settings" property are the settings to be used by the buffer
+   * and source-view for the underlying file.
+   *
+   * These are automatically discovered and kept up to date based on the
+   * #IdeFileSettings extension points.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_FILE_SETTINGS] =
+    g_param_spec_object ("file-settings",
+                         "File Settings",
+                         "The file settings for the buffer",
+                         IDE_TYPE_FILE_SETTINGS,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:has-diagnostics:
+   *
+   * The "has-diagnostics" property denotes that there are a non-zero number
+   * of diangostics registered for the buffer.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_DIAGNOSTICS] =
+    g_param_spec_boolean ("has-diagnostics",
+                          "Has Diagnostics",
+                          "The diagnostics for the buffer",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:has-symbol-resolvers:
+   *
+   * The "has-symbol-resolvers" property is %TRUE if there are any symbol
+   * resolvers loaded.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_SYMBOL_RESOLVERS] =
+    g_param_spec_boolean ("has-symbol-resolvers",
+                          "Has symbol resolvers",
+                          "If there is at least one symbol resolver available",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:highlight-diagnostics:
+   *
+   * The "highlight-diagnostics" property indicates that diagnostics which
+   * are discovered should be styled.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HIGHLIGHT_DIAGNOSTICS] =
+    g_param_spec_boolean ("highlight-diagnostics",
+                          "Highlight Diagnostics",
+                          "If diagnostics should be highlighted",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:is-temporary:
+   *
+   * The "is-temporary" property denotes the #IdeBuffer:file property points
+   * to a temporary file. When saving the the buffer, various UI components
+   * know to check this property and provide a file chooser to allow the user
+   * to select the destination file.
+   *
+   * Upon saving the file, the property will change to %FALSE.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_IS_TEMPORARY] =
+    g_param_spec_boolean ("is-temporary",
+                          "Is Temporary",
+                          "If the file property is a temporary file",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:language-id:
+   *
+   * The "language-id" property is a convenience property to set the
+   * #GtkSourceBuffer:langauge property using a string name.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_LANGUAGE_ID] =
+    g_param_spec_string ("language-id",
+                         "Language Id",
+                         "The language identifier as a string",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:read-only:
+   *
+   * The "read-only" property is set to %TRUE when it has been
+   * discovered that the file represented by the #IdeBuffer is read-only
+   * on the underlying storage.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_READ_ONLY] =
+    g_param_spec_boolean ("read-only",
+                          "Read Only",
+                          "If the buffer's file is read-only",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:state:
+   *
+   * The "state" property can be used to determine if the buffer is
+   * currently performing any specific background work, such as loading
+   * from or saving a buffer to storage.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_STATE] =
+    g_param_spec_enum ("state",
+                       "State",
+                       "The state for the buffer",
+                       IDE_TYPE_BUFFER_STATE,
+                       IDE_BUFFER_STATE_READY,
+                       (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:style-scheme-name:
+   *
+   * The "style-scheme-name" is the name of the style scheme that is used.
+   * It is a convenience property so that you do not need to use the
+   * #GtkSourceStyleSchemeManager to lookup style schemes.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_STYLE_SCHEME_NAME] =
+    g_param_spec_string ("style-scheme-name",
+                         "Style Scheme Name",
+                         "The name of the GtkSourceStyleScheme to use",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuffer:title:
+   *
+   * The "title" for the buffer which includes some variant of the path
+   * to the underlying file.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title for the buffer",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeBuffer::change-settled:
+   * @self: an #IdeBuffer
+   *
+   * The "change-settled" signal is emitted when the buffer has stopped
+   * being edited for a short period of time. This is useful to connect
+   * to when you want to perform work as the user is editing, but you
+   * don't want to get in the way of their editing.
+   *
+   * Since: 3.32
+   */
+  signals [CHANGE_SETTLED] =
+    g_signal_new ("change-settled",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [CHANGE_SETTLED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+
+  /**
+   * IdeBuffer::cursor-moved:
+   * @self: an #IdeBuffer
+   * @location: a #GtkTextIter
+   *
+   * This signal is emitted when the insertion location has moved. You might
+   * want to attach to this signal to update the location of the insert mark in
+   * the display.
+   *
+   * Since: 3.32
+   */
+  signals [CURSOR_MOVED] =
+    g_signal_new ("cursor-moved",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_VOID__BOXED,
+                  G_TYPE_NONE,
+                  1,
+                  GTK_TYPE_TEXT_ITER | G_SIGNAL_TYPE_STATIC_SCOPE);
+  g_signal_set_va_marshaller (signals [CURSOR_MOVED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__BOXEDv);
+
+  /**
+   * IdeBuffer::line-flags-changed:
+   * @self: an #IdeBuffer
+   *
+   * The "line-flags-changed" signal is emitted when the buffer has detected
+   * ancillary information has changed for lines in the buffer. Such information
+   * might include diagnostics or version control information.
+   *
+   * Since: 3.32
+   */
+  signals [LINE_FLAGS_CHANGED] =
+    g_signal_new_class_handler ("line-flags-changed",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                NULL,
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [LINE_FLAGS_CHANGED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+
+  /**
+   * IdeBuffer::loaded:
+   * @self: an #IdeBuffer
+   *
+   * The "loaded" signal is emitted after the buffer is loaded.
+   *
+   * This is useful to watch if you want to perform a given action but do
+   * not want to interfere with buffer loading.
+   *
+   * Since: 3.32
+   */
+  signals [LOADED] =
+    g_signal_new_class_handler ("loaded",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_buffer_real_loaded),
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [LOADED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+}
+
+static void
+ide_buffer_init (IdeBuffer *self)
+{
+  self->source_file = gtk_source_file_new ();
+  self->can_restore_cursor = TRUE;
+  self->highlight_diagnostics = TRUE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_signal_connect (self,
+                    "notify::language",
+                    G_CALLBACK (ide_buffer_notify_language),
+                    NULL);
+
+  g_signal_connect (self,
+                    "notify::style-scheme",
+                    G_CALLBACK (ide_buffer_notify_style_scheme),
+                    NULL);
+}
+
+static void
+ide_buffer_rename_provider_notify_extension (IdeBuffer           *self,
+                                             GParamSpec          *pspec,
+                                             IdeExtensionAdapter *adapter)
+{
+  IdeRenameProvider *provider;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (IDE_IS_EXTENSION_ADAPTER (adapter));
+
+  if ((provider = ide_extension_adapter_get_extension (adapter)))
+    {
+      g_object_set (provider, "buffer", self, NULL);
+      ide_rename_provider_load (provider);
+    }
+}
+
+static void
+ide_buffer_formatter_notify_extension (IdeBuffer           *self,
+                                       GParamSpec          *pspec,
+                                       IdeExtensionAdapter *adapter)
+{
+  IdeFormatter *formatter;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (IDE_IS_EXTENSION_ADAPTER (adapter));
+
+  if ((formatter = ide_extension_adapter_get_extension (adapter)))
+    ide_formatter_load (formatter);
+}
+
+static void
+ide_buffer_symbol_resolver_added (IdeExtensionSetAdapter *adapter,
+                                  PeasPluginInfo         *plugin_info,
+                                  PeasExtension          *extension,
+                                  gpointer                user_data)
+{
+  IdeSymbolResolver *resolver = (IdeSymbolResolver *)extension;
+  IdeBuffer *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_SYMBOL_RESOLVER (resolver));
+  g_assert (IDE_IS_BUFFER (self));
+
+  IDE_TRACE_MSG ("Loading symbol resolver %s", G_OBJECT_TYPE_NAME (resolver));
+
+  ide_symbol_resolver_load (resolver);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_SYMBOL_RESOLVERS]);
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_symbol_resolver_removed (IdeExtensionSetAdapter *adapter,
+                                    PeasPluginInfo         *plugin_info,
+                                    PeasExtension          *extension,
+                                    gpointer                user_data)
+{
+  IdeSymbolResolver *resolver = (IdeSymbolResolver *)extension;
+  IdeBuffer *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_SYMBOL_RESOLVER (resolver));
+  g_assert (IDE_IS_BUFFER (self));
+
+  IDE_TRACE_MSG ("Unloading symbol resolver %s", G_OBJECT_TYPE_NAME (resolver));
+
+  ide_symbol_resolver_unload (resolver);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_SYMBOL_RESOLVERS]);
+
+  IDE_EXIT;
+}
+
+void
+_ide_buffer_attach (IdeBuffer *self,
+                    IdeObject *parent)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_OBJECT_BOX (parent));
+  g_return_if_fail (ide_object_box_contains (IDE_OBJECT_BOX (parent), self));
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (self->addins == NULL);
+  g_return_if_fail (self->highlight_engine == NULL);
+  g_return_if_fail (self->formatter == NULL);
+  g_return_if_fail (self->rename_provider == NULL);
+
+  /* Setup the semantic highlight engine */
+  self->highlight_engine = ide_highlight_engine_new (self);
+
+  /* Load buffer addins */
+  self->addins = ide_extension_set_adapter_new (parent,
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_BUFFER_ADDIN,
+                                                "Buffer-Addin-Languages",
+                                                ide_buffer_get_language_id (self));
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (_ide_buffer_addin_load_cb),
+                    self);
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (_ide_buffer_addin_unload_cb),
+                    self);
+  ide_extension_set_adapter_foreach (self->addins,
+                                     _ide_buffer_addin_load_cb,
+                                     self);
+
+  /* Setup our rename provider, if any */
+  self->rename_provider = ide_extension_adapter_new (parent,
+                                                     peas_engine_get_default (),
+                                                     IDE_TYPE_RENAME_PROVIDER,
+                                                     "Rename-Provider-Languages",
+                                                     ide_buffer_get_language_id (self));
+  g_signal_connect_object (self->rename_provider,
+                           "notify::extension",
+                           G_CALLBACK (ide_buffer_rename_provider_notify_extension),
+                           self,
+                           G_CONNECT_SWAPPED);
+  ide_buffer_rename_provider_notify_extension (self, NULL, self->rename_provider);
+
+  /* Setup our formatter, if any */
+  self->formatter = ide_extension_adapter_new (parent,
+                                               peas_engine_get_default (),
+                                               IDE_TYPE_FORMATTER,
+                                               "Formatter-Languages",
+                                               ide_buffer_get_language_id (self));
+  g_signal_connect_object (self->formatter,
+                           "notify::extension",
+                           G_CALLBACK (ide_buffer_formatter_notify_extension),
+                           self,
+                           G_CONNECT_SWAPPED);
+  ide_buffer_formatter_notify_extension (self, NULL, self->formatter);
+
+  /* Setup symbol resolvers */
+  self->symbol_resolvers = ide_extension_set_adapter_new (parent,
+                                                          peas_engine_get_default (),
+                                                          IDE_TYPE_SYMBOL_RESOLVER,
+                                                          "Symbol-Resolver-Languages",
+                                                          ide_buffer_get_language_id (self));
+  g_signal_connect_object (self->symbol_resolvers,
+                           "extension-added",
+                           G_CALLBACK (ide_buffer_symbol_resolver_added),
+                           self,
+                           0);
+  g_signal_connect_object (self->symbol_resolvers,
+                           "extension-removed",
+                           G_CALLBACK (ide_buffer_symbol_resolver_removed),
+                           self,
+                           0);
+  ide_extension_set_adapter_foreach (self->symbol_resolvers,
+                                     ide_buffer_symbol_resolver_added,
+                                     self);
+}
+
+/**
+ * ide_buffer_get_file:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeBuffer:file property.
+ *
+ * Returns: (transfer none): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_buffer_get_file (IdeBuffer *self)
+{
+  GFile *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  ret = gtk_source_file_get_location (self->source_file);
+
+  g_return_val_if_fail (G_IS_FILE (ret), NULL);
+
+  return ret;
+}
+
+/**
+ * ide_buffer_dup_uri:
+ * @self: a #IdeBuffer
+ *
+ * Gets the URI for the underlying file and returns a copy of it.
+ *
+ * Returns: (transfer full): a new string
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_buffer_dup_uri (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return g_file_get_uri (ide_buffer_get_file (self));
+}
+
+/**
+ * ide_buffer_get_is_temporary:
+ *
+ * Checks if the buffer represents a temporary file.
+ *
+ * This is useful to check by views that want to provide a save-as dialog
+ * when the user requests to save the buffer.
+ *
+ * Returns: %TRUE if the buffer is for a temporary file
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_is_temporary (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->is_temporary;
+}
+
+/**
+ * ide_buffer_get_state:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeBuffer:state property.
+ *
+ * This will changed while files are loaded or saved to disk.
+ *
+ * Returns: an #IdeBufferState
+ *
+ * Since: 3.32
+ */
+IdeBufferState
+ide_buffer_get_state (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), 0);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), 0);
+
+  return self->state;
+}
+
+static void
+ide_buffer_progress_cb (goffset  current_num_bytes,
+                        goffset  total_num_bytes,
+                        gpointer user_data)
+{
+  IdeNotification *notif = user_data;
+  gdouble progress = 0.0;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  if (total_num_bytes)
+    progress = (gdouble)current_num_bytes / (gdouble)total_num_bytes;
+
+  ide_notification_set_progress (notif, progress);
+}
+
+static void
+ide_buffer_load_file_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  GtkSourceFileLoader *loader = (GtkSourceFileLoader *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GtkTextIter iter;
+  LoadState *state;
+  IdeBuffer *self;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GTK_SOURCE_IS_FILE_LOADER (loader));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  state = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (state != NULL);
+  g_assert (G_IS_FILE (state->file));
+  g_assert (IDE_IS_NOTIFICATION (state->notif));
+
+  if (!gtk_source_file_loader_load_finish (loader, result, &error))
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        {
+          ide_buffer_set_state (self, IDE_BUFFER_STATE_FAILED);
+          ide_notification_set_progress (state->notif, 0.0);
+          ide_task_return_error (task, g_steal_pointer (&error));
+          IDE_EXIT;
+        }
+
+      g_clear_error (&error);
+    }
+
+  /* First move the insert cursor back to 0:0, plugins might move it
+   * but we certainly don't want to leave it at the end.
+   */
+  gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (self), &iter);
+  gtk_text_buffer_select_range (GTK_TEXT_BUFFER (self), &iter, &iter);
+
+  ide_highlight_engine_unpause (self->highlight_engine);
+  ide_buffer_set_state (self, IDE_BUFFER_STATE_READY);
+  ide_notification_set_progress (state->notif, 1.0);
+  ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+void
+_ide_buffer_load_file_async (IdeBuffer            *self,
+                             GCancellable         *cancellable,
+                             IdeNotification     **notif,
+                             GAsyncReadyCallback   callback,
+                             gpointer              user_data)
+{
+  g_autoptr(GtkSourceFileLoader) loader = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  LoadState *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (ide_buffer_get_file (self) != NULL);
+  ide_clear_param (notif, NULL);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, _ide_buffer_load_file_async);
+
+  if (self->state != IDE_BUFFER_STATE_READY &&
+      self->state != IDE_BUFFER_STATE_FAILED)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_BUSY,
+                                 "Cannot load file while buffer is busy");
+      IDE_EXIT;
+    }
+
+  state = g_slice_new0 (LoadState);
+  state->file = g_object_ref (ide_buffer_get_file (self));
+  state->notif = ide_notification_new ();
+  state->highlight_syntax = gtk_source_buffer_get_highlight_syntax (GTK_SOURCE_BUFFER (self));
+  ide_task_set_task_data (task, state, load_state_free);
+
+  ide_buffer_set_state (self, IDE_BUFFER_STATE_LOADING);
+
+  /* Disable some features while we reload */
+  gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (self), FALSE);
+  ide_highlight_engine_pause (self->highlight_engine);
+
+  loader = gtk_source_file_loader_new (GTK_SOURCE_BUFFER (self), self->source_file);
+  gtk_source_file_loader_load_async (loader,
+                                     G_PRIORITY_DEFAULT,
+                                     cancellable,
+                                     ide_buffer_progress_cb,
+                                     g_object_ref (state->notif),
+                                     g_object_unref,
+                                     ide_buffer_load_file_cb,
+                                     g_steal_pointer (&task));
+
+  /* Load file settings immediately so that we can increase the chance
+   * they are settled by the the load operation is finished. The modelines
+   * file settings will auto-monitor for IdeBufferManager::buffer-loaded
+   * and settle the file settings when we complete.
+   */
+  ide_buffer_reload_file_settings (self);
+
+  if (notif != NULL)
+    *notif = g_object_ref (state->notif);
+
+  IDE_EXIT;
+}
+
+/**
+ * _ide_buffer_load_file_finish:
+ * @self: an #IdeBuffer
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * This should be called by the buffer manager to complete loading the initial
+ * state of a buffer. It can also be used to reload a buffer after it was
+ * modified on disk.
+ *
+ * You MUST call this function after using _ide_buffer_load_file_async() so
+ * that the completion of signals and addins may be notified.
+ *
+ * Returns: %TRUE if the file was successfully loaded
+ *
+ * Since: 3.32
+ */
+gboolean
+_ide_buffer_load_file_finish (IdeBuffer     *self,
+                              GAsyncResult  *result,
+                              GError       **error)
+{
+  LoadState *state;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  if (!ide_task_propagate_boolean (IDE_TASK (result), error))
+    return FALSE;
+
+  /* Restore various buffer features we disabled while loading */
+  state = ide_task_get_task_data (IDE_TASK (result));
+  if (state->highlight_syntax)
+    gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (self), TRUE);
+
+  /* Let consumers know they can access the buffer now */
+  g_signal_emit (self, signals [LOADED], 0);
+
+  /* Notify buffer addins that a file has been loaded */
+  if (self->addins != NULL)
+    {
+      IdeBufferFileLoad closure = { self, state->file };
+      ide_extension_set_adapter_foreach (self->addins,
+                                         _ide_buffer_addin_file_loaded_cb,
+                                         &closure);
+    }
+
+  return TRUE;
+}
+
+static void
+ide_buffer_save_file_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  GtkSourceFileSaver *saver = (GtkSourceFileSaver *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeBuffer *self;
+  SaveState *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GTK_SOURCE_IS_FILE_SAVER (saver));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  state = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (state != NULL);
+  g_assert (G_IS_FILE (state->file));
+  g_assert (IDE_IS_NOTIFICATION (state->notif));
+
+  if (!gtk_source_file_saver_save_finish (saver, result, &error))
+    {
+      ide_notification_set_progress (state->notif, 0.0);
+      ide_buffer_set_state (self, IDE_BUFFER_STATE_FAILED);
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  ide_notification_set_progress (state->notif, 1.0);
+  ide_buffer_set_state (self, IDE_BUFFER_STATE_READY);
+
+  /* Notify addins that a save has completed */
+  if (self->addins != NULL)
+    {
+      IdeBufferFileSave closure = { self, state->file };
+      ide_extension_set_adapter_foreach (self->addins,
+                                         _ide_buffer_addin_file_saved_cb,
+                                         &closure);
+    }
+
+  if (self->buffer_manager)
+    _ide_buffer_manager_buffer_saved (self->buffer_manager, self);
+
+  ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_save_file_async:
+ * @self: an #IdeBuffer
+ * @file: (nullable): a #GFile or %NULL
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously saves the buffer contents to @file.
+ *
+ * If @file is %NULL, then the #IdeBuffer:file property is used.
+ *
+ * The buffer is marked as busy during the operation, and must not have
+ * further editing until the operation is complete.
+ *
+ * @callback is executed upon completion and should call
+ * ide_buffer_save_file_finish() to get the result of the operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_save_file_async (IdeBuffer            *self,
+                            GFile                *file,
+                            GCancellable         *cancellable,
+                            IdeNotification     **notif,
+                            GAsyncReadyCallback   callback,
+                            gpointer              user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GtkSourceFile) alternate = NULL;
+  g_autoptr(GtkSourceFileSaver) saver = NULL;
+  g_autoptr(IdeNotification) local_notif = NULL;
+  GtkSourceFile *source_file;
+  SaveState *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (!file || G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  ide_clear_param (notif, NULL);
+
+  /* If the user is requesting to save a file and our current file
+   * is a temporary file, then we want to transition to become that
+   * file instead of our temporary one.
+   */
+  if (file != NULL && self->is_temporary)
+    {
+      _ide_buffer_set_file (self, file);
+      self->is_temporary = FALSE;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_TEMPORARY]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+
+  if (file == NULL)
+    file = ide_buffer_get_file (self);
+
+  local_notif = ide_notification_new ();
+  ide_notification_set_has_progress (local_notif, TRUE);
+
+  state = g_slice_new0 (SaveState);
+  state->file = g_object_ref (file);
+  state->notif = g_object_ref (local_notif);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_save_file_async);
+  ide_task_set_task_data (task, state, save_state_free);
+
+  if (self->state != IDE_BUFFER_STATE_READY)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_BUSY,
+                                 "Failed to save buffer as it is busy");
+      IDE_EXIT;
+    }
+
+  source_file = self->source_file;
+
+  if (file && !g_file_equal (file, ide_buffer_get_file (self)))
+    {
+      alternate = gtk_source_file_new ();
+      gtk_source_file_set_location (alternate, file);
+      source_file = alternate;
+    }
+
+  if (self->addins != NULL)
+    {
+      IdeBufferFileSave closure = { self, file };
+      ide_extension_set_adapter_foreach (self->addins,
+                                         _ide_buffer_addin_save_file_cb,
+                                         &closure);
+    }
+
+  saver = gtk_source_file_saver_new (GTK_SOURCE_BUFFER (self), source_file);
+  ide_buffer_set_state (self, IDE_BUFFER_STATE_SAVING);
+  gtk_source_file_saver_save_async (saver,
+                                    G_PRIORITY_DEFAULT,
+                                    cancellable,
+                                    ide_buffer_progress_cb,
+                                    g_object_ref (local_notif),
+                                    g_object_unref,
+                                    ide_buffer_save_file_cb,
+                                    g_steal_pointer (&task));
+
+  if (notif != NULL)
+    *notif = g_steal_pointer (&local_notif);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_save_file_finish:
+ * @self: an #IdeBuffer
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to save the buffer via
+ * ide_buffer_save_file_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_save_file_finish (IdeBuffer     *self,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_buffer_get_language_id:
+ * @self: an #IdeBuffer
+ *
+ * A helper to get the language identifier of the buffers current language.
+ *
+ * Returns: (nullable): a string containing the language id, or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_buffer_get_language_id (IdeBuffer *self)
+{
+  GtkSourceLanguage *lang;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if ((lang = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (self))))
+    return gtk_source_language_get_id (lang);
+
+  return NULL;
+}
+
+void
+ide_buffer_set_language_id (IdeBuffer   *self,
+                            const gchar *language_id)
+{
+  GtkSourceLanguage *language = NULL;
+
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  if (language_id != NULL)
+    {
+      GtkSourceLanguageManager *manager;
+
+      manager = gtk_source_language_manager_get_default ();
+      language = gtk_source_language_manager_get_language (manager, language_id);
+    }
+
+  gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (self), language);
+}
+
+IdeHighlightEngine *
+_ide_buffer_get_highlight_engine (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->highlight_engine;
+}
+
+void
+_ide_buffer_set_failure (IdeBuffer    *self,
+                         const GError *error)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  if (error == self->failure)
+    return;
+
+  if (error != NULL)
+    self->state = IDE_BUFFER_STATE_FAILED;
+
+  g_clear_pointer (&self->failure, g_error_free);
+  self->failure = g_error_copy (error);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FAILED]);
+}
+
+/**
+ * ide_buffer_get_failure:
+ *
+ * Gets a #GError representing a failure that has occurred for the
+ * buffer.
+ *
+ * Returns: (transfer none): a #GError, or %NULL
+ *
+ * Since: 3.32
+ */
+const GError *
+ide_buffer_get_failure (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->failure;
+}
+
+/**
+ * ide_buffer_get_failed:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeBuffer:failed property, denoting if the buffer has failed
+ * in some aspect such as loading or saving.
+ *
+ * Returns: %TRUE if the buffer is in a failed state
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_failed (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->state == IDE_BUFFER_STATE_FAILED;
+}
+
+static void
+ide_buffer_set_file_settings (IdeBuffer       *self,
+                              IdeFileSettings *file_settings)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  if (self->file_settings == file_settings)
+    return;
+
+  ide_clear_and_destroy_object (&self->file_settings);
+  self->file_settings = g_object_ref (file_settings);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE_SETTINGS]);
+}
+
+static void
+ide_buffer_reload_file_settings (IdeBuffer *self)
+{
+  IdeObjectBox *box;
+  const gchar *lang_id;
+  GFile *file;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  file = ide_buffer_get_file (self);
+  lang_id = ide_buffer_get_language_id (self);
+
+  /* Bail if we'll just create the same settings as before */
+  if (self->file_settings != NULL &&
+      (g_file_equal (file, ide_file_settings_get_file (self->file_settings)) &&
+       ide_str_equal0 (lang_id, ide_file_settings_get_language (self->file_settings))))
+    return;
+
+  /* Now apply the settings (and they'll settle in the background) */
+  if ((box = ide_object_box_from_object (G_OBJECT (self))))
+    {
+      g_autoptr(IdeFileSettings) file_settings = NULL;
+
+      file_settings = ide_file_settings_new (IDE_OBJECT (box), file, lang_id);
+      ide_buffer_set_file_settings (self, file_settings);
+    }
+}
+
+static void
+ide_buffer_emit_cursor_moved (IdeBuffer *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  if (!ide_buffer_get_loading (self))
+    {
+      GtkTextMark *mark;
+      GtkTextIter iter;
+
+      mark = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (self));
+      gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (self), &iter, mark);
+      g_signal_emit (self, signals [CURSOR_MOVED], 0, &iter);
+    }
+}
+
+/**
+ * ide_buffer_get_loading:
+ * @self: an #IdeBuffer
+ *
+ * This checks to see if the buffer is currently loading. This is equivalent
+ * to calling ide_buffer_get_state() and checking for %IDE_BUFFER_STATE_LOADING.
+ *
+ * Returns: %TRUE if the buffer is loading; otherwise %FALSE.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_loading (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return ide_buffer_get_state (self) == IDE_BUFFER_STATE_LOADING;
+}
+
+static void
+ide_buffer_changed (GtkTextBuffer *buffer)
+{
+  IdeBuffer *self = (IdeBuffer *)buffer;
+
+  g_assert (IDE_IS_BUFFER (self));
+
+  GTK_TEXT_BUFFER_CLASS (ide_buffer_parent_class)->changed (buffer);
+
+  self->change_count++;
+  g_clear_pointer (&self->content, g_bytes_unref);
+  ide_buffer_delay_settling (self);
+}
+
+static void
+ide_buffer_delete_range (GtkTextBuffer *buffer,
+                         GtkTextIter   *begin,
+                         GtkTextIter   *end)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    gint begin_line, begin_offset;
+    gint end_line, end_offset;
+
+    begin_line = gtk_text_iter_get_line (begin);
+    begin_offset = gtk_text_iter_get_line_offset (begin);
+    end_line = gtk_text_iter_get_line (end);
+    end_offset = gtk_text_iter_get_line_offset (end);
+
+    IDE_TRACE_MSG ("delete-range (%d:%d, %d:%d)",
+                   begin_line, begin_offset,
+                   end_line, end_offset);
+  }
+#endif
+
+  GTK_TEXT_BUFFER_CLASS (ide_buffer_parent_class)->delete_range (buffer, begin, end);
+
+  ide_buffer_emit_cursor_moved (IDE_BUFFER (buffer));
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_insert_text (GtkTextBuffer *buffer,
+                        GtkTextIter   *location,
+                        const gchar   *text,
+                        gint           len)
+{
+  gboolean recheck_language = FALSE;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (location != NULL);
+  g_assert (text != NULL);
+
+  /*
+   * If we are inserting a \n at the end of the first line, then we might want
+   * to adjust the GtkSourceBuffer:language property to reflect the format.
+   * This is similar to emacs "modelines", which is apparently a bit of an
+   * overloaded term as is not to be confused with editor setting modelines.
+   */
+  if ((gtk_text_iter_get_line (location) == 0) && gtk_text_iter_ends_line (location) &&
+      ((text [0] == '\n') || ((len > 1) && (strchr (text, '\n') != NULL))))
+    recheck_language = TRUE;
+
+  GTK_TEXT_BUFFER_CLASS (ide_buffer_parent_class)->insert_text (buffer, location, text, len);
+
+  ide_buffer_emit_cursor_moved (IDE_BUFFER (buffer));
+
+  if (recheck_language)
+    ide_buffer_guess_language (IDE_BUFFER (buffer));
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_mark_set (GtkTextBuffer     *buffer,
+                     const GtkTextIter *iter,
+                     GtkTextMark       *mark)
+{
+  IdeBuffer *self = (IdeBuffer *)buffer;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  GTK_TEXT_BUFFER_CLASS (ide_buffer_parent_class)->mark_set (buffer, iter, mark);
+
+  if (!ide_buffer_get_loading (self))
+    {
+      if (mark == gtk_text_buffer_get_insert (buffer))
+        ide_buffer_emit_cursor_moved (IDE_BUFFER (buffer));
+    }
+}
+
+/**
+ * ide_buffer_get_changed_on_volume:
+ * @self: an #IdeBuffer
+ *
+ * Returns %TRUE if the #IdeBuffer is known to have been modified on storage
+ * externally from this #IdeBuffer.
+ *
+ * Returns: %TRUE if @self is known to be modified on storage
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_changed_on_volume (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->changed_on_volume;
+}
+
+/**
+ * _ide_buffer_set_changed_on_volume:
+ * @self: an #IdeBuffer
+ * @changed_on_volume: if the buffer was changed externally
+ *
+ * Sets the #IdeBuffer:changed-on-volume property.
+ *
+ * Set this to %TRUE if the buffer has been discovered to have changed
+ * outside of this buffer.
+ *
+ * Since: 3.32
+ */
+void
+_ide_buffer_set_changed_on_volume (IdeBuffer *self,
+                                   gboolean   changed_on_volume)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  changed_on_volume = !!changed_on_volume;
+
+  if (changed_on_volume != self->changed_on_volume)
+    {
+      self->changed_on_volume = changed_on_volume;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHANGED_ON_VOLUME]);
+    }
+}
+
+/**
+ * ide_buffer_get_read_only:
+ *
+ * This function returns %TRUE if the underlying file has been discovered to
+ * be read-only. This may be used by the interface to display information to
+ * the user about saving the file.
+ *
+ * Returns: %TRUE if the underlying file is read-only
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_read_only (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->read_only;
+}
+
+/**
+ * _ide_buffer_set_read_only:
+ * @self: an #IdeBuffer
+ * @read_only: if the buffer is read-only
+ *
+ * Sets the #IdeBuffer:read-only property, which should be set when the buffer
+ * has been discovered to be read-only on disk.
+ *
+ * Since: 3.32
+ */
+void
+_ide_buffer_set_read_only (IdeBuffer *self,
+                           gboolean   read_only)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  read_only = !!read_only;
+
+  if (read_only != self->read_only)
+    {
+      self->read_only = read_only;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_READ_ONLY]);
+    }
+}
+
+/**
+ * ide_buffer_get_style_scheme_name:
+ * @self: an #IdeBuffer
+ *
+ * Gets the name of the #GtkSourceStyleScheme from the #IdeBuffer:style-scheme
+ * property.
+ *
+ * Returns: (nullable): a string containing the style scheme or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_buffer_get_style_scheme_name (IdeBuffer *self)
+{
+  GtkSourceStyleScheme *scheme;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if ((scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (self))))
+    return gtk_source_style_scheme_get_id (scheme);
+
+  return NULL;
+}
+
+/**
+ * ide_buffer_set_style_scheme_name:
+ * @self: an #IdeBuffer
+ * @style_scheme_name: (nullable): string containing the style scheme's name
+ *
+ * Sets the #IdeBuffer:style-scheme property by locating the style scheme
+ * matching @style_scheme_name.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_set_style_scheme_name (IdeBuffer   *self,
+                                  const gchar *style_scheme_name)
+{
+  GtkSourceStyleSchemeManager *manager;
+  GtkSourceStyleScheme *scheme;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  if ((manager = gtk_source_style_scheme_manager_get_default ()) &&
+      (scheme = gtk_source_style_scheme_manager_get_scheme (manager, style_scheme_name)))
+    gtk_source_buffer_set_style_scheme (GTK_SOURCE_BUFFER (self), scheme);
+  else
+    gtk_source_buffer_set_style_scheme (GTK_SOURCE_BUFFER (self), NULL);
+}
+
+/**
+ * ide_buffer_get_title:
+ * @self: an #IdeBuffer
+ *
+ * Gets a string to represent the title of the buffer. An attempt is made to
+ * make this relative to the project workdir if possible.
+ *
+ * Returns: (transfer full): a string containing a title
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_buffer_dup_title (IdeBuffer *self)
+{
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GFile) home = NULL;
+  GFile *file;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  file = ide_buffer_get_file (self);
+
+  if (self->is_temporary)
+    return g_file_get_basename (file);
+
+  /* Unlikely, but better to be safe */
+  if (!(context = ide_buffer_ref_context (self)))
+    return g_file_get_basename (file);
+
+  workdir = ide_context_ref_workdir (context);
+
+  if (g_file_has_prefix (file, workdir))
+    return g_file_get_relative_path (workdir, file);
+
+  home = g_file_new_for_path (g_get_home_dir ());
+
+  if (g_file_has_prefix (file, home))
+    {
+      g_autofree gchar *relative = g_file_get_relative_path (home, file);
+      return g_strdup_printf ("~/%s", relative);
+    }
+
+  if (!g_file_is_native (file))
+    return g_file_get_uri (file);
+  else
+    return g_file_get_path (file);
+}
+
+/**
+ * ide_buffer_get_highlight_diagnostics:
+ * @self: an #IdeBuffer
+ *
+ * Checks if diagnostics should be highlighted.
+ *
+ * Returns: %TRUE if diagnostics should be highlighted
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_get_highlight_diagnostics (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->highlight_diagnostics;
+}
+
+/**
+ * ide_buffer_set_highlight_diagnostics:
+ * @self: an #IdeBuffer
+ * @highlight_diagnostics: if diagnostics should be highlighted
+ *
+ * Sets the #IdeBuffer:highlight-diagnostics property.
+ *
+ * If set to %TRUE, diagnostics will be styled in the buffer.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_set_highlight_diagnostics (IdeBuffer *self,
+                                      gboolean   highlight_diagnostics)
+{
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  highlight_diagnostics = !!highlight_diagnostics;
+
+  if (self->highlight_diagnostics != highlight_diagnostics)
+    {
+      ide_buffer_clear_diagnostics (self);
+      self->highlight_diagnostics = highlight_diagnostics;
+      ide_buffer_apply_diagnostics (self);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HIGHLIGHT_DIAGNOSTICS]);
+    }
+}
+
+/**
+ * ide_buffer_get_iter_location:
+ * @self: an #IdeBuffer
+ * @iter: a #GtkTextIter
+ *
+ * Gets an #IdeLocation for the position represented by @iter.
+ *
+ * Returns: (transfer full): an #IdeLocation
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_buffer_get_iter_location (IdeBuffer         *self,
+                              const GtkTextIter *iter)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+  g_return_val_if_fail (iter != NULL, NULL);
+
+  return ide_location_new_with_offset (ide_buffer_get_file (self),
+                                       gtk_text_iter_get_line (iter),
+                                       gtk_text_iter_get_line_offset (iter),
+                                       gtk_text_iter_get_offset (iter));
+}
+
+/**
+ * ide_buffer_get_selection_range:
+ * @self: an #IdeBuffer
+ *
+ * Gets an #IdeRange to represent the current buffer selection.
+ *
+ * Returns: (transfer full): an #IdeRange
+ *
+ * Since: 3.32
+ */
+IdeRange *
+ide_buffer_get_selection_range (IdeBuffer *self)
+{
+  g_autoptr(IdeLocation) begin = NULL;
+  g_autoptr(IdeLocation) end = NULL;
+  GtkTextIter begin_iter;
+  GtkTextIter end_iter;
+
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self), &begin_iter, &end_iter);
+  gtk_text_iter_order (&begin_iter, &end_iter);
+
+  begin = ide_buffer_get_iter_location (self, &begin_iter);
+  end = ide_buffer_get_iter_location (self, &end_iter);
+
+  return ide_range_new (begin, end);
+}
+
+/**
+ * ide_buffer_get_change_count:
+ * @self: an #IdeBuffer
+ *
+ * Gets the monotonic change count for the buffer.
+ *
+ * Returns: the change count for the buffer
+ *
+ * Since: 3.32
+ */
+guint
+ide_buffer_get_change_count (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), 0);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), 0);
+
+  return self->change_count;
+}
+
+static gboolean
+ide_buffer_settled_cb (gpointer user_data)
+{
+  IdeBuffer *self = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  self->settling_source = 0;
+  g_signal_emit (self, signals [CHANGE_SETTLED], 0);
+
+  if (self->addins != NULL)
+    ide_extension_set_adapter_foreach (self->addins,
+                                       _ide_buffer_addin_change_settled_cb,
+                                       self);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_buffer_delay_settling (IdeBuffer *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  g_clear_handle_id (&self->settling_source, g_source_remove);
+  self->settling_source = gdk_threads_add_timeout (SETTLING_DELAY_MSEC,
+                                                   ide_buffer_settled_cb,
+                                                   self);
+}
+
+/**
+ * ide_buffer_set_diagnostics:
+ * @self: an #IdeBuffer
+ * @diagnostics: (nullable): an #IdeDiagnostics
+ *
+ * Sets the #IdeDiagnostics for the buffer. These will be used to highlight
+ * the buffer for errors and warnings if #IdeBuffer:highlight-diagnostics
+ * is %TRUE.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_set_diagnostics (IdeBuffer      *self,
+                            IdeDiagnostics *diagnostics)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (!diagnostics || IDE_IS_DIAGNOSTICS (diagnostics));
+
+  if (diagnostics == self->diagnostics)
+    return;
+
+  if (self->diagnostics)
+    {
+      ide_buffer_clear_diagnostics (self);
+      g_clear_object (&self->diagnostics);
+    }
+
+  if (diagnostics)
+    {
+      self->diagnostics = g_object_ref (diagnostics);
+      ide_buffer_apply_diagnostics (self);
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DIAGNOSTICS]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
+
+  _ide_buffer_line_flags_changed (self);
+}
+
+/**
+ * ide_buffer_get_diagnostics:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeDiagnostics for the buffer if any have been registered.
+ *
+ * Returns: (transfer none) (nullable): an #IdeDiagnostics or %NULL
+ *
+ * Since: 3.32
+ */
+IdeDiagnostics *
+ide_buffer_get_diagnostics (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->diagnostics;
+}
+
+/**
+ * ide_buffer_has_diagnostics:
+ * @self: a #IdeBuffer
+ *
+ * Returns %TRUE if any diagnostics have been registered for the buffer.
+ *
+ * Returns: %TRUE if there are a non-zero number of diagnostics.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_has_diagnostics (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  if (self->diagnostics)
+    return g_list_model_get_n_items (G_LIST_MODEL (self->diagnostics)) > 0;
+
+  return FALSE;
+}
+
+static void
+ide_buffer_clear_diagnostics (IdeBuffer *self)
+{
+  GtkTextTagTable *table;
+  GtkTextTag *tag;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  if (!self->highlight_diagnostics)
+    return;
+
+  gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (self), &begin, &end);
+
+  table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (self));
+
+  if (NULL != (tag = gtk_text_tag_table_lookup (table, TAG_NOTE)))
+    dzl_gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (self), tag, &begin, &end, TRUE);
+
+  if (NULL != (tag = gtk_text_tag_table_lookup (table, TAG_WARNING)))
+    dzl_gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (self), tag, &begin, &end, TRUE);
+
+  if (NULL != (tag = gtk_text_tag_table_lookup (table, TAG_DEPRECATED)))
+    dzl_gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (self), tag, &begin, &end, TRUE);
+
+  if (NULL != (tag = gtk_text_tag_table_lookup (table, TAG_ERROR)))
+    dzl_gtk_text_buffer_remove_tag (GTK_TEXT_BUFFER (self), tag, &begin, &end, TRUE);
+}
+
+static void
+ide_buffer_apply_diagnostic (IdeBuffer     *self,
+                             IdeDiagnostic *diagnostic)
+{
+  IdeDiagnosticSeverity severity;
+  const gchar *tag_name = NULL;
+  IdeLocation *location;
+  guint n_ranges;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (IDE_IS_DIAGNOSTIC (diagnostic));
+
+  severity = ide_diagnostic_get_severity (diagnostic);
+
+  switch (severity)
+    {
+    case IDE_DIAGNOSTIC_NOTE:
+      tag_name = TAG_NOTE;
+      break;
+
+    case IDE_DIAGNOSTIC_DEPRECATED:
+      tag_name = TAG_DEPRECATED;
+      break;
+
+    case IDE_DIAGNOSTIC_WARNING:
+      tag_name = TAG_WARNING;
+      break;
+
+    case IDE_DIAGNOSTIC_ERROR:
+    case IDE_DIAGNOSTIC_FATAL:
+      tag_name = TAG_ERROR;
+      break;
+
+    case IDE_DIAGNOSTIC_IGNORED:
+    default:
+      return;
+    }
+
+  if ((location = ide_diagnostic_get_location (diagnostic)))
+    {
+      GtkTextIter begin_iter;
+      GtkTextIter end_iter;
+
+      ide_buffer_get_iter_at_location (self, &begin_iter, location);
+      end_iter = begin_iter;
+
+      if (!gtk_text_iter_ends_line (&end_iter))
+        gtk_text_iter_forward_to_line_end (&end_iter);
+      else
+        gtk_text_iter_backward_char (&begin_iter);
+
+      gtk_text_buffer_apply_tag_by_name (GTK_TEXT_BUFFER (self), tag_name, &begin_iter, &end_iter);
+    }
+
+  n_ranges = ide_diagnostic_get_n_ranges (diagnostic);
+
+  for (guint i = 0; i < n_ranges; i++)
+    {
+      GtkTextIter begin_iter;
+      GtkTextIter end_iter;
+      IdeLocation *begin;
+      IdeLocation *end;
+      IdeRange *range;
+      GFile *file;
+
+      range = ide_diagnostic_get_range (diagnostic, i);
+      begin = ide_range_get_begin (range);
+      end = ide_range_get_end (range);
+      file = ide_location_get_file (begin);
+
+      if (file != NULL)
+        {
+          if (!g_file_equal (file, ide_buffer_get_file (self)))
+            continue;
+        }
+
+      ide_buffer_get_iter_at_location (self, &begin_iter, begin);
+      ide_buffer_get_iter_at_location (self, &end_iter, end);
+
+      if (gtk_text_iter_equal (&begin_iter, &end_iter))
+        {
+          if (!gtk_text_iter_ends_line (&end_iter))
+            gtk_text_iter_forward_char (&end_iter);
+          else
+            gtk_text_iter_backward_char (&begin_iter);
+        }
+
+      gtk_text_buffer_apply_tag_by_name (GTK_TEXT_BUFFER (self), tag_name, &begin_iter, &end_iter);
+    }
+}
+
+static void
+ide_buffer_apply_diagnostics (IdeBuffer *self)
+{
+  guint n_items;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  if (!self->highlight_diagnostics)
+    return;
+
+  if (self->diagnostics == NULL)
+    return;
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->diagnostics));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeDiagnostic) diagnostic = NULL;
+
+      diagnostic = g_list_model_get_item (G_LIST_MODEL (self->diagnostics), i);
+      ide_buffer_apply_diagnostic (self, diagnostic);
+    }
+}
+
+/**
+ * ide_buffer_get_iter_at_location:
+ * @self: an #IdeBuffer
+ * @iter: (out): a #GtkTextIter
+ * @location: a #IdeLocation
+ *
+ * Set @iter to the position designated by @location.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_get_iter_at_location (IdeBuffer   *self,
+                                 GtkTextIter *iter,
+                                 IdeLocation *location)
+{
+  gint line;
+  gint line_offset;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (location != NULL);
+
+  line = ide_location_get_line (location);
+  line_offset = ide_location_get_line_offset (location);
+
+  gtk_text_buffer_get_iter_at_line_offset (GTK_TEXT_BUFFER (self),
+                                           iter,
+                                           MAX (0, line),
+                                           MAX (0, line_offset));
+
+  /* Advance to first non-space if offset < 0 */
+  if (line_offset < 0)
+    {
+      while (!gtk_text_iter_ends_line (iter))
+        {
+          if (!g_unichar_isspace (gtk_text_iter_get_char (iter)))
+            break;
+          gtk_text_iter_forward_char (iter);
+        }
+    }
+}
+
+/**
+ * ide_buffer_get_change_monitor:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeBuffer:change-monitor for the buffer.
+ *
+ * Returns: (transfer none) (nullable): an #IdeBufferChangeMonitor or %NULL
+ *
+ * Since: 3.32
+ */
+IdeBufferChangeMonitor *
+ide_buffer_get_change_monitor (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->change_monitor;
+}
+
+/**
+ * ide_buffer_set_change_monitor:
+ * @self: an #IdeBuffer
+ * @change_monitor: (nullable): an #IdeBufferChangeMonitor or %NULL
+ *
+ * Sets an #IdeBufferChangeMonitor to use for the buffer.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_set_change_monitor (IdeBuffer              *self,
+                               IdeBufferChangeMonitor *change_monitor)
+{
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (!change_monitor || IDE_IS_BUFFER_CHANGE_MONITOR (change_monitor));
+
+  if (g_set_object (&self->change_monitor, change_monitor))
+    {
+      /* Destroy change monitor with us if we can */
+      if (change_monitor && ide_object_is_root (IDE_OBJECT (change_monitor)))
+        {
+          IdeObjectBox *box = ide_object_box_from_object (G_OBJECT (self));
+          ide_object_append (IDE_OBJECT (box), IDE_OBJECT (change_monitor));
+        }
+
+      if (change_monitor != NULL)
+        ide_buffer_change_monitor_reload (change_monitor);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHANGE_MONITOR]);
+    }
+}
+
+static gboolean
+ide_buffer_can_do_newline_hack (IdeBuffer *self,
+                                guint      len)
+{
+  guint next_pow2;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  /*
+   * If adding two bytes to our length (one for \n and one for \0) is still
+   * under the next power of two, then we can avoid making a copy of the buffer
+   * when saving the buffer to our drafts.
+   *
+   * HACK: This relies on the fact that GtkTextBuffer returns a GString
+   *       allocated string which grows the string in powers of two.
+   */
+
+  if ((len == 0) || (len & (len - 1)) == 0)
+    return FALSE;
+
+  next_pow2 = len;
+  next_pow2 |= next_pow2 >> 1;
+  next_pow2 |= next_pow2 >> 2;
+  next_pow2 |= next_pow2 >> 4;
+  next_pow2 |= next_pow2 >> 8;
+  next_pow2 |= next_pow2 >> 16;
+  next_pow2++;
+
+  return ((len + 2) < next_pow2);
+}
+
+/**
+ * ide_buffer_dup_content:
+ * @self: an #IdeBuffer.
+ *
+ * Gets the contents of the buffer as GBytes.
+ *
+ * By using this function to get the bytes, you allow #IdeBuffer to avoid
+ * calculating the buffer text unnecessarily, potentially saving on
+ * allocations.
+ *
+ * Additionally, this allows the buffer to update the state in #IdeUnsavedFiles
+ * if the content is out of sync.
+ *
+ * Returns: (transfer full): a #GBytes containing the buffer content.
+ *
+ * Since: 3.32
+ */
+GBytes *
+ide_buffer_dup_content (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if (self->content == NULL)
+    {
+      g_autoptr(IdeContext) context = NULL;
+      IdeUnsavedFiles *unsaved_files;
+      GtkTextIter begin;
+      GtkTextIter end;
+      GFile *file;
+      gchar *text;
+      gsize len;
+
+      gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (self), &begin, &end);
+      text = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (self), &begin, &end, TRUE);
+
+      /*
+       * If implicit newline is set, add a \n in place of the \0 and avoid
+       * duplicating the buffer. Make sure to track length beforehand, since we
+       * would overwrite afterwards. Since conversion to \r\n is dealth with
+       * during save operations, this should be fine for both. The unsaved
+       * files will restore to a buffer, for which \n is acceptable.
+       */
+      len = strlen (text);
+      if (gtk_source_buffer_get_implicit_trailing_newline (GTK_SOURCE_BUFFER (self)) &&
+          (len == 0 || text[len - 1] != '\n'))
+        {
+          if (!ide_buffer_can_do_newline_hack (self, len))
+            {
+              gchar *copy;
+
+              copy = g_malloc (len + 2);
+              memcpy (copy, text, len);
+              g_free (text);
+              text = copy;
+            }
+
+          text [len] = '\n';
+          text [++len] = '\0';
+        }
+
+      /*
+       * We pass a buffer that is longer than the length we tell GBytes about.
+       * This way, compilers that don't want to see the trailing \0 can ignore
+       * that data, but compilers that rely on valid C strings can also rely
+       * on the buffer to be valid.
+       */
+      self->content = g_bytes_new_take (g_steal_pointer (&text), len);
+
+      file = ide_buffer_get_file (self);
+      context = ide_buffer_ref_context (IDE_BUFFER (self));
+      unsaved_files = ide_unsaved_files_from_context (context);
+      ide_unsaved_files_update (unsaved_files, file, self->content);
+    }
+
+  return g_bytes_ref (self->content);
+}
+
+static void
+ide_buffer_format_selection_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeFormatter *formatter = (IdeFormatter *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+
+  g_assert (IDE_IS_FORMATTER (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_formatter_format_finish (formatter, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_buffer_format_selection_range_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  IdeFormatter *formatter = (IdeFormatter *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+
+  g_assert (IDE_IS_FORMATTER (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_formatter_format_range_finish (formatter, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+/**
+ * ide_buffer_format_selection_async:
+ * @self: an #IdeBuffer
+ * @options: options for the formatting
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @callback: the callback upon completion
+ * @user_data: user data for @callback
+ *
+ * Formats the selection using an available #IdeFormatter for the buffer.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_format_selection_async (IdeBuffer           *self,
+                                   IdeFormatterOptions *options,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  IdeFormatter *formatter;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (IDE_IS_FORMATTER_OPTIONS (options));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_format_selection_async);
+
+  if (!(formatter = ide_extension_adapter_get_extension (self->formatter)))
+    {
+      const gchar *language_id = ide_buffer_get_language_id (self);
+
+      if (language_id == NULL)
+        language_id = "none";
+
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "No formatter registered for language %s",
+                                 language_id);
+
+      IDE_EXIT;
+    }
+
+  if (!gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self), &begin, &end))
+    {
+      ide_formatter_format_async (formatter,
+                                  self,
+                                  options,
+                                  cancellable,
+                                  ide_buffer_format_selection_cb,
+                                  g_steal_pointer (&task));
+      IDE_EXIT;
+    }
+
+  gtk_text_iter_order (&begin, &end);
+
+  ide_formatter_format_range_async (formatter,
+                                    self,
+                                    options,
+                                    &begin,
+                                    &end,
+                                    cancellable,
+                                    ide_buffer_format_selection_range_cb,
+                                    g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_buffer_format_selection_finish:
+ * @self: an #IdeBuffer
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_buffer_format_selection_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_format_selection_finish (IdeBuffer     *self,
+                                    GAsyncResult  *result,
+                                    GError       **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_buffer_get_insert_location:
+ *
+ * Gets the location of the insert mark as an #IdeLocation.
+ *
+ * Returns: (transfer full): An #IdeLocation
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_buffer_get_insert_location (IdeBuffer *self)
+{
+  GtkTextMark *mark;
+  GtkTextIter iter;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  mark = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (self));
+  gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (self), &iter, mark);
+
+  return ide_buffer_get_iter_location (self, &iter);
+}
+
+/**
+ * ide_buffer_get_word_at_iter:
+ * @self: an #IdeBuffer.
+ * @iter: a #GtkTextIter.
+ *
+ * Gets the word found under the position denoted by @iter.
+ *
+ * Returns: (transfer full): A newly allocated string.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_buffer_get_word_at_iter (IdeBuffer         *self,
+                             const GtkTextIter *iter)
+{
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+  g_return_val_if_fail (iter != NULL, NULL);
+
+  end = begin = *iter;
+
+  if (!_ide_source_iter_starts_word (&begin))
+    _ide_source_iter_backward_extra_natural_word_start (&begin);
+
+  if (!_ide_source_iter_ends_word (&end))
+    _ide_source_iter_forward_extra_natural_word_end (&end);
+
+  return gtk_text_iter_get_slice (&begin, &end);
+}
+
+/**
+ * ide_buffer_get_rename_provider:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeRenameProvider for this buffer, or %NULL.
+ *
+ * Returns: (nullable) (transfer none): An #IdeRenameProvider or %NULL if
+ *   there is no #IdeRenameProvider that can statisfy the buffer.
+ *
+ * Since: 3.32
+ */
+IdeRenameProvider *
+ide_buffer_get_rename_provider (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if (self->rename_provider != NULL)
+    return ide_extension_adapter_get_extension (self->rename_provider);
+
+  return NULL;
+}
+
+/**
+ * ide_buffer_get_file_settings:
+ * @self: an #IdeBuffer
+ *
+ * Gets the #IdeBuffer:file-settings property.
+ *
+ * The #IdeFileSettings are updated when changes to the file or language
+ * syntax are chnaged.
+ *
+ * Returns: (transfer none) (nullable): an #IdeFileSettings or %NULL
+ *
+ * Since: 3.32
+ */
+IdeFileSettings *
+ide_buffer_get_file_settings (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->file_settings;
+}
+
+/**
+ * ide_buffer_ref_context:
+ * @self: an #IdeBuffer
+ *
+ * Locates the #IdeContext for the buffer and returns it.
+ *
+ * Returns: (transfer full): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_buffer_ref_context (IdeBuffer *self)
+{
+  g_autoptr(IdeObject) root = NULL;
+
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if (self->buffer_manager != NULL)
+    root = ide_object_ref_root (IDE_OBJECT (self->buffer_manager));
+
+  g_return_val_if_fail (root != NULL, NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (root), NULL);
+
+  return IDE_CONTEXT (g_steal_pointer (&root));
+}
+
+static void
+apply_style (GtkTextTag  *tag,
+             const gchar *first_property,
+             ...)
+{
+  va_list args;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (!tag || GTK_IS_TEXT_TAG (tag));
+  g_assert (first_property != NULL);
+
+  if (tag == NULL)
+    return;
+
+  va_start (args, first_property);
+  g_object_set_valist (G_OBJECT (tag), first_property, args);
+  va_end (args);
+}
+
+static void
+ide_buffer_notify_style_scheme (IdeBuffer  *self,
+                                GParamSpec *pspec,
+                                gpointer    unused)
+{
+  GtkSourceStyleScheme *style_scheme;
+  GtkTextTagTable *table;
+  GdkRGBA deprecated_rgba;
+  GdkRGBA error_rgba;
+  GdkRGBA note_rgba;
+  GdkRGBA warning_rgba;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (pspec != NULL);
+
+  style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (self));
+  table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (self));
+
+#define GET_TAG(name) (gtk_text_tag_table_lookup(table, name))
+
+  if (style_scheme != NULL)
+    {
+      /* These are a fall-back if our style scheme isn't installed. */
+      gdk_rgba_parse (&deprecated_rgba, DEPRECATED_COLOR);
+      gdk_rgba_parse (&error_rgba, ERROR_COLOR);
+      gdk_rgba_parse (&note_rgba, NOTE_COLOR);
+      gdk_rgba_parse (&warning_rgba, WARNING_COLOR);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_DEPRECATED,
+                                                GET_TAG (TAG_DEPRECATED)))
+        apply_style (GET_TAG (TAG_DEPRECATED),
+                     "underline", PANGO_UNDERLINE_ERROR,
+                     "underline-rgba", &deprecated_rgba,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_ERROR,
+                                                GET_TAG (TAG_ERROR)))
+        apply_style (GET_TAG (TAG_ERROR),
+                     "underline", PANGO_UNDERLINE_ERROR,
+                     "underline-rgba", &error_rgba,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_NOTE,
+                                                GET_TAG (TAG_NOTE)))
+        apply_style (GET_TAG (TAG_NOTE),
+                     "underline", PANGO_UNDERLINE_ERROR,
+                     "underline-rgba", &note_rgba,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_WARNING,
+                                                GET_TAG (TAG_WARNING)))
+        apply_style (GET_TAG (TAG_WARNING),
+                     "underline", PANGO_UNDERLINE_ERROR,
+                     "underline-rgba", &warning_rgba,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_SNIPPET_TAB_STOP,
+                                                GET_TAG (TAG_SNIPPET_TAB_STOP)))
+        apply_style (GET_TAG (TAG_SNIPPET_TAB_STOP),
+                     "underline", PANGO_UNDERLINE_SINGLE,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_DEFINITION,
+                                                GET_TAG (TAG_DEFINITION)))
+        apply_style (GET_TAG (TAG_DEFINITION),
+                     "underline", PANGO_UNDERLINE_SINGLE,
+                     NULL);
+
+      if (!ide_source_style_scheme_apply_style (style_scheme,
+                                                TAG_CURRENT_BKPT,
+                                                GET_TAG (TAG_CURRENT_BKPT)))
+        apply_style (GET_TAG (TAG_CURRENT_BKPT),
+                     "paragraph-background", CURRENT_BKPT_BG,
+                     "foreground", CURRENT_BKPT_FG,
+                     NULL);
+    }
+
+#undef GET_TAG
+
+  if (self->addins != NULL)
+    ide_extension_set_adapter_foreach (self->addins,
+                                       _ide_buffer_addin_style_scheme_changed_cb,
+                                       self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STYLE_SCHEME_NAME]);
+
+}
+
+static void
+ide_buffer_on_tag_added (IdeBuffer       *self,
+                         GtkTextTag      *tag,
+                         GtkTextTagTable *table)
+{
+  GtkTextTag *chunk_tag;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+  g_assert (GTK_IS_TEXT_TAG (tag));
+  g_assert (GTK_IS_TEXT_TAG_TABLE (table));
+
+  /* Adjust priority of our tab-stop tag. */
+  chunk_tag = gtk_text_tag_table_lookup (table, "snippet::tab-stop");
+  if (chunk_tag != NULL)
+    gtk_text_tag_set_priority (chunk_tag,
+                               gtk_text_tag_table_get_size (table) - 1);
+}
+
+static void
+ide_buffer_init_tags (IdeBuffer *self)
+{
+  GtkTextTagTable *tag_table;
+  GtkSourceStyleScheme *style_scheme;
+  g_autoptr(GtkTextTag) deprecated_tag = NULL;
+  g_autoptr(GtkTextTag) error_tag = NULL;
+  g_autoptr(GtkTextTag) note_tag = NULL;
+  g_autoptr(GtkTextTag) warning_tag = NULL;
+  GdkRGBA deprecated_rgba;
+  GdkRGBA error_rgba;
+  GdkRGBA note_rgba;
+  GdkRGBA warning_rgba;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  tag_table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (self));
+  style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (self));
+
+  /* These are fall-back if our style scheme isn't installed. */
+  gdk_rgba_parse (&deprecated_rgba, DEPRECATED_COLOR);
+  gdk_rgba_parse (&error_rgba, ERROR_COLOR);
+  gdk_rgba_parse (&note_rgba, NOTE_COLOR);
+  gdk_rgba_parse (&warning_rgba, WARNING_COLOR);
+
+  /*
+   * NOTE:
+   *
+   * The tag table assigns priority upon insert. Each successive insert
+   * is higher priority than the last.
+   */
+
+  deprecated_tag = gtk_text_tag_new (TAG_DEPRECATED);
+  error_tag = gtk_text_tag_new (TAG_ERROR);
+  note_tag = gtk_text_tag_new (TAG_NOTE);
+  warning_tag = gtk_text_tag_new (TAG_WARNING);
+
+  if (!ide_source_style_scheme_apply_style (style_scheme, TAG_DEPRECATED, deprecated_tag))
+    apply_style (deprecated_tag,
+                 "underline", PANGO_UNDERLINE_ERROR,
+                 "underline-rgba", &deprecated_rgba,
+                 NULL);
+
+  if (!ide_source_style_scheme_apply_style (style_scheme, TAG_ERROR, error_tag))
+    apply_style (error_tag,
+                 "underline", PANGO_UNDERLINE_ERROR,
+                 "underline-rgba", &error_rgba,
+                 NULL);
+
+  if (!ide_source_style_scheme_apply_style (style_scheme, TAG_NOTE, note_tag))
+    apply_style (note_tag,
+                 "underline", PANGO_UNDERLINE_ERROR,
+                 "underline-rgba", &note_rgba,
+                 NULL);
+
+  if (!ide_source_style_scheme_apply_style (style_scheme, TAG_NOTE, warning_tag))
+    apply_style (warning_tag,
+                 "underline", PANGO_UNDERLINE_ERROR,
+                 "underline-rgba", &warning_rgba,
+                 NULL);
+
+  gtk_text_tag_table_add (tag_table, deprecated_tag);
+  gtk_text_tag_table_add (tag_table, error_tag);
+  gtk_text_tag_table_add (tag_table, note_tag);
+  gtk_text_tag_table_add (tag_table, warning_tag);
+
+  gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (self), TAG_SNIPPET_TAB_STOP,
+                              NULL);
+  gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (self), TAG_DEFINITION,
+                              "underline", PANGO_UNDERLINE_SINGLE,
+                              NULL);
+  gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (self), TAG_CURRENT_BKPT,
+                              "paragraph-background", CURRENT_BKPT_BG,
+                              "foreground", CURRENT_BKPT_FG,
+                              NULL);
+
+  g_signal_connect_object (tag_table,
+                           "tag-added",
+                           G_CALLBACK (ide_buffer_on_tag_added),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+/**
+ * ide_buffer_get_formatter:
+ * @self: an #IdeBuffer
+ *
+ * Gets an #IdeFormatter for the buffer, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeFormatter or %NULL
+ *
+ * Since: 3.32
+ */
+IdeFormatter *
+ide_buffer_get_formatter (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  if (self->formatter == NULL)
+    return NULL;
+
+  return ide_extension_adapter_get_extension (self->formatter);
+}
+
+void
+_ide_buffer_sync_to_unsaved_files (IdeBuffer *self)
+{
+  GBytes *content;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  if ((content = ide_buffer_dup_content (self)))
+    g_bytes_unref (content);
+}
+
+/**
+ * ide_buffer_rehighlight:
+ * @self: an #IdeBuffer
+ *
+ * Force @self to rebuild the highlighted words.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_rehighlight (IdeBuffer *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  /* In case we are disposing */
+  if (self->highlight_engine == NULL || ide_buffer_get_loading (self))
+    IDE_EXIT;
+
+  if (gtk_source_buffer_get_highlight_syntax (GTK_SOURCE_BUFFER (self)))
+    ide_highlight_engine_rebuild (self->highlight_engine);
+  else
+    ide_highlight_engine_clear (self->highlight_engine);
+
+  IDE_EXIT;
+}
+
+static void
+ide_buffer_get_symbol_at_location_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  IdeSymbolResolver *symbol_resolver = (IdeSymbolResolver *)object;
+  g_autoptr(IdeSymbol) symbol = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  LookUpSymbolData *data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SYMBOL_RESOLVER (symbol_resolver));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  data = ide_task_get_task_data (task);
+  g_assert (data->resolvers != NULL);
+  g_assert (data->resolvers->len > 0);
+
+  if ((symbol = ide_symbol_resolver_lookup_symbol_finish (symbol_resolver, result, &error)))
+    {
+      /*
+       * Store symbol which has definition location. If no symbol has
+       * definition location then store symbol which has declaration location.
+       */
+      if ((data->symbol == NULL) ||
+          (ide_symbol_get_location (symbol) != NULL) ||
+          (ide_symbol_get_location (data->symbol) == NULL &&
+           ide_symbol_get_header_location (symbol)))
+        {
+          g_clear_object (&data->symbol);
+          data->symbol = g_steal_pointer (&symbol);
+        }
+    }
+
+  g_ptr_array_remove_index (data->resolvers, data->resolvers->len - 1);
+
+  if (data->resolvers->len > 0)
+    {
+      IdeSymbolResolver *resolver;
+      GCancellable *cancellable;
+
+      resolver = g_ptr_array_index (data->resolvers, data->resolvers->len - 1);
+      cancellable = ide_task_get_cancellable (task);
+
+      ide_symbol_resolver_lookup_symbol_async (resolver,
+                                               data->location,
+                                               cancellable,
+                                               ide_buffer_get_symbol_at_location_cb,
+                                               g_steal_pointer (&task));
+    }
+  else if (data->symbol == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_FOUND,
+                                 "Symbol not found");
+    }
+  else
+    {
+      ide_task_return_pointer (task,
+                               g_steal_pointer (&data->symbol),
+                               g_object_unref);
+    }
+}
+
+/**
+ * ide_buffer_get_symbol_at_location_async:
+ * @self: an #IdeBuffer
+ * @location: a #GtkTextIter indicating a position to search for a symbol
+ * @cancellable: a #GCancellable
+ * @callback: a #GAsyncReadyCallback
+ * @user_data: a #gpointer to hold user data
+ *
+ * Asynchronously get a possible symbol at @location.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_get_symbol_at_location_async (IdeBuffer           *self,
+                                         const GtkTextIter   *location,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  g_autoptr(IdeLocation) srcloc = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) resolvers = NULL;
+  IdeSymbolResolver *resolver;
+  LookUpSymbolData *data;
+  guint line;
+  guint line_offset;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (location != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  resolvers = ide_buffer_get_symbol_resolvers (self);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (resolvers, g_object_unref);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buffer_get_symbol_at_location_async);
+
+  if (resolvers->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 _("The current language lacks a symbol resolver."));
+      return;
+    }
+
+  _ide_buffer_sync_to_unsaved_files (self);
+
+  line = gtk_text_iter_get_line (location);
+  line_offset = gtk_text_iter_get_line_offset (location);
+  srcloc = ide_location_new (ide_buffer_get_file (self), line, line_offset);
+
+  data = g_slice_new0 (LookUpSymbolData);
+  data->resolvers = g_steal_pointer (&resolvers);
+  data->location = g_steal_pointer (&srcloc);
+  ide_task_set_task_data (task, data, lookup_symbol_data_free);
+
+  /* Try lookup_symbol on each symbol resolver one by by one. */
+  resolver = g_ptr_array_index (data->resolvers, data->resolvers->len - 1);
+  ide_symbol_resolver_lookup_symbol_async (resolver,
+                                           data->location,
+                                           cancellable,
+                                           ide_buffer_get_symbol_at_location_cb,
+                                           g_steal_pointer (&task));
+}
+
+/**
+ * ide_buffer_get_symbol_at_location_finish:
+ * @self: an #IdeBuffer
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError
+ *
+ * Completes an asynchronous request to locate a symbol at a location.
+ *
+ * Returns: (transfer full): An #IdeSymbol or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeSymbol *
+ide_buffer_get_symbol_at_location_finish (IdeBuffer     *self,
+                                          GAsyncResult  *result,
+                                          GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+/**
+ * ide_buffer_get_selection_bounds:
+ * @self: an #IdeBuffer
+ * @insert: (out): a #GtkTextIter to get the insert position
+ * @selection: (out): a #GtkTextIter to get the selection position
+ *
+ * This function acts like gtk_text_buffer_get_selection_bounds() except that
+ * it always places the location of the insert mark at @insert and the location
+ * of the selection mark at @selection.
+ *
+ * Calling gtk_text_iter_order() with the results of this function would be
+ * equivalent to calling gtk_text_buffer_get_selection_bounds().
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_get_selection_bounds (IdeBuffer   *self,
+                                 GtkTextIter *insert,
+                                 GtkTextIter *selection)
+{
+  GtkTextMark *mark;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  if (insert != NULL)
+    {
+      mark = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (self));
+      gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (self), insert, mark);
+    }
+
+  if (selection != NULL)
+    {
+      mark = gtk_text_buffer_get_selection_bound (GTK_TEXT_BUFFER (self));
+      gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (self), selection, mark);
+    }
+}
+
+/**
+ * ide_buffer_trim_trailing_whitespace:
+ * @self: an #IdeBuffer
+ *
+ * Trim trailing whitespaces from the buffer.
+ *
+ * Only lines that are marked as changed by the underlying buffer
+ * monitor will be trimmed. If no #IdeBufferChangeMonitor is present,
+ * then all lines will be trimmed.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_trim_trailing_whitespace (IdeBuffer *self)
+{
+  GtkTextBuffer *buffer;
+  GtkTextIter iter;
+  gint line;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  buffer = GTK_TEXT_BUFFER (self);
+
+  gtk_text_buffer_get_end_iter (buffer, &iter);
+
+  for (line = gtk_text_iter_get_line (&iter); line >= 0; line--)
+    {
+      IdeBufferLineChange change = IDE_BUFFER_LINE_CHANGE_CHANGED;
+
+      if (self->change_monitor)
+        change = ide_buffer_change_monitor_get_change (self->change_monitor, line);
+
+      if (change != IDE_BUFFER_LINE_CHANGE_NONE)
+        {
+          gtk_text_buffer_get_iter_at_line (buffer, &iter, line);
+
+/*
+ * Preserve all whitespace that isn't space or tab.
+ * This could include line feed, form feed, etc.
+ */
+#define TEXT_ITER_IS_SPACE(ptr) \
+  ({  \
+    gunichar ch = gtk_text_iter_get_char (ptr); \
+    (ch == ' ' || ch == '\t'); \
+  })
+
+          /*
+           * Move to the first character at the end of the line (skipping the newline)
+           * and progress to trip if it is white space.
+           */
+          if (gtk_text_iter_forward_to_line_end (&iter) &&
+              !gtk_text_iter_starts_line (&iter) &&
+              gtk_text_iter_backward_char (&iter) &&
+              TEXT_ITER_IS_SPACE (&iter))
+            {
+              GtkTextIter begin = iter;
+
+              gtk_text_iter_forward_to_line_end (&iter);
+
+              while (TEXT_ITER_IS_SPACE (&begin))
+                {
+                  if (gtk_text_iter_starts_line (&begin))
+                    break;
+
+                  if (!gtk_text_iter_backward_char (&begin))
+                    break;
+                }
+
+              if (!TEXT_ITER_IS_SPACE (&begin) && !gtk_text_iter_ends_line (&begin))
+                gtk_text_iter_forward_char (&begin);
+
+              if (!gtk_text_iter_equal (&begin, &iter))
+                gtk_text_buffer_delete (buffer, &begin, &iter);
+            }
+
+#undef TEXT_ITER_IS_SPACE
+        }
+    }
+}
+
+static void
+ide_buffer_get_symbol_resolvers_cb (IdeExtensionSetAdapter *set,
+                                    PeasPluginInfo         *plugin_info,
+                                    PeasExtension          *exten,
+                                    gpointer                user_data)
+{
+  IdeSymbolResolver *resolver = (IdeSymbolResolver *)exten;
+  GPtrArray *ar = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_SYMBOL_RESOLVER (resolver));
+  g_assert (ar != NULL);
+
+  g_ptr_array_add (ar, g_object_ref (resolver));
+}
+
+/**
+ * ide_buffer_get_symbol_resolvers:
+ * @self: an #IdeBuffer
+ *
+ * Gets the symbol resolvers for the buffer based on the current language. The
+ * resolvers in the resulting array are sorted by priority.
+ *
+ * Returns: (transfer full) (element-type IdeSymbolResolver): a #GPtrArray
+ *   of #IdeSymbolResolver.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_buffer_get_symbol_resolvers (IdeBuffer *self)
+{
+  GPtrArray *ar;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  ar = g_ptr_array_new_with_free_func (g_object_unref);
+
+  if (self->symbol_resolvers != NULL)
+    ide_extension_set_adapter_foreach_by_priority (self->symbol_resolvers,
+                                                   ide_buffer_get_symbol_resolvers_cb,
+                                                   ar);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ar);
+}
+
+/**
+ * ide_buffer_get_line_text:
+ * @self: a #IdeBuffer
+ * @line: a line number starting from 0
+ *
+ * Gets the contents of a single line within the buffer.
+ *
+ * Returns: (transfer full) (nullable): a string containing the line's text
+ *   or %NULL if the line does not exist.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_buffer_get_line_text (IdeBuffer *self,
+                          guint      line)
+{
+  GtkTextIter begin;
+
+  g_assert (IDE_IS_BUFFER (self));
+
+  gtk_text_buffer_get_iter_at_line (GTK_TEXT_BUFFER (self), &begin, line);
+
+  if (gtk_text_iter_get_line (&begin) == line)
+    {
+      GtkTextIter end = begin;
+
+      if (gtk_text_iter_ends_line (&end) ||
+          gtk_text_iter_forward_to_line_end (&end))
+        return gtk_text_iter_get_slice (&begin, &end);
+    }
+
+  return g_strdup ("");
+}
+
+static void
+ide_buffer_guess_language (IdeBuffer *self)
+{
+  GtkSourceLanguageManager *manager;
+  GtkSourceLanguage *lang;
+  g_autofree gchar *basename = NULL;
+  g_autofree gchar *content_type = NULL;
+  g_autofree gchar *line = NULL;
+  const gchar *path;
+  GFile *file;
+  gboolean uncertain = FALSE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (self));
+
+  line = ide_buffer_get_line_text (self, 0);
+  file = ide_buffer_get_file (self);
+
+  if (!g_file_is_native (file))
+    path = basename = g_file_get_basename (file);
+  else
+    path = g_file_peek_path (file);
+
+  content_type = g_content_type_guess (path, (const guchar *)line, strlen (line), &uncertain);
+  if (uncertain)
+    return;
+
+  manager = gtk_source_language_manager_get_default ();
+  if (!(lang = gtk_source_language_manager_guess_language (manager, path, content_type)))
+    return;
+
+  if (!ide_str_equal0 (gtk_source_language_get_id (lang), ide_buffer_get_language_id (self)))
+    gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (self), lang);
+}
+
+gboolean
+_ide_buffer_can_restore_cursor (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->can_restore_cursor;
+}
+
+void
+_ide_buffer_cancel_cursor_restore (IdeBuffer *self)
+{
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  self->can_restore_cursor = FALSE;
+}
+
+/**
+ * ide_buffer_hold:
+ * @self: a #IdeBuffer
+ *
+ * Increases the "hold count" of the #IdeBuffer by one.
+ *
+ * The hold count is similar to a reference count, as it allows the buffer
+ * manager to know when a buffer may be destroyed cleanly.
+ *
+ * Doing so ensures that the buffer wont be unloaded or have reference
+ * cycles broken.
+ *
+ * Release the hold with ide_buffer_release().
+ *
+ * When the hold count reaches zero, the buffer will be destroyed.
+ *
+ * Returns: (transfer full): @self
+ *
+ * Since: 3.32
+ */
+IdeBuffer *
+ide_buffer_hold (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  self->hold++;
+
+  return g_object_ref (self);
+}
+
+/**
+ * ide_buffer_release:
+ * @self: a #IdeBuffer
+ *
+ * Releases the "hold count" on a buffer.
+ *
+ * The buffer will be destroyed and unloaded when the hold count
+ * reaches zero.
+ *
+ * Since: 3.32
+ */
+void
+ide_buffer_release (IdeBuffer *self)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+  g_return_if_fail (self->hold > 0);
+
+  self->hold--;
+
+  if (self->hold == 0)
+    {
+      IdeObjectBox *box = ide_object_box_from_object (G_OBJECT (self));
+
+      if (box != NULL)
+        ide_object_destroy (IDE_OBJECT (box));
+    }
+
+  g_object_unref (self);
+}
+
+IdeExtensionSetAdapter *
+_ide_buffer_get_addins (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUFFER (self), NULL);
+
+  return self->addins;
+}
+
+void
+_ide_buffer_line_flags_changed (IdeBuffer *self)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUFFER (self));
+
+  g_signal_emit (self, signals [LINE_FLAGS_CHANGED], 0);
+}
+
+/**
+ * ide_buffer_has_symbol_resolvers:
+ * @self: a #IdeBuffer
+ *
+ * Checks if any symbol resolvers are available.
+ *
+ * Returns: %TRUE if at least one symbol resolvers is available
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_buffer_has_symbol_resolvers (IdeBuffer *self)
+{
+  g_return_val_if_fail (IDE_IS_BUFFER (self), FALSE);
+
+  return self->symbol_resolvers != NULL &&
+         ide_extension_set_adapter_get_n_extensions (self->symbol_resolvers) > 0;
+}
diff --git a/src/libide/code/ide-buffer.h b/src/libide/code/ide-buffer.h
new file mode 100644
index 000000000..c6d5ac636
--- /dev/null
+++ b/src/libide/code/ide-buffer.h
@@ -0,0 +1,178 @@
+/* ide-buffer.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <gtksourceview/gtksource.h>
+#include <libide-core.h>
+
+#include "ide-buffer-change-monitor.h"
+#include "ide-diagnostics.h"
+#include "ide-file-settings.h"
+#include "ide-formatter.h"
+#include "ide-location.h"
+#include "ide-range.h"
+#include "ide-rename-provider.h"
+#include "ide-symbol.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUFFER (ide_buffer_get_type())
+
+typedef enum
+{
+  IDE_BUFFER_STATE_READY,
+  IDE_BUFFER_STATE_LOADING,
+  IDE_BUFFER_STATE_SAVING,
+  IDE_BUFFER_STATE_FAILED,
+} IdeBufferState;
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeBuffer, ide_buffer, IDE, BUFFER, GtkSourceBuffer)
+
+IDE_AVAILABLE_IN_3_32
+GBytes                 *ide_buffer_dup_content                   (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gchar                  *ide_buffer_dup_title                     (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_format_selection_async        (IdeBuffer               *self,
+                                                                  IdeFormatterOptions     *options,
+                                                                  GCancellable            *cancellable,
+                                                                  GAsyncReadyCallback      callback,
+                                                                  gpointer                 user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_format_selection_finish       (IdeBuffer               *self,
+                                                                  GAsyncResult            *result,
+                                                                  GError                 **error);
+IDE_AVAILABLE_IN_3_32
+guint                   ide_buffer_get_change_count              (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeBufferChangeMonitor *ide_buffer_get_change_monitor            (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_changed_on_volume         (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeDiagnostics         *ide_buffer_get_diagnostics               (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeLocation            *ide_buffer_get_insert_location           (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_is_temporary              (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_failed                    (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+const GError           *ide_buffer_get_failure                   (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gchar                  *ide_buffer_dup_uri                       (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+GFile                  *ide_buffer_get_file                      (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeFileSettings        *ide_buffer_get_file_settings             (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeFormatter           *ide_buffer_get_formatter                 (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_highlight_diagnostics     (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_get_iter_at_location          (IdeBuffer               *self,
+                                                                  GtkTextIter             *iter,
+                                                                  IdeLocation             *location);
+IDE_AVAILABLE_IN_3_32
+IdeLocation            *ide_buffer_get_iter_location             (IdeBuffer               *self,
+                                                                  const GtkTextIter       *iter);
+IDE_AVAILABLE_IN_3_32
+const gchar            *ide_buffer_get_language_id               (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_set_language_id               (IdeBuffer               *self,
+                                                                  const gchar             *language_id);
+IDE_AVAILABLE_IN_3_32
+gchar                  *ide_buffer_get_line_text                 (IdeBuffer               *self,
+                                                                  guint                    line);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_loading                   (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_get_read_only                 (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeRenameProvider      *ide_buffer_get_rename_provider           (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_get_selection_bounds          (IdeBuffer               *self,
+                                                                  GtkTextIter             *insert,
+                                                                  GtkTextIter             *selection);
+IDE_AVAILABLE_IN_3_32
+IdeRange               *ide_buffer_get_selection_range           (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeBufferState          ide_buffer_get_state                     (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+const gchar            *ide_buffer_get_style_scheme_name         (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_get_symbol_at_location_async  (IdeBuffer               *self,
+                                                                  const GtkTextIter       *location,
+                                                                  GCancellable            *cancellable,
+                                                                  GAsyncReadyCallback      callback,
+                                                                  gpointer                 user_data);
+IDE_AVAILABLE_IN_3_32
+IdeSymbol              *ide_buffer_get_symbol_at_location_finish (IdeBuffer               *self,
+                                                                  GAsyncResult            *result,
+                                                                  GError                 **error);
+IDE_AVAILABLE_IN_3_32
+GPtrArray              *ide_buffer_get_symbol_resolvers          (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gchar                  *ide_buffer_get_word_at_iter              (IdeBuffer               *self,
+                                                                  const GtkTextIter       *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_has_diagnostics               (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_has_symbol_resolvers          (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeBuffer              *ide_buffer_hold                          (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext             *ide_buffer_ref_context                   (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_rehighlight                   (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_release                       (IdeBuffer               *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_save_file_async               (IdeBuffer               *self,
+                                                                  GFile                   *file,
+                                                                  GCancellable            *cancellable,
+                                                                  IdeNotification        **notif,
+                                                                  GAsyncReadyCallback      callback,
+                                                                  gpointer                 user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean                ide_buffer_save_file_finish              (IdeBuffer               *self,
+                                                                  GAsyncResult            *result,
+                                                                  GError                 **error);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_set_change_monitor            (IdeBuffer               *self,
+                                                                  IdeBufferChangeMonitor  *change_monitor);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_set_diagnostics               (IdeBuffer               *self,
+                                                                  IdeDiagnostics          *diagnostics);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_set_highlight_diagnostics     (IdeBuffer               *self,
+                                                                  gboolean                 
highlight_diagnostics);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_set_style_scheme_name         (IdeBuffer               *self,
+                                                                  const gchar             
*style_scheme_name);
+IDE_AVAILABLE_IN_3_32
+void                    ide_buffer_trim_trailing_whitespace      (IdeBuffer               *self);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-code-global.c b/src/libide/code/ide-code-global.c
new file mode 100644
index 000000000..c623adef7
--- /dev/null
+++ b/src/libide/code/ide-code-global.c
@@ -0,0 +1,44 @@
+/* ide-code-global.c
+ *
+ * Copyright 2018-2019 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
+ */
+
+#include "ide-file-settings.h"
+#include "ide-gsettings-file-settings.h"
+
+#include "../../gconstructor.h"
+
+#if defined (G_HAS_CONSTRUCTORS)
+# ifdef G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA
+#  pragma G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(ide_init_ctor)
+# endif
+G_DEFINE_CONSTRUCTOR(ide_code_init_ctor)
+#else
+# error Your platform/compiler is missing constructor support
+#endif
+
+static void
+ide_code_init_ctor (void)
+{
+  g_io_extension_point_register (IDE_FILE_SETTINGS_EXTENSION_POINT);
+
+  g_io_extension_point_implement (IDE_FILE_SETTINGS_EXTENSION_POINT,
+                                  IDE_TYPE_GSETTINGS_FILE_SETTINGS,
+                                  IDE_FILE_SETTINGS_EXTENSION_POINT".gsettings",
+                                  -300);
+}
diff --git a/src/libide/code/ide-code-index-entries.c b/src/libide/code/ide-code-index-entries.c
new file mode 100644
index 000000000..aac522e97
--- /dev/null
+++ b/src/libide/code/ide-code-index-entries.c
@@ -0,0 +1,178 @@
+/* ide-code-index-entries.c
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2017 Anoop Chandu <anoopchandu96 gmail 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 "ide-code-index-entries"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-code-index-entry.h"
+#include "ide-code-index-entries.h"
+
+G_DEFINE_INTERFACE (IdeCodeIndexEntries, ide_code_index_entries, G_TYPE_OBJECT)
+
+static void
+ide_code_index_entries_real_next_entries_async (IdeCodeIndexEntries *self,
+                                                GCancellable        *cancellable,
+                                                GAsyncReadyCallback  callback,
+                                                gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) ret = NULL;
+  IdeCodeIndexEntry *entry;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CODE_INDEX_ENTRIES (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_code_index_entries_real_next_entries_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_kind (task, IDE_TASK_KIND_INDEXER);
+
+  ret = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_code_index_entry_free);
+
+  while ((entry = ide_code_index_entries_get_next_entry (self)))
+    g_ptr_array_add (ret, g_steal_pointer (&entry));
+
+  ide_task_return_pointer (task, g_steal_pointer (&ret), (GDestroyNotify)g_ptr_array_unref);
+}
+
+static GPtrArray *
+ide_code_index_entries_real_next_entries_finish (IdeCodeIndexEntries  *self,
+                                                 GAsyncResult         *result,
+                                                 GError              **error)
+{
+  GPtrArray *ret;
+
+  g_assert (IDE_IS_CODE_INDEX_ENTRIES (self));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ret);
+}
+
+static IdeCodeIndexEntry *
+ide_code_index_entries_real_get_next_entry (IdeCodeIndexEntries *self)
+{
+  return NULL;
+}
+
+static void
+ide_code_index_entries_default_init (IdeCodeIndexEntriesInterface *iface)
+{
+  iface->get_next_entry = ide_code_index_entries_real_get_next_entry;
+  iface->next_entries_async = ide_code_index_entries_real_next_entries_async;
+  iface->next_entries_finish = ide_code_index_entries_real_next_entries_finish;
+}
+
+/**
+ * ide_code_index_entries_get_next_entry:
+ * @self: An #IdeCodeIndexEntries instance.
+ *
+ * This will fetch next entry in index.
+ *
+ * When all of the entries have been exhausted, %NULL should be returned.
+ *
+ * Returns: (nullable) (transfer full): An #IdeCodeIndexEntry.
+ *
+ * Since: 3.32
+ */
+IdeCodeIndexEntry *
+ide_code_index_entries_get_next_entry (IdeCodeIndexEntries *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CODE_INDEX_ENTRIES (self), NULL);
+
+  return IDE_CODE_INDEX_ENTRIES_GET_IFACE (self)->get_next_entry (self);
+}
+
+/**
+ * ide_code_index_entries_get_file:
+ * @self: a #IdeCodeIndexEntries
+ *
+ * The file that was indexed.
+ *
+ * Returns: (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_code_index_entries_get_file (IdeCodeIndexEntries *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CODE_INDEX_ENTRIES (self), NULL);
+
+  return IDE_CODE_INDEX_ENTRIES_GET_IFACE (self)->get_file (self);
+}
+
+/**
+ * ide_code_index_entries_next_entries_async:
+ * @self: a #IdeCodeIndexEntries
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: user data for @callback, or %NULL
+ *
+ * Requests the next set of results from the code index asynchronously.
+ * This allows implementations to possibly process data off the main thread
+ * without blocking the main loop.
+ *
+ * Since: 3.32
+ */
+void
+ide_code_index_entries_next_entries_async (IdeCodeIndexEntries *self,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CODE_INDEX_ENTRIES (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_CODE_INDEX_ENTRIES_GET_IFACE (self)->next_entries_async (self, cancellable, callback, user_data);
+}
+
+/**
+ * ide_code_index_entries_next_entries_finish:
+ * @self: a #IdeCodeIndexEntries
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request for the next set of entries from the index.
+ *
+ * Returns: (transfer full) (element-type IdeCodeIndexEntry): a #GPtrArray
+ *   of #IdeCodeIndexEntry.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_code_index_entries_next_entries_finish (IdeCodeIndexEntries  *self,
+                                            GAsyncResult         *result,
+                                            GError              **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CODE_INDEX_ENTRIES (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_CODE_INDEX_ENTRIES_GET_IFACE (self)->next_entries_finish (self, result, error);
+}
diff --git a/src/libide/code/ide-code-index-entries.h b/src/libide/code/ide-code-index-entries.h
new file mode 100644
index 000000000..176e36ab2
--- /dev/null
+++ b/src/libide/code/ide-code-index-entries.h
@@ -0,0 +1,68 @@
+/* ide-code-index-entries.h
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2017 Anoop Chandu <anoopchandu96 gmail 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CODE_INDEX_ENTRIES (ide_code_index_entries_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeCodeIndexEntries, ide_code_index_entries, IDE, CODE_INDEX_ENTRIES, GObject)
+
+struct _IdeCodeIndexEntriesInterface
+{
+  GTypeInterface       parent_iface;
+
+  GFile             *(*get_file)            (IdeCodeIndexEntries  *self);
+  IdeCodeIndexEntry *(*get_next_entry)      (IdeCodeIndexEntries  *self);
+  void               (*next_entries_async)  (IdeCodeIndexEntries  *self,
+                                             GCancellable         *cancellable,
+                                             GAsyncReadyCallback   callback,
+                                             gpointer              user_data);
+  GPtrArray         *(*next_entries_finish) (IdeCodeIndexEntries  *self,
+                                             GAsyncResult         *result,
+                                             GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeCodeIndexEntry *ide_code_index_entries_get_next_entry      (IdeCodeIndexEntries  *self);
+IDE_AVAILABLE_IN_3_32
+GFile             *ide_code_index_entries_get_file            (IdeCodeIndexEntries  *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_code_index_entries_next_entries_async  (IdeCodeIndexEntries  *self,
+                                                               GCancellable         *cancellable,
+                                                               GAsyncReadyCallback   callback,
+                                                               gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GPtrArray         *ide_code_index_entries_next_entries_finish (IdeCodeIndexEntries  *self,
+                                                               GAsyncResult         *result,
+                                                               GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-code-index-entry.c b/src/libide/code/ide-code-index-entry.c
new file mode 100644
index 000000000..70cd5bd95
--- /dev/null
+++ b/src/libide/code/ide-code-index-entry.c
@@ -0,0 +1,271 @@
+/* ide-code-index-entry.c
+ *
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
+ * Copyright 2017 Anoop Chandu <anoopchandu96 gmail 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 "ide-code-index-entry"
+
+#include "config.h"
+
+#include "ide-code-index-entry.h"
+
+/**
+ * SECTION:ide-code-index-entry
+ * @title: IdeCodeIndexEntry
+ * @short_description: information about code index entry
+ *
+ * The #IdeCodeIndexEntry structure contains information about something to be
+ * indexed in the code index. It is an immutable data object so that it can be
+ * passed between threads where data is indexed. Plugins should use
+ * #IdeCodeIndexEntryBuilder to create index entries.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeCodeIndexEntry
+{
+  gchar          *key;
+  gchar          *name;
+
+  IdeSymbolKind   kind;
+  IdeSymbolFlags  flags;
+
+  guint           begin_line;
+  guint           begin_line_offset;
+  guint           end_line;
+  guint           end_line_offset;
+};
+
+struct _IdeCodeIndexEntryBuilder
+{
+  IdeCodeIndexEntry entry;
+};
+
+G_DEFINE_BOXED_TYPE (IdeCodeIndexEntry,
+                     ide_code_index_entry,
+                     ide_code_index_entry_copy,
+                     ide_code_index_entry_free)
+G_DEFINE_BOXED_TYPE (IdeCodeIndexEntryBuilder,
+                     ide_code_index_entry_builder,
+                     ide_code_index_entry_builder_copy,
+                     ide_code_index_entry_builder_free)
+
+void
+ide_code_index_entry_free (IdeCodeIndexEntry *self)
+{
+  if (self != NULL)
+    {
+      g_clear_pointer (&self->name, g_free);
+      g_clear_pointer (&self->key, g_free);
+      g_slice_free (IdeCodeIndexEntry, self);
+    }
+}
+
+IdeCodeIndexEntry *
+ide_code_index_entry_copy (const IdeCodeIndexEntry *self)
+{
+  IdeCodeIndexEntry *copy = NULL;
+
+  if (self != NULL)
+    {
+      copy = g_slice_dup (IdeCodeIndexEntry, self);
+      copy->name = g_strdup (self->name);
+      copy->key = g_strdup (self->key);
+    }
+
+  return copy;
+}
+
+const gchar *
+ide_code_index_entry_get_key (const IdeCodeIndexEntry *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+
+  return self->key;
+}
+
+const gchar *
+ide_code_index_entry_get_name (const IdeCodeIndexEntry *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+
+  return self->name;
+}
+
+IdeSymbolKind
+ide_code_index_entry_get_kind (const IdeCodeIndexEntry *self)
+{
+  g_return_val_if_fail (self != NULL, IDE_SYMBOL_KIND_NONE);
+
+  return self->kind;
+}
+
+IdeSymbolFlags
+ide_code_index_entry_get_flags (const IdeCodeIndexEntry *self)
+{
+  g_return_val_if_fail (self != NULL, IDE_SYMBOL_FLAGS_NONE);
+
+  return self->flags;
+}
+
+/**
+ * ide_code_index_entry_get_range:
+ * @self: a #IdeCodeIndexEntry
+ * @begin_line: (out): first line
+ * @begin_line_offset: (out): first line offset
+ * @end_line: (out): last line
+ * @end_line_offset: (out): last line offset
+ *
+ * Since: 3.32
+ */
+void
+ide_code_index_entry_get_range (const IdeCodeIndexEntry *self,
+                                guint                   *begin_line,
+                                guint                   *begin_line_offset,
+                                guint                   *end_line,
+                                guint                   *end_line_offset)
+{
+  g_return_if_fail (self != NULL);
+
+  if (begin_line != NULL)
+    *begin_line = self->begin_line;
+
+  if (begin_line_offset != NULL)
+    *begin_line_offset = self->begin_line_offset;
+
+  if (end_line != NULL)
+    *end_line = self->end_line;
+
+  if (end_line_offset != NULL)
+    *end_line_offset = self->end_line_offset;
+}
+
+IdeCodeIndexEntryBuilder *
+ide_code_index_entry_builder_new (void)
+{
+  return g_slice_new0 (IdeCodeIndexEntryBuilder);
+}
+
+void
+ide_code_index_entry_builder_free (IdeCodeIndexEntryBuilder *builder)
+{
+  if (builder != NULL)
+    {
+      g_clear_pointer (&builder->entry.key, g_free);
+      g_clear_pointer (&builder->entry.name, g_free);
+      g_slice_free (IdeCodeIndexEntryBuilder, builder);
+    }
+}
+
+void
+ide_code_index_entry_builder_set_range (IdeCodeIndexEntryBuilder *builder,
+                                        guint                     begin_line,
+                                        guint                     begin_line_offset,
+                                        guint                     end_line,
+                                        guint                     end_line_offset)
+{
+  g_return_if_fail (builder != NULL);
+
+  builder->entry.begin_line = begin_line;
+  builder->entry.begin_line_offset = begin_line_offset;
+  builder->entry.end_line = end_line;
+  builder->entry.end_line_offset = end_line_offset;
+}
+
+void
+ide_code_index_entry_builder_set_name (IdeCodeIndexEntryBuilder *builder,
+                                       const gchar              *name)
+{
+  g_return_if_fail (builder != NULL);
+
+  if (name != builder->entry.name)
+    {
+      g_free (builder->entry.name);
+      builder->entry.name = g_strdup (name);
+    }
+}
+
+void
+ide_code_index_entry_builder_set_key (IdeCodeIndexEntryBuilder *builder,
+                                      const gchar              *key)
+{
+  g_return_if_fail (builder != NULL);
+
+  if (key != builder->entry.key)
+    {
+      g_free (builder->entry.key);
+      builder->entry.key = g_strdup (key);
+    }
+}
+
+void
+ide_code_index_entry_builder_set_flags (IdeCodeIndexEntryBuilder *builder,
+                                        IdeSymbolFlags            flags)
+{
+  g_return_if_fail (builder != NULL);
+
+  builder->entry.flags = flags;
+}
+
+void
+ide_code_index_entry_builder_set_kind (IdeCodeIndexEntryBuilder *builder,
+                                       IdeSymbolKind             kind)
+{
+  g_return_if_fail (builder != NULL);
+
+  builder->entry.kind = kind;
+}
+
+/**
+ * ide_code_index_entry_builder_build:
+ * @builder: a #IdeCodeIndexEntryBuilder
+ *
+ * Creates an immutable #IdeCodeIndexEntry from the builder content.
+ *
+ * Returns: (transfer full): an #IdeCodeIndexEntry
+ *
+ * Since: 3.32
+ */
+IdeCodeIndexEntry *
+ide_code_index_entry_builder_build (IdeCodeIndexEntryBuilder *builder)
+{
+  g_return_val_if_fail (builder != NULL, NULL);
+
+  return ide_code_index_entry_copy (&builder->entry);
+}
+
+/**
+ * ide_code_index_entry_builder_copy:
+ * @builder: a #IdeCodeIndexEntryBuilder
+ *
+ * Returns: (transfer full): a deep copy of @builder
+ *
+ * Since: 3.32
+ */
+IdeCodeIndexEntryBuilder *
+ide_code_index_entry_builder_copy (IdeCodeIndexEntryBuilder *builder)
+{
+  IdeCodeIndexEntryBuilder *copy;
+
+  copy = g_slice_dup (IdeCodeIndexEntryBuilder, builder);
+  copy->entry.key = g_strdup (builder->entry.key);
+  copy->entry.name = g_strdup (builder->entry.name);
+
+  return copy;
+}
diff --git a/src/libide/code/ide-code-index-entry.h b/src/libide/code/ide-code-index-entry.h
new file mode 100644
index 000000000..be13a4666
--- /dev/null
+++ b/src/libide/code/ide-code-index-entry.h
@@ -0,0 +1,92 @@
+/* ide-code-index-entry.h
+ *
+ * Copyright 2017 Anoop Chandu <anoopchandu96 gmail 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+#include "ide-symbol.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CODE_INDEX_ENTRY         (ide_code_index_entry_get_type())
+#define IDE_TYPE_CODE_INDEX_ENTRY_BUILDER (ide_code_index_entry_builder_get_type())
+
+typedef struct _IdeCodeIndexEntry        IdeCodeIndexEntry;
+typedef struct _IdeCodeIndexEntryBuilder IdeCodeIndexEntryBuilder;
+
+IDE_AVAILABLE_IN_3_32
+GType                     ide_code_index_entry_get_type          (void);
+IDE_AVAILABLE_IN_3_32
+GType                     ide_code_index_entry_builder_get_type  (void);
+IDE_AVAILABLE_IN_3_32
+IdeCodeIndexEntryBuilder *ide_code_index_entry_builder_new       (void);
+IDE_AVAILABLE_IN_3_32
+void                      ide_code_index_entry_builder_set_range (IdeCodeIndexEntryBuilder *builder,
+                                                                  guint                     begin_line,
+                                                                  guint                     
begin_line_offset,
+                                                                  guint                     end_line,
+                                                                  guint                     end_line_offset);
+IDE_AVAILABLE_IN_3_32
+void                      ide_code_index_entry_builder_set_key   (IdeCodeIndexEntryBuilder *builder,
+                                                                  const gchar              *key);
+IDE_AVAILABLE_IN_3_32
+void                      ide_code_index_entry_builder_set_name  (IdeCodeIndexEntryBuilder *builder,
+                                                                  const gchar              *name);
+IDE_AVAILABLE_IN_3_32
+void                      ide_code_index_entry_builder_set_kind  (IdeCodeIndexEntryBuilder *builder,
+                                                                  IdeSymbolKind             kind);
+IDE_AVAILABLE_IN_3_32
+void                      ide_code_index_entry_builder_set_flags (IdeCodeIndexEntryBuilder *builder,
+                                                                  IdeSymbolFlags            flags);
+IDE_AVAILABLE_IN_3_32
+IdeCodeIndexEntry        *ide_code_index_entry_builder_build     (IdeCodeIndexEntryBuilder *builder);
+IDE_AVAILABLE_IN_3_32
+IdeCodeIndexEntryBuilder *ide_code_index_entry_builder_copy      (IdeCodeIndexEntryBuilder *builder);
+IDE_AVAILABLE_IN_3_32
+void                      ide_code_index_entry_builder_free      (IdeCodeIndexEntryBuilder *builder);
+IDE_AVAILABLE_IN_3_32
+void                      ide_code_index_entry_free              (IdeCodeIndexEntry        *self);
+IDE_AVAILABLE_IN_3_32
+IdeCodeIndexEntry        *ide_code_index_entry_copy              (const IdeCodeIndexEntry  *self);
+IDE_AVAILABLE_IN_3_32
+const gchar              *ide_code_index_entry_get_key           (const IdeCodeIndexEntry  *self);
+IDE_AVAILABLE_IN_3_32
+const gchar              *ide_code_index_entry_get_name          (const IdeCodeIndexEntry  *self);
+IDE_AVAILABLE_IN_3_32
+IdeSymbolKind             ide_code_index_entry_get_kind          (const IdeCodeIndexEntry  *self);
+IDE_AVAILABLE_IN_3_32
+IdeSymbolFlags            ide_code_index_entry_get_flags         (const IdeCodeIndexEntry  *self);
+IDE_AVAILABLE_IN_3_32
+void                      ide_code_index_entry_get_range         (const IdeCodeIndexEntry  *self,
+                                                                  guint                    *begin_line,
+                                                                  guint                    
*begin_line_offset,
+                                                                  guint                    *end_line,
+                                                                  guint                    *end_line_offset);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeCodeIndexEntry, ide_code_index_entry_free)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeCodeIndexEntryBuilder, ide_code_index_entry_builder_free)
+
+G_END_DECLS
diff --git a/src/libide/code/ide-code-indexer.c b/src/libide/code/ide-code-indexer.c
new file mode 100644
index 000000000..0c1c903ce
--- /dev/null
+++ b/src/libide/code/ide-code-indexer.c
@@ -0,0 +1,234 @@
+/* ide-code-indexer.c
+ *
+ * Copyright 2017 Anoop Chandu <anoopchandu96 gmail com>
+ * Copyright 2018-2019 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 "ide-code-indexer"
+
+#include "config.h"
+
+#include "ide-code-indexer.h"
+#include "ide-location.h"
+
+/**
+ * SECTION:ide-code-indexer
+ * @title: IdeCodeIndexer
+ * @short_description: Interface for background indexing source code
+ *
+ * The #IdeCodeIndexer interface is used to index source code in the project.
+ * Plugins that want to provide global search features for source code should
+ * implement this interface and specify which languages they support in their
+ * .plugin definition, using "X-Code-Indexer-Languages". For example. to index
+ * Python source code, you might use:
+ *
+ *   X-Code-Indexer-Languages=python,python3
+ *
+ * Since: 3.32
+ */
+
+G_DEFINE_INTERFACE (IdeCodeIndexer, ide_code_indexer, IDE_TYPE_OBJECT)
+
+static void
+ide_code_indexer_real_index_file_async (IdeCodeIndexer      *self,
+                                        GFile               *file,
+                                        const gchar * const *build_flags,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  g_assert (IDE_IS_CODE_INDEXER (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_task_report_new_error (self, callback, user_data,
+                           ide_code_indexer_real_index_file_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Get key is not supported");
+}
+
+static IdeCodeIndexEntries *
+ide_code_indexer_real_index_file_finish (IdeCodeIndexer  *self,
+                                         GAsyncResult    *result,
+                                         GError         **error)
+{
+  g_assert (IDE_IS_CODE_INDEXER (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+ide_code_indexer_real_generate_key_async (IdeCodeIndexer      *self,
+                                          IdeLocation   *location,
+                                          const gchar * const *build_flags,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
+{
+  g_assert (IDE_IS_CODE_INDEXER (self));
+  g_assert (location != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_task_report_new_error (self, callback, user_data,
+                           ide_code_indexer_real_generate_key_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Get key is not supported");
+}
+
+static gchar *
+ide_code_indexer_real_generate_key_finish (IdeCodeIndexer  *self,
+                                           GAsyncResult    *result,
+                                           GError         **error)
+{
+  g_assert (IDE_IS_CODE_INDEXER (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+ide_code_indexer_default_init (IdeCodeIndexerInterface *iface)
+{
+  iface->index_file_async = ide_code_indexer_real_index_file_async;
+  iface->index_file_finish = ide_code_indexer_real_index_file_finish;
+  iface->generate_key_async = ide_code_indexer_real_generate_key_async;
+  iface->generate_key_finish = ide_code_indexer_real_generate_key_finish;
+}
+
+/**
+ * ide_code_indexer_index_file_async:
+ * @self: An #IdeCodeIndexer instance.
+ * @file: Source file to index.
+ * @build_flags: (nullable) (array zero-terminated=1): array of build flags to parse @file.
+ * @cancellable: (nullable): a #GCancellable.
+ * @callback: a #GAsyncReadyCallback
+ * @user_data: closure data for @callback
+ *
+ * This function will take index source file and create an array of symbols in
+ * @file. @callback is called upon completion and must call
+ * ide_code_indexer_index_file_finish() to complete the operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_code_indexer_index_file_async (IdeCodeIndexer      *self,
+                                   GFile               *file,
+                                   const gchar * const *build_flags,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+#ifdef IDE_ENABLE_TRACE
+  g_autoptr(GFile) copy = NULL;
+#endif
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CODE_INDEXER (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+#ifdef IDE_ENABLE_TRACE
+  /* Simplify leak detection */
+  file = copy = g_file_dup (file);
+#endif
+
+  return IDE_CODE_INDEXER_GET_IFACE (self)->index_file_async (self, file, build_flags, cancellable, 
callback, user_data);
+}
+
+/**
+ * ide_code_indexer_index_file_finish:
+ * @self: a #IdeCodeIndexer
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_code_indexer_index_file_async().
+ *
+ * Returns: (transfer full): an #IdeCodeIndexEntries if successful; otherwise %NULL
+ *   and @error is set.
+ *
+ * Since: 3.32
+ */
+IdeCodeIndexEntries *
+ide_code_indexer_index_file_finish (IdeCodeIndexer  *self,
+                                    GAsyncResult    *result,
+                                    GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CODE_INDEXER (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_CODE_INDEXER_GET_IFACE (self)->index_file_finish (self, result, error);
+}
+
+/**
+ * ide_code_indexer_generate_key_async:
+ * @self: An #IdeCodeIndexer instance.
+ * @location: (not nullable): Source location of refernece.
+ * @build_flags: (nullable) (array zero-terminated=1): array of build flags to parse @file.
+ * @cancellable: (nullable): a #GCancellable.
+ * @callback: A callback to execute upon indexing.
+ * @user_data: User data to pass to @callback.
+ *
+ * This function will get key of reference located at #IdeSoureLocation.
+ *
+ * In 3.30 this function gained the @build_flags parameter.
+ *
+ * Since: 3.32
+ */
+void
+ide_code_indexer_generate_key_async (IdeCodeIndexer      *self,
+                                     IdeLocation         *location,
+                                     const gchar * const *build_flags,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CODE_INDEXER (self));
+  g_return_if_fail (IDE_IS_LOCATION (location));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_CODE_INDEXER_GET_IFACE (self)->generate_key_async (self, location, build_flags, cancellable, callback, 
user_data);
+}
+
+/**
+ * ide_code_indexer_generate_key_finish:
+ * @self: an #IdeCodeIndexer
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Returns key for declaration of reference at a location.
+ *
+ * Returns: (transfer full) : A string which contains key.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_code_indexer_generate_key_finish (IdeCodeIndexer  *self,
+                                      GAsyncResult    *result,
+                                      GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CODE_INDEXER (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_CODE_INDEXER_GET_IFACE (self)->generate_key_finish (self, result, error);
+}
diff --git a/src/libide/code/ide-code-indexer.h b/src/libide/code/ide-code-indexer.h
new file mode 100644
index 000000000..46972f8f9
--- /dev/null
+++ b/src/libide/code/ide-code-indexer.h
@@ -0,0 +1,85 @@
+/* ide-code-indexer.h
+ *
+ * Copyright 2017 Anoop Chandu <anoopchandu96 gmail 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CODE_INDEXER (ide_code_indexer_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeCodeIndexer, ide_code_indexer, IDE, CODE_INDEXER, IdeObject)
+
+struct _IdeCodeIndexerInterface
+{
+  GTypeInterface         parent_iface;
+
+  void                 (*generate_key_async)     (IdeCodeIndexer       *self,
+                                                  IdeLocation          *location,
+                                                  const gchar * const  *build_flags,
+                                                  GCancellable         *cancellable,
+                                                  GAsyncReadyCallback   callback,
+                                                  gpointer              user_data);
+  gchar               *(*generate_key_finish)    (IdeCodeIndexer       *self,
+                                                  GAsyncResult         *result,
+                                                  GError              **error);
+  void                 (*index_file_async)       (IdeCodeIndexer       *self,
+                                                  GFile                *file,
+                                                  const gchar * const  *build_flags,
+                                                  GCancellable         *cancellable,
+                                                  GAsyncReadyCallback   callback,
+                                                  gpointer              user_data);
+  IdeCodeIndexEntries *(*index_file_finish)      (IdeCodeIndexer       *self,
+                                                  GAsyncResult         *result,
+                                                  GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void                  ide_code_indexer_index_file_async    (IdeCodeIndexer       *self,
+                                                            GFile                *file,
+                                                            const gchar * const  *build_flags,
+                                                            GCancellable         *cancellable,
+                                                            GAsyncReadyCallback   callback,
+                                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+IdeCodeIndexEntries  *ide_code_indexer_index_file_finish   (IdeCodeIndexer       *self,
+                                                            GAsyncResult         *result,
+                                                            GError              **error);
+IDE_AVAILABLE_IN_3_32
+void                  ide_code_indexer_generate_key_async  (IdeCodeIndexer       *self,
+                                                            IdeLocation          *location,
+                                                            const gchar * const  *build_flags,
+                                                            GCancellable         *cancellable,
+                                                            GAsyncReadyCallback   callback,
+                                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gchar                *ide_code_indexer_generate_key_finish (IdeCodeIndexer       *self,
+                                                            GAsyncResult         *result,
+                                                            GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-code-types.h b/src/libide/code/ide-code-types.h
new file mode 100644
index 000000000..4684bbc19
--- /dev/null
+++ b/src/libide/code/ide-code-types.h
@@ -0,0 +1,60 @@
+/* ide-code-types.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef struct _IdeBuffer IdeBuffer;
+typedef struct _IdeBufferAddin IdeBufferAddin;
+typedef struct _IdeBufferChangeMonitor IdeBufferChangeMonitor;
+typedef struct _IdeCodeIndexEntries IdeCodeIndexEntries;
+typedef struct _IdeCodeIndexEntry IdeCodeIndexEntry;
+typedef struct _IdeCodeIndexer IdeCodeIndexer;
+typedef struct _IdeBufferManager IdeBufferManager;
+typedef struct _IdeDiagnostic IdeDiagnostic;
+typedef struct _IdeDiagnosticProvider IdeDiagnosticProvider;
+typedef struct _IdeDiagnostics IdeDiagnostics;
+typedef struct _IdeDiagnosticsManager IdeDiagnosticsManager;
+typedef struct _IdeFile IdeFile;
+typedef struct _IdeFileSettings IdeFileSettings;
+typedef struct _IdeFormatter IdeFormatter;
+typedef struct _IdeFormatterOptions IdeFormatterOptions;
+typedef struct _IdeHighlightEngine IdeHighlightEngine;
+typedef struct _IdeHighlightIndex IdeHighlightIndex;
+typedef struct _IdeHighlighter IdeHighlighter;
+typedef struct _IdeLocation IdeLocation;
+typedef struct _IdeRange IdeRange;
+typedef struct _IdeRenameProvider IdeRenameProvider;
+typedef struct _IdeSymbol IdeSymbol;
+typedef struct _IdeSymbolNode IdeSymbolNode;
+typedef struct _IdeSymbolResolver IdeSymbolResolver;
+typedef struct _IdeSymbolTree IdeSymbolTree;
+typedef struct _IdeTextEdit IdeTextEdit;
+typedef struct _IdeUnsavedFile IdeUnsavedFile;
+typedef struct _IdeUnsavedFiles IdeUnsavedFiles;
+
+G_END_DECLS
diff --git a/src/libide/code/ide-diagnostic-provider.c b/src/libide/code/ide-diagnostic-provider.c
new file mode 100644
index 000000000..134901356
--- /dev/null
+++ b/src/libide/code/ide-diagnostic-provider.c
@@ -0,0 +1,156 @@
+/* ide-diagnostic-provider.c
+ *
+ * Copyright 2018-2019 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 "ide-diagnostic-provider"
+
+#include "config.h"
+
+#include "ide-buffer.h"
+#include "ide-diagnostic-provider.h"
+
+G_DEFINE_INTERFACE (IdeDiagnosticProvider, ide_diagnostic_provider, IDE_TYPE_OBJECT)
+
+enum {
+  INVALIDATED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+ide_diagnostic_provider_default_init (IdeDiagnosticProviderInterface *iface)
+{
+  /**
+   * IdeDiagnosticProvider::invlaidated:
+   *
+   * This signal should be emitted by diagnostic providers when they know their
+   * diagnostics have been invalidated out-of-band.
+   *
+   * Since: 3.32
+   */
+  signals [INVALIDATED] =
+    g_signal_new ("invalidated",
+                  G_TYPE_FROM_INTERFACE (iface),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+/**
+ * ide_diagnostic_provider_load:
+ * @self: a #IdeDiagnosticProvider
+ *
+ * Loads the provider, discovering any necessary resources.
+ *
+ * Since: 3.32
+ */
+void
+ide_diagnostic_provider_load (IdeDiagnosticProvider *self)
+{
+  g_return_if_fail (IDE_IS_DIAGNOSTIC_PROVIDER (self));
+
+  if (IDE_DIAGNOSTIC_PROVIDER_GET_IFACE (self)->load)
+    IDE_DIAGNOSTIC_PROVIDER_GET_IFACE (self)->load (self);
+}
+
+/**
+ * ide_diagnostic_provider_unload:
+ * @self: a #IdeDiagnosticProvider
+ *
+ * Unloads the provider and any allocated resources.
+ *
+ * Since: 3.32
+ */
+void
+ide_diagnostic_provider_unload (IdeDiagnosticProvider *self)
+{
+  g_return_if_fail (IDE_IS_DIAGNOSTIC_PROVIDER (self));
+
+  if (IDE_DIAGNOSTIC_PROVIDER_GET_IFACE (self)->unload)
+    IDE_DIAGNOSTIC_PROVIDER_GET_IFACE (self)->unload (self);
+}
+
+/**
+ * ide_diagnostic_provider_diagnose_async:
+ * @self: a #IdeDiagnosticProvider
+ * @file: a #GFile
+ * @contents: (nullable): the content for the buffer
+ * @lang_id: (nullable): the language id for the buffer
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Requests the provider diagnose @file using @contents as the contents of
+ * the file.
+ *
+ * @callback is executed upon completion, and the caller should call
+ * ide_diagnostic_provider_diagnose_finish() to get the result.
+ *
+ * Since: 3.32
+ */
+void
+ide_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *self,
+                                        GFile                 *file,
+                                        GBytes                *contents,
+                                        const gchar           *lang_id,
+                                        GCancellable          *cancellable,
+                                        GAsyncReadyCallback    callback,
+                                        gpointer               user_data)
+{
+  g_return_if_fail (IDE_IS_DIAGNOSTIC_PROVIDER (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_DIAGNOSTIC_PROVIDER_GET_IFACE (self)->diagnose_async (self,
+                                                            file,
+                                                            contents,
+                                                            lang_id,
+                                                            cancellable,
+                                                            callback,
+                                                            user_data);
+}
+
+/**
+ * ide_diagnostic_provider_diagnose_finish:
+ * @self: a #IdeDiagnosticProvider
+ *
+ * Completes an asynchronous request to diagnose a file.
+ *
+ * Returns: (transfer full): an #IdeDiagnostics or %NULL and @error is set.
+ *
+ * Since: 3.32
+ */
+IdeDiagnostics *
+ide_diagnostic_provider_diagnose_finish (IdeDiagnosticProvider  *self,
+                                         GAsyncResult           *result,
+                                         GError                **error)
+{
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC_PROVIDER (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_DIAGNOSTIC_PROVIDER_GET_IFACE (self)->diagnose_finish (self, result, error);
+}
+
+void
+ide_diagnostic_provider_emit_invalidated (IdeDiagnosticProvider *self)
+{
+  g_return_if_fail (IDE_IS_DIAGNOSTIC_PROVIDER (self));
+
+  g_signal_emit (self, signals [INVALIDATED], 0);
+}
diff --git a/src/libide/code/ide-diagnostic-provider.h b/src/libide/code/ide-diagnostic-provider.h
new file mode 100644
index 000000000..6f5c1b71b
--- /dev/null
+++ b/src/libide/code/ide-diagnostic-provider.h
@@ -0,0 +1,76 @@
+/* ide-diagnostic-provider.h
+ *
+ * Copyright 2015-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DIAGNOSTIC_PROVIDER (ide_diagnostic_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeDiagnosticProvider, ide_diagnostic_provider, IDE, DIAGNOSTIC_PROVIDER, IdeObject)
+
+struct _IdeDiagnosticProviderInterface
+{
+  GTypeInterface parent_iface;
+
+  void            (*load)            (IdeDiagnosticProvider  *self);
+  void            (*unload)          (IdeDiagnosticProvider  *self);
+  void            (*diagnose_async)  (IdeDiagnosticProvider  *self,
+                                      GFile                  *file,
+                                      GBytes                 *contents,
+                                      const gchar            *lang_id,
+                                      GCancellable           *cancellable,
+                                      GAsyncReadyCallback     callback,
+                                      gpointer                user_data);
+  IdeDiagnostics *(*diagnose_finish) (IdeDiagnosticProvider  *self,
+                                      GAsyncResult           *result,
+                                      GError                **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void            ide_diagnostic_provider_load             (IdeDiagnosticProvider  *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_diagnostic_provider_unload           (IdeDiagnosticProvider  *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_diagnostic_provider_emit_invalidated (IdeDiagnosticProvider  *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_diagnostic_provider_diagnose_async   (IdeDiagnosticProvider  *self,
+                                                          GFile                  *file,
+                                                          GBytes                 *contents,
+                                                          const gchar            *lang_id,
+                                                          GCancellable           *cancellable,
+                                                          GAsyncReadyCallback     callback,
+                                                          gpointer                user_data);
+IDE_AVAILABLE_IN_3_32
+IdeDiagnostics *ide_diagnostic_provider_diagnose_finish  (IdeDiagnosticProvider  *self,
+                                                          GAsyncResult           *result,
+                                                          GError                **error);
+
+
+G_END_DECLS
diff --git a/src/libide/code/ide-diagnostic.c b/src/libide/code/ide-diagnostic.c
new file mode 100644
index 000000000..83e9b3c13
--- /dev/null
+++ b/src/libide/code/ide-diagnostic.c
@@ -0,0 +1,748 @@
+/* ide-diagnostic.c
+ *
+ * Copyright 2018-2019 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 "ide-diagnostic"
+
+#include "config.h"
+
+#include "ide-code-enums.h"
+#include "ide-diagnostic.h"
+#include "ide-location.h"
+#include "ide-range.h"
+#include "ide-text-edit.h"
+
+typedef struct
+{
+  IdeDiagnosticSeverity  severity;
+  guint                  hash;
+  gchar                 *text;
+  IdeLocation           *location;
+  GPtrArray             *ranges;
+  GPtrArray             *fixits;
+} IdeDiagnosticPrivate;
+
+enum {
+  PROP_0,
+  PROP_LOCATION,
+  PROP_SEVERITY,
+  PROP_TEXT,
+  PROP_DISPLAY_TEXT,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeDiagnostic, ide_diagnostic, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_diagnostic_set_location (IdeDiagnostic *self,
+                             IdeLocation   *location)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
+
+  g_set_object (&priv->location, location);
+}
+
+static void
+ide_diagnostic_set_text (IdeDiagnostic *self,
+                         const gchar   *text)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
+
+  priv->text = g_strdup (text);
+}
+
+static void
+ide_diagnostic_set_severity (IdeDiagnostic         *self,
+                             IdeDiagnosticSeverity  severity)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
+
+  priv->severity = severity;
+}
+
+static void
+ide_diagnostic_finalize (GObject *object)
+{
+  IdeDiagnostic *self = (IdeDiagnostic *)object;
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_clear_pointer (&priv->text, g_free);
+  g_clear_pointer (&priv->ranges, g_ptr_array_unref);
+  g_clear_pointer (&priv->fixits, g_ptr_array_unref);
+  g_clear_object (&priv->location);
+
+  G_OBJECT_CLASS (ide_diagnostic_parent_class)->finalize (object);
+}
+
+static void
+ide_diagnostic_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeDiagnostic *self = IDE_DIAGNOSTIC (object);
+
+  switch (prop_id)
+    {
+    case PROP_LOCATION:
+      g_value_set_object (value, ide_diagnostic_get_location (self));
+      break;
+
+    case PROP_SEVERITY:
+      g_value_set_enum (value, ide_diagnostic_get_severity (self));
+      break;
+
+    case PROP_DISPLAY_TEXT:
+      g_value_take_string (value, ide_diagnostic_get_text_for_display (self));
+      break;
+
+    case PROP_TEXT:
+      g_value_set_string (value, ide_diagnostic_get_text (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_diagnostic_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeDiagnostic *self = IDE_DIAGNOSTIC (object);
+
+  switch (prop_id)
+    {
+    case PROP_LOCATION:
+      ide_diagnostic_set_location (self, g_value_get_object (value));
+      break;
+
+    case PROP_SEVERITY:
+      ide_diagnostic_set_severity (self, g_value_get_enum (value));
+      break;
+
+    case PROP_TEXT:
+      ide_diagnostic_set_text (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_diagnostic_class_init (IdeDiagnosticClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_diagnostic_finalize;
+  object_class->get_property = ide_diagnostic_get_property;
+  object_class->set_property = ide_diagnostic_set_property;
+
+  properties [PROP_LOCATION] =
+    g_param_spec_object ("location",
+                         "Location",
+                         "The location of the diagnostic",
+                         IDE_TYPE_LOCATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SEVERITY] =
+    g_param_spec_enum ("severity",
+                       "Severity",
+                       "The severity of the diagnostic",
+                       IDE_TYPE_DIAGNOSTIC_SEVERITY,
+                       IDE_DIAGNOSTIC_IGNORED,
+                       (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TEXT] =
+    g_param_spec_string ("text",
+                         "Text",
+                         "The text of the diagnostic",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DISPLAY_TEXT] =
+    g_param_spec_string ("display-text",
+                         "Display Text",
+                         "The text formatted for display",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_diagnostic_init (IdeDiagnostic *self)
+{
+}
+
+/**
+ * ide_diagnostic_get_location:
+ * @self: a #IdeDiagnostic
+ *
+ * Gets the location of the diagnostic.
+ *
+ * See also: ide_diagnostic_get_range().
+ *
+ * Returns: (transfer none) (nullable): an #IdeLocation or %NULL
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_diagnostic_get_location (IdeDiagnostic *self)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);
+
+  if (priv->location != NULL)
+    return priv->location;
+
+  if (priv->ranges != NULL && priv->ranges->len > 0)
+    {
+      IdeRange *range = g_ptr_array_index (priv->ranges, 0);
+      return ide_range_get_begin (range);
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_diagnostic_get_file:
+ * @self: a #IdeDiagnostic
+ *
+ * Gets the file containing the diagnostic, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeLocation or %NULL
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_diagnostic_get_file (IdeDiagnostic *self)
+{
+  IdeLocation *location;
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);
+
+  if ((location = ide_diagnostic_get_location (self)))
+    return ide_location_get_file (location);
+
+  return NULL;
+}
+
+/**
+ * ide_diagnostic_get_text_for_display:
+ * @self: an #IdeDiagnostic
+ *
+ * This creates a new string that is formatted using the diagnostics
+ * line number, column, severity, and message text in the format
+ * "line:column: severity: message".
+ *
+ * This can be convenient when wanting to quickly display a
+ * diagnostic such as in a tooltip.
+ *
+ * Returns: (transfer full): string containing the text formatted for
+ *   display.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_diagnostic_get_text_for_display (IdeDiagnostic *self)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+  IdeLocation *location;
+  const gchar *severity;
+  guint line = 0;
+  guint column = 0;
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);
+
+  severity = ide_diagnostic_severity_to_string (priv->severity);
+  location = ide_diagnostic_get_location (self);
+
+  if (location != NULL)
+    {
+      line = ide_location_get_line (location) + 1;
+      column = ide_location_get_line_offset (location) + 1;
+    }
+
+  return g_strdup_printf ("%u:%u: %s: %s", line, column, severity, priv->text);
+}
+
+/**
+ * ide_diagnostic_severity_to_string:
+ * @severity: a #IdeDiagnosticSeverity
+ *
+ * Returns a string suitable to represent the diagnsotic severity.
+ *
+ * Returns: a string
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_diagnostic_severity_to_string (IdeDiagnosticSeverity severity)
+{
+  switch (severity)
+    {
+    case IDE_DIAGNOSTIC_IGNORED:
+      return "ignored";
+
+    case IDE_DIAGNOSTIC_NOTE:
+      return "note";
+
+    case IDE_DIAGNOSTIC_DEPRECATED:
+      return "deprecated";
+
+    case IDE_DIAGNOSTIC_WARNING:
+      return "warning";
+
+    case IDE_DIAGNOSTIC_ERROR:
+      return "error";
+
+    case IDE_DIAGNOSTIC_FATAL:
+      return "fatal";
+
+    default:
+      return "unknown";
+    }
+}
+
+guint
+ide_diagnostic_get_n_ranges (IdeDiagnostic *self)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), 0);
+
+  return priv->ranges ? priv->ranges->len : 0;
+}
+
+/**
+ * ide_diagnostic_get_range:
+ *
+ * Retrieves the range found at @index. It is a programming error to call this
+ * function with a value greater or equal to ide_diagnostic_get_n_ranges().
+ *
+ * Returns: (transfer none) (nullable): An #IdeRange
+ *
+ * Since: 3.32
+ */
+IdeRange *
+ide_diagnostic_get_range (IdeDiagnostic *self,
+                          guint          index)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);
+
+  if (priv->ranges)
+    {
+      if (index < priv->ranges->len)
+        return g_ptr_array_index (priv->ranges, index);
+    }
+
+  return NULL;
+}
+
+guint
+ide_diagnostic_get_n_fixits (IdeDiagnostic *self)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), 0);
+
+  return (priv->fixits != NULL) ? priv->fixits->len : 0;
+}
+
+/**
+ * ide_diagnostic_get_fixit:
+ * @self: an #IdeDiagnostic.
+ * @index: The index of the fixit.
+ *
+ * Gets the fixit denoted by @index. This value should be less than the value
+ * returned from ide_diagnostic_get_n_fixits().
+ *
+ * Returns: (transfer none) (nullable): An #IdeTextEdit
+ *
+ * Since: 3.32
+ */
+IdeTextEdit *
+ide_diagnostic_get_fixit (IdeDiagnostic *self,
+                          guint          index)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_val_if_fail (self, NULL);
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);
+  g_return_val_if_fail (priv->fixits, NULL);
+
+  if (priv->fixits != NULL)
+    {
+      if (index < priv->fixits->len)
+        return g_ptr_array_index (priv->fixits, index);
+    }
+
+  return NULL;
+}
+
+gint
+ide_diagnostic_compare (IdeDiagnostic *a,
+                        IdeDiagnostic *b)
+{
+  IdeDiagnosticPrivate *priv_a = ide_diagnostic_get_instance_private (a);
+  IdeDiagnosticPrivate *priv_b = ide_diagnostic_get_instance_private (b);
+  gint ret;
+
+  g_assert (IDE_IS_DIAGNOSTIC (a));
+  g_assert (IDE_IS_DIAGNOSTIC (b));
+
+  /* Severity is 0..N where N is more important. So reverse comparator. */
+  if (0 != (ret = (gint)priv_b->severity - (gint)priv_a->severity))
+    return ret;
+
+  if (priv_a->location && priv_b->location)
+    {
+      if (0 != (ret = ide_location_compare (priv_a->location, priv_b->location)))
+        return ret;
+    }
+
+  return g_strcmp0 (priv_a->text, priv_b->text);
+}
+
+const gchar *
+ide_diagnostic_get_text (IdeDiagnostic *self)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);
+
+  return priv->text;
+}
+
+IdeDiagnosticSeverity
+ide_diagnostic_get_severity (IdeDiagnostic *self)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), 0);
+
+  return priv->severity;
+}
+
+/**
+ * ide_diagnostic_add_range:
+ * @self: a #IdeDiagnostic
+ * @range: an #IdeRange
+ *
+ * Adds a source range to the diagnostic.
+ *
+ * Since: 3.32
+ */
+void
+ide_diagnostic_add_range (IdeDiagnostic *self,
+                          IdeRange      *range)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
+  g_return_if_fail (IDE_IS_RANGE (range));
+
+  if (priv->ranges == NULL)
+    priv->ranges = g_ptr_array_new_with_free_func (g_object_unref);
+  g_ptr_array_add (priv->ranges, g_object_ref (range));
+}
+
+/**
+ * ide_diagnostic_take_range:
+ * @self: a #IdeDiagnostic
+ * @range: (transfer full): an #IdeRange
+ *
+ * Adds a source range to the diagnostic, but does not increment the
+ * reference count of @range.
+ *
+ * Since: 3.32
+ */
+void
+ide_diagnostic_take_range (IdeDiagnostic *self,
+                           IdeRange      *range)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
+  g_return_if_fail (IDE_IS_RANGE (range));
+
+  if (priv->ranges == NULL)
+    priv->ranges = g_ptr_array_new_with_free_func (g_object_unref);
+  g_ptr_array_add (priv->ranges, g_steal_pointer (&range));
+}
+
+/**
+ * ide_diagnostic_add_fixit:
+ * @self: a #IdeDiagnostic
+ * @fixit: an #IdeTextEdit
+ *
+ * Adds a source fixit to the diagnostic.
+ *
+ * Since: 3.32
+ */
+void
+ide_diagnostic_add_fixit (IdeDiagnostic *self,
+                          IdeTextEdit   *fixit)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
+  g_return_if_fail (IDE_IS_TEXT_EDIT (fixit));
+
+  if (priv->fixits == NULL)
+    priv->fixits = g_ptr_array_new_with_free_func (g_object_unref);
+  g_ptr_array_add (priv->fixits, g_object_ref (fixit));
+}
+
+/**
+ * ide_diagnostic_take_fixit:
+ * @self: a #IdeDiagnostic
+ * @fixit: (transfer full): an #IdeTextEdit
+ *
+ * Adds a source fixit to the diagnostic, but does not increment the
+ * reference count of @fixit.
+ *
+ * Since: 3.32
+ */
+void
+ide_diagnostic_take_fixit (IdeDiagnostic *self,
+                           IdeTextEdit   *fixit)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DIAGNOSTIC (self));
+  g_return_if_fail (IDE_IS_TEXT_EDIT (fixit));
+
+  if (priv->fixits == NULL)
+    priv->fixits = g_ptr_array_new_with_free_func (g_object_unref);
+  g_ptr_array_add (priv->fixits, g_steal_pointer (&fixit));
+}
+
+IdeDiagnostic *
+ide_diagnostic_new (IdeDiagnosticSeverity  severity,
+                    const gchar           *message,
+                    IdeLocation           *location)
+{
+  return g_object_new (IDE_TYPE_DIAGNOSTIC,
+                       "severity", severity,
+                       "location", location,
+                       "text", message,
+                       NULL);
+}
+
+guint
+ide_diagnostic_hash (IdeDiagnostic *self)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), 0);
+
+  if (priv->hash == 0)
+    {
+      guint hash = g_str_hash (priv->text ?: "");
+      if (priv->location)
+        hash ^= ide_location_hash (priv->location);
+      if (priv->fixits)
+        hash ^= g_int_hash (&priv->fixits->len);
+      if (priv->ranges)
+        hash ^= g_int_hash (&priv->ranges->len);
+      priv->hash = hash;
+    }
+
+  return priv->hash;
+}
+
+/**
+ * ide_diagnostic_to_variant:
+ * @self: a #IdeDiagnostic
+ *
+ * Creates a #GVariant to represent the diagnostic. This can be useful when
+ * working in subprocesses to serialize the diagnostic.
+ *
+ * This function will never return a floating variant.
+ *
+ * Returns: (transfer full): a #GVariant
+ *
+ * Since: 3.32
+ */
+GVariant *
+ide_diagnostic_to_variant (IdeDiagnostic *self)
+{
+  IdeDiagnosticPrivate *priv = ide_diagnostic_get_instance_private (self);
+  GVariantDict dict;
+
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (IDE_IS_DIAGNOSTIC (self), NULL);
+
+  g_variant_dict_init (&dict, NULL);
+
+  g_variant_dict_insert (&dict, "text", "s", priv->text ?: "");
+  g_variant_dict_insert (&dict, "severity", "u", priv->severity);
+
+  if (priv->location != NULL)
+    {
+      g_autoptr(GVariant) vloc = ide_location_to_variant (priv->location);
+
+      if (vloc != NULL)
+        g_variant_dict_insert_value (&dict, "location", vloc);
+    }
+
+  if (priv->ranges != NULL && priv->ranges->len > 0)
+    {
+      GVariantBuilder builder;
+
+      g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
+
+      for (guint i = 0; i < priv->ranges->len; i++)
+        {
+          IdeRange *range = g_ptr_array_index (priv->ranges, i);
+          g_autoptr(GVariant) vrange = ide_range_to_variant (range);
+
+          g_variant_builder_add_value (&builder, vrange);
+        }
+
+      g_variant_dict_insert_value (&dict, "ranges", g_variant_builder_end (&builder));
+    }
+
+  if (priv->fixits != NULL && priv->fixits->len > 0)
+    {
+      GVariantBuilder builder;
+
+      g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
+
+      for (guint i = 0; i < priv->fixits->len; i++)
+        {
+          IdeTextEdit *fixit = g_ptr_array_index (priv->fixits, i);
+          g_autoptr(GVariant) vfixit = ide_text_edit_to_variant (fixit);
+
+          g_variant_builder_add_value (&builder, vfixit);
+        }
+
+      g_variant_dict_insert_value (&dict, "fixits", g_variant_builder_end (&builder));
+    }
+
+  return g_variant_take_ref (g_variant_dict_end (&dict));
+}
+
+/**
+ * ide_diagnostic_new_from_variant:
+ * @variant: (nullable): a #GVariant or %NULL
+ *
+ * Creates a new #GVariant using the data contained in @variant.
+ *
+ * If @variant is %NULL or Upon failure, %NULL is returned.
+ *
+ * Returns: (nullable) (transfer full): a #IdeDiagnostic or %NULL
+ *
+ * Since: 3.32
+ */
+IdeDiagnostic *
+ide_diagnostic_new_from_variant (GVariant *variant)
+{
+  g_autoptr(IdeLocation) loc = NULL;
+  g_autoptr(GVariant) vloc = NULL;
+  g_autoptr(GVariant) unboxed = NULL;
+  g_autoptr(GVariant) ranges = NULL;
+  g_autoptr(GVariant) fixits = NULL;
+  IdeDiagnostic *self;
+  GVariantDict dict;
+  GVariantIter iter;
+  const gchar *text;
+  guint32 severity;
+
+  if (variant == NULL)
+    return NULL;
+
+  if (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARIANT))
+    variant = unboxed = g_variant_get_variant (variant);
+
+  if (!g_variant_is_of_type (variant, G_VARIANT_TYPE_VARDICT))
+    return NULL;
+
+  g_variant_dict_init (&dict, variant);
+
+  if (!g_variant_dict_lookup (&dict, "text", "&s", &text))
+    text = NULL;
+
+  if (!g_variant_dict_lookup (&dict, "severity", "u", &severity))
+    severity = 0;
+
+  if ((vloc = g_variant_dict_lookup_value (&dict, "location", NULL)))
+    loc = ide_location_new_from_variant (vloc);
+
+  if (!(self = ide_diagnostic_new (severity, text, loc)))
+    goto failure;
+
+  /* Ranges */
+  if ((ranges = g_variant_dict_lookup_value (&dict, "ranges", NULL)))
+    {
+      GVariant *vrange;
+
+      g_variant_iter_init (&iter, ranges);
+
+      while ((vrange = g_variant_iter_next_value (&iter)))
+        {
+          IdeRange *range;
+
+          if ((range = ide_range_new_from_variant (vrange)))
+            ide_diagnostic_take_range (self, g_steal_pointer (&range));
+
+          g_variant_unref (vrange);
+        }
+    }
+
+  /* Fixits */
+  if ((fixits = g_variant_dict_lookup_value (&dict, "fixits", NULL)))
+    {
+      GVariant *vfixit;
+
+      g_variant_iter_init (&iter, fixits);
+
+      while ((vfixit = g_variant_iter_next_value (&iter)))
+        {
+          IdeTextEdit *fixit;
+
+          if ((fixit = ide_text_edit_new_from_variant (vfixit)))
+            ide_diagnostic_take_fixit (self, g_steal_pointer (&fixit));
+
+          g_variant_unref (vfixit);
+        }
+    }
+
+failure:
+
+  g_variant_dict_clear (&dict);
+
+  return self;
+}
diff --git a/src/libide/code/ide-diagnostic.h b/src/libide/code/ide-diagnostic.h
new file mode 100644
index 000000000..6f9b093a9
--- /dev/null
+++ b/src/libide/code/ide-diagnostic.h
@@ -0,0 +1,104 @@
+/* ide-diagnostic.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DIAGNOSTIC (ide_diagnostic_get_type())
+
+typedef enum
+{
+  IDE_DIAGNOSTIC_IGNORED    = 0,
+  IDE_DIAGNOSTIC_NOTE       = 1,
+  IDE_DIAGNOSTIC_DEPRECATED = 2,
+  IDE_DIAGNOSTIC_WARNING    = 3,
+  IDE_DIAGNOSTIC_ERROR      = 4,
+  IDE_DIAGNOSTIC_FATAL      = 5,
+} IdeDiagnosticSeverity;
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeDiagnostic, ide_diagnostic, IDE, DIAGNOSTIC, IdeObject)
+
+struct _IdeDiagnosticClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeDiagnostic         *ide_diagnostic_new                  (IdeDiagnosticSeverity  severity,
+                                                            const gchar           *message,
+                                                            IdeLocation           *location);
+IDE_AVAILABLE_IN_3_32
+IdeDiagnostic         *ide_diagnostic_new_from_variant     (GVariant              *variant);
+IDE_AVAILABLE_IN_3_32
+guint                  ide_diagnostic_hash                 (IdeDiagnostic         *self);
+IDE_AVAILABLE_IN_3_32
+IdeLocation           *ide_diagnostic_get_location         (IdeDiagnostic         *self);
+IDE_AVAILABLE_IN_3_32
+const gchar           *ide_diagnostic_get_text             (IdeDiagnostic         *self);
+IDE_AVAILABLE_IN_3_32
+IdeDiagnosticSeverity  ide_diagnostic_get_severity         (IdeDiagnostic         *self);
+IDE_AVAILABLE_IN_3_32
+GFile                 *ide_diagnostic_get_file             (IdeDiagnostic         *self);
+IDE_AVAILABLE_IN_3_32
+gchar                 *ide_diagnostic_get_text_for_display (IdeDiagnostic         *self);
+IDE_AVAILABLE_IN_3_32
+const gchar           *ide_diagnostic_severity_to_string   (IdeDiagnosticSeverity  severity);
+IDE_AVAILABLE_IN_3_32
+guint                  ide_diagnostic_get_n_ranges         (IdeDiagnostic         *self);
+IDE_AVAILABLE_IN_3_32
+IdeRange              *ide_diagnostic_get_range            (IdeDiagnostic         *self,
+                                                            guint                  index);
+IDE_AVAILABLE_IN_3_32
+guint                  ide_diagnostic_get_n_fixits         (IdeDiagnostic         *self);
+IDE_AVAILABLE_IN_3_32
+IdeTextEdit           *ide_diagnostic_get_fixit            (IdeDiagnostic         *self,
+                                                            guint                  index);
+IDE_AVAILABLE_IN_3_32
+void                   ide_diagnostic_add_range            (IdeDiagnostic         *self,
+                                                            IdeRange              *range);
+IDE_AVAILABLE_IN_3_32
+void                   ide_diagnostic_take_range           (IdeDiagnostic         *self,
+                                                            IdeRange              *range);
+IDE_AVAILABLE_IN_3_32
+void                   ide_diagnostic_add_fixit            (IdeDiagnostic         *self,
+                                                            IdeTextEdit           *fixit);
+IDE_AVAILABLE_IN_3_32
+void                   ide_diagnostic_take_fixit           (IdeDiagnostic         *self,
+                                                            IdeTextEdit           *fixit);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_diagnostic_compare              (IdeDiagnostic         *a,
+                                                            IdeDiagnostic         *b);
+IDE_AVAILABLE_IN_3_32
+GVariant              *ide_diagnostic_to_variant           (IdeDiagnostic         *self);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-diagnostics-manager-private.h 
b/src/libide/code/ide-diagnostics-manager-private.h
new file mode 100644
index 000000000..d431bd9b2
--- /dev/null
+++ b/src/libide/code/ide-diagnostics-manager-private.h
@@ -0,0 +1,41 @@
+/* ide-diagnostics-manager-private.h
+ *
+ * Copyright 2018-2019 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 "ide-buffer.h"
+#include "ide-diagnostics-manager.h"
+
+G_BEGIN_DECLS
+
+void _ide_diagnostics_manager_file_opened      (IdeDiagnosticsManager *self,
+                                                GFile                 *file,
+                                                const gchar           *lang_id);
+void _ide_diagnostics_manager_file_closed      (IdeDiagnosticsManager *self,
+                                                GFile                 *file);
+void _ide_diagnostics_manager_language_changed (IdeDiagnosticsManager *self,
+                                                GFile                 *file,
+                                                const gchar           *lang_id);
+void _ide_diagnostics_manager_file_changed     (IdeDiagnosticsManager *self,
+                                                GFile                 *file,
+                                                GBytes                *contents,
+                                                const gchar           *lang_id);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-diagnostics-manager.c b/src/libide/code/ide-diagnostics-manager.c
new file mode 100644
index 000000000..64627e3f7
--- /dev/null
+++ b/src/libide/code/ide-diagnostics-manager.c
@@ -0,0 +1,1177 @@
+/* ide-diagnostics-manager.c
+ *
+ * Copyright 2016-2019 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 "ide-diagnostics-manager"
+
+#include "config.h"
+
+#include <gtksourceview/gtksource.h>
+#include <libide-plugins.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-manager.h"
+#include "ide-buffer-private.h"
+#include "ide-diagnostic.h"
+#include "ide-diagnostic-provider.h"
+#include "ide-diagnostics.h"
+#include "ide-diagnostics-manager.h"
+#include "ide-diagnostics-manager-private.h"
+
+#define DEFAULT_DIAGNOSE_DELAY 333
+#define DIAG_GROUP_MAGIC       0xF1282727
+#define IS_DIAGNOSTICS_GROUP(g) ((g) && (g)->magic == DIAG_GROUP_MAGIC)
+
+typedef struct
+{
+  /*
+   * Used to give ourself a modicum of assurance our structure hasn't
+   * been miss-used.
+   */
+  guint magic;
+
+  /*
+   * This is our identifier for the diagnostics. We use this as the key in
+   * the hash table so that we can quickly find the target buffer. If the
+   * IdeBuffer:file property changes, we will have to fallback to the
+   * buffer to clear old entries.
+   */
+  GFile *file;
+
+  /*
+   * This hash table uses the given provider as the key and the last
+   * reported IdeDiagnostics as the value.
+   */
+  GHashTable *diagnostics_by_provider;
+
+  /*
+   * This extension set adapter is used to update the providers that are
+   * available based on the buffers current language. They may change
+   * at runtime due to the buffers language changing. When that happens
+   * we purge items from @diagnostics_by_provider and queue a diagnose
+   * request of the new provider.
+   */
+  IdeExtensionSetAdapter *adapter;
+
+  /* The most recent bytes we received for a future diagnosis. */
+  GBytes *contents;
+
+  /* The last language id we were notified about */
+  const gchar *lang_id;
+
+  /*
+   * This is our sequence number for diagnostics. It is monotonically
+   * increasing with every diagnostic discovered.
+   */
+  guint sequence;
+
+  /*
+   * If we are currently diagnosing, then this will be set to a
+   * number greater than zero.
+   */
+  guint in_diagnose;
+
+  /*
+   * If we need a diagnose this bit will be set. If we complete a
+   * diagnosis and this bit is set, then we will automatically queue
+   * another diagnose upon completion.
+   */
+  guint needs_diagnose : 1;
+
+  /*
+   * This bit is set if we know the file or buffer has diagnostics. This
+   * is useful when we've cleaned up our extensions and no longer have
+   * the diagnostics loaded in memory, but we know that it previously
+   * had diagnostics which have not been rectified.
+   */
+  guint has_diagnostics : 1;
+
+  /*
+   * This bit is set when the group has been removed from the
+   * IdeDiagnosticsManager. That allows the providers to cleanup
+   * as necessary when their async operations complete.
+   */
+  guint was_removed : 1;
+
+} IdeDiagnosticsGroup;
+
+struct _IdeDiagnosticsManager
+{
+  IdeObject parent_instance;
+
+  /*
+   * This hashtable contains a mapping of GFile to the IdeDiagnosticsGroup
+   * for the file. When a buffer is renamed (the IdeBuffer:file property
+   * is changed) we need to update this entry so it reflects the new
+   * location.
+   */
+  GHashTable *groups_by_file;
+
+  /*
+   * If any group has a queued diagnose in process, this will be set so
+   * we can coalesce the dispatch of everything at the same time.
+   */
+  guint queued_diagnose_source;
+};
+
+enum {
+  PROP_0,
+  PROP_BUSY,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+
+static gboolean ide_diagnostics_manager_clear_by_provider (IdeDiagnosticsManager *self,
+                                                           IdeDiagnosticProvider *provider);
+static void     ide_diagnostics_manager_add_diagnostic    (IdeDiagnosticsManager *self,
+                                                           IdeDiagnosticProvider *provider,
+                                                           IdeDiagnostic         *diagnostic);
+static void     ide_diagnostics_group_queue_diagnose      (IdeDiagnosticsGroup   *group,
+                                                           IdeDiagnosticsManager *self);
+
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+G_DEFINE_TYPE (IdeDiagnosticsManager, ide_diagnostics_manager, IDE_TYPE_OBJECT)
+
+static void
+free_diagnostics (gpointer data)
+{
+  IdeDiagnostics *diagnostics = data;
+
+  g_clear_object (&diagnostics);
+}
+
+static inline guint
+diagnostics_get_size (IdeDiagnostics *diags)
+{
+  return diags ? g_list_model_get_n_items (G_LIST_MODEL (diags)) : 0;
+}
+
+static void
+ide_diagnostics_group_finalize (IdeDiagnosticsGroup *group)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+  group->magic = 0;
+
+  g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);
+  g_clear_pointer (&group->contents, g_bytes_unref);
+  ide_clear_and_destroy_object (&group->adapter);
+  g_clear_object (&group->file);
+}
+
+static IdeDiagnosticsGroup *
+ide_diagnostics_group_new (GFile *file)
+{
+  IdeDiagnosticsGroup *group;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_FILE (file));
+
+  group = g_rc_box_new0 (IdeDiagnosticsGroup);
+  group->magic = DIAG_GROUP_MAGIC;
+  group->file = g_object_ref (file);
+
+  return group;
+}
+
+static IdeDiagnosticsGroup *
+ide_diagnostics_group_ref (IdeDiagnosticsGroup *group)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+  return g_rc_box_acquire (group);
+}
+
+static void
+ide_diagnostics_group_unref (IdeDiagnosticsGroup *group)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+  g_rc_box_release_full (group, (GDestroyNotify)ide_diagnostics_group_finalize);
+}
+
+static guint
+ide_diagnostics_group_has_diagnostics (IdeDiagnosticsGroup *group)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+  if (group->diagnostics_by_provider != NULL)
+    {
+      GHashTableIter iter;
+      gpointer value;
+
+      g_hash_table_iter_init (&iter, group->diagnostics_by_provider);
+
+      while (g_hash_table_iter_next (&iter, NULL, &value))
+        {
+          IdeDiagnostics *diagnostics = value;
+
+          if (diagnostics_get_size (diagnostics) > 0)
+            return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static gboolean
+ide_diagnostics_group_can_dispose (IdeDiagnosticsGroup *group)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+  /*
+   * We can cleanup this group if we don't have a buffer loaded and
+   * the adapters have been unloaded and there are no diagnostics
+   * registered for the group.
+   */
+
+  return group->adapter == NULL &&
+         group->has_diagnostics == FALSE;
+}
+
+static void
+ide_diagnostics_group_add (IdeDiagnosticsGroup   *group,
+                           IdeDiagnosticProvider *provider,
+                           IdeDiagnostic         *diagnostic)
+{
+  IdeDiagnostics *diagnostics;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (diagnostic != NULL);
+
+  if (group->diagnostics_by_provider == NULL)
+    group->diagnostics_by_provider = g_hash_table_new_full (NULL, NULL, NULL, free_diagnostics);
+
+  diagnostics = g_hash_table_lookup (group->diagnostics_by_provider, provider);
+
+  if (diagnostics == NULL)
+    {
+      diagnostics = ide_diagnostics_new ();
+      g_hash_table_insert (group->diagnostics_by_provider, provider, diagnostics);
+    }
+
+  ide_diagnostics_add (diagnostics, diagnostic);
+
+  group->has_diagnostics = TRUE;
+  group->sequence++;
+}
+
+static void
+ide_diagnostics_group_diagnose_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)object;
+  g_autoptr(IdeDiagnosticsManager) self = user_data;
+  g_autoptr(IdeDiagnostics) diagnostics = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeDiagnosticsGroup *group;
+  gboolean changed;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  IDE_TRACE_MSG ("%s diagnosis completed", G_OBJECT_TYPE_NAME (provider));
+
+  diagnostics = ide_diagnostic_provider_diagnose_finish (provider, result, &error);
+
+  if (error != NULL &&
+      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
+      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+    g_debug ("%s", error->message);
+
+  /*
+   * This fetches the group our provider belongs to. Since the group is
+   * reference counted (and we only release it when our provider is
+   * finalized), we should be guaranteed we have a valid group.
+   */
+  group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");
+
+  if (group == NULL)
+    {
+      /* Warning and bail if we failed to get the diagnostic group.
+       * This shouldn't be happening, but I have definitely seen it
+       * so it is probably related to disposal.
+       */
+      g_warning ("Failed to locate group, possibly disposed.");
+      IDE_EXIT;
+    }
+
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+  /*
+   * Clear all of our old diagnostics no matter where they ended up.
+   */
+  changed = ide_diagnostics_manager_clear_by_provider (self, provider);
+
+  /*
+   * The following adds diagnostics to the appropriate group, but tries the
+   * group we belong to first as our fast path. That will almost always be
+   * the case, except when a diagnostic came up for a header or something
+   * while parsing a given file.
+   */
+  if (diagnostics != NULL)
+    {
+      guint length = diagnostics_get_size (diagnostics);
+
+      for (guint i = 0; i < length; i++)
+        {
+          g_autoptr(IdeDiagnostic) diagnostic = g_list_model_get_item (G_LIST_MODEL (diagnostics), i);
+          GFile *file = ide_diagnostic_get_file (diagnostic);
+
+          if (file != NULL)
+            {
+              if (g_file_equal (file, group->file))
+                ide_diagnostics_group_add (group, provider, diagnostic);
+              else
+                ide_diagnostics_manager_add_diagnostic (self, provider, diagnostic);
+            }
+        }
+
+      if (length > 0)
+        changed = TRUE;
+    }
+
+  group->in_diagnose--;
+
+  /*
+   * Ensure we increment our sequence number even when no diagnostics were
+   * reported. This ensures that the gutter gets cleared and line-flags
+   * cache updated.
+   */
+  group->sequence++;
+
+  /*
+   * Since the individual groups have sequence numbers associated with changes,
+   * it's okay to emit this for every provider completion. That allows the UIs
+   * to update faster as each provider completes at the expensive of a little
+   * more CPU activity.
+   */
+  if (changed)
+    g_signal_emit (self, signals [CHANGED], 0);
+
+  /*
+   * If there are no more diagnostics providers active and the group needs
+   * another diagnosis, then we can start the next one now.
+   *
+   * If we are completing this diagnosis and the buffer was already released
+   * (and other diagnose providers have unloaded), we might be able to clean
+   * up the group and be done with things.
+   */
+  if (group->was_removed == FALSE && group->in_diagnose == 0 && group->needs_diagnose)
+    {
+      ide_diagnostics_group_queue_diagnose (group, self);
+    }
+  else if (ide_diagnostics_group_can_dispose (group))
+    {
+      group->was_removed = TRUE;
+      g_hash_table_remove (self->groups_by_file, group->file);
+      IDE_EXIT;
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_diagnostics_group_diagnose_foreach (IdeExtensionSetAdapter *adapter,
+                                        PeasPluginInfo         *plugin_info,
+                                        PeasExtension          *exten,
+                                        gpointer                user_data)
+{
+  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
+  IdeDiagnosticsManager *self = user_data;
+  IdeDiagnosticsGroup *group;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+
+  group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");
+
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+  group->in_diagnose++;
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    g_autofree gchar *uri = g_file_get_uri (group->file);
+    IDE_TRACE_MSG ("Beginning diagnose on %s with provider %s",
+                   uri, G_OBJECT_TYPE_NAME (provider));
+  }
+#endif
+
+  ide_diagnostic_provider_diagnose_async (provider,
+                                          group->file,
+                                          group->contents,
+                                          group->lang_id,
+                                          NULL,
+                                          ide_diagnostics_group_diagnose_cb,
+                                          g_object_ref (self));
+}
+
+static void
+ide_diagnostics_group_diagnose (IdeDiagnosticsGroup   *group,
+                                IdeDiagnosticsManager *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (group != NULL);
+  g_assert (group->in_diagnose == FALSE);
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (group->adapter));
+
+  group->needs_diagnose = FALSE;
+  group->has_diagnostics = FALSE;
+
+  ide_extension_set_adapter_foreach (group->adapter,
+                                     ide_diagnostics_group_diagnose_foreach,
+                                     self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_diagnostics_manager_begin_diagnose (gpointer data)
+{
+  IdeDiagnosticsManager *self = data;
+  GHashTableIter iter;
+  gpointer value;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  self->queued_diagnose_source = 0;
+
+  g_hash_table_iter_init (&iter, self->groups_by_file);
+
+  while (g_hash_table_iter_next (&iter, NULL, &value))
+    {
+      IdeDiagnosticsGroup *group = value;
+
+      g_assert (group != NULL);
+      g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+      if (group->needs_diagnose && group->adapter != NULL && group->in_diagnose == 0)
+        ide_diagnostics_group_diagnose (group, self);
+    }
+
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+ide_diagnostics_group_queue_diagnose (IdeDiagnosticsGroup   *group,
+                                      IdeDiagnosticsManager *self)
+{
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  /*
+   * This checks to see if we are diagnosing and if not queues a diagnose.
+   * If a diagnosis is already running, we don't need to do anything now
+   * because the completion of the diagnose will tick off the next diagnose
+   * upon seening group->needs_diagnose==TRUE.
+   */
+
+  group->needs_diagnose = TRUE;
+
+  if (group->in_diagnose == 0 && self->queued_diagnose_source == 0)
+    self->queued_diagnose_source = g_timeout_add_full (G_PRIORITY_LOW,
+                                                       DEFAULT_DIAGNOSE_DELAY,
+                                                       ide_diagnostics_manager_begin_diagnose,
+                                                       self, NULL);
+}
+
+static void
+ide_diagnostics_manager_finalize (GObject *object)
+{
+  IdeDiagnosticsManager *self = (IdeDiagnosticsManager *)object;
+
+  g_clear_handle_id (&self->queued_diagnose_source, g_source_remove);
+  g_clear_pointer (&self->groups_by_file, g_hash_table_unref);
+
+  G_OBJECT_CLASS (ide_diagnostics_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_diagnostics_manager_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeDiagnosticsManager *self = (IdeDiagnosticsManager *)object;
+
+  switch (prop_id)
+    {
+    case PROP_BUSY:
+      g_value_set_boolean (value, ide_diagnostics_manager_get_busy (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_diagnostics_manager_class_init (IdeDiagnosticsManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_diagnostics_manager_finalize;
+  object_class->get_property = ide_diagnostics_manager_get_property;
+
+  properties [PROP_BUSY] =
+    g_param_spec_boolean ("busy",
+                          "Busy",
+                          "If the diagnostics manager is busy",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeDiagnosticsManager::changed:
+   * @self: an #IdeDiagnosticsManager
+   *
+   * This signal is emitted when the diagnostics have changed for any
+   * file managed by the IdeDiagnosticsManager.
+   *
+   * Since: 3.32
+   */
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+static void
+ide_diagnostics_manager_init (IdeDiagnosticsManager *self)
+{
+  self->groups_by_file = g_hash_table_new_full (g_file_hash,
+                                                (GEqualFunc)g_file_equal,
+                                                NULL,
+                                                (GDestroyNotify)ide_diagnostics_group_unref);
+}
+
+static void
+ide_diagnostics_manager_add_diagnostic (IdeDiagnosticsManager *self,
+                                        IdeDiagnosticProvider *provider,
+                                        IdeDiagnostic         *diagnostic)
+{
+  IdeDiagnosticsGroup *group;
+  GFile *file;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (diagnostic != NULL);
+
+  /*
+   * This is our slow path for adding a diagnostic to the system. We have
+   * to locate the proper group for the diagnostic and then insert it
+   * into that group.
+   */
+
+  if (NULL == (file = ide_diagnostic_get_file (diagnostic)))
+    return;
+
+  group = g_hash_table_lookup (self->groups_by_file, file);
+
+  if (group == NULL)
+    {
+      group = ide_diagnostics_group_new (file);
+      g_hash_table_insert (self->groups_by_file, group->file, group);
+    }
+
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+  ide_diagnostics_group_add (group, provider, diagnostic);
+}
+
+static IdeDiagnosticsGroup *
+ide_diagnostics_manager_find_group (IdeDiagnosticsManager *self,
+                                    GFile                 *file)
+{
+  IdeDiagnosticsGroup *group;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (G_IS_FILE (file));
+
+  if (!(group = g_hash_table_lookup (self->groups_by_file, file)))
+    {
+      group = ide_diagnostics_group_new (file);
+      g_hash_table_insert (self->groups_by_file, group->file, group);
+    }
+
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+  return group;
+}
+
+static IdeDiagnosticsGroup *
+ide_diagnostics_manager_find_group_from_adapter (IdeDiagnosticsManager  *self,
+                                                 IdeExtensionSetAdapter *adapter)
+{
+  GHashTableIter iter;
+  gpointer value;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+
+  g_hash_table_iter_init (&iter, self->groups_by_file);
+
+  while (g_hash_table_iter_next (&iter, NULL, &value))
+    {
+      IdeDiagnosticsGroup *group = value;
+
+      g_assert (group != NULL);
+      g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+      if (group->adapter == adapter)
+        return group;
+    }
+
+  g_assert_not_reached ();
+
+  return NULL;
+}
+
+static void
+ide_diagnostics_manager_provider_invalidated (IdeDiagnosticsManager *self,
+                                              IdeDiagnosticProvider *provider)
+{
+  IdeDiagnosticsGroup *group;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+
+  group = g_object_get_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP");
+
+  ide_diagnostics_group_queue_diagnose (group, self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_diagnostics_manager_extension_added (IdeExtensionSetAdapter *adapter,
+                                         PeasPluginInfo         *plugin_info,
+                                         PeasExtension          *exten,
+                                         gpointer                user_data)
+{
+  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
+  IdeDiagnosticsManager *self = user_data;
+  IdeDiagnosticsGroup *group;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  group = ide_diagnostics_manager_find_group_from_adapter (self, adapter);
+
+  /*
+   * We will need access to the group upon completion of the diagnostics,
+   * so we add a reference to the group and allow it to be automatically
+   * cleaned up when the provider finalizes.
+   */
+  g_object_set_data_full (G_OBJECT (provider),
+                          "IDE_DIAGNOSTICS_GROUP",
+                          ide_diagnostics_group_ref (group),
+                          (GDestroyNotify)ide_diagnostics_group_unref);
+
+  /*
+   * We insert a dummy entry into the hashtable upon creation so
+   * that when an async diagnosis completes we can use the presence
+   * of this key to know if we've been unloaded.
+   */
+  g_hash_table_insert (group->diagnostics_by_provider, provider, NULL);
+
+  /*
+   * We need to keep track of when the provider has been invalidated so
+   * that we can queue another request to fetch the diagnostics.
+   */
+  g_signal_connect_object (provider,
+                           "invalidated",
+                           G_CALLBACK (ide_diagnostics_manager_provider_invalidated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_diagnostic_provider_load (provider);
+
+  ide_diagnostics_group_queue_diagnose (group, self);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_diagnostics_manager_clear_by_provider (IdeDiagnosticsManager *self,
+                                           IdeDiagnosticProvider *provider)
+{
+  GHashTableIter iter;
+  gpointer value;
+  gboolean changed = FALSE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+
+  g_hash_table_iter_init (&iter, self->groups_by_file);
+
+  while (g_hash_table_iter_next (&iter, NULL, &value))
+    {
+      IdeDiagnosticsGroup *group = value;
+
+      g_assert (group != NULL);
+      g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+      if (group->diagnostics_by_provider != NULL)
+        {
+          g_hash_table_remove (group->diagnostics_by_provider, provider);
+
+          /*
+           * If we caused this hashtable to become empty, we can release the
+           * hashtable. The hashtable is guaranteed to not be empty if there
+           * are other providers loaded for this group.
+           */
+          if (g_hash_table_size (group->diagnostics_by_provider) == 0)
+            g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);
+
+          /*
+           * TODO: If this provider is not part of this group, we can possibly
+           *       dispose of the group if there are no diagnostics.
+           */
+
+          changed = TRUE;
+        }
+    }
+
+  return changed;
+}
+
+static void
+ide_diagnostics_manager_extension_removed (IdeExtensionSetAdapter *adapter,
+                                           PeasPluginInfo         *plugin_info,
+                                           PeasExtension          *exten,
+                                           gpointer                user_data)
+{
+  IdeDiagnosticProvider *provider = (IdeDiagnosticProvider *)exten;
+  IdeDiagnosticsManager *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  g_signal_handlers_disconnect_by_func (provider,
+                                        G_CALLBACK (ide_diagnostics_manager_provider_invalidated),
+                                        self);
+
+  /*
+   * The goal of the following is to reomve our diagnostics from any file
+   * that has been loaded. It is possible for diagnostic providers to effect
+   * files outside the buffer they are loaded for and this ensures that we
+   * clean those up.
+   */
+  ide_diagnostics_manager_clear_by_provider (self, provider);
+
+  /* Clear the diagnostics group */
+  g_object_set_data (G_OBJECT (provider), "IDE_DIAGNOSTICS_GROUP", NULL);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_diagnostics_manager_get_busy:
+ *
+ * Gets if the diagnostics manager is currently executing a diagnosis.
+ *
+ * Returns: %TRUE if the #IdeDiagnosticsManager is busy diagnosing.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_diagnostics_manager_get_busy (IdeDiagnosticsManager *self)
+{
+  GHashTableIter iter;
+  gpointer value;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), FALSE);
+
+  g_hash_table_iter_init (&iter, self->groups_by_file);
+
+  while (g_hash_table_iter_next (&iter, NULL, &value))
+    {
+      IdeDiagnosticsGroup *group = value;
+
+      g_assert (group != NULL);
+      g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+      if (group->in_diagnose > 0)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_diagnostics_manager_get_diagnostics_for_file:
+ * @self: An #IdeDiagnosticsManager
+ * @file: a #GFile to retrieve diagnostics for
+ *
+ * This function collects all of the diagnostics that have been collected
+ * for @file and returns them as a new #IdeDiagnostics to the caller.
+ *
+ * The #IdeDiagnostics structure will contain zero items if there are
+ * no diagnostics discovered. Therefore, this function will never return
+ * a %NULL value.
+ *
+ * Returns: (transfer full): A new #IdeDiagnostics.
+ *
+ * Since: 3.32
+ */
+IdeDiagnostics *
+ide_diagnostics_manager_get_diagnostics_for_file (IdeDiagnosticsManager *self,
+                                                  GFile                 *file)
+{
+  g_autoptr(IdeDiagnostics) ret = NULL;
+  IdeDiagnosticsGroup *group;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  ret = ide_diagnostics_new ();
+
+  group = g_hash_table_lookup (self->groups_by_file, file);
+
+  if (group != NULL && group->diagnostics_by_provider != NULL)
+    {
+      GHashTableIter iter;
+      gpointer value;
+
+      g_hash_table_iter_init (&iter, group->diagnostics_by_provider);
+
+      while (g_hash_table_iter_next (&iter, NULL, &value))
+        {
+          IdeDiagnostics *diagnostics = value;
+          guint length;
+
+          if (diagnostics == NULL)
+            continue;
+
+          length = g_list_model_get_n_items (G_LIST_MODEL (diagnostics));
+
+          for (guint i = 0; i < length; i++)
+            {
+              g_autoptr(IdeDiagnostic) diagnostic = NULL;
+
+              diagnostic = g_list_model_get_item (G_LIST_MODEL (diagnostics), i);
+              ide_diagnostics_add (ret, diagnostic);
+            }
+        }
+    }
+
+  return g_steal_pointer (&ret);
+}
+
+guint
+ide_diagnostics_manager_get_sequence_for_file (IdeDiagnosticsManager *self,
+                                               GFile                 *file)
+{
+  IdeDiagnosticsGroup *group;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), 0);
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self), 0);
+  g_return_val_if_fail (G_IS_FILE (file), 0);
+
+  group = g_hash_table_lookup (self->groups_by_file, file);
+
+  if (group != NULL)
+    {
+      g_assert (IS_DIAGNOSTICS_GROUP (group));
+      g_assert (G_IS_FILE (group->file));
+      g_assert (g_file_equal (group->file, file));
+
+      return group->sequence;
+    }
+
+  return 0;
+}
+
+/**
+ * ide_diagnostics_manager_rediagnose:
+ * @self: an #IdeDiagnosticsManager
+ * @buffer: an #IdeBuffer
+ *
+ * Requests that the diagnostics be reloaded for @buffer.
+ *
+ * You may want to call this if you changed something that a buffer depends on,
+ * and want to seamlessly update its diagnostics with that updated information.
+ *
+ * Since: 3.32
+ */
+void
+ide_diagnostics_manager_rediagnose (IdeDiagnosticsManager *self,
+                                    IdeBuffer             *buffer)
+{
+  IdeDiagnosticsGroup *group;
+  GFile *file;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  file = ide_buffer_get_file (buffer);
+  group = ide_diagnostics_manager_find_group (self, file);
+
+  ide_diagnostics_group_queue_diagnose (group, self);
+}
+
+/**
+ * ide_diagnostics_manager_from_context:
+ * @context: an #IdeContext
+ *
+ * Gets the diagnostics manager for the context.
+ *
+ * Returns: (transfer none): an #IdeDiagnosticsManager
+ *
+ * Since: 3.32
+ */
+IdeDiagnosticsManager *
+ide_diagnostics_manager_from_context (IdeContext *context)
+{
+  IdeDiagnosticsManager *self;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  ide_object_lock (IDE_OBJECT (context));
+  if (!(self = ide_context_peek_child_typed (context, IDE_TYPE_DIAGNOSTICS_MANAGER)))
+    {
+      g_autoptr(IdeDiagnosticsManager) created = NULL;
+      created = ide_object_ensure_child_typed (IDE_OBJECT (context),
+                                               IDE_TYPE_DIAGNOSTICS_MANAGER);
+      self = ide_context_peek_child_typed (context, IDE_TYPE_DIAGNOSTICS_MANAGER);
+    }
+  ide_object_unlock (IDE_OBJECT (context));
+
+  return self;
+}
+
+void
+_ide_diagnostics_manager_file_closed (IdeDiagnosticsManager *self,
+                                      GFile                 *file)
+{
+  IdeDiagnosticsGroup *group;
+  gboolean has_diagnostics;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  /*
+   * The goal here is to cleanup everything we can about this group that
+   * is part of a loaded buffer. We might want to keep the group around
+   * in case it is useful from other providers.
+   */
+
+  group = ide_diagnostics_manager_find_group (self, file);
+
+  g_assert (group != NULL);
+  g_assert (IS_DIAGNOSTICS_GROUP (group));
+
+  /* Clear some state we've been tracking */
+  g_clear_pointer (&group->contents, g_bytes_unref);
+  group->lang_id = NULL;
+  group->needs_diagnose = FALSE;
+
+  /*
+   * We track if we have diagnostics now so that after we unload the
+   * the providers, we can save that bit for later.
+   */
+  has_diagnostics = ide_diagnostics_group_has_diagnostics (group);
+
+  /*
+   * Force our diagnostic providers to unload. This will cause them
+   * extension-removed signal to be called for each provider which
+   * in turn will perform per-provider cleanup including the removal
+   * of its diagnostics from all groups. (A provider can in practice
+   * affect another group since a .c file could create a diagnostic
+   * for a .h).
+   */
+  ide_clear_and_destroy_object (&group->adapter);
+
+  /*
+   * Even after unloading the diagnostic providers, we might still have
+   * diagnostics that were created from other files (this could happen when
+   * one diagnostic is created for a header from a source file). So we don't
+   * want to wipe out the hashtable unless everything was unloaded. The other
+   * provider will cleanup during its own destruction.
+   */
+  if (group->diagnostics_by_provider != NULL &&
+      g_hash_table_size (group->diagnostics_by_provider) == 0)
+    g_clear_pointer (&group->diagnostics_by_provider, g_hash_table_unref);
+
+  group->has_diagnostics = has_diagnostics;
+
+  IDE_EXIT;
+}
+
+void
+_ide_diagnostics_manager_file_changed (IdeDiagnosticsManager *self,
+                                       GFile                 *file,
+                                       GBytes                *contents,
+                                       const gchar           *lang_id)
+{
+  IdeDiagnosticsGroup *group;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  group = ide_diagnostics_manager_find_group (self, file);
+
+  g_clear_pointer (&group->contents, g_bytes_unref);
+
+  group->lang_id = g_intern_string (lang_id);
+  group->contents = contents ? g_bytes_ref (contents) : NULL;
+
+  ide_diagnostics_group_queue_diagnose (group, self);
+}
+
+void
+_ide_diagnostics_manager_language_changed (IdeDiagnosticsManager *self,
+                                           GFile                 *file,
+                                           const gchar           *lang_id)
+{
+  IdeDiagnosticsGroup *group;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_DIAGNOSTICS_MANAGER (self));
+
+  group = ide_diagnostics_manager_find_group (self, file);
+  group->lang_id = g_intern_string (lang_id);
+
+  if (group->adapter != NULL)
+    ide_extension_set_adapter_set_value (group->adapter, lang_id);
+
+  ide_diagnostics_group_queue_diagnose (group, self);
+}
+
+void
+_ide_diagnostics_manager_file_opened (IdeDiagnosticsManager *self,
+                                      GFile                 *file,
+                                      const gchar           *lang_id)
+{
+  IdeDiagnosticsGroup *group;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (self));
+  g_assert (G_IS_FILE (file));
+
+  group = ide_diagnostics_manager_find_group (self, file);
+
+  if (group->diagnostics_by_provider == NULL)
+    group->diagnostics_by_provider =
+      g_hash_table_new_full (NULL, NULL, NULL, free_diagnostics);
+
+  group->lang_id = g_intern_string (lang_id);
+
+  if (group->adapter == NULL)
+    {
+      group->adapter = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                      peas_engine_get_default (),
+                                                      IDE_TYPE_DIAGNOSTIC_PROVIDER,
+                                                      "Diagnostic-Provider-Languages",
+                                                      lang_id);
+
+      g_signal_connect_object (group->adapter,
+                               "extension-added",
+                               G_CALLBACK (ide_diagnostics_manager_extension_added),
+                               self,
+                               0);
+
+      g_signal_connect_object (group->adapter,
+                               "extension-removed",
+                               G_CALLBACK (ide_diagnostics_manager_extension_removed),
+                               self,
+                               0);
+
+      ide_extension_set_adapter_foreach (group->adapter,
+                                         ide_diagnostics_manager_extension_added,
+                                         self);
+    }
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (group->adapter));
+  g_assert (g_hash_table_lookup (self->groups_by_file, file) == group);
+
+  ide_diagnostics_group_queue_diagnose (group, self);
+
+  IDE_EXIT;
+}
+
diff --git a/src/libide/code/ide-diagnostics-manager.h b/src/libide/code/ide-diagnostics-manager.h
new file mode 100644
index 000000000..2653b43d9
--- /dev/null
+++ b/src/libide/code/ide-diagnostics-manager.h
@@ -0,0 +1,48 @@
+/* ide-diagnostics-manager.h
+ *
+ * Copyright 2016-2019 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-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DIAGNOSTICS_MANAGER (ide_diagnostics_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeDiagnosticsManager, ide_diagnostics_manager, IDE, DIAGNOSTICS_MANAGER, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeDiagnosticsManager *ide_diagnostics_manager_from_context (IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_diagnostics_manager_get_busy                 (IdeDiagnosticsManager *self);
+IDE_AVAILABLE_IN_3_32
+IdeDiagnostics *ide_diagnostics_manager_get_diagnostics_for_file (IdeDiagnosticsManager *self,
+                                                                  GFile                 *file);
+IDE_AVAILABLE_IN_3_32
+guint           ide_diagnostics_manager_get_sequence_for_file    (IdeDiagnosticsManager *self,
+                                                                  GFile                 *file);
+IDE_AVAILABLE_IN_3_32
+void            ide_diagnostics_manager_rediagnose               (IdeDiagnosticsManager *self,
+                                                                  IdeBuffer             *buffer);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-diagnostics.c b/src/libide/code/ide-diagnostics.c
new file mode 100644
index 000000000..0bb462465
--- /dev/null
+++ b/src/libide/code/ide-diagnostics.c
@@ -0,0 +1,506 @@
+/* ide-diagnostics.c
+ *
+ * Copyright 2015-2019 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 "ide-diagnostics"
+
+#include "config.h"
+
+#include "ide-diagnostic.h"
+#include "ide-diagnostics.h"
+#include "ide-location.h"
+
+typedef struct
+{
+  GPtrArray  *items;
+  GHashTable *caches;
+  guint       n_warnings;
+  guint       n_errors;
+} IdeDiagnosticsPrivate;
+
+typedef struct
+{
+  gint                  line : 28;
+  IdeDiagnosticSeverity severity : 4;
+} IdeDiagnosticsCacheLine;
+
+typedef struct
+{
+  GFile  *file;
+  GArray *lines;
+} IdeDiagnosticsCache;
+
+typedef struct
+{
+  guint begin;
+  guint end;
+} LookupKey;
+
+enum {
+  PROP_0,
+  PROP_HAS_ERRORS,
+  PROP_HAS_WARNINGS,
+  PROP_N_ERRORS,
+  PROP_N_WARNINGS,
+  N_PROPS
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeDiagnostics, ide_diagnostics, IDE_TYPE_OBJECT,
+                         G_ADD_PRIVATE (IdeDiagnostics)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_diagnostics_cache_free (gpointer data)
+{
+  IdeDiagnosticsCache *cache = data;
+
+  g_clear_object (&cache->file);
+  g_clear_pointer (&cache->lines, g_array_unref);
+  g_slice_free (IdeDiagnosticsCache, cache);
+}
+
+static void
+ide_diagnostics_finalize (GObject *object)
+{
+  IdeDiagnostics *self = (IdeDiagnostics *)object;
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+
+  g_clear_pointer (&priv->items, g_ptr_array_unref);
+  g_clear_pointer (&priv->caches, g_hash_table_unref);
+
+  G_OBJECT_CLASS (ide_diagnostics_parent_class)->finalize (object);
+}
+
+static void
+ide_diagnostics_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeDiagnostics *self = IDE_DIAGNOSTICS (object);
+
+  switch (prop_id)
+    {
+    case PROP_HAS_WARNINGS:
+      g_value_set_boolean (value, ide_diagnostics_get_has_warnings (self));
+      break;
+
+    case PROP_HAS_ERRORS:
+      g_value_set_boolean (value, ide_diagnostics_get_has_errors (self));
+      break;
+
+    case PROP_N_ERRORS:
+      g_value_set_uint (value, ide_diagnostics_get_n_errors (self));
+      break;
+
+    case PROP_N_WARNINGS:
+      g_value_set_uint (value, ide_diagnostics_get_n_warnings (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_diagnostics_class_init (IdeDiagnosticsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_diagnostics_finalize;
+  object_class->get_property = ide_diagnostics_get_property;
+
+  properties [PROP_HAS_WARNINGS] =
+    g_param_spec_boolean ("has-warnings",
+                         "Has Warnings",
+                         "If there are any warnings in the diagnostic set",
+                         FALSE,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HAS_ERRORS] =
+    g_param_spec_boolean ("has-errors",
+                         "Has Errors",
+                         "If there are any errors in the diagnostic set",
+                         FALSE,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_N_WARNINGS] =
+    g_param_spec_uint ("n-warnings",
+                       "N Warnings",
+                       "Number of warnings in diagnostic set",
+                       0, G_MAXUINT, 0,
+                       (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_N_ERRORS] =
+    g_param_spec_uint ("n-errors",
+                       "N Errors",
+                       "Number of errors in diagnostic set",
+                       0, G_MAXUINT, 0,
+                       (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_diagnostics_init (IdeDiagnostics *self)
+{
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+
+  priv->items = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+IdeDiagnostics *
+ide_diagnostics_new (void)
+{
+  return g_object_new (IDE_TYPE_DIAGNOSTICS, NULL);
+}
+
+void
+ide_diagnostics_add (IdeDiagnostics *self,
+                     IdeDiagnostic  *diagnostic)
+{
+  g_return_if_fail (IDE_IS_DIAGNOSTICS (self));
+  g_return_if_fail (IDE_IS_DIAGNOSTIC (diagnostic));
+
+  ide_diagnostics_take (self, g_object_ref (diagnostic));
+}
+
+void
+ide_diagnostics_take (IdeDiagnostics *self,
+                      IdeDiagnostic  *diagnostic)
+{
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+  IdeDiagnosticSeverity severity;
+  guint position;
+
+  g_return_if_fail (IDE_IS_DIAGNOSTICS (self));
+  g_return_if_fail (IDE_IS_DIAGNOSTIC (diagnostic));
+
+  severity = ide_diagnostic_get_severity (diagnostic);
+
+  position = priv->items->len;
+  g_ptr_array_add (priv->items, g_steal_pointer (&diagnostic));
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+
+  switch (severity)
+    {
+    case IDE_DIAGNOSTIC_ERROR:
+    case IDE_DIAGNOSTIC_FATAL:
+      priv->n_errors++;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_ERRORS]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_N_ERRORS]);
+      break;
+
+    case IDE_DIAGNOSTIC_WARNING:
+    case IDE_DIAGNOSTIC_DEPRECATED:
+      priv->n_warnings++;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_WARNINGS]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_N_WARNINGS]);
+      break;
+
+    case IDE_DIAGNOSTIC_IGNORED:
+    case IDE_DIAGNOSTIC_NOTE:
+    default:
+      break;
+    }
+}
+
+void
+ide_diagnostics_merge (IdeDiagnostics *self,
+                       IdeDiagnostics *other)
+{
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+  IdeDiagnosticsPrivate *other_priv = ide_diagnostics_get_instance_private (other);
+  guint position;
+
+  g_return_if_fail (IDE_IS_DIAGNOSTICS (self));
+  g_return_if_fail (IDE_IS_DIAGNOSTICS (other));
+
+  position = priv->items->len;
+
+  for (guint i = 0; i < other_priv->items->len; i++)
+    {
+      IdeDiagnostic *diagnostic = g_ptr_array_index (other_priv->items, i);
+      ide_diagnostics_take (self, g_object_ref (diagnostic));
+    }
+
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, other_priv->items->len);
+}
+
+gboolean
+ide_diagnostics_get_has_errors (IdeDiagnostics *self)
+{
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS (self), FALSE);
+
+  return priv->n_errors > 0;
+}
+
+guint
+ide_diagnostics_get_n_errors (IdeDiagnostics *self)
+{
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS (self), 0);
+
+  return priv->n_errors;
+}
+
+gboolean
+ide_diagnostics_get_has_warnings (IdeDiagnostics *self)
+{
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS (self), FALSE);
+
+  return priv->n_warnings > 0;
+}
+
+guint
+ide_diagnostics_get_n_warnings (IdeDiagnostics *self)
+{
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS (self), 0);
+
+  return priv->n_warnings;
+}
+
+static GType
+ide_diagnostics_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_DIAGNOSTIC;
+}
+
+static guint
+ide_diagnostics_get_n_items (GListModel *model)
+{
+  IdeDiagnostics *self = (IdeDiagnostics *)model;
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS (self), 0);
+
+  return priv->items->len;
+}
+
+static gpointer
+ide_diagnostics_get_item (GListModel *model,
+                          guint       position)
+{
+  IdeDiagnostics *self = (IdeDiagnostics *)model;
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS (self), NULL);
+
+  if (position < priv->items->len)
+    return g_object_ref (g_ptr_array_index (priv->items, position));
+
+  return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_n_items = ide_diagnostics_get_n_items;
+  iface->get_item_type = ide_diagnostics_get_item_type;
+  iface->get_item = ide_diagnostics_get_item;
+}
+
+static gint
+compare_lines (gconstpointer a,
+               gconstpointer b)
+{
+  const IdeDiagnosticsCacheLine *line_a = a;
+  const IdeDiagnosticsCacheLine *line_b = b;
+
+  return line_a->line - line_b->line;
+}
+
+static void
+ide_diagnostics_build_caches (IdeDiagnostics *self)
+{
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+  g_autoptr(GHashTable) caches = NULL;
+  IdeDiagnosticsCache *cache;
+  GHashTableIter iter;
+  GFile *file;
+
+  g_assert (IDE_IS_DIAGNOSTICS (self));
+  g_assert (priv->caches == NULL);
+
+  caches = g_hash_table_new_full (g_file_hash,
+                                  (GEqualFunc)g_file_equal,
+                                  g_object_unref,
+                                  ide_diagnostics_cache_free);
+
+  for (guint i = 0; i < priv->items->len; i++)
+    {
+      IdeDiagnostic *diag = g_ptr_array_index (priv->items, i);
+      IdeDiagnosticsCacheLine val;
+      IdeLocation *location;
+
+      if (!(file = ide_diagnostic_get_file (diag)))
+        continue;
+
+      if (!(location = ide_diagnostic_get_location (diag)))
+        continue;
+
+      if (!(cache = g_hash_table_lookup (caches, file)))
+        {
+          cache = g_slice_new0 (IdeDiagnosticsCache);
+          cache->file = g_object_ref (file);
+          cache->lines = g_array_new (FALSE, FALSE, sizeof (IdeDiagnosticsCacheLine));
+          g_hash_table_insert (caches, g_object_ref (file), cache);
+        }
+
+      val.severity = ide_diagnostic_get_severity (diag);
+      val.line = ide_location_get_line (location);
+
+      g_array_append_val (cache->lines, val);
+    }
+
+  g_hash_table_iter_init (&iter, caches);
+
+  while (g_hash_table_iter_next (&iter, (gpointer *)&file, (gpointer *)&cache))
+    g_array_sort (cache->lines, compare_lines);
+
+  priv->caches = g_steal_pointer (&caches);
+}
+
+/**
+ * ide_diagnostics_foreach_line_in_range:
+ * @self: an #IdeDiagnostics
+ * @file: a #GFile
+ * @begin_line: the starting line
+ * @end_line: the ending line
+ * @callback: (scope call): a callback to execute for each matching line
+ * @user_data: user data for @callback
+ *
+ * This function calls @callback for every line with diagnostics between
+ * @begin_line and @end_line. This is useful when drawing information about
+ * diagnostics in an editor where a known number of lines are visible.
+ *
+ * Since: 3.32
+ */
+void
+ide_diagnostics_foreach_line_in_range (IdeDiagnostics             *self,
+                                       GFile                      *file,
+                                       guint                       begin_line,
+                                       guint                       end_line,
+                                       IdeDiagnosticsLineCallback  callback,
+                                       gpointer                    user_data)
+{
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+  const IdeDiagnosticsCache *cache;
+
+  g_return_if_fail (IDE_IS_DIAGNOSTICS (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  if (priv->items->len == 0)
+    return;
+
+  if (priv->caches == NULL)
+    ide_diagnostics_build_caches (self);
+
+  if (!(cache = g_hash_table_lookup (priv->caches, file)))
+    return;
+
+  for (guint i = 0; i < cache->lines->len; i++)
+    {
+      const IdeDiagnosticsCacheLine *line = &g_array_index (cache->lines, IdeDiagnosticsCacheLine, i);
+
+      if (line->line < begin_line)
+        continue;
+
+      if (line->line > end_line)
+        break;
+
+      callback (line->line, line->severity, user_data);
+    }
+}
+
+/**
+ * ide_diagnostics_get_diagnostic_at_line:
+ * @self: a #IdeDiagnostics
+ * @file: the target file
+ * @line: a line number
+ *
+ * Locates an #IdeDiagnostic in @file at @line.
+ *
+ * Returns: (transfer none) (nullable): an #IdeDiagnostic or %NULL
+ *
+ * Since: 3.32
+ */
+IdeDiagnostic *
+ide_diagnostics_get_diagnostic_at_line (IdeDiagnostics *self,
+                                        GFile          *file,
+                                        guint           line)
+{
+  IdeDiagnosticsPrivate *priv = ide_diagnostics_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DIAGNOSTICS (self), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  for (guint i = 0; i < priv->items->len; i++)
+    {
+      IdeDiagnostic *diag = g_ptr_array_index (priv->items, i);
+      IdeLocation *loc = ide_diagnostic_get_location (diag);
+      GFile *loc_file;
+      guint loc_line;
+
+      if (loc == NULL)
+        continue;
+
+      loc_file = ide_location_get_file (loc);
+      loc_line = ide_location_get_line (loc);
+
+      if (loc_line == line && g_file_equal (file, loc_file))
+        return diag;
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_diagnostics_new_from_array:
+ * @array: (nullable) (element-type IdeDiagnostic): optional array
+ *   of diagnostics to add.
+ *
+ * Returns: (transfer full): an #IdeDiagnostics
+ *
+ * Since: 3.32
+ */
+IdeDiagnostics *
+ide_diagnostics_new_from_array (GPtrArray *array)
+{
+  IdeDiagnostics *ret = ide_diagnostics_new ();
+
+  if (array != NULL)
+    {
+      for (guint i = 0; i < array->len; i++)
+        ide_diagnostics_add (ret, g_ptr_array_index (array, i));
+    }
+
+  return g_steal_pointer (&ret);
+}
diff --git a/src/libide/code/ide-diagnostics.h b/src/libide/code/ide-diagnostics.h
new file mode 100644
index 000000000..258dda731
--- /dev/null
+++ b/src/libide/code/ide-diagnostics.h
@@ -0,0 +1,97 @@
+/* ide-diagnostics.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+#include "ide-diagnostic.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DIAGNOSTICS (ide_diagnostics_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeDiagnostics, ide_diagnostics, IDE, DIAGNOSTICS, IdeObject)
+
+/**
+ * IdeDiagnosticsLineCallback:
+ * @line: the line number, starting from 0
+ * @severity: the severity of the diagnostic
+ * @user_data: user data provided with callback
+ *
+ * This function prototype is used to notify a caller of every line that has a
+ * diagnostic, and the most severe #IdeDiagnosticSeverity for that line.
+ *
+ * Since: 3.32
+ */
+typedef void (*IdeDiagnosticsLineCallback) (guint                 line,
+                                            IdeDiagnosticSeverity severity,
+                                            gpointer              user_data);
+
+struct _IdeDiagnosticsClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeDiagnostics        *ide_diagnostics_new                   (void);
+IDE_AVAILABLE_IN_3_32
+IdeDiagnostics        *ide_diagnostics_new_from_array        (GPtrArray                  *array);
+IDE_AVAILABLE_IN_3_32
+void                   ide_diagnostics_add                   (IdeDiagnostics             *self,
+                                                              IdeDiagnostic              *diagnostic);
+IDE_AVAILABLE_IN_3_32
+void                   ide_diagnostics_take                  (IdeDiagnostics             *self,
+                                                              IdeDiagnostic              *diagnostic);
+IDE_AVAILABLE_IN_3_32
+void                   ide_diagnostics_merge                 (IdeDiagnostics             *self,
+                                                              IdeDiagnostics             *other);
+IDE_AVAILABLE_IN_3_32
+guint                  ide_diagnostics_get_n_errors          (IdeDiagnostics             *self);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_diagnostics_get_has_errors        (IdeDiagnostics             *self);
+IDE_AVAILABLE_IN_3_32
+guint                  ide_diagnostics_get_n_warnings        (IdeDiagnostics             *self);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_diagnostics_get_has_warnings      (IdeDiagnostics             *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_diagnostics_foreach_line_in_range (IdeDiagnostics             *self,
+                                                              GFile                      *file,
+                                                              guint                       begin_line,
+                                                              guint                       end_line,
+                                                              IdeDiagnosticsLineCallback  callback,
+                                                              gpointer                    user_data);
+IDE_AVAILABLE_IN_3_32
+IdeDiagnostic         *ide_diagnostics_get_diagnostic_at_line (IdeDiagnostics             *self,
+                                                               GFile                      *file,
+                                                               guint                       line);
+
+#define ide_diagnostics_get_size(d) ((gsize)g_list_model_get_n_items(G_LIST_MODEL(d)))
+
+G_END_DECLS
diff --git a/src/libide/code/ide-doc-seq-private.h b/src/libide/code/ide-doc-seq-private.h
new file mode 100644
index 000000000..e426333df
--- /dev/null
+++ b/src/libide/code/ide-doc-seq-private.h
@@ -0,0 +1,30 @@
+/* ide-doc-seq-private.h
+ *
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
+ *
+ * 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.h>
+
+G_BEGIN_DECLS
+
+guint ide_doc_seq_acquire (void);
+void  ide_doc_seq_release (guint seq_id);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-doc-seq.c b/src/libide/code/ide-doc-seq.c
new file mode 100644
index 000000000..3e307f53d
--- /dev/null
+++ b/src/libide/code/ide-doc-seq.c
@@ -0,0 +1,57 @@
+/* ide-doc-seq.c
+ *
+ * Copyright 2014-2019 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 "ide-doc-seq"
+
+#include "config.h"
+
+#include "ide-doc-seq-private.h"
+
+static GHashTable *seq;
+
+guint
+ide_doc_seq_acquire (void)
+{
+  guint seq_id;
+
+  if (!seq)
+    seq = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+  for (seq_id = 1; seq_id < G_MAXUINT; seq_id++)
+    {
+      gpointer key;
+
+      key = GINT_TO_POINTER (seq_id);
+
+      if (!g_hash_table_lookup (seq, key))
+        {
+          g_hash_table_insert (seq, key, GINT_TO_POINTER (TRUE));
+          return seq_id;
+        }
+    }
+
+  return 0;
+}
+
+void
+ide_doc_seq_release (guint seq_id)
+{
+  g_hash_table_remove (seq, GINT_TO_POINTER (seq_id));
+}
diff --git a/src/libide/code/ide-file-settings.c b/src/libide/code/ide-file-settings.c
new file mode 100644
index 000000000..97aa85577
--- /dev/null
+++ b/src/libide/code/ide-file-settings.c
@@ -0,0 +1,540 @@
+/* ide-file-settings.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-file-settings"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+
+#include "ide-code-enums.h"
+#include "ide-file-settings.h"
+
+/*
+ * WARNING: This file heavily uses XMACROS.
+ *
+ * XMACROS are not as difficult as you might imagine. It's basically just an
+ * inverstion of macros. We have a defs file (in this case
+ * ide-file-settings.defs) which defines information we need about properties.
+ * Then we define the macro called from that defs file to do something we need,
+ * then include the .defs file.
+ *
+ * We do that over and over again until we have all the aspects of the object
+ * defined.
+ */
+
+typedef struct
+{
+  GPtrArray   *children;
+  GFile       *file;
+  const gchar *language;
+  guint        unsettled_count;
+
+#define IDE_FILE_SETTINGS_PROPERTY(_1, name, field_type, _3, _pname, _4, _5, _6) \
+  field_type name;
+#include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+#define IDE_FILE_SETTINGS_PROPERTY(_1, name, field_type, _3, _pname, _4, _5, _6) \
+  guint name##_set : 1;
+#include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+} IdeFileSettingsPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeFileSettings, ide_file_settings, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_FILE,
+  PROP_LANGUAGE,
+  PROP_SETTLED,
+#define IDE_FILE_SETTINGS_PROPERTY(NAME, _1, _2, _3, _pname, _4, _5, _6) \
+  PROP_##NAME, \
+  PROP_##NAME##_SET,
+#include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+#define IDE_FILE_SETTINGS_PROPERTY(_1, name, _2, ret_type, _pname, _3, _4, _5) \
+ret_type ide_file_settings_get_##name (IdeFileSettings *self) \
+{ \
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self); \
+  g_return_val_if_fail (IDE_IS_FILE_SETTINGS (self), (ret_type)0); \
+  if (!ide_file_settings_get_##name##_set (self) && priv->children != NULL) \
+    { \
+      for (guint i = 0; i < priv->children->len; i++) \
+        { \
+          IdeFileSettings *child = g_ptr_array_index (priv->children, i); \
+          if (ide_file_settings_get_##name##_set (child)) \
+            return ide_file_settings_get_##name (child); \
+        } \
+    } \
+  return priv->name; \
+}
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+#define IDE_FILE_SETTINGS_PROPERTY(_1, name, field_name, ret_type, _pname, _3, _4, _5) \
+gboolean ide_file_settings_get_##name##_set (IdeFileSettings *self) \
+{ \
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self); \
+  g_return_val_if_fail (IDE_IS_FILE_SETTINGS (self), FALSE); \
+  return priv->name##_set; \
+}
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+#define IDE_FILE_SETTINGS_PROPERTY(NAME, name, _1, ret_type, _pname, _3, assign_stmt, _4) \
+void ide_file_settings_set_##name (IdeFileSettings *self, \
+                                   ret_type         name) \
+{ \
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self); \
+  g_return_if_fail (IDE_IS_FILE_SETTINGS (self)); \
+  G_STMT_START { assign_stmt } G_STMT_END; \
+  priv->name##_set = TRUE; \
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_##NAME]); \
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_##NAME##_SET]); \
+}
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+#define IDE_FILE_SETTINGS_PROPERTY(NAME, name, _1, _2, _pname, _3, _4, _5) \
+void ide_file_settings_set_##name##_set (IdeFileSettings *self, \
+                                         gboolean         name##_set) \
+{ \
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self); \
+  g_return_if_fail (IDE_IS_FILE_SETTINGS (self)); \
+  priv->name##_set = !!name##_set; \
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_##NAME##_SET]); \
+}
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+/**
+ * ide_file_settings_get_file:
+ * @self: An #IdeFileSettings.
+ *
+ * Retrieves the underlying file that @self refers to.
+ *
+ * This may be used by #IdeFileSettings implementations to discover additional
+ * information about the settings. For example, a modeline parser might load
+ * some portion of the file looking for modelines. An editorconfig
+ * implementation might look for ".editorconfig" files.
+ *
+ * Returns: (transfer none): An #IdeFile.
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_file_settings_get_file (IdeFileSettings *self)
+{
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_FILE_SETTINGS (self), NULL);
+
+  return priv->file;
+}
+
+static void
+ide_file_settings_set_file (IdeFileSettings *self,
+                            GFile           *file)
+{
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_FILE_SETTINGS (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (priv->file == NULL);
+
+  priv->file = g_object_ref (file);
+}
+
+/**
+ * ide_file_settings_get_language:
+ * @self: a #IdeFileSettings
+ *
+ * If the language for file settings is known up-front, this will indicate
+ * the language identifier known to GtkSourceView such as "c" or "sh".
+ *
+ * Returns: (nullable): a string containing the language id or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_file_settings_get_language (IdeFileSettings *self)
+{
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_FILE_SETTINGS (self), NULL);
+
+  return priv->language;
+}
+
+static void
+ide_file_settings_set_language (IdeFileSettings *self,
+                                const gchar     *language)
+{
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_FILE_SETTINGS (self));
+
+  priv->language = g_intern_string (language);
+}
+
+/**
+ * ide_file_settings_get_settled:
+ * @self: An #IdeFileSettings.
+ *
+ * Gets the #IdeFileSettings:settled property.
+ *
+ * This property is %TRUE when all of the children file settings have completed loading.
+ *
+ * Some file setting implementations require that various I/O be performed on disk in
+ * the background. This property will change to %TRUE when all of the settings have
+ * been loaded.
+ *
+ * Normally, this is not a problem, since the editor will respond to changes and update them
+ * accordingly. However, if you are writing a tool that prints the file settings
+ * (such as ide-list-file-settings), you probably want to wait until the values have
+ * settled.
+ *
+ * Returns: %TRUE if all the settings have loaded.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_file_settings_get_settled (IdeFileSettings *self)
+{
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_FILE_SETTINGS (self), FALSE);
+
+  return (priv->unsettled_count == 0);
+}
+
+static gchar *
+ide_file_settings_repr (IdeObject *object)
+{
+  IdeFileSettings *self = (IdeFileSettings *)object;
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self);
+
+  if (priv->file != NULL)
+    {
+      g_autofree gchar *uri = NULL;
+
+      if (g_file_is_native (priv->file))
+        return g_strdup_printf ("%s path=\"%s\"",
+                                G_OBJECT_TYPE_NAME (self),
+                                g_file_peek_path (priv->file));
+
+      uri = g_file_get_uri (priv->file);
+      return g_strdup_printf ("%s uri=\"%s\"", G_OBJECT_TYPE_NAME (self), uri);
+    }
+
+  return IDE_OBJECT_CLASS (ide_file_settings_parent_class)->repr (object);
+}
+
+static void
+ide_file_settings_destroy (IdeObject *object)
+{
+  IdeFileSettings *self = (IdeFileSettings *)object;
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self);
+
+  g_clear_pointer (&priv->children, g_ptr_array_unref);
+  g_clear_pointer (&priv->encoding, g_free);
+  g_clear_object (&priv->file);
+
+  IDE_OBJECT_CLASS (ide_file_settings_parent_class)->destroy (object);
+}
+
+static void
+ide_file_settings_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeFileSettings *self = IDE_FILE_SETTINGS (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      g_value_set_object (value, ide_file_settings_get_file (self));
+      break;
+
+    case PROP_LANGUAGE:
+      g_value_set_static_string (value, ide_file_settings_get_language (self));
+      break;
+
+    case PROP_SETTLED:
+      g_value_set_boolean (value, ide_file_settings_get_settled (self));
+      break;
+
+#define IDE_FILE_SETTINGS_PROPERTY(NAME, name, _2, _3, _4, _5, _6, value_type) \
+    case PROP_##NAME: \
+      g_value_set_##value_type (value, ide_file_settings_get_##name (self)); \
+      break;
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+#define IDE_FILE_SETTINGS_PROPERTY(NAME, name, _1, _2, _pname, _3, _4, _5) \
+    case PROP_##NAME##_SET: \
+      g_value_set_boolean (value, ide_file_settings_get_##name##_set (self)); \
+      break;
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_file_settings_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeFileSettings *self = IDE_FILE_SETTINGS (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      ide_file_settings_set_file (self, g_value_get_object (value));
+      break;
+
+    case PROP_LANGUAGE:
+      ide_file_settings_set_language (self, g_value_get_string (value));
+      break;
+
+#define IDE_FILE_SETTINGS_PROPERTY(NAME, name, _2, _3, _4, _5, _6, value_type) \
+    case PROP_##NAME: \
+      ide_file_settings_set_##name (self, g_value_get_##value_type (value)); \
+      break;
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+#define IDE_FILE_SETTINGS_PROPERTY(NAME, name, _1, _2, _pname, _3, _4, _5) \
+    case PROP_##NAME##_SET: \
+      ide_file_settings_set_##name##_set (self, g_value_get_boolean (value)); \
+      break;
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_file_settings_class_init (IdeFileSettingsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_file_settings_get_property;
+  object_class->set_property = ide_file_settings_set_property;
+
+  i_object_class->destroy = ide_file_settings_destroy;
+  i_object_class->repr = ide_file_settings_repr;
+
+  properties [PROP_FILE] =
+    g_param_spec_object ("file",
+                         "File",
+                         "The GFile the settings represent",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LANGUAGE] =
+    g_param_spec_string ("language",
+                         "Langauge",
+                         "The language the settings represent",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SETTLED] =
+    g_param_spec_boolean ("settled",
+                          "Settled",
+                          "If the file settings implementations have settled",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+#define IDE_FILE_SETTINGS_PROPERTY(NAME, name, _1, _2, _pname, pspec, _4, _5) \
+  properties [PROP_##NAME] = pspec;
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+#define IDE_FILE_SETTINGS_PROPERTY(NAME, name, _1, _2, _pname, pspec, _4, _5) \
+  properties [PROP_##NAME##_SET] = \
+    g_param_spec_boolean (_pname"-set", \
+                          _pname"-set", \
+                          "If IdeFileSettings:"_pname" is set.", \
+                          FALSE, \
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_file_settings_init (IdeFileSettings *self)
+{
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self);
+
+  priv->indent_style = IDE_INDENT_STYLE_SPACES;
+  priv->indent_width = -1;
+  priv->insert_trailing_newline = TRUE;
+  priv->newline_type = GTK_SOURCE_NEWLINE_TYPE_LF;
+  priv->right_margin_position = 80;
+  priv->tab_width = 8;
+  priv->trim_trailing_whitespace = TRUE;
+}
+
+static void
+ide_file_settings_child_notify (IdeFileSettings *self,
+                                GParamSpec      *pspec,
+                                IdeFileSettings *child)
+{
+  g_assert (IDE_IS_FILE_SETTINGS (self));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_FILE_SETTINGS (child));
+
+  if (pspec->owner_type == IDE_TYPE_FILE_SETTINGS)
+    g_object_notify_by_pspec (G_OBJECT (self), pspec);
+}
+
+static void
+_ide_file_settings_append (IdeFileSettings *self,
+                           IdeFileSettings *child)
+{
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self);
+
+  g_assert (IDE_IS_FILE_SETTINGS (self));
+  g_assert (IDE_IS_FILE_SETTINGS (child));
+  g_assert (self != child);
+
+  g_signal_connect_object (child,
+                           "notify",
+                           G_CALLBACK (ide_file_settings_child_notify),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  if (priv->children == NULL)
+    priv->children = g_ptr_array_new_with_free_func (g_object_unref);
+
+  g_ptr_array_add (priv->children, g_object_ref (child));
+}
+
+static void
+ide_file_settings__init_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  g_autoptr(IdeFileSettings) self = user_data;
+  IdeFileSettingsPrivate *priv = ide_file_settings_get_instance_private (self);
+  GAsyncInitable *initable = (GAsyncInitable *)object;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_FILE_SETTINGS (self));
+  g_assert (G_IS_ASYNC_INITABLE (initable));
+
+  if (!g_async_initable_init_finish (initable, result, &error))
+    g_warning ("%s", error->message);
+
+  if (--priv->unsettled_count == 0)
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SETTLED]);
+}
+
+IdeFileSettings *
+ide_file_settings_new (IdeObject   *parent,
+                       GFile       *file,
+                       const gchar *language)
+{
+  IdeFileSettingsPrivate *priv;
+  GIOExtensionPoint *extension_point;
+  IdeFileSettings *ret;
+  GList *list;
+
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+  g_return_val_if_fail (IDE_IS_OBJECT (parent), NULL);
+
+  ret = g_object_new (IDE_TYPE_FILE_SETTINGS,
+                      "file", file,
+                      "language", language,
+                      NULL);
+  priv = ide_file_settings_get_instance_private (ret);
+
+  ide_object_append (parent, IDE_OBJECT (ret));
+
+  extension_point = g_io_extension_point_lookup (IDE_FILE_SETTINGS_EXTENSION_POINT);
+  list = g_io_extension_point_get_extensions (extension_point);
+
+  /*
+   * Don't allow our unsettled count to hit zero until we are finished.
+   */
+  priv->unsettled_count++;
+
+  for (; list; list = list->next)
+    {
+      GIOExtension *extension = list->data;
+      g_autoptr(IdeFileSettings) child = NULL;
+      GType gtype;
+
+      gtype = g_io_extension_get_type (extension);
+
+      if (!g_type_is_a (gtype, IDE_TYPE_FILE_SETTINGS))
+        {
+          g_warning ("%s is not an IdeFileSettings", g_type_name (gtype));
+          continue;
+        }
+
+      child = g_object_new (gtype,
+                            "file", file,
+                            "language", language,
+                            NULL);
+      ide_object_append (IDE_OBJECT (ret), IDE_OBJECT (child));
+
+      if (G_IS_INITABLE (child))
+        {
+          g_autoptr(GError) error = NULL;
+
+          if (!g_initable_init (G_INITABLE (child), NULL, &error))
+            g_warning ("%s", error->message);
+        }
+      else if (G_IS_ASYNC_INITABLE (child))
+        {
+          priv->unsettled_count++;
+          g_async_initable_init_async (G_ASYNC_INITABLE (child),
+                                       G_PRIORITY_DEFAULT,
+                                       NULL,
+                                       ide_file_settings__init_cb,
+                                       g_object_ref (ret));
+        }
+
+      _ide_file_settings_append (ret, child);
+    }
+
+  priv->unsettled_count--;
+
+  return ret;
+}
diff --git a/src/libide/files/ide-file-settings.defs b/src/libide/code/ide-file-settings.defs
similarity index 100%
rename from src/libide/files/ide-file-settings.defs
rename to src/libide/code/ide-file-settings.defs
diff --git a/src/libide/code/ide-file-settings.h b/src/libide/code/ide-file-settings.h
new file mode 100644
index 000000000..8caf27d63
--- /dev/null
+++ b/src/libide/code/ide-file-settings.h
@@ -0,0 +1,83 @@
+/* ide-file-settings.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <gtksourceview/gtksource.h>
+
+#include "ide-code-types.h"
+#include "ide-indent-style.h"
+#include "ide-spaces-style.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FILE_SETTINGS            (ide_file_settings_get_type())
+#define IDE_FILE_SETTINGS_EXTENSION_POINT "org.gnome.libide.extensions.file-settings"
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeFileSettings, ide_file_settings, IDE, FILE_SETTINGS, IdeObject)
+
+struct _IdeFileSettingsClass
+{
+  IdeObjectClass parent;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeFileSettings *ide_file_settings_new          (IdeObject       *parent,
+                                                 GFile           *file,
+                                                 const gchar     *language);
+IDE_AVAILABLE_IN_3_32
+GFile           *ide_file_settings_get_file     (IdeFileSettings *self);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_file_settings_get_language (IdeFileSettings *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_file_settings_get_settled  (IdeFileSettings *self);
+
+#define IDE_FILE_SETTINGS_PROPERTY(_1, name, _2, ret_type, _3, _4, _5, _6) \
+  _IDE_EXTERN ret_type ide_file_settings_get_##name (IdeFileSettings *self);
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+#define IDE_FILE_SETTINGS_PROPERTY(_1, name, _2, ret_type, _3, _4, _5, _6) \
+  _IDE_EXTERN void ide_file_settings_set_##name (IdeFileSettings *self, \
+                                                 ret_type         name);
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+#define IDE_FILE_SETTINGS_PROPERTY(_1, name, _2, _3, _4, _5, _6, _7) \
+  _IDE_EXTERN gboolean ide_file_settings_get_##name##_set (IdeFileSettings *self);
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+#define IDE_FILE_SETTINGS_PROPERTY(_1, name, _2, _3, _4, _5, _6, _7) \
+  _IDE_EXTERN void ide_file_settings_set_##name##_set (IdeFileSettings *self, \
+                                                       gboolean         name##_set);
+# include "ide-file-settings.defs"
+#undef IDE_FILE_SETTINGS_PROPERTY
+
+G_END_DECLS
diff --git a/src/libide/code/ide-formatter-options.c b/src/libide/code/ide-formatter-options.c
new file mode 100644
index 000000000..5dc199110
--- /dev/null
+++ b/src/libide/code/ide-formatter-options.c
@@ -0,0 +1,170 @@
+/* ide-formatter-options.c
+ *
+ * Copyright 2017-2019 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 "ide-formatter-options"
+
+#include "config.h"
+
+#include "ide-formatter-options.h"
+
+struct _IdeFormatterOptions
+{
+  GObject parent_instance;
+  guint   tab_width;
+  guint   insert_spaces : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_TAB_WIDTH,
+  PROP_INSERT_SPACES,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeFormatterOptions, ide_formatter_options, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_formatter_options_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  IdeFormatterOptions *self = IDE_FORMATTER_OPTIONS (object);
+
+  switch (prop_id)
+    {
+    case PROP_TAB_WIDTH:
+      g_value_set_uint (value, ide_formatter_options_get_tab_width (self));
+      break;
+
+    case PROP_INSERT_SPACES:
+      g_value_set_boolean (value, ide_formatter_options_get_insert_spaces (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_formatter_options_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  IdeFormatterOptions *self = IDE_FORMATTER_OPTIONS (object);
+
+  switch (prop_id)
+    {
+    case PROP_TAB_WIDTH:
+      ide_formatter_options_set_tab_width (self, g_value_get_uint (value));
+      break;
+
+    case PROP_INSERT_SPACES:
+      ide_formatter_options_set_insert_spaces (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_formatter_options_class_init (IdeFormatterOptionsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_formatter_options_get_property;
+  object_class->set_property = ide_formatter_options_set_property;
+
+  properties [PROP_INSERT_SPACES] =
+    g_param_spec_boolean ("insert-spaces",
+                          "Insert Spaces",
+                          "Insert spaces instead of tabs",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TAB_WIDTH] =
+    g_param_spec_uint ("tab-width",
+                       "Tab Width",
+                       "The width of a tab in spaces",
+                       1, 32, 8,
+                       (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_formatter_options_init (IdeFormatterOptions *self)
+{
+  self->tab_width = 8;
+}
+
+guint
+ide_formatter_options_get_tab_width (IdeFormatterOptions *self)
+{
+  g_return_val_if_fail (IDE_IS_FORMATTER_OPTIONS (self), 0);
+
+  return self->tab_width;
+}
+
+void
+ide_formatter_options_set_tab_width (IdeFormatterOptions *self,
+                                     guint                tab_width)
+{
+  g_return_if_fail (IDE_IS_FORMATTER_OPTIONS (self));
+
+  if (tab_width != self->tab_width)
+    {
+      self->tab_width = tab_width;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TAB_WIDTH]);
+    }
+}
+
+gboolean
+ide_formatter_options_get_insert_spaces (IdeFormatterOptions *self)
+{
+  g_return_val_if_fail (IDE_IS_FORMATTER_OPTIONS (self), FALSE);
+
+  return self->insert_spaces;
+}
+
+void
+ide_formatter_options_set_insert_spaces (IdeFormatterOptions *self,
+                                         gboolean             insert_spaces)
+{
+  g_return_if_fail (IDE_IS_FORMATTER_OPTIONS (self));
+
+  insert_spaces = !!insert_spaces;
+
+  if (insert_spaces != self->insert_spaces)
+    {
+      self->insert_spaces = insert_spaces;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_INSERT_SPACES]);
+    }
+}
+
+IdeFormatterOptions *
+ide_formatter_options_new (void)
+{
+  return g_object_new (IDE_TYPE_FORMATTER_OPTIONS, NULL);
+}
diff --git a/src/libide/code/ide-formatter-options.h b/src/libide/code/ide-formatter-options.h
new file mode 100644
index 000000000..068a765ba
--- /dev/null
+++ b/src/libide/code/ide-formatter-options.h
@@ -0,0 +1,49 @@
+/* ide-formatter-options.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FORMATTER_OPTIONS (ide_formatter_options_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeFormatterOptions, ide_formatter_options, IDE, FORMATTER_OPTIONS, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeFormatterOptions *ide_formatter_options_new               (void);
+IDE_AVAILABLE_IN_3_32
+guint                ide_formatter_options_get_tab_width     (IdeFormatterOptions *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_formatter_options_set_tab_width     (IdeFormatterOptions *self,
+                                                              guint                tab_width);
+IDE_AVAILABLE_IN_3_32
+gboolean             ide_formatter_options_get_insert_spaces (IdeFormatterOptions *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_formatter_options_set_insert_spaces (IdeFormatterOptions *self,
+                                                              gboolean             insert_spaces);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-formatter.c b/src/libide/code/ide-formatter.c
new file mode 100644
index 000000000..2275c6146
--- /dev/null
+++ b/src/libide/code/ide-formatter.c
@@ -0,0 +1,175 @@
+/* ide-formatter.c
+ *
+ * Copyright 2017-2019 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 "ide-formatter"
+
+#include "config.h"
+
+#include "ide-buffer.h"
+#include "ide-formatter.h"
+#include "ide-formatter-options.h"
+
+G_DEFINE_INTERFACE (IdeFormatter, ide_formatter, G_TYPE_OBJECT)
+
+static void
+ide_formatter_real_format_async (IdeFormatter        *self,
+                                 IdeBuffer           *buffer,
+                                 IdeFormatterOptions *options,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_assert (IDE_IS_FORMATTER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_FORMATTER_OPTIONS (options));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_task_report_new_error (self,
+                           callback,
+                           user_data,
+                           ide_formatter_real_format_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "The operation is not supported");
+}
+
+static gboolean
+ide_formatter_real_format_finish (IdeFormatter  *self,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  g_assert (IDE_IS_FORMATTER (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_formatter_real_format_range_async (IdeFormatter        *self,
+                                       IdeBuffer           *buffer,
+                                       IdeFormatterOptions *options,
+                                       const GtkTextIter   *begin,
+                                       const GtkTextIter   *end,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  g_assert (IDE_IS_FORMATTER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_FORMATTER_OPTIONS (options));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_task_report_new_error (self,
+                           callback,
+                           user_data,
+                           ide_formatter_real_format_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "The operation is not supported");
+}
+
+static gboolean
+ide_formatter_real_format_range_finish (IdeFormatter  *self,
+                                        GAsyncResult  *result,
+                                        GError       **error)
+{
+  g_assert (IDE_IS_FORMATTER (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_formatter_default_init (IdeFormatterInterface *iface)
+{
+  iface->format_async = ide_formatter_real_format_async;
+  iface->format_finish = ide_formatter_real_format_finish;
+  iface->format_range_async = ide_formatter_real_format_range_async;
+  iface->format_range_finish = ide_formatter_real_format_range_finish;
+}
+
+void
+ide_formatter_format_async (IdeFormatter        *self,
+                            IdeBuffer           *buffer,
+                            IdeFormatterOptions *options,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_FORMATTER (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (IDE_IS_FORMATTER_OPTIONS (options));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_FORMATTER_GET_IFACE (self)->format_async (self, buffer, options, cancellable, callback, user_data);
+}
+
+gboolean
+ide_formatter_format_finish (IdeFormatter  *self,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_FORMATTER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_FORMATTER_GET_IFACE (self)->format_finish (self, result, error);
+}
+
+void
+ide_formatter_format_range_async (IdeFormatter        *self,
+                                  IdeBuffer           *buffer,
+                                  IdeFormatterOptions *options,
+                                  const GtkTextIter   *begin,
+                                  const GtkTextIter   *end,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_FORMATTER (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (IDE_IS_FORMATTER_OPTIONS (options));
+  g_return_if_fail (begin != NULL);
+  g_return_if_fail (end != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_FORMATTER_GET_IFACE (self)->format_range_async (self, buffer, options, begin, end, cancellable, 
callback, user_data);
+}
+
+gboolean
+ide_formatter_format_range_finish (IdeFormatter  *self,
+                                   GAsyncResult  *result,
+                                   GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_FORMATTER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_FORMATTER_GET_IFACE (self)->format_range_finish (self, result, error);
+}
+
+void
+ide_formatter_load (IdeFormatter *self)
+{
+  g_return_if_fail (IDE_IS_FORMATTER (self));
+
+  if (IDE_FORMATTER_GET_IFACE (self)->load)
+    IDE_FORMATTER_GET_IFACE (self)->load (self);
+}
diff --git a/src/libide/code/ide-formatter.h b/src/libide/code/ide-formatter.h
new file mode 100644
index 000000000..fc3213948
--- /dev/null
+++ b/src/libide/code/ide-formatter.h
@@ -0,0 +1,93 @@
+/* ide-formatter.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FORMATTER (ide_formatter_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeFormatter, ide_formatter, IDE, FORMATTER, IdeObject)
+
+struct _IdeFormatterInterface
+{
+  GTypeInterface parent;
+
+  void     (*load)                (IdeFormatter         *self);
+  void     (*format_async)        (IdeFormatter         *self,
+                                   IdeBuffer            *buffer,
+                                   IdeFormatterOptions  *options,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data);
+  gboolean (*format_finish)       (IdeFormatter         *self,
+                                   GAsyncResult         *result,
+                                   GError              **error);
+  void     (*format_range_async)  (IdeFormatter         *self,
+                                   IdeBuffer            *buffer,
+                                   IdeFormatterOptions  *options,
+                                   const GtkTextIter    *begin,
+                                   const GtkTextIter    *end,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data);
+  gboolean (*format_range_finish) (IdeFormatter         *self,
+                                   GAsyncResult         *result,
+                                   GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_formatter_load                (IdeFormatter         *self);
+IDE_AVAILABLE_IN_3_32
+void     ide_formatter_format_async        (IdeFormatter         *self,
+                                            IdeBuffer            *buffer,
+                                            IdeFormatterOptions  *options,
+                                            GCancellable         *cancellable,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_formatter_format_finish       (IdeFormatter         *self,
+                                            GAsyncResult         *result,
+                                            GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_formatter_format_range_async  (IdeFormatter         *self,
+                                            IdeBuffer            *buffer,
+                                            IdeFormatterOptions  *options,
+                                            const GtkTextIter    *begin,
+                                            const GtkTextIter    *end,
+                                            GCancellable         *cancellable,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_formatter_format_range_finish (IdeFormatter         *self,
+                                            GAsyncResult         *result,
+                                            GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-gsettings-file-settings.c b/src/libide/code/ide-gsettings-file-settings.c
new file mode 100644
index 000000000..fe998a5d7
--- /dev/null
+++ b/src/libide/code/ide-gsettings-file-settings.c
@@ -0,0 +1,187 @@
+/* ide-gsettings-file-settings.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-gsettings-file-settings"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-core.h>
+
+#include "ide-code-enums.h"
+#include "ide-gsettings-file-settings.h"
+#include "ide-language-defaults.h"
+
+struct _IdeGsettingsFileSettings
+{
+  IdeFileSettings  parent_instance;
+  IdeSettings     *language_settings;
+};
+
+typedef struct
+{
+  const gchar             *key;
+  const gchar             *property;
+  GSettingsBindGetMapping  get_mapping;
+} SettingsMapping;
+
+G_DEFINE_TYPE (IdeGsettingsFileSettings, ide_gsettings_file_settings, IDE_TYPE_FILE_SETTINGS)
+
+static gboolean
+indent_style_get (GValue   *value,
+                  GVariant *variant,
+                  gpointer  user_data)
+{
+  if (g_variant_get_boolean (variant))
+    g_value_set_enum (value, IDE_INDENT_STYLE_SPACES);
+  else
+    g_value_set_enum (value, IDE_INDENT_STYLE_TABS);
+  return TRUE;
+}
+
+static gboolean
+spaces_style_get (GValue   *value,
+                  GVariant *variant,
+                  gpointer  user_data)
+{
+  g_autofree const gchar **strv = g_variant_get_strv (variant, NULL);
+  GFlagsClass *klass, *unref_class = NULL;
+  guint flags = 0;
+
+  if (!(klass = g_type_class_peek (IDE_TYPE_SPACES_STYLE)))
+    klass = unref_class = g_type_class_ref (IDE_TYPE_SPACES_STYLE);
+
+  for (guint i = 0; strv[i] != NULL; i++)
+    {
+      GFlagsValue *val = g_flags_get_value_by_nick (klass, strv[i]);
+
+      if (val == NULL)
+        {
+          g_warning ("No such nick %s", strv[i]);
+          continue;
+        }
+
+      flags |= val->value;
+    }
+
+  g_value_set_flags (value, flags);
+
+  if (unref_class != NULL)
+    g_type_class_unref (unref_class);
+
+  return TRUE;
+}
+
+static SettingsMapping language_mappings [] = {
+  { "auto-indent",                   "auto-indent",              NULL             },
+  { "indent-width",                  "indent-width",             NULL             },
+  { "insert-spaces-instead-of-tabs", "indent-style",             indent_style_get },
+  { "right-margin-position",         "right-margin-position",    NULL             },
+  { "show-right-margin",             "show-right-margin",        NULL             },
+  { "tab-width",                     "tab-width",                NULL             },
+  { "trim-trailing-whitespace",      "trim-trailing-whitespace", NULL             },
+  { "insert-matching-brace",         "insert-matching-brace",    NULL             },
+  { "insert-trailing-newline",       "insert-trailing-newline",  NULL             },
+  { "overwrite-braces",              "overwrite-braces",         NULL             },
+  { "spaces-style",                  "spaces-style",             spaces_style_get },
+};
+
+static void
+ide_gsettings_file_settings_apply (IdeGsettingsFileSettings *self)
+{
+  g_autofree gchar *relative_path = NULL;
+  g_autofree gchar *project_id = NULL;
+  const gchar *lang_id;
+  IdeContext *context;
+
+  g_assert (IDE_IS_GSETTINGS_FILE_SETTINGS (self));
+
+  g_clear_object (&self->language_settings);
+
+  if (!(lang_id = ide_file_settings_get_language (IDE_FILE_SETTINGS (self))))
+    lang_id = "plain-text";
+
+  g_assert (lang_id != NULL);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  project_id = ide_context_dup_project_id (context);
+  relative_path = g_strdup_printf ("/editor/language/%s/", lang_id);
+  self->language_settings = ide_settings_new (project_id,
+                                              "org.gnome.builder.editor.language",
+                                              relative_path,
+                                              FALSE);
+
+  for (guint i = 0; i < G_N_ELEMENTS (language_mappings); i++)
+    {
+      SettingsMapping *mapping = &language_mappings [i];
+
+      ide_settings_bind_with_mapping (self->language_settings,
+                                      mapping->key,
+                                      self,
+                                      mapping->property,
+                                      G_SETTINGS_BIND_GET,
+                                      mapping->get_mapping,
+                                      NULL,
+                                      NULL,
+                                      NULL);
+    }
+}
+
+static void
+ide_gsettings_file_settings_parent_set (IdeObject *object,
+                                        IdeObject *parent)
+{
+  IdeGsettingsFileSettings *self = (IdeGsettingsFileSettings *)object;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_GSETTINGS_FILE_SETTINGS (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (parent != NULL)
+    ide_gsettings_file_settings_apply (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_gsettings_file_settings_destroy (IdeObject *object)
+{
+  IdeGsettingsFileSettings *self = (IdeGsettingsFileSettings *)object;
+
+  g_clear_object (&self->language_settings);
+
+  IDE_OBJECT_CLASS (ide_gsettings_file_settings_parent_class)->destroy (object);
+}
+
+static void
+ide_gsettings_file_settings_class_init (IdeGsettingsFileSettingsClass *klass)
+{
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  i_object_class->parent_set = ide_gsettings_file_settings_parent_set;
+  i_object_class->destroy = ide_gsettings_file_settings_destroy;
+}
+
+static void
+ide_gsettings_file_settings_init (IdeGsettingsFileSettings *self)
+{
+}
diff --git a/src/libide/code/ide-gsettings-file-settings.h b/src/libide/code/ide-gsettings-file-settings.h
new file mode 100644
index 000000000..630d1ce1c
--- /dev/null
+++ b/src/libide/code/ide-gsettings-file-settings.h
@@ -0,0 +1,31 @@
+/* ide-gsettings-file-settings.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-file-settings.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_GSETTINGS_FILE_SETTINGS (ide_gsettings_file_settings_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeGsettingsFileSettings, ide_gsettings_file_settings, IDE, GSETTINGS_FILE_SETTINGS, 
IdeFileSettings)
+
+G_END_DECLS
diff --git a/src/libide/code/ide-highlight-engine.c b/src/libide/code/ide-highlight-engine.c
new file mode 100644
index 000000000..d95b74b00
--- /dev/null
+++ b/src/libide/code/ide-highlight-engine.c
@@ -0,0 +1,1189 @@
+/* ide-highlight-engine.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-highlight-engine"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+#include <libide-plugins.h>
+#include <string.h>
+
+#include "ide-buffer.h"
+#include "ide-buffer-private.h"
+#include "ide-highlight-engine.h"
+#include "ide-highlight-index.h"
+#include "ide-highlighter.h"
+
+#define HIGHLIGHT_QUANTA_USEC 5000
+#define PRIVATE_TAG_PREFIX    "gb-private-tag"
+
+struct _IdeHighlightEngine
+{
+  IdeObject            parent_instance;
+
+  GWeakRef             buffer_wref;
+
+  DzlSignalGroup      *signal_group;
+  IdeHighlighter      *highlighter;
+  GSettings           *settings;
+
+  IdeExtensionAdapter *extension;
+
+  GtkTextMark         *invalid_begin;
+  GtkTextMark         *invalid_end;
+
+  GSList              *private_tags;
+  GSList              *public_tags;
+
+  gint64               quanta_expiration;
+
+  guint                work_timeout;
+
+  guint                enabled : 1;
+};
+
+G_DEFINE_TYPE (IdeHighlightEngine, ide_highlight_engine, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_BUFFER,
+  PROP_HIGHLIGHTER,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static gboolean
+get_invalidation_area (GtkTextIter *begin,
+                       GtkTextIter *end)
+{
+  GtkTextIter begin_tmp;
+  GtkTextIter end_tmp;
+
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+  /*
+   * Move to the beginning of line.We dont use gtk_text_iter_backward_line
+   * because if begin is at the beginning of the line we dont want to
+   * move to the previous line
+   */
+  gtk_text_iter_set_line_offset (begin, 0);
+
+  /* Move to the beginning of the next line. */
+  gtk_text_iter_forward_line (end);
+
+  /* Save the original locations. We will need them down the line. */
+  begin_tmp = *begin;
+  end_tmp = *end;
+
+  /*
+   * Fordward begin iter character by character until:
+   * - We reach a non space character
+   * - We reach end iter
+   */
+  while (g_unichar_isspace (gtk_text_iter_get_char (begin)) &&
+         gtk_text_iter_compare (begin, &end_tmp) < 0)
+    gtk_text_iter_forward_char (begin);
+
+
+  /*
+   * If after moving forward the begin iter, we reached the end iter,
+   * there is no need to play with the end iter.
+   */
+  if (gtk_text_iter_compare (begin, end) < 0)
+    {
+      /*
+       * Backward end iter character by character until:
+       * - We reach a non space character
+       * - We reach begin iter
+       */
+      while (g_unichar_isspace (gtk_text_iter_get_char (end)) &&
+             gtk_text_iter_compare (end, &begin_tmp) > 0)
+        gtk_text_iter_backward_char (end);
+
+      /*
+       * If we found the character we are looking for then move one
+       * character forward in order to include it as the last
+       * character of the begin - end range.
+       */
+      if (gtk_text_iter_compare (end, &end_tmp) < 0)
+        gtk_text_iter_forward_char (end);
+    }
+
+  return gtk_text_iter_compare (begin, end) < 0;
+}
+
+static void
+sync_tag_style (GtkSourceStyleScheme *style_scheme,
+                GtkTextTag           *tag)
+{
+  g_autofree gchar *foreground = NULL;
+  g_autofree gchar *background = NULL;
+  g_autofree gchar *tag_name = NULL;
+  gchar *style_name = NULL;
+  const gchar *colon;
+  GtkSourceStyle *style;
+  gboolean foreground_set = FALSE;
+  gboolean background_set = FALSE;
+  gboolean bold = FALSE;
+  gboolean bold_set = FALSE;
+  gboolean underline = FALSE;
+  gboolean underline_set = FALSE;
+  gboolean italic = FALSE;
+  gboolean italic_set = FALSE;
+  gsize tag_name_len;
+  gsize prefix_len;
+
+  g_object_set (tag,
+                "foreground-set", FALSE,
+                "background-set", FALSE,
+                "weight-set", FALSE,
+                "underline-set", FALSE,
+                "style-set", FALSE,
+                NULL);
+
+  g_object_get (tag, "name", &tag_name, NULL);
+
+  if (tag_name == NULL || style_scheme == NULL)
+    return;
+
+  prefix_len = strlen (PRIVATE_TAG_PREFIX);
+  tag_name_len = strlen (tag_name);
+  style_name = tag_name;
+
+  /*
+   * Check if this is a private tag.A tag is private if it starts with
+   * PRIVATE_TAG_PREFIX "gb-private-tag".
+   * ex: gb-private-tag:c:boolean
+   * If the tag is private extract the original style name by moving the string
+   * strlen (PRIVATE_TAG_PREFIX) + 1 (the colon) characters.
+   */
+  if (tag_name_len > prefix_len && memcmp (tag_name, PRIVATE_TAG_PREFIX, prefix_len) == 0)
+    style_name = tag_name + prefix_len + 1;
+
+  style = gtk_source_style_scheme_get_style (style_scheme, style_name);
+  if (style == NULL && (colon = strchr (style_name, ':')))
+    {
+      gchar defname[64];
+      g_snprintf (defname, sizeof defname, "def%s", colon);
+      style = gtk_source_style_scheme_get_style (style_scheme, defname);
+      if (style == NULL)
+        return;
+    }
+
+  g_object_get (style,
+                "background", &background,
+                "background-set", &background_set,
+                "foreground", &foreground,
+                "foreground-set", &foreground_set,
+                "bold", &bold,
+                "bold-set", &bold_set,
+                "pango-underline", &underline,
+                "underline-set", &underline_set,
+                "italic", &italic,
+                "italic-set", &italic_set,
+                NULL);
+
+  if (background_set)
+    g_object_set (tag, "background", background, NULL);
+
+  if (foreground_set)
+    g_object_set (tag, "foreground", foreground, NULL);
+
+  if (bold_set && bold)
+    g_object_set (tag, "weight", PANGO_WEIGHT_BOLD, NULL);
+
+  if (italic_set && italic)
+    g_object_set (tag, "style", PANGO_STYLE_ITALIC, NULL);
+
+  if (underline_set && underline)
+    g_object_set (tag, "underline", PANGO_UNDERLINE_SINGLE, NULL);
+}
+
+static GtkTextTag *
+create_tag_from_style (IdeHighlightEngine *self,
+                       const gchar        *style_name)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+  GtkSourceStyleScheme *style_scheme;
+  GtkTextTag *tag = NULL;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (style_name != NULL);
+
+  buffer = g_weak_ref_get (&self->buffer_wref);
+
+  if (buffer != NULL)
+    {
+      tag = gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (buffer), style_name, NULL);
+      gtk_text_tag_set_priority (tag, 0);
+      style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
+      sync_tag_style (style_scheme, tag);
+    }
+
+  return tag;
+}
+
+static GtkTextTag *
+get_tag_from_style (IdeHighlightEngine *self,
+                    const gchar        *style_name,
+                    gboolean            private_tag)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+  g_autofree gchar *tmp_style_name = NULL;
+  GtkTextTagTable *tag_table;
+  GtkTextTag *tag;
+
+  g_return_val_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self), NULL);
+  g_return_val_if_fail (style_name != NULL, NULL);
+
+  buffer = g_weak_ref_get (&self->buffer_wref);
+  if (buffer == NULL)
+    return NULL;
+
+  /*
+   * If is private tag prepend the PRIVATE_TAG_PREFIX (gb-private-tag)
+   * to the string.This is used because tag name is the key used
+   * for saving tags in GtkTextTagTable and we dont want conflicts between
+   * public and private tags.
+   */
+  if (private_tag)
+    tmp_style_name = g_strdup_printf ("%s:%s", PRIVATE_TAG_PREFIX, style_name);
+  else
+    tmp_style_name = g_strdup (style_name);
+
+  tag_table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (buffer));
+  tag = gtk_text_tag_table_lookup (tag_table, tmp_style_name);
+
+  if (tag == NULL)
+    {
+      tag = create_tag_from_style (self, tmp_style_name);
+      if (private_tag)
+        self->private_tags = g_slist_prepend (self->private_tags, tag);
+      else
+        self->public_tags = g_slist_prepend (self->public_tags, tag);
+    }
+
+  return tag;
+}
+
+
+static IdeHighlightResult
+ide_highlight_engine_apply_style (const GtkTextIter *begin,
+                                  const GtkTextIter *end,
+                                  const gchar       *style_name)
+{
+  IdeHighlightEngine *self;
+  GtkTextBuffer *buffer;
+  GtkTextTag *tag;
+
+  buffer = gtk_text_iter_get_buffer (begin);
+  self = _ide_buffer_get_highlight_engine (IDE_BUFFER (buffer));
+  tag = get_tag_from_style (self, style_name, TRUE);
+
+  gtk_text_buffer_apply_tag (buffer, tag, begin, end);
+
+  if (g_get_monotonic_time () >= self->quanta_expiration)
+    return IDE_HIGHLIGHT_STOP;
+
+  return IDE_HIGHLIGHT_CONTINUE;
+}
+
+static gboolean
+ide_highlight_engine_tick (IdeHighlightEngine *self)
+{
+  g_autoptr(GtkTextBuffer) buffer = NULL;
+  GtkTextIter iter;
+  GtkTextIter invalid_begin;
+  GtkTextIter invalid_end;
+  GSList *tags_iter;
+
+  IDE_PROBE;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (self->highlighter != NULL);
+  g_assert (self->invalid_begin != NULL);
+  g_assert (self->invalid_end != NULL);
+
+  buffer = g_weak_ref_get (&self->buffer_wref);
+  if (buffer == NULL)
+    return G_SOURCE_REMOVE;
+
+  self->quanta_expiration = g_get_monotonic_time () + HIGHLIGHT_QUANTA_USEC;
+
+  gtk_text_buffer_get_iter_at_mark (buffer, &invalid_begin, self->invalid_begin);
+  gtk_text_buffer_get_iter_at_mark (buffer, &invalid_end, self->invalid_end);
+
+  IDE_TRACE_MSG ("Highlight Range [%u:%u,%u:%u] (%s)",
+                 gtk_text_iter_get_line (&invalid_begin),
+                 gtk_text_iter_get_line_offset (&invalid_begin),
+                 gtk_text_iter_get_line (&invalid_end),
+                 gtk_text_iter_get_line_offset (&invalid_end),
+                 G_OBJECT_TYPE_NAME (self->highlighter));
+
+  if (gtk_text_iter_compare (&invalid_begin, &invalid_end) >= 0)
+    IDE_GOTO (up_to_date);
+
+  /* Clear all our tags */
+  for (tags_iter = self->private_tags; tags_iter; tags_iter = tags_iter->next)
+    gtk_text_buffer_remove_tag (buffer,
+                                GTK_TEXT_TAG (tags_iter->data),
+                                &invalid_begin,
+                                &invalid_end);
+
+  iter = invalid_begin;
+
+  ide_highlighter_update (self->highlighter, ide_highlight_engine_apply_style,
+                          &invalid_begin, &invalid_end, &iter);
+
+  if (gtk_text_iter_compare (&iter, &invalid_end) >= 0)
+    IDE_GOTO (up_to_date);
+
+  /* Stop processing until further instruction if no movement was made */
+  if (gtk_text_iter_equal (&iter, &invalid_begin))
+    return G_SOURCE_REMOVE;
+
+  gtk_text_buffer_move_mark (buffer, self->invalid_begin, &iter);
+
+  return G_SOURCE_CONTINUE;
+
+up_to_date:
+  gtk_text_buffer_get_start_iter (buffer, &iter);
+  gtk_text_buffer_move_mark (buffer, self->invalid_begin, &iter);
+  gtk_text_buffer_move_mark (buffer, self->invalid_end, &iter);
+
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+ide_highlight_engine_work_timeout_handler (gpointer data)
+{
+  IdeHighlightEngine *self = data;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+
+  if (self->enabled)
+    {
+      if (ide_highlight_engine_tick (self))
+        return G_SOURCE_CONTINUE;
+    }
+
+  self->work_timeout = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_highlight_engine_queue_work (IdeHighlightEngine *self)
+{
+  g_autoptr(GtkTextBuffer) buffer = NULL;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+
+  buffer = g_weak_ref_get (&self->buffer_wref);
+  if (self->highlighter == NULL || buffer == NULL || self->work_timeout != 0)
+    return;
+
+  /*
+   * NOTE: It would be really nice if we could use the GdkFrameClock here to
+   *       drive the next update instead of a timeout. It's possible that our
+   *       callback could get scheduled right before the frame processing would
+   *       begin. However, since that gets driven by something like a Wayland
+   *       callback, it won't yet be scheduled. So instead our function gets
+   *       called and we potentially cause a frame to drop.
+   */
+
+  self->work_timeout = gdk_threads_add_idle_full (G_PRIORITY_LOW + 1,
+                                                  ide_highlight_engine_work_timeout_handler,
+                                                  self,
+                                                  NULL);
+}
+
+/**
+ * ide_highlight_engine_advance:
+ * @self: a #IdeHighlightEngine
+ *
+ * This function is useful for #IdeHighlighter implementations that need to
+ * asynchronously do work to process the highlighting.
+ *
+ * If they return from their update function without advancing, nothing will
+ * happen until they call this method to proceed.
+ *
+ * Since: 3.32
+ */
+void
+ide_highlight_engine_advance (IdeHighlightEngine *self)
+{
+  g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
+
+  ide_highlight_engine_queue_work (self);
+}
+
+static gboolean
+invalidate_and_highlight (IdeHighlightEngine *self,
+                          GtkTextIter        *begin,
+                          GtkTextIter        *end)
+{
+  GtkTextBuffer *text_buffer;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+  if (!self->enabled)
+    return FALSE;
+
+  text_buffer = gtk_text_iter_get_buffer (begin);
+
+  if (get_invalidation_area (begin, end))
+    {
+      GtkTextIter begin_tmp;
+      GtkTextIter end_tmp;
+
+      gtk_text_buffer_get_iter_at_mark (text_buffer, &begin_tmp, self->invalid_begin);
+      gtk_text_buffer_get_iter_at_mark (text_buffer, &end_tmp, self->invalid_end);
+
+      if (gtk_text_iter_equal (&begin_tmp, &end_tmp))
+        {
+          gtk_text_buffer_move_mark (text_buffer, self->invalid_begin, begin);
+          gtk_text_buffer_move_mark (text_buffer, self->invalid_end, end);
+        }
+      else
+        {
+          if (gtk_text_iter_compare (begin, &begin_tmp) < 0)
+            gtk_text_buffer_move_mark (text_buffer, self->invalid_begin, begin);
+          if (gtk_text_iter_compare (end, &end_tmp) > 0)
+            gtk_text_buffer_move_mark (text_buffer, self->invalid_end, end);
+        }
+
+      ide_highlight_engine_queue_work (self);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_highlight_engine_reload (IdeHighlightEngine *self)
+{
+  g_autoptr(GtkTextBuffer) buffer = NULL;
+  GtkTextIter begin;
+  GtkTextIter end;
+  GSList *iter;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+
+  dzl_clear_source (&self->work_timeout);
+
+  buffer = g_weak_ref_get (&self->buffer_wref);
+  if (buffer == NULL)
+    IDE_EXIT;
+
+  gtk_text_buffer_get_bounds (buffer, &begin, &end);
+
+  /*
+   * Invalidate the whole buffer.
+   */
+  gtk_text_buffer_move_mark (buffer, self->invalid_begin, &begin);
+  gtk_text_buffer_move_mark (buffer, self->invalid_end, &end);
+
+  /*
+   * Remove our highlight tags from the buffer.
+   */
+  for (iter = self->private_tags; iter; iter = iter->next)
+    gtk_text_buffer_remove_tag (buffer, iter->data, &begin, &end);
+  g_clear_pointer (&self->private_tags, g_slist_free);
+
+  for (iter = self->public_tags; iter; iter = iter->next)
+    gtk_text_buffer_remove_tag (buffer, iter->data, &begin, &end);
+  g_clear_pointer (&self->public_tags, g_slist_free);
+
+  if (self->highlighter == NULL)
+    IDE_EXIT;
+
+  ide_highlight_engine_queue_work (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_highlight_engine_set_highlighter (IdeHighlightEngine *self,
+                                      IdeHighlighter     *highlighter)
+{
+  g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_return_if_fail (!highlighter || IDE_IS_HIGHLIGHTER (highlighter));
+
+  if (g_set_object (&self->highlighter, highlighter))
+    {
+      if (highlighter != NULL)
+        {
+          IDE_HIGHLIGHTER_GET_IFACE (highlighter)->set_engine (highlighter, self);
+          ide_highlighter_load (highlighter);
+        }
+
+      ide_highlight_engine_reload (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HIGHLIGHTER]);
+    }
+}
+
+static void
+ide_highlight_engine__buffer_insert_text_cb (IdeHighlightEngine *self,
+                                             GtkTextIter        *location,
+                                             gchar              *text,
+                                             gint                len,
+                                             IdeBuffer          *buffer)
+{
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (location);
+  g_assert (text);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (!self->enabled)
+    IDE_EXIT;
+
+  /*
+   * Backward the begin iter len characters from location
+   * (location points to the end of the string) in order to get
+   * the iter position where our inserted text was started.
+   */
+  begin = *location;
+  gtk_text_iter_backward_chars (&begin, g_utf8_strlen (text, len));
+
+  end = *location;
+
+  invalidate_and_highlight (self, &begin, &end);
+
+  IDE_EXIT;
+}
+
+static void
+ide_highlight_engine__buffer_delete_range_cb (IdeHighlightEngine *self,
+                                              GtkTextIter        *range_begin,
+                                              GtkTextIter        *range_end,
+                                              IdeBuffer          *buffer)
+{
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (range_begin);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (!self->enabled)
+    IDE_EXIT;
+
+  /*
+   * No need to use the range_end since everything that
+   * was after range_end will now be after range_begin
+   */
+  begin = *range_begin;
+  end = *range_begin;
+
+  invalidate_and_highlight (self, &begin, &end);
+
+  IDE_EXIT;
+}
+
+static void
+ide_highlight_engine__notify_language_cb (IdeHighlightEngine *self,
+                                          GParamSpec         *pspec,
+                                          IdeBuffer          *buffer)
+{
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (self->extension != NULL)
+    {
+      GtkSourceLanguage *language;
+      const gchar *lang_id = NULL;
+
+      if ((language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer))))
+        lang_id = gtk_source_language_get_id (language);
+
+      ide_extension_adapter_set_value (self->extension, lang_id);
+    }
+}
+
+static void
+ide_highlight_engine__notify_style_scheme_cb (IdeHighlightEngine *self,
+                                              GParamSpec         *pspec,
+                                              IdeBuffer          *buffer)
+{
+  GtkSourceStyleScheme *style_scheme;
+  GSList *iter;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
+
+  for (iter = self->private_tags; iter; iter = iter->next)
+    sync_tag_style (style_scheme, iter->data);
+  for (iter = self->public_tags; iter; iter = iter->next)
+    sync_tag_style (style_scheme, iter->data);
+}
+
+void
+ide_highlight_engine_clear (IdeHighlightEngine *self)
+{
+  g_autoptr(GtkTextBuffer) buffer = NULL;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+
+  buffer = g_weak_ref_get (&self->buffer_wref);
+
+  if (buffer != NULL)
+    {
+      gtk_text_buffer_get_bounds (buffer, &begin, &end);
+
+      for (const GSList *iter = self->public_tags; iter; iter = iter->next)
+        {
+          GtkTextTag *tag = iter->data;
+          gtk_text_buffer_remove_tag (buffer, tag, &begin, &end);
+        }
+    }
+}
+
+static void
+ide_highlight_engine__bind_buffer_cb (IdeHighlightEngine *self,
+                                      IdeBuffer          *buffer,
+                                      DzlSignalGroup     *group)
+{
+  GtkTextBuffer *text_buffer = (GtkTextBuffer *)buffer;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (DZL_IS_SIGNAL_GROUP (group));
+  g_assert (self->invalid_begin == NULL);
+  g_assert (self->invalid_end == NULL);
+
+  g_weak_ref_set (&self->buffer_wref, buffer);
+
+  gtk_text_buffer_get_bounds (text_buffer, &begin, &end);
+
+  self->invalid_begin = gtk_text_buffer_create_mark (text_buffer, NULL, &begin, TRUE);
+  self->invalid_end = gtk_text_buffer_create_mark (text_buffer, NULL, &end, FALSE);
+
+  /* We can hold a full reference to the text marks, without
+   * taking a reference to the buffer. We want to avoid a reference
+   * to the buffer for cyclic reasons.
+   */
+  g_object_ref (self->invalid_begin);
+  g_object_ref (self->invalid_end);
+
+  ide_highlight_engine__notify_style_scheme_cb (self, NULL, buffer);
+  ide_highlight_engine__notify_language_cb (self, NULL, buffer);
+
+  ide_highlight_engine_reload (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_highlight_engine__unbind_buffer_cb (IdeHighlightEngine  *self,
+                                        DzlSignalGroup      *group)
+{
+  g_autoptr(GtkTextBuffer) text_buffer = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (DZL_IS_SIGNAL_GROUP (group));
+
+  text_buffer = g_weak_ref_get (&self->buffer_wref);
+
+  dzl_clear_source (&self->work_timeout);
+
+  if (text_buffer != NULL)
+    {
+      g_autoptr(GSList) private_tags = NULL;
+      g_autoptr(GSList) public_tags = NULL;
+      GtkTextTagTable *tag_table;
+      GtkTextIter begin;
+      GtkTextIter end;
+
+      tag_table = gtk_text_buffer_get_tag_table (text_buffer);
+
+      gtk_text_buffer_delete_mark (text_buffer, self->invalid_begin);
+      gtk_text_buffer_delete_mark (text_buffer, self->invalid_end);
+
+      gtk_text_buffer_get_bounds (text_buffer, &begin, &end);
+
+      private_tags = g_steal_pointer (&self->private_tags);
+      public_tags = g_steal_pointer (&self->public_tags);
+
+      for (const GSList *iter = private_tags; iter; iter = iter->next)
+        {
+          gtk_text_buffer_remove_tag (text_buffer, iter->data, &begin, &end);
+          gtk_text_tag_table_remove (tag_table, iter->data);
+        }
+
+      for (const GSList *iter = public_tags; iter; iter = iter->next)
+        {
+          gtk_text_buffer_remove_tag (text_buffer, iter->data, &begin, &end);
+          gtk_text_tag_table_remove (tag_table, iter->data);
+        }
+    }
+
+  g_clear_pointer (&self->public_tags, g_slist_free);
+  g_clear_pointer (&self->private_tags, g_slist_free);
+
+  g_clear_object (&self->invalid_begin);
+  g_clear_object (&self->invalid_end);
+
+  IDE_EXIT;
+}
+
+static void
+ide_highlight_engine_set_buffer (IdeHighlightEngine *self,
+                                 IdeBuffer          *buffer)
+{
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (!buffer || GTK_IS_TEXT_BUFFER (buffer));
+
+  /* We can get GtkSourceBuffer intermittently here. */
+  if (!buffer || IDE_IS_BUFFER (buffer))
+    {
+      dzl_signal_group_set_target (self->signal_group, buffer);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUFFER]);
+    }
+}
+
+static void
+ide_highlight_engine_settings_changed (IdeHighlightEngine *self,
+                                       const gchar        *key,
+                                       GSettings          *settings)
+{
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (G_IS_SETTINGS (settings));
+
+  if (g_settings_get_boolean (settings, "semantic-highlighting"))
+    {
+      self->enabled = TRUE;
+      ide_highlight_engine_rebuild (self);
+    }
+  else
+    {
+      self->enabled = FALSE;
+      ide_highlight_engine_clear (self);
+    }
+}
+
+static void
+ide_highlight_engine__notify_extension (IdeHighlightEngine  *self,
+                                        GParamSpec          *pspec,
+                                        IdeExtensionAdapter *adapter)
+{
+  IdeHighlighter *highlighter;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (IDE_IS_EXTENSION_ADAPTER (adapter));
+
+  highlighter = ide_extension_adapter_get_extension (adapter);
+  g_return_if_fail (!highlighter || IDE_IS_HIGHLIGHTER (highlighter));
+
+  ide_highlight_engine_set_highlighter (self, highlighter);
+}
+
+static void
+ide_highlight_engine_parent_set (IdeObject *object,
+                                 IdeObject *parent)
+{
+  IdeHighlightEngine *self = (IdeHighlightEngine *)object;
+
+  g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (parent == NULL)
+    {
+      g_clear_object (&self->extension);
+      return;
+    }
+
+  self->extension = ide_extension_adapter_new (IDE_OBJECT (self),
+                                               NULL,
+                                               IDE_TYPE_HIGHLIGHTER,
+                                               "Highlighter-Languages",
+                                               NULL);
+  g_signal_connect_object (self->extension,
+                           "notify::extension",
+                           G_CALLBACK (ide_highlight_engine__notify_extension),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+ide_highlight_engine_dispose (GObject *object)
+{
+  IdeHighlightEngine *self = (IdeHighlightEngine *)object;
+
+  g_weak_ref_set (&self->buffer_wref, NULL);
+  g_clear_object (&self->signal_group);
+  g_clear_object (&self->extension);
+  g_clear_object (&self->highlighter);
+  g_clear_object (&self->settings);
+
+  G_OBJECT_CLASS (ide_highlight_engine_parent_class)->dispose (object);
+}
+
+static void
+ide_highlight_engine_finalize (GObject *object)
+{
+  IdeHighlightEngine *self = (IdeHighlightEngine *)object;
+
+  g_weak_ref_clear (&self->buffer_wref);
+
+  G_OBJECT_CLASS (ide_highlight_engine_parent_class)->finalize (object);
+}
+
+static void
+ide_highlight_engine_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  IdeHighlightEngine *self = IDE_HIGHLIGHT_ENGINE (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      g_value_set_object (value, ide_highlight_engine_get_buffer (self));
+      break;
+
+    case PROP_HIGHLIGHTER:
+      g_value_set_object (value, ide_highlight_engine_get_highlighter (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_highlight_engine_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  IdeHighlightEngine *self = IDE_HIGHLIGHT_ENGINE (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      ide_highlight_engine_set_buffer (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_highlight_engine_class_init (IdeHighlightEngineClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_highlight_engine_dispose;
+  object_class->finalize = ide_highlight_engine_finalize;
+  object_class->get_property = ide_highlight_engine_get_property;
+  object_class->set_property = ide_highlight_engine_set_property;
+
+  i_object_class->parent_set = ide_highlight_engine_parent_set;
+
+  properties [PROP_BUFFER] =
+    g_param_spec_object ("buffer",
+                         "Buffer",
+                         "The buffer to highlight.",
+                         IDE_TYPE_BUFFER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HIGHLIGHTER] =
+    g_param_spec_object ("highlighter",
+                         "Highlighter",
+                         "The highlighter to use for type information.",
+                         IDE_TYPE_HIGHLIGHTER,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_highlight_engine_init (IdeHighlightEngine *self)
+{
+  g_weak_ref_init (&self->buffer_wref, NULL);
+
+  self->settings = g_settings_new ("org.gnome.builder.code-insight");
+  self->enabled = g_settings_get_boolean (self->settings, "semantic-highlighting");
+  self->signal_group = dzl_signal_group_new (IDE_TYPE_BUFFER);
+
+  dzl_signal_group_connect_object (self->signal_group,
+                                   "insert-text",
+                                   G_CALLBACK (ide_highlight_engine__buffer_insert_text_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+  dzl_signal_group_connect_object (self->signal_group,
+                                   "delete-range",
+                                   G_CALLBACK (ide_highlight_engine__buffer_delete_range_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+  dzl_signal_group_connect_object (self->signal_group,
+                                   "notify::language",
+                                   G_CALLBACK (ide_highlight_engine__notify_language_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->signal_group,
+                                   "notify::style-scheme",
+                                   G_CALLBACK (ide_highlight_engine__notify_style_scheme_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->signal_group,
+                           "bind",
+                           G_CALLBACK (ide_highlight_engine__bind_buffer_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->signal_group,
+                           "unbind",
+                           G_CALLBACK (ide_highlight_engine__unbind_buffer_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->settings,
+                           "changed::semantic-highlighting",
+                           G_CALLBACK (ide_highlight_engine_settings_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+IdeHighlightEngine *
+ide_highlight_engine_new (IdeBuffer *buffer)
+{
+  IdeHighlightEngine *self;
+  IdeObjectBox *box;
+
+  g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
+
+  self = g_object_new (IDE_TYPE_HIGHLIGHT_ENGINE,
+                       "buffer", buffer,
+                       NULL);
+
+  box = ide_object_box_from_object (G_OBJECT (buffer));
+  ide_object_append (IDE_OBJECT (box), IDE_OBJECT (self));
+
+  return g_steal_pointer (&self);
+}
+
+/**
+ * ide_highlight_engine_get_highlighter:
+ * @self: an #IdeHighlightEngine.
+ *
+ * Gets the IdeHighlightEngine:highlighter property.
+ *
+ * Returns: (transfer none): An #IdeHighlighter.
+ *
+ * Since: 3.32
+ */
+IdeHighlighter *
+ide_highlight_engine_get_highlighter (IdeHighlightEngine *self)
+{
+  g_return_val_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self), NULL);
+
+  return self->highlighter;
+}
+
+/**
+ * ide_highlight_engine_get_buffer:
+ * @self: an #IdeHighlightEngine.
+ *
+ * Gets the IdeHighlightEngine:buffer property.
+ *
+ * Returns: (transfer none): An #IdeBuffer.
+ *
+ * Since: 3.32
+ */
+IdeBuffer *
+ide_highlight_engine_get_buffer (IdeHighlightEngine *self)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+
+  g_return_val_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self), NULL);
+
+  /* We don't need the "thread-safety" provided by GWeakRef here,
+   * (where it gives us a new object reference). It is safe to
+   * return a borrowed reference instead.
+   */
+  buffer = g_weak_ref_get (&self->buffer_wref);
+  return buffer;
+}
+
+void
+ide_highlight_engine_rebuild (IdeHighlightEngine *self)
+{
+  g_autoptr(GtkTextBuffer) buffer = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
+
+  buffer = g_weak_ref_get (&self->buffer_wref);
+
+  if (buffer != NULL)
+    {
+      GtkTextIter begin;
+      GtkTextIter end;
+
+      gtk_text_buffer_get_bounds (buffer, &begin, &end);
+      gtk_text_buffer_move_mark (buffer, self->invalid_begin, &begin);
+      gtk_text_buffer_move_mark (buffer, self->invalid_end, &end);
+
+      ide_highlight_engine_queue_work (self);
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_highlight_engine_invalidate:
+ * @self: An #IdeHighlightEngine.
+ * @begin: the beginning of the range to invalidate
+ * @end: the end of the range to invalidate
+ *
+ * This function will extend the invalidated range of the buffer to include
+ * the range of @begin to @end.
+ *
+ * The highlighter will be queued to interactively update the invalidated
+ * region.
+ *
+ * Updating the invalidated region of the buffer may take some time, as it is
+ * important that the highlighter does not block for more than 1-2 milliseconds
+ * to avoid dropping frames.
+ *
+ * Since: 3.32
+ */
+void
+ide_highlight_engine_invalidate (IdeHighlightEngine *self,
+                                 const GtkTextIter  *begin,
+                                 const GtkTextIter  *end)
+{
+  GtkTextBuffer *buffer;
+  GtkTextIter mark_begin;
+  GtkTextIter mark_end;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_return_if_fail (begin != NULL);
+  g_return_if_fail (end != NULL);
+
+  buffer = gtk_text_iter_get_buffer (begin);
+
+  gtk_text_buffer_get_iter_at_mark (buffer, &mark_begin, self->invalid_begin);
+  gtk_text_buffer_get_iter_at_mark (buffer, &mark_end, self->invalid_end);
+
+  if (gtk_text_iter_equal (&mark_begin, &mark_end))
+    {
+      gtk_text_buffer_move_mark (buffer, self->invalid_begin, begin);
+      gtk_text_buffer_move_mark (buffer, self->invalid_end, end);
+    }
+  else
+    {
+      if (gtk_text_iter_compare (begin, &mark_begin) < 0)
+        gtk_text_buffer_move_mark (buffer, self->invalid_begin, begin);
+      if (gtk_text_iter_compare (end, &mark_end) > 0)
+        gtk_text_buffer_move_mark (buffer, self->invalid_end, end);
+    }
+
+  ide_highlight_engine_queue_work (self);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_highlight_engine_get_style:
+ * @self: the #IdeHighlightEngine
+ * @style_name: the name of the style to retrieve
+ *
+ * A #GtkTextTag for @style_name.
+ *
+ * Returns: (transfer none): a #GtkTextTag.
+ *
+ * Since: 3.32
+ */
+GtkTextTag *
+ide_highlight_engine_get_style (IdeHighlightEngine *self,
+                                const gchar        *style_name)
+{
+  return get_tag_from_style (self, style_name, FALSE);
+}
+
+void
+ide_highlight_engine_pause (IdeHighlightEngine *self)
+{
+  g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
+
+  dzl_signal_group_block (self->signal_group);
+}
+
+void
+ide_highlight_engine_unpause (IdeHighlightEngine *self)
+{
+  g_autoptr(IdeBuffer) buffer = NULL;
+
+  g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
+  g_return_if_fail (self->signal_group != NULL);
+
+  dzl_signal_group_unblock (self->signal_group);
+
+  buffer = g_weak_ref_get (&self->buffer_wref);
+
+  if (buffer != NULL)
+    {
+      /* Notify of some blocked signals */
+      ide_highlight_engine__notify_style_scheme_cb (self, NULL, buffer);
+      ide_highlight_engine__notify_language_cb (self, NULL, buffer);
+
+      ide_highlight_engine_reload (self);
+    }
+}
diff --git a/src/libide/code/ide-highlight-engine.h b/src/libide/code/ide-highlight-engine.h
new file mode 100644
index 000000000..eb4012278
--- /dev/null
+++ b/src/libide/code/ide-highlight-engine.h
@@ -0,0 +1,62 @@
+/* ide-highlight-engine.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HIGHLIGHT_ENGINE (ide_highlight_engine_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeHighlightEngine, ide_highlight_engine, IDE, HIGHLIGHT_ENGINE, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeHighlightEngine *ide_highlight_engine_new             (IdeBuffer          *buffer);
+IDE_AVAILABLE_IN_3_32
+IdeBuffer          *ide_highlight_engine_get_buffer      (IdeHighlightEngine *self);
+IDE_AVAILABLE_IN_3_32
+IdeHighlighter     *ide_highlight_engine_get_highlighter (IdeHighlightEngine *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_highlight_engine_rebuild         (IdeHighlightEngine *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_highlight_engine_clear           (IdeHighlightEngine *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_highlight_engine_invalidate      (IdeHighlightEngine *self,
+                                                          const GtkTextIter  *begin,
+                                                          const GtkTextIter  *end);
+IDE_AVAILABLE_IN_3_32
+GtkTextTag         *ide_highlight_engine_get_style       (IdeHighlightEngine *self,
+                                                          const gchar        *style_name);
+IDE_AVAILABLE_IN_3_32
+void                ide_highlight_engine_pause           (IdeHighlightEngine *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_highlight_engine_unpause         (IdeHighlightEngine *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_highlight_engine_advance         (IdeHighlightEngine *self);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-highlight-index.c b/src/libide/code/ide-highlight-index.c
new file mode 100644
index 000000000..464badd0e
--- /dev/null
+++ b/src/libide/code/ide-highlight-index.c
@@ -0,0 +1,244 @@
+/* ide-highlight-index.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-highlight-index"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <string.h>
+
+#include "ide-highlight-index.h"
+
+G_DEFINE_BOXED_TYPE (IdeHighlightIndex, ide_highlight_index,
+                     ide_highlight_index_ref, ide_highlight_index_unref)
+
+struct _IdeHighlightIndex
+{
+  /* For debugging info */
+  guint          count;
+  gsize          chunk_size;
+
+  GStringChunk  *strings;
+  GHashTable    *index;
+  GVariant      *variant;
+};
+
+IdeHighlightIndex *
+ide_highlight_index_new (void)
+{
+  IdeHighlightIndex *ret;
+
+  ret = g_atomic_rc_box_new0 (IdeHighlightIndex);
+  ret->strings = g_string_chunk_new (ide_get_system_page_size ());
+  ret->index = g_hash_table_new (g_str_hash, g_str_equal);
+
+  return ret;
+}
+
+IdeHighlightIndex *
+ide_highlight_index_new_from_variant (GVariant *variant)
+{
+  IdeHighlightIndex *self;
+
+  self = ide_highlight_index_new ();
+
+  if (variant != NULL)
+    {
+      g_autoptr(GVariant) unboxed = NULL;
+
+      self->variant = g_variant_ref_sink (variant);
+
+      if (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARIANT))
+        variant = unboxed = g_variant_get_variant (variant);
+
+      if (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARDICT))
+        {
+          GVariantIter iter;
+          GVariant *value;
+          const gchar *tag;
+
+          g_variant_iter_init (&iter, variant);
+
+          while (g_variant_iter_loop (&iter, "{&sv}", &tag, &value))
+            {
+              if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING_ARRAY))
+                {
+                  g_autofree const gchar **strv = NULL;
+                  gsize len;
+
+                  strv = g_variant_get_strv (value, &len);
+
+                  for (gsize i = 0; i < len; i++)
+                    {
+                      const gchar *word = strv[i];
+
+                      if (g_hash_table_contains (self->index, word))
+                        continue;
+
+                      /* word is guaranteed to be alive and valid inside our
+                       * variant that we are storing. No need to add to the
+                       * string chunk too.
+                       */
+                      g_hash_table_insert (self->index, (gchar *)word, (gchar *)tag);
+                      self->count++;
+                    }
+                }
+            }
+        }
+    }
+
+  return self;
+}
+
+void
+ide_highlight_index_insert (IdeHighlightIndex *self,
+                            const gchar       *word,
+                            gpointer           tag)
+{
+  gchar *key;
+
+  g_assert (self);
+  g_assert (tag != NULL);
+
+  if (word == NULL || word[0] == '\0')
+    return;
+
+  if (g_hash_table_contains (self->index, word))
+    return;
+
+  self->count++;
+  self->chunk_size += strlen (word) + 1;
+
+  key = g_string_chunk_insert (self->strings, word);
+  g_hash_table_insert (self->index, key, tag);
+}
+
+/**
+ * ide_highlight_index_lookup:
+ * @self: An #IdeHighlightIndex.
+ *
+ * Gets the pointer tag that was registered for @word, or %NULL.  This can be
+ * any arbitrary value. Some highlight engines might use it to point at
+ * internal structures or strings they know about to optimize later work.
+ *
+ * Returns: (transfer none) (nullable): Highlighter specific tag.
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_highlight_index_lookup (IdeHighlightIndex *self,
+                            const gchar       *word)
+{
+  g_assert (self);
+  g_assert (word);
+
+  return g_hash_table_lookup (self->index, word);
+}
+
+IdeHighlightIndex *
+ide_highlight_index_ref (IdeHighlightIndex *self)
+{
+  g_assert (self);
+
+  return g_atomic_rc_box_acquire (self);
+}
+
+static void
+ide_highlight_index_finalize (IdeHighlightIndex *self)
+{
+  IDE_ENTRY;
+
+  g_clear_pointer (&self->strings, g_string_chunk_free);
+  g_clear_pointer (&self->index, g_hash_table_unref);
+
+  IDE_EXIT;
+}
+
+void
+ide_highlight_index_unref (IdeHighlightIndex *self)
+{
+  g_assert (self);
+
+  g_atomic_rc_box_release_full (self, (GDestroyNotify)ide_highlight_index_finalize);
+}
+
+void
+ide_highlight_index_dump (IdeHighlightIndex *self)
+{
+  g_autofree gchar *format = NULL;
+
+  g_assert (self);
+
+  format = g_format_size (self->chunk_size);
+  g_debug ("IdeHighlightIndex (%p) contains %u items and consumes %s.",
+           self, self->count, format);
+}
+
+/**
+ * ide_highlight_index_to_variant:
+ * @self: a #IdeHighlightIndex
+ *
+ * Creates a variant to represent the index. Useful to transport across IPC boundaries.
+ *
+ * Returns: (transfer full): a #GVariant
+ *
+ * Since: 3.32
+ */
+GVariant *
+ide_highlight_index_to_variant (IdeHighlightIndex *self)
+{
+  g_autoptr(GHashTable) arrays = NULL;
+  GHashTableIter iter;
+  const gchar *k, *v;
+  GPtrArray *ar;
+  GVariantDict dict;
+
+  g_return_val_if_fail (self != NULL, NULL);
+
+  arrays = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify)g_ptr_array_unref);
+
+  g_hash_table_iter_init (&iter, self->index);
+  while (g_hash_table_iter_next (&iter, (gpointer *)&k, (gpointer *)&v))
+    {
+      if G_UNLIKELY (!(ar = g_hash_table_lookup (arrays, v)))
+        {
+          ar = g_ptr_array_new ();
+          g_hash_table_insert (arrays, (gchar *)v, ar);
+        }
+
+      g_ptr_array_add (ar, (gchar *)k);
+    }
+
+  g_variant_dict_init (&dict, NULL);
+
+  g_hash_table_iter_init (&iter, arrays);
+  while (g_hash_table_iter_next (&iter, (gpointer *)&k, (gpointer *)&ar))
+    {
+      GVariant *keys;
+
+      g_ptr_array_add (ar, NULL);
+
+      keys = g_variant_new_strv ((const gchar * const *)ar->pdata, ar->len - 1);
+      g_variant_dict_insert_value (&dict, k, g_steal_pointer (&keys));
+    }
+
+  return g_variant_take_ref (g_variant_dict_end (&dict));
+}
diff --git a/src/libide/code/ide-highlight-index.h b/src/libide/code/ide-highlight-index.h
new file mode 100644
index 000000000..b5649cbbb
--- /dev/null
+++ b/src/libide/code/ide-highlight-index.h
@@ -0,0 +1,61 @@
+/* ide-highlight-index.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HIGHLIGHT_INDEX (ide_highlight_index_get_type())
+
+typedef struct _IdeHighlightIndex IdeHighlightIndex;
+
+IDE_AVAILABLE_IN_3_32
+GType              ide_highlight_index_get_type         (void);
+IDE_AVAILABLE_IN_3_32
+IdeHighlightIndex *ide_highlight_index_new              (void);
+IDE_AVAILABLE_IN_3_32
+IdeHighlightIndex *ide_highlight_index_new_from_variant (GVariant          *variant);
+IDE_AVAILABLE_IN_3_32
+IdeHighlightIndex *ide_highlight_index_ref              (IdeHighlightIndex *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_highlight_index_unref            (IdeHighlightIndex *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_highlight_index_insert           (IdeHighlightIndex *self,
+                                                         const gchar       *word,
+                                                         gpointer           tag);
+IDE_AVAILABLE_IN_3_32
+gpointer           ide_highlight_index_lookup           (IdeHighlightIndex *self,
+                                                         const gchar       *word);
+IDE_AVAILABLE_IN_3_32
+void               ide_highlight_index_dump             (IdeHighlightIndex *self);
+IDE_AVAILABLE_IN_3_32
+GVariant          *ide_highlight_index_to_variant       (IdeHighlightIndex *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeHighlightIndex, ide_highlight_index_unref)
+
+G_END_DECLS
diff --git a/src/libide/code/ide-highlighter.c b/src/libide/code/ide-highlighter.c
new file mode 100644
index 000000000..d156dc4da
--- /dev/null
+++ b/src/libide/code/ide-highlighter.c
@@ -0,0 +1,93 @@
+/* ide-highlighter.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-highlighter"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-highlighter.h"
+
+G_DEFINE_INTERFACE (IdeHighlighter, ide_highlighter, IDE_TYPE_OBJECT)
+
+static void
+ide_highlighter_real_update (IdeHighlighter       *self,
+                             IdeHighlightCallback  callback,
+                             const GtkTextIter    *range_begin,
+                             const GtkTextIter    *range_end,
+                             GtkTextIter          *location)
+{
+}
+
+static void
+ide_highlighter_real_set_engine (IdeHighlighter     *self,
+                                 IdeHighlightEngine *engine)
+{
+}
+
+static void
+ide_highlighter_default_init (IdeHighlighterInterface *iface)
+{
+  iface->update = ide_highlighter_real_update;
+  iface->set_engine = ide_highlighter_real_set_engine;
+}
+
+/**
+ * ide_highlighter_update:
+ * @self: an #IdeHighlighter.
+ * @callback: (scope call): A callback to apply a given style.
+ * @range_begin: The beginning of the range to update.
+ * @range_end: The end of the range to update.
+ * @location: (out): How far the highlighter got in the update.
+ *
+ * Incrementally processes more of the buffer for highlighting.  If @callback
+ * returns %IDE_HIGHLIGHT_STOP, then this vfunc should stop processing and
+ * return, having set @location to the current position of processing.
+ *
+ * If processing the entire range was successful, then @location should be set
+ * to @range_end.
+ *
+ * Since: 3.32
+ */
+void
+ide_highlighter_update (IdeHighlighter       *self,
+                        IdeHighlightCallback  callback,
+                        const GtkTextIter    *range_begin,
+                        const GtkTextIter    *range_end,
+                        GtkTextIter          *location)
+{
+  g_return_if_fail (IDE_IS_HIGHLIGHTER (self));
+  g_return_if_fail (callback != NULL);
+  g_return_if_fail (range_begin != NULL);
+  g_return_if_fail (range_end != NULL);
+  g_return_if_fail (location != NULL);
+
+  IDE_HIGHLIGHTER_GET_IFACE (self)->update (self, callback, range_begin, range_end, location);
+}
+
+void
+ide_highlighter_load (IdeHighlighter *self)
+{
+  g_return_if_fail (IDE_IS_HIGHLIGHTER (self));
+
+  if (IDE_HIGHLIGHTER_GET_IFACE (self)->load)
+    IDE_HIGHLIGHTER_GET_IFACE (self)->load (self);
+}
diff --git a/src/libide/code/ide-highlighter.h b/src/libide/code/ide-highlighter.h
new file mode 100644
index 000000000..06beabdb2
--- /dev/null
+++ b/src/libide/code/ide-highlighter.h
@@ -0,0 +1,91 @@
+/* ide-highlighter.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HIGHLIGHTER (ide_highlighter_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeHighlighter, ide_highlighter, IDE, HIGHLIGHTER, IdeObject)
+
+typedef enum
+{
+  IDE_HIGHLIGHT_STOP,
+  IDE_HIGHLIGHT_CONTINUE,
+} IdeHighlightResult;
+
+typedef IdeHighlightResult (*IdeHighlightCallback) (const GtkTextIter *begin,
+                                                    const GtkTextIter *end,
+                                                    const gchar       *style_name);
+
+struct _IdeHighlighterInterface
+{
+  GTypeInterface parent_interface;
+
+  /**
+   * IdeHighlighter::update:
+   *
+   * #IdeHighlighter should call callback() with the range and style-name of
+   * the style to apply. Callback will ensure that the style exists and style
+   * it appropriately based on the style scheme.
+   *
+   * If @callback returns %IDE_HIGHLIGHT_STOP, the caller has run out of its
+   * time slice and should yield back to the highlight engine.
+   *
+   * @location should be set to the position that the highlighter got to
+   * before yielding back to the engine.
+   *
+   * Since: 3.32
+   */
+  void (*update)     (IdeHighlighter       *self,
+                      IdeHighlightCallback  callback,
+                      const GtkTextIter    *range_begin,
+                      const GtkTextIter    *range_end,
+                      GtkTextIter          *location);
+
+  void (*set_engine) (IdeHighlighter       *self,
+                      IdeHighlightEngine   *engine);
+
+  void (*load)       (IdeHighlighter       *self);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_highlighter_load                    (IdeHighlighter       *self);
+IDE_AVAILABLE_IN_3_32
+void ide_highlighter_update                  (IdeHighlighter       *self,
+                                              IdeHighlightCallback  callback,
+                                              const GtkTextIter    *range_begin,
+                                              const GtkTextIter    *range_end,
+                                              GtkTextIter          *location);
+void _ide_highlighter_set_highlighter_engine (IdeHighlighter       *self,
+                                              IdeHighlightEngine   *highlight_engine) G_GNUC_INTERNAL;
+
+G_END_DECLS
diff --git a/src/libide/code/ide-indent-style.h b/src/libide/code/ide-indent-style.h
new file mode 100644
index 000000000..c0129332d
--- /dev/null
+++ b/src/libide/code/ide-indent-style.h
@@ -0,0 +1,37 @@
+/* ide-indent-style.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_INDENT_STYLE_SPACES = 1,
+  IDE_INDENT_STYLE_TABS   = 2,
+} IdeIndentStyle;
+
+G_END_DECLS
diff --git a/src/libide/code/ide-language-defaults.c b/src/libide/code/ide-language-defaults.c
new file mode 100644
index 000000000..373446abd
--- /dev/null
+++ b/src/libide/code/ide-language-defaults.c
@@ -0,0 +1,461 @@
+/* ide-language-defaults.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-language-defaults"
+
+#include "config.h"
+
+#include <errno.h>
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+
+#include "ide-language-defaults.h"
+
+#define SCHEMA_ID "org.gnome.builder.editor.language"
+#define PATH_BASE "/org/gnome/builder/editor/language/"
+
+static gboolean  initialized;
+static gboolean  initializing;
+static GList    *tasks;
+
+G_LOCK_DEFINE (lock);
+
+static gboolean
+strv_equal (gchar **a,
+            gchar **b)
+{
+  if (a == b)
+    return TRUE;
+  else if (!a && b)
+    return FALSE;
+  else if (a && !b)
+    return FALSE;
+
+  for (;;)
+    {
+      if (*a == NULL && *b == NULL)
+        return TRUE;
+
+      if (*a == NULL || *b == NULL)
+        return FALSE;
+
+      if (g_strcmp0 (*a, *b) == 0)
+        continue;
+
+      return FALSE;
+    }
+}
+
+static gboolean
+ide_language_defaults_migrate (GKeyFile  *key_file,
+                               gint       current_version,
+                               gint       new_version,
+                               GError   **error)
+{
+  gchar **groups;
+  gsize n_groups = 0;
+
+  g_assert (key_file);
+  g_assert (current_version >= 0);
+  g_assert (current_version >= 0);
+  g_assert (new_version > current_version);
+
+  groups = g_key_file_get_groups (key_file, &n_groups);
+
+  for (gsize i = 0; i < n_groups; i++)
+    {
+      const gchar *group = groups [i];
+      g_autoptr(GSettings) settings = NULL;
+      g_autofree gchar *lang_path = NULL;
+      gchar **keys;
+      gsize n_keys = 0;
+
+      g_assert (group != NULL);
+
+      if (g_str_equal (group, "global"))
+        continue;
+
+      lang_path = g_strdup_printf (PATH_BASE"%s/", group);
+      g_assert (lang_path);
+
+      settings = g_settings_new_with_path (SCHEMA_ID, lang_path);
+      g_assert (G_IS_SETTINGS (settings));
+
+      keys = g_key_file_get_keys (key_file, group, &n_keys, NULL);
+      g_assert (keys);
+
+      for (gsize j = 0; j < n_keys; j++)
+        {
+          const gchar *key = keys [j];
+          g_autoptr(GVariant) default_value = NULL;
+
+          g_assert (key);
+
+          default_value = g_settings_get_default_value (settings, key);
+          g_assert (default_value);
+
+          /*
+           * For all of the variant types we support, check to see if the value
+           * is matching the default schema value. If so, update the key to the
+           * new override value.
+           *
+           * This will not overwrite any change settings for files that the
+           * user has previously loaded, but will for others. Overriding things
+           * we have overriden gets pretty nasty, since we change things out
+           * from under the user.
+           *
+           * That may change in the future, but not today.
+           */
+
+          if (g_variant_is_of_type (default_value, G_VARIANT_TYPE_STRING))
+            {
+              g_autofree gchar *current_str = NULL;
+              g_autofree gchar *override_str = NULL;
+              const gchar *default_str;
+
+              default_str = g_variant_get_string (default_value, NULL);
+              current_str = g_settings_get_string (settings, key);
+              override_str = g_key_file_get_string (key_file, group, key, NULL);
+
+              if (g_strcmp0 (default_str, current_str) == 0)
+                g_settings_set_string (settings, key, override_str);
+            }
+          else if (g_variant_is_of_type (default_value, G_VARIANT_TYPE_BOOLEAN))
+            {
+              gboolean current_bool;
+              gboolean override_bool;
+              gboolean default_bool;
+
+              default_bool = g_variant_get_boolean (default_value);
+              current_bool = g_settings_get_boolean (settings, key);
+              override_bool = g_key_file_get_boolean (key_file, group, key, NULL);
+
+              if (default_bool == current_bool)
+                g_settings_set_boolean (settings, key, override_bool);
+            }
+          else if (g_variant_is_of_type (default_value, G_VARIANT_TYPE_INT32))
+            {
+              gint32 current_int32;
+              gint32 override_int32;
+              gint32 default_int32;
+
+              default_int32 = g_variant_get_int32 (default_value);
+              current_int32 = g_settings_get_int (settings, key);
+              override_int32 = g_key_file_get_integer (key_file, group, key, NULL);
+
+              if (default_int32 == current_int32)
+                g_settings_set_int (settings, key, override_int32);
+            }
+          else if (g_variant_is_of_type (default_value, G_VARIANT_TYPE_STRING_ARRAY))
+            {
+              g_auto(GStrv) current_strv = NULL;
+              g_auto(GStrv) override_strv = NULL;
+              g_autofree const gchar **default_strv = NULL;
+
+              default_strv = g_variant_get_strv (default_value, NULL);
+              current_strv = g_settings_get_strv (settings, key);
+              override_strv = g_key_file_get_string_list (key_file, group, key, NULL, NULL);
+
+              if (strv_equal ((gchar **)default_strv, current_strv))
+                g_settings_set_strv (settings, key, (const gchar * const *)override_strv);
+            }
+          else
+            {
+              g_error ("Teach me about variant type: %s",
+                       g_variant_get_type_string (default_value));
+              g_assert_not_reached ();
+            }
+        }
+    }
+
+  return TRUE;
+}
+
+static gint
+ide_language_defaults_get_current_version (const gchar  *path,
+                                           GError      **error)
+{
+  GError *local_error = NULL;
+  g_autofree gchar *contents = NULL;
+  gsize length = 0;
+  gint64 version;
+
+  if (!g_file_get_contents (path, &contents, &length, &local_error))
+    {
+      if (g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+        {
+          g_clear_error (&local_error);
+          return 0;
+        }
+      else
+        {
+          g_propagate_error (error, local_error);
+          return -1;
+        }
+    }
+
+  if (!g_str_is_ascii (contents))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   _("%s contained invalid ASCII"),
+                   path);
+      return -1;
+    }
+
+  if ((length == 0) || (contents [0] == '\0'))
+    return 0;
+
+  version = g_ascii_strtoll (contents, NULL, 10);
+
+  if ((version < 0) || (version >= G_MAXINT))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   _("Failed to parse integer from “%s”"),
+                   path);
+      return -1;
+    }
+
+  return version;
+}
+
+static GBytes *
+ide_language_defaults_get_defaults (GError **error)
+{
+  return g_resources_lookup_data ("/org/gnome/builder/file-settings/defaults.ini",
+                                  G_RESOURCE_LOOKUP_FLAGS_NONE, error);
+}
+
+static void
+ide_language_defaults_init_worker (IdeTask      *task,
+                                   gpointer      source_object,
+                                   gpointer      task_data,
+                                   GCancellable *cancellable)
+{
+  g_autofree gchar *version_path = NULL;
+  g_autofree gchar *version_contents = NULL;
+  g_autofree gchar *version_dir = NULL;
+  g_autoptr(GBytes) defaults = NULL;
+  g_autoptr(GKeyFile) key_file = NULL;
+  g_autoptr(GError) error = NULL;
+  gint global_version;
+  gint current_version;
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (source_object == NULL);
+  g_assert (task_data == NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  version_path = g_build_filename (g_get_user_config_dir (),
+                                   ide_get_program_name (),
+                                   "syntax",
+                                   ".defaults",
+                                   NULL);
+  current_version = ide_language_defaults_get_current_version (version_path, &error);
+
+  if (current_version < 0)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_GOTO (failure);
+    }
+
+  IDE_TRACE_MSG ("Current language defaults at version %d", current_version);
+
+  defaults = ide_language_defaults_get_defaults (&error);
+  g_assert (defaults != NULL);
+
+  key_file = g_key_file_new ();
+  ret = g_key_file_load_from_data (key_file,
+                                   g_bytes_get_data (defaults, NULL),
+                                   g_bytes_get_size (defaults),
+                                   G_KEY_FILE_NONE,
+                                   &error);
+
+  if (!ret)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_GOTO (failure);
+    }
+
+  if (!g_key_file_has_group (key_file, "global") ||
+      !g_key_file_has_key (key_file, "global", "version", NULL))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_DATA,
+                                 _("language defaults missing version in [global] group."));
+      IDE_GOTO (failure);
+    }
+
+  global_version = g_key_file_get_integer (key_file, "global", "version", &error);
+
+  if (global_version == 0 && error != NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_GOTO (failure);
+    }
+
+  g_clear_error (&error);
+
+  if (global_version > current_version)
+    {
+      if (!ide_language_defaults_migrate (key_file, current_version, global_version, &error))
+        {
+          ide_task_return_error (task, g_steal_pointer (&error));
+          IDE_GOTO (failure);
+        }
+
+      version_contents = g_strdup_printf ("%d", global_version);
+
+      version_dir = g_path_get_dirname (version_path);
+
+      if (!g_file_test (version_dir, G_FILE_TEST_IS_DIR))
+        {
+          if (g_mkdir_with_parents (version_dir, 0750) == -1)
+            {
+              ide_task_return_new_error (task,
+                                         G_IO_ERROR,
+                                         g_io_error_from_errno (errno),
+                                         "%s", g_strerror (errno));
+              IDE_GOTO (failure);
+            }
+        }
+
+      IDE_TRACE_MSG ("Writing new language defaults version to \"%s\"", version_path);
+
+      if (!g_file_set_contents (version_path, version_contents, -1, &error))
+        {
+          ide_task_return_error (task, g_steal_pointer (&error));
+          IDE_GOTO (failure);
+        }
+    }
+
+  ide_task_return_boolean (task, TRUE);
+
+  {
+    GList *list;
+    GList *iter;
+
+    G_LOCK (lock);
+
+    initializing = FALSE;
+    initialized = TRUE;
+
+    list = tasks;
+    tasks = NULL;
+
+    G_UNLOCK (lock);
+
+    for (iter = list; iter; iter = iter->next)
+      {
+        ide_task_return_boolean (iter->data, TRUE);
+        g_object_unref (iter->data);
+      }
+
+    g_list_free (list);
+  }
+
+  IDE_EXIT;
+
+failure:
+  {
+    GList *list;
+    GList *iter;
+
+    G_LOCK (lock);
+
+    initializing = FALSE;
+    initialized = TRUE;
+
+    list = tasks;
+    tasks = NULL;
+
+    G_UNLOCK (lock);
+
+    for (iter = list; iter; iter = iter->next)
+      {
+        ide_task_return_new_error (iter->data,
+                                   G_IO_ERROR,
+                                   G_IO_ERROR_FAILED,
+                                   _("Failed to initialize defaults."));
+        g_object_unref (iter->data);
+      }
+
+    g_list_free (list);
+  }
+
+  IDE_EXIT;
+}
+
+void
+ide_language_defaults_init_async (GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (NULL, cancellable, callback, user_data);
+
+  G_LOCK (lock);
+
+  if (initialized)
+    {
+      ide_task_return_boolean (task, TRUE);
+    }
+  else if (initializing)
+    {
+      tasks = g_list_prepend (tasks, g_object_ref (task));
+    }
+  else
+    {
+      initializing = TRUE;
+      ide_task_run_in_thread (task, ide_language_defaults_init_worker);
+    }
+
+  G_UNLOCK (lock);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_language_defaults_init_finish (GAsyncResult  *result,
+                                   GError       **error)
+{
+  IdeTask *task = (IdeTask *)result;
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TASK (task), FALSE);
+
+  ret = ide_task_propagate_boolean (task, error);
+
+  IDE_RETURN (ret);
+}
diff --git a/src/libide/code/ide-language-defaults.h b/src/libide/code/ide-language-defaults.h
new file mode 100644
index 000000000..5b6c3473a
--- /dev/null
+++ b/src/libide/code/ide-language-defaults.h
@@ -0,0 +1,33 @@
+/* ide-language-defaults.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+void     ide_language_defaults_init_async  (GCancellable         *cancellable,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
+gboolean ide_language_defaults_init_finish (GAsyncResult         *result,
+                                            GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-language.c b/src/libide/code/ide-language.c
new file mode 100644
index 000000000..b0aabe392
--- /dev/null
+++ b/src/libide/code/ide-language.c
@@ -0,0 +1,109 @@
+/* ide-language.c
+ *
+ * Copyright 2016-2019 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 "ide-language"
+
+#include "config.h"
+
+#include <libide-io.h>
+#include <string.h>
+#include <tmpl-glib.h>
+
+#include "ide-language.h"
+
+gchar *
+ide_language_format_header (GtkSourceLanguage *self,
+                            const gchar       *header)
+{
+  IdeLineReader reader;
+  const gchar *first_prefix;
+  const gchar *last_prefix;
+  const gchar *line_prefix;
+  const gchar *line;
+  gboolean first = TRUE;
+  GString *outstr;
+  gsize len;
+  guint prefix_len;
+
+  g_return_val_if_fail (GTK_SOURCE_IS_LANGUAGE (self), NULL);
+  g_return_val_if_fail (header != NULL, NULL);
+
+  first_prefix = gtk_source_language_get_metadata (self, "block-comment-start");
+  last_prefix = gtk_source_language_get_metadata (self, "block-comment-end");
+  line_prefix = gtk_source_language_get_metadata (self, "line-comment-start");
+
+  if ((g_strcmp0 (first_prefix, "/*") == 0) &&
+      (g_strcmp0 (last_prefix, "*/") == 0))
+    line_prefix = " *";
+
+  if (first_prefix == NULL || last_prefix == NULL)
+    {
+      first_prefix = line_prefix;
+      last_prefix = line_prefix;
+    }
+
+  prefix_len = strlen (first_prefix);
+
+  outstr = g_string_new (NULL);
+
+  ide_line_reader_init (&reader, (gchar *)header, -1);
+
+  while (NULL != (line = ide_line_reader_next (&reader, &len)))
+    {
+      if (first)
+        {
+          g_string_append (outstr, first_prefix);
+          first = FALSE;
+        }
+      else if (line_prefix == NULL)
+        {
+          for (guint i = 0; i < prefix_len; i++)
+            g_string_append_c (outstr, ' ');
+        }
+      else
+        {
+          g_string_append (outstr, line_prefix);
+        }
+
+      if (len)
+        {
+          g_string_append_c (outstr, ' ');
+          g_string_append_len (outstr, line, len);
+        }
+
+      /* Lines ending in expansion need an extra \n */
+      if (outstr->len > 2 &&
+          outstr->str[outstr->len - 2] == '}' &&
+          outstr->str[outstr->len - 1] == '}')
+        g_string_append_c (outstr, '\n');
+
+      g_string_append_c (outstr, '\n');
+    }
+
+  if (last_prefix && g_strcmp0 (first_prefix, last_prefix) != 0)
+    {
+      if (line_prefix && *line_prefix == ' ')
+        g_string_append_c (outstr, ' ');
+      g_string_append (outstr, last_prefix);
+      g_string_append_c (outstr, '\n');
+    }
+
+  return g_string_free (outstr, FALSE);
+}
diff --git a/src/libide/code/ide-language.h b/src/libide/code/ide-language.h
new file mode 100644
index 000000000..9af163164
--- /dev/null
+++ b/src/libide/code/ide-language.h
@@ -0,0 +1,36 @@
+/* ide-language.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <gtksourceview/gtksource.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+gchar *ide_language_format_header (GtkSourceLanguage *language,
+                                   const gchar       *header);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-location.c b/src/libide/code/ide-location.c
new file mode 100644
index 000000000..27c903af0
--- /dev/null
+++ b/src/libide/code/ide-location.c
@@ -0,0 +1,503 @@
+/* ide-location.c
+ *
+ * Copyright 2018-2019 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 "ide-location"
+
+#include "config.h"
+
+#include "ide-location.h"
+
+typedef struct
+{
+  GFile *file;
+  gint   line;
+  gint   line_offset;
+  gint   offset;
+} IdeLocationPrivate;
+
+enum {
+  PROP_0,
+  PROP_FILE,
+  PROP_LINE,
+  PROP_LINE_OFFSET,
+  PROP_OFFSET,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeLocation, ide_location, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_location_set_file (IdeLocation *self,
+                       GFile       *file)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_assert (IDE_IS_LOCATION (self));
+
+  g_set_object (&priv->file, file);
+}
+
+static void
+ide_location_set_line (IdeLocation *self,
+                       gint         line)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_assert (IDE_IS_LOCATION (self));
+
+  priv->line = CLAMP (line, -1, G_MAXINT);
+}
+
+static void
+ide_location_set_line_offset (IdeLocation *self,
+                              gint         line_offset)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_assert (IDE_IS_LOCATION (self));
+
+  priv->line_offset = CLAMP (line_offset, -1, G_MAXINT);
+}
+
+static void
+ide_location_set_offset (IdeLocation *self,
+                         gint         offset)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_assert (IDE_IS_LOCATION (self));
+
+  priv->offset = CLAMP (offset, -1, G_MAXINT);
+}
+
+static void
+ide_location_dispose (GObject *object)
+{
+  IdeLocation *self = (IdeLocation *)object;
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_clear_object (&priv->file);
+
+  G_OBJECT_CLASS (ide_location_parent_class)->dispose (object);
+}
+
+static void
+ide_location_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  IdeLocation *self = IDE_LOCATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      g_value_set_object (value, ide_location_get_file (self));
+      break;
+
+    case PROP_LINE:
+      g_value_set_int (value, ide_location_get_line (self));
+      break;
+
+    case PROP_LINE_OFFSET:
+      g_value_set_int (value, ide_location_get_line_offset (self));
+      break;
+
+    case PROP_OFFSET:
+      g_value_set_int (value, ide_location_get_offset (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_location_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  IdeLocation *self = IDE_LOCATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      ide_location_set_file (self, g_value_get_object (value));
+      break;
+
+    case PROP_LINE:
+      ide_location_set_line (self, g_value_get_int (value));
+      break;
+
+    case PROP_LINE_OFFSET:
+      ide_location_set_line_offset (self, g_value_get_int (value));
+      break;
+
+    case PROP_OFFSET:
+      ide_location_set_offset (self, g_value_get_int (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_location_class_init (IdeLocationClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_location_dispose;
+  object_class->get_property = ide_location_get_property;
+  object_class->set_property = ide_location_set_property;
+
+  properties [PROP_FILE] =
+    g_param_spec_object ("file",
+                         "File",
+                         "The file representing the location",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LINE] =
+    g_param_spec_int ("line",
+                      "Line",
+                      "The line number within the file, starting from 0 or -1 for unknown",
+                      -1, G_MAXINT, -1,
+                      (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LINE_OFFSET] =
+    g_param_spec_int ("line-offset",
+                      "Line Offset",
+                      "The offset within the line, starting from 0 or -1 for unknown",
+                      -1, G_MAXINT, -1,
+                      (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_OFFSET] =
+    g_param_spec_int ("offset",
+                      "Offset",
+                      "The offset within the file in characters, or -1 if unknown",
+                      -1, G_MAXINT, -1,
+                      (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_location_init (IdeLocation *self)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  priv->line = -1;
+  priv->line_offset = -1;
+  priv->offset = -1;
+}
+
+/**
+ * ide_location_get_file:
+ * @self: a #IdeLocation
+ *
+ * Gets the file within the location.
+ *
+ * Returns: (transfer none) (nullable): a #GFile or %NULL
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_location_get_file (IdeLocation *self)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LOCATION (self), NULL);
+
+  return priv->file;
+}
+
+/**
+ * ide_location_get_line:
+ * @self: a #IdeLocation
+ *
+ * Gets the line within the #IdeLocation:file, or -1 if it is unknown.
+ *
+ * Returns: the line number, or -1.
+ *
+ * Since: 3.32
+ */
+gint
+ide_location_get_line (IdeLocation *self)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LOCATION (self), -1);
+
+  return priv->line;
+}
+
+/**
+ * ide_location_get_line_offset:
+ * @self: a #IdeLocation
+ *
+ * Gets the offset within the #IdeLocation:line, or -1 if it is unknown.
+ *
+ * Returns: the line offset, or -1.
+ *
+ * Since: 3.32
+ */
+gint
+ide_location_get_line_offset (IdeLocation *self)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LOCATION (self), -1);
+
+  return priv->line_offset;
+}
+
+/**
+ * ide_location_get_offset:
+ * @self: a #IdeLocation
+ *
+ * Gets the offset within the file in characters, or -1 if it is unknown.
+ *
+ * Returns: the line offset, or -1.
+ *
+ * Since: 3.32
+ */
+gint
+ide_location_get_offset (IdeLocation *self)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LOCATION (self), -1);
+
+  return priv->offset;
+}
+
+/**
+ * ide_location_dup:
+ * @self: a #IdeLocation
+ *
+ * Makes a deep copy of @self.
+ *
+ * Returns: (transfer full): a new #IdeLocation
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_location_dup (IdeLocation *self)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_return_val_if_fail (!self || IDE_IS_LOCATION (self), NULL);
+
+  if (self == NULL)
+    return NULL;
+
+  return g_object_new (IDE_TYPE_LOCATION,
+                       "file", priv->file,
+                       "line", priv->line,
+                       "line-offset", priv->line_offset,
+                       "offset", priv->offset,
+                       NULL);
+}
+
+/**
+ * ide_location_to_variant:
+ * @self: a #IdeLocation
+ *
+ * Serializes the location into a variant that can be used to transport
+ * across IPC boundaries.
+ *
+ * This function will never return a variant with a floating reference.
+ *
+ * Returns: (transfer full): a #GVariant
+ *
+ * Since: 3.32
+ */
+GVariant *
+ide_location_to_variant (IdeLocation *self)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+  g_autofree gchar *uri = NULL;
+  GVariantDict dict;
+
+  g_return_val_if_fail (self != NULL, NULL);
+
+  g_variant_dict_init (&dict, NULL);
+
+  uri = g_file_get_uri (priv->file);
+
+  g_variant_dict_insert (&dict, "uri", "s", uri);
+  g_variant_dict_insert (&dict, "line", "i", priv->line);
+  g_variant_dict_insert (&dict, "line-offset", "i", priv->line_offset);
+
+  return g_variant_take_ref (g_variant_dict_end (&dict));
+}
+
+IdeLocation *
+ide_location_new (GFile *file,
+                  gint   line,
+                  gint   line_offset)
+{
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  line = CLAMP (line, -1, G_MAXINT);
+  line_offset = CLAMP (line_offset, -1, G_MAXINT);
+
+  return g_object_new (IDE_TYPE_LOCATION,
+                       "file", file,
+                       "line", line,
+                       "line-offset", line_offset,
+                       NULL);
+}
+
+/**
+ * ide_location_new_with_offset:
+ * @file: a #GFile
+ * @line: a line number starting from 0, or -1 if unknown
+ * @line_offset: a line offset starting from 0, or -1 if unknown
+ * @offset: a charcter offset in file starting from 0, or -1 if unknown
+ *
+ * Returns: (transfer full): an #IdeLocation
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_location_new_with_offset (GFile *file,
+                              gint   line,
+                              gint   line_offset,
+                              gint   offset)
+{
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  line = CLAMP (line, -1, G_MAXINT);
+  line_offset = CLAMP (line_offset, -1, G_MAXINT);
+  offset = CLAMP (offset, -1, G_MAXINT);
+
+  return g_object_new (IDE_TYPE_LOCATION,
+                       "file", file,
+                       "line", line,
+                       "line-offset", line_offset,
+                       "offset", offset,
+                       NULL);
+}
+
+/**
+ * ide_location_new_from_variant:
+ * @variant: (nullable): a #GVariant or %NULL
+ *
+ * Creates a new #IdeLocation using the serialized form from a
+ * previously serialized #GVariant.
+ *
+ * As a convenience, if @variant is %NULL, %NULL is returned.
+ *
+ * See also: ide_location_to_variant()
+ *
+ * Returns: (transfer full) (nullable): a #GVariant if succesful;
+ *   otherwise %NULL.
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_location_new_from_variant (GVariant *variant)
+{
+  g_autoptr(GVariant) unboxed = NULL;
+  g_autoptr(GFile) file = NULL;
+  IdeLocation *self = NULL;
+  GVariantDict dict;
+  const gchar *uri;
+  guint32 line;
+  guint32 line_offset;
+
+  if (variant == NULL)
+    return NULL;
+
+  if (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARIANT))
+    variant = unboxed = g_variant_get_variant (variant);
+
+  g_variant_dict_init (&dict, variant);
+
+  if (!g_variant_dict_lookup (&dict, "uri", "&s", &uri))
+    goto failure;
+
+  if (!g_variant_dict_lookup (&dict, "line", "i", &line))
+    line = 0;
+
+  if (!g_variant_dict_lookup (&dict, "line-offset", "i", &line_offset))
+    line_offset = 0;
+
+  file = g_file_new_for_uri (uri);
+
+  self = ide_location_new (file, line, line_offset);
+
+failure:
+  g_variant_dict_clear (&dict);
+
+  return self;
+}
+
+static gint
+file_compare (GFile *a,
+              GFile *b)
+{
+  g_autofree gchar *uri_a = g_file_get_uri (a);
+  g_autofree gchar *uri_b = g_file_get_uri (b);
+
+  return g_strcmp0 (uri_a, uri_b);
+}
+
+gboolean
+ide_location_compare (IdeLocation *a,
+                      IdeLocation *b)
+{
+  IdeLocationPrivate *priv_a = ide_location_get_instance_private (a);
+  IdeLocationPrivate *priv_b = ide_location_get_instance_private (b);
+  gint ret;
+
+  g_assert (IDE_IS_LOCATION (a));
+  g_assert (IDE_IS_LOCATION (b));
+
+  if (priv_a->file && priv_b->file)
+    {
+      if (0 != (ret = file_compare (priv_a->file, priv_b->file)))
+        return ret;
+    }
+  else if (priv_a->file)
+    return -1;
+  else if (priv_b->file)
+    return 1;
+
+  if (0 != (ret = priv_a->line - priv_b->line))
+    return ret;
+
+  return priv_a->line_offset - priv_b->line_offset;
+}
+
+guint
+ide_location_hash (IdeLocation *self)
+{
+  IdeLocationPrivate *priv = ide_location_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LOCATION (self), 0);
+
+  return g_file_hash (priv->file) ^ g_int_hash (&priv->line) ^ g_int_hash (&priv->line_offset);
+}
diff --git a/src/libide/code/ide-location.h b/src/libide/code/ide-location.h
new file mode 100644
index 000000000..1820a8027
--- /dev/null
+++ b/src/libide/code/ide-location.h
@@ -0,0 +1,75 @@
+/* ide-location.h
+ *
+ * Copyright 2014-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LOCATION (ide_location_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeLocation, ide_location, IDE, LOCATION, GObject)
+
+struct _IdeLocationClass
+{
+  GObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeLocation *ide_location_new_from_variant (GVariant    *variant);
+IDE_AVAILABLE_IN_3_32
+IdeLocation *ide_location_new              (GFile       *file,
+                                            gint         line,
+                                            gint         line_offset);
+IDE_AVAILABLE_IN_3_32
+IdeLocation *ide_location_new_with_offset  (GFile       *file,
+                                            gint         line,
+                                            gint         line_offset,
+                                            gint         offset);
+IDE_AVAILABLE_IN_3_32
+IdeLocation *ide_location_dup              (IdeLocation *self);
+IDE_AVAILABLE_IN_3_32
+gint         ide_location_get_line         (IdeLocation *self);
+IDE_AVAILABLE_IN_3_32
+gint         ide_location_get_line_offset  (IdeLocation *self);
+IDE_AVAILABLE_IN_3_32
+gint         ide_location_get_offset       (IdeLocation *self);
+IDE_AVAILABLE_IN_3_32
+GFile       *ide_location_get_file         (IdeLocation *self);
+IDE_AVAILABLE_IN_3_32
+GVariant    *ide_location_to_variant       (IdeLocation *self);
+IDE_AVAILABLE_IN_3_32
+gboolean     ide_location_compare          (IdeLocation *a,
+                                            IdeLocation *b);
+IDE_AVAILABLE_IN_3_32
+guint        ide_location_hash             (IdeLocation *self);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-range.c b/src/libide/code/ide-range.c
new file mode 100644
index 000000000..e805f2e12
--- /dev/null
+++ b/src/libide/code/ide-range.c
@@ -0,0 +1,290 @@
+/* ide-range.c
+ *
+ * Copyright 2018-2019 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 "ide-range"
+
+#include "config.h"
+
+#include "ide-location.h"
+#include "ide-range.h"
+
+typedef struct
+{
+  IdeLocation *begin;
+  IdeLocation *end;
+} IdeRangePrivate;
+
+enum {
+  PROP_0,
+  PROP_BEGIN,
+  PROP_END,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeRange, ide_range, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_range_set_begin (IdeRange    *self,
+                     IdeLocation *location)
+{
+  IdeRangePrivate *priv = ide_range_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RANGE (self));
+  g_return_if_fail (IDE_IS_LOCATION (location));
+
+  g_set_object (&priv->begin, location);
+}
+
+static void
+ide_range_set_end (IdeRange    *self,
+                   IdeLocation *location)
+{
+  IdeRangePrivate *priv = ide_range_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RANGE (self));
+  g_return_if_fail (IDE_IS_LOCATION (location));
+
+  g_set_object (&priv->end, location);
+}
+
+static void
+ide_range_finalize (GObject *object)
+{
+  IdeRange *self = (IdeRange *)object;
+  IdeRangePrivate *priv = ide_range_get_instance_private (self);
+
+  g_clear_object (&priv->begin);
+  g_clear_object (&priv->end);
+
+  G_OBJECT_CLASS (ide_range_parent_class)->finalize (object);
+}
+
+static void
+ide_range_get_property (GObject    *object,
+                        guint       prop_id,
+                        GValue     *value,
+                        GParamSpec *pspec)
+{
+  IdeRange *self = IDE_RANGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_BEGIN:
+      g_value_set_object (value, ide_range_get_begin (self));
+      break;
+
+    case PROP_END:
+      g_value_set_object (value, ide_range_get_end (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_range_set_property (GObject      *object,
+                        guint         prop_id,
+                        const GValue *value,
+                        GParamSpec   *pspec)
+{
+  IdeRange *self = IDE_RANGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_BEGIN:
+      ide_range_set_begin (self, g_value_get_object (value));
+      break;
+
+    case PROP_END:
+      ide_range_set_end (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_range_class_init (IdeRangeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_range_finalize;
+  object_class->get_property = ide_range_get_property;
+  object_class->set_property = ide_range_set_property;
+
+  properties [PROP_BEGIN] =
+    g_param_spec_object ("begin",
+                         "Begin",
+                         "The start of the range",
+                         IDE_TYPE_LOCATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_END] =
+    g_param_spec_object ("end",
+                         "End",
+                         "The end of the range",
+                         IDE_TYPE_LOCATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_range_init (IdeRange *self)
+{
+}
+
+IdeRange *
+ide_range_new (IdeLocation *begin,
+               IdeLocation *end)
+{
+  g_return_val_if_fail (IDE_IS_LOCATION (begin), NULL);
+  g_return_val_if_fail (IDE_IS_LOCATION (end), NULL);
+
+  return g_object_new (IDE_TYPE_RANGE,
+                       "begin", begin,
+                       "end", end,
+                       NULL);
+}
+
+/**
+ * ide_range_get_begin:
+ * @self: a #IdeRange
+ *
+ * Returns: (transfer none): the beginning of the range
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_range_get_begin (IdeRange *self)
+{
+  IdeRangePrivate *priv = ide_range_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RANGE (self), NULL);
+
+  return priv->begin;
+}
+
+/**
+ * ide_range_get_end:
+ * @self: a #IdeRange
+ *
+ * Returns: (transfer none): the end of the range
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_range_get_end (IdeRange *self)
+{
+  IdeRangePrivate *priv = ide_range_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RANGE (self), NULL);
+
+  return priv->end;
+}
+
+/**
+ * ide_range_to_variant:
+ * @self: a #IdeRange
+ *
+ * Creates a variant to represent the range.
+ *
+ * This function will never return a floating variant.
+ *
+ * Returns: (transfer full): a #GVariant
+ *
+ * Since: 3.32
+ */
+GVariant *
+ide_range_to_variant (IdeRange *self)
+{
+  IdeRangePrivate *priv = ide_range_get_instance_private (self);
+  GVariantDict dict;
+
+  g_return_val_if_fail (IDE_IS_RANGE (self), NULL);
+
+  g_variant_dict_init (&dict, NULL);
+
+  if (priv->begin)
+    {
+      g_autoptr(GVariant) begin = NULL;
+
+      if ((begin = ide_location_to_variant (priv->begin)))
+        g_variant_dict_insert_value (&dict, "begin", begin);
+    }
+
+  if (priv->end)
+    {
+      g_autoptr(GVariant) end = NULL;
+
+      if ((end = ide_location_to_variant (priv->end)))
+        g_variant_dict_insert_value (&dict, "end", end);
+    }
+
+  return g_variant_take_ref (g_variant_dict_end (&dict));
+}
+
+/**
+ * ide_range_new_from_variant:
+ * @variant: a #GVariant
+ *
+ * Returns: (transfer full) (nullable): a new range or %NULL
+ *
+ * Since: 3.32
+ */
+IdeRange *
+ide_range_new_from_variant (GVariant *variant)
+{
+  g_autoptr(GVariant) unboxed = NULL;
+  g_autoptr(GVariant) vbegin = NULL;
+  g_autoptr(GVariant) vend = NULL;
+  g_autoptr(IdeLocation) begin = NULL;
+  g_autoptr(IdeLocation) end = NULL;
+  IdeRange *self = NULL;
+  GVariantDict dict;
+
+  if (variant == NULL)
+    return NULL;
+
+  if (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARIANT))
+    variant = unboxed = g_variant_get_variant (variant);
+
+  g_variant_dict_init (&dict, variant);
+
+  if (!(vbegin = g_variant_dict_lookup_value (&dict, "begin", NULL)) ||
+      !(begin = ide_location_new_from_variant (vbegin)))
+    goto failure;
+
+  if (!(vend = g_variant_dict_lookup_value (&dict, "end", NULL)) ||
+      !(end = ide_location_new_from_variant (vend)))
+    goto failure;
+
+  self = ide_range_new (begin, end);
+
+  g_variant_dict_clear (&dict);
+
+failure:
+
+  return self;
+}
diff --git a/src/libide/code/ide-range.h b/src/libide/code/ide-range.h
new file mode 100644
index 000000000..f1e855c1d
--- /dev/null
+++ b/src/libide/code/ide-range.h
@@ -0,0 +1,58 @@
+/* ide-range.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RANGE (ide_range_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeRange, ide_range, IDE, RANGE, GObject)
+
+struct _IdeRangeClass
+{
+  GObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeRange    *ide_range_new_from_variant (GVariant    *variant);
+IDE_AVAILABLE_IN_3_32
+IdeRange    *ide_range_new              (IdeLocation *begin,
+                                         IdeLocation *end);
+IDE_AVAILABLE_IN_3_32
+IdeLocation *ide_range_get_begin        (IdeRange    *self);
+IDE_AVAILABLE_IN_3_32
+IdeLocation *ide_range_get_end          (IdeRange    *self);
+IDE_AVAILABLE_IN_3_32
+GVariant    *ide_range_to_variant       (IdeRange    *self);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-rename-provider.c b/src/libide/code/ide-rename-provider.c
new file mode 100644
index 000000000..9f9cff717
--- /dev/null
+++ b/src/libide/code/ide-rename-provider.c
@@ -0,0 +1,162 @@
+/* ide-rename-provider.c
+ *
+ * Copyright 2016-2019 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 "ide-rename-provider.h"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-rename-provider.h"
+
+G_DEFINE_INTERFACE (IdeRenameProvider, ide_rename_provider, IDE_TYPE_OBJECT)
+
+static void
+ide_rename_provider_real_rename_async (IdeRenameProvider   *self,
+                                       IdeLocation   *location,
+                                       const gchar         *new_name,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_RENAME_PROVIDER (self));
+  g_assert (location != NULL);
+  g_assert (new_name != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_rename_provider_real_rename_async);
+
+  ide_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "%s has not implemented rename_async",
+                             G_OBJECT_TYPE_NAME (self));
+}
+
+static gboolean
+ide_rename_provider_real_rename_finish (IdeRenameProvider  *self,
+                                        GAsyncResult       *result,
+                                        GPtrArray         **edits,
+                                        GError            **error)
+{
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_rename_provider_default_init (IdeRenameProviderInterface *iface)
+{
+  iface->rename_async = ide_rename_provider_real_rename_async;
+  iface->rename_finish = ide_rename_provider_real_rename_finish;
+}
+
+/**
+ * ide_rename_provider_rename_async:
+ * @self: An #IdeRenameProvider
+ * @location: An #IdeLocation
+ * @new_name: The replacement name for the symbol
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to complete the request
+ * @user_data: user data for @callback
+ *
+ * This requests the provider to determine the edits that must be made to the
+ * project to perform the renaming of a symbol found at @location.
+ *
+ * Use ide_rename_provider_rename_finish() to get the results.
+ *
+ * Since: 3.32
+ */
+void
+ide_rename_provider_rename_async (IdeRenameProvider   *self,
+                                  IdeLocation         *location,
+                                  const gchar         *new_name,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RENAME_PROVIDER (self));
+  g_return_if_fail (location != NULL);
+  g_return_if_fail (new_name != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_RENAME_PROVIDER_GET_IFACE (self)->rename_async (self, location, new_name, cancellable, callback, 
user_data);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_rename_provider_rename_finish:
+ * @self: An #IdeRenameProvider
+ * @result: a #GAsyncResult
+ * @edits: (out) (transfer full) (element-type IdeTextEdit) (optional): A location
+ *   for a #GPtrArray of #IdeTextEdit instances.
+ * @error: a location for a #GError, or %NULL.
+ *
+ * Completes a request to ide_rename_provider_rename_async().
+ *
+ * You can use the resulting #GPtrArray of #IdeTextEdit instances to edit the
+ * project to complete the symbol rename.
+ *
+ * Returns: %TRUE if successful and @edits is set. Otherwise %FALSE and @error
+ *   is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_rename_provider_rename_finish (IdeRenameProvider  *self,
+                                   GAsyncResult       *result,
+                                   GPtrArray         **edits,
+                                   GError            **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_RENAME_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  ret = IDE_RENAME_PROVIDER_GET_IFACE (self)->rename_finish (self, result, edits, error);
+
+  IDE_RETURN (ret);
+}
+
+void
+ide_rename_provider_load (IdeRenameProvider *self)
+{
+  g_return_if_fail (IDE_IS_RENAME_PROVIDER (self));
+
+  if (IDE_RENAME_PROVIDER_GET_IFACE (self)->load)
+    IDE_RENAME_PROVIDER_GET_IFACE (self)->load (self);
+}
+
+void
+ide_rename_provider_unload (IdeRenameProvider *self)
+{
+  g_return_if_fail (IDE_IS_RENAME_PROVIDER (self));
+
+  if (IDE_RENAME_PROVIDER_GET_IFACE (self)->unload)
+    IDE_RENAME_PROVIDER_GET_IFACE (self)->unload (self);
+}
diff --git a/src/libide/code/ide-rename-provider.h b/src/libide/code/ide-rename-provider.h
new file mode 100644
index 000000000..4d84a0546
--- /dev/null
+++ b/src/libide/code/ide-rename-provider.h
@@ -0,0 +1,73 @@
+/* ide-rename-provider.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RENAME_PROVIDER (ide_rename_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeRenameProvider, ide_rename_provider, IDE, RENAME_PROVIDER, IdeObject)
+
+struct _IdeRenameProviderInterface
+{
+  GTypeInterface parent_iface;
+
+  void     (*load)          (IdeRenameProvider    *self);
+  void     (*unload)        (IdeRenameProvider    *self);
+  void     (*rename_async)  (IdeRenameProvider    *self,
+                             IdeLocation          *location,
+                             const gchar          *new_name,
+                             GCancellable         *cancellable,
+                             GAsyncReadyCallback   callback,
+                             gpointer              user_data);
+  gboolean (*rename_finish) (IdeRenameProvider    *self,
+                             GAsyncResult         *result,
+                             GPtrArray           **edits,
+                             GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void      ide_rename_provider_load          (IdeRenameProvider     *self);
+IDE_AVAILABLE_IN_3_32
+void      ide_rename_provider_unload        (IdeRenameProvider     *self);
+IDE_AVAILABLE_IN_3_32
+void      ide_rename_provider_rename_async  (IdeRenameProvider     *self,
+                                             IdeLocation           *location,
+                                             const gchar           *new_name,
+                                             GCancellable          *cancellable,
+                                             GAsyncReadyCallback    callback,
+                                             gpointer               user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean  ide_rename_provider_rename_finish (IdeRenameProvider     *self,
+                                             GAsyncResult          *result,
+                                             GPtrArray            **edits,
+                                             GError               **error);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-source-iter.c b/src/libide/code/ide-source-iter.c
new file mode 100644
index 000000000..fda13e314
--- /dev/null
+++ b/src/libide/code/ide-source-iter.c
@@ -0,0 +1,626 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */
+/* ide-source-iter.c
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2014 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+/* GtkTextIter functions. Contains forward/backward functions for word
+ * movements, with custom word boundaries that are used for word selection
+ * (double-click) and cursor movements (Ctrl+left, Ctrl+right, etc).  The
+ * initial idea was to use those word boundaries directly in GTK+, for all text
+ * widgets. But in the end only the GtkTextView::extend-selection signal has
+ * been added to be able to customize the boundaries for double- and
+ * triple-click (the ::move-cursor and ::delete-from-cursor signals were already
+ * present to customize boundaries for cursor movements). The GTK+ developers
+ * didn't want to change the word boundaries for text widgets. More information:
+ * https://mail.gnome.org/archives/gtk-devel-list/2014-September/msg00019.html
+ * https://bugzilla.gnome.org/show_bug.cgi?id=111503
+ */
+
+#include "config.h"
+
+#include "ide-source-iter.h"
+
+/* Go to the end of the next or current "full word". A full word is a group of
+ * non-blank chars.
+ * In other words, this function is the same as the 'E' Vim command.
+ *
+ * Examples ('|' is the iter position):
+ * "|---- abcd"   -> "----| abcd"
+ * "|  ---- abcd" -> "  ----| abcd"
+ * "--|-- abcd"   -> "----| abcd"
+ * "---- a|bcd"   -> "---- abcd|"
+ */
+void
+_ide_source_iter_forward_full_word_end (GtkTextIter *iter)
+{
+       GtkTextIter pos;
+       gboolean non_blank_found = FALSE;
+
+       /* It would be better to use gtk_text_iter_forward_visible_char(), but
+        * it doesn't exist. So move by cursor position instead, it should be
+        * equivalent here.
+        */
+
+       pos = *iter;
+
+       while (g_unichar_isspace (gtk_text_iter_get_char (&pos)))
+       {
+               gtk_text_iter_forward_visible_cursor_position (&pos);
+       }
+
+       while (!gtk_text_iter_is_end (&pos) &&
+              !g_unichar_isspace (gtk_text_iter_get_char (&pos)))
+       {
+               non_blank_found = TRUE;
+               gtk_text_iter_forward_visible_cursor_position (&pos);
+       }
+
+       if (non_blank_found)
+       {
+               *iter = pos;
+       }
+}
+
+/* Symmetric of iter_forward_full_word_end(). */
+void
+_ide_source_iter_backward_full_word_start (GtkTextIter *iter)
+{
+       GtkTextIter pos;
+       GtkTextIter prev;
+       gboolean non_blank_found = FALSE;
+
+       pos = *iter;
+
+       while (!gtk_text_iter_is_start (&pos))
+       {
+               prev = pos;
+               gtk_text_iter_backward_visible_cursor_position (&prev);
+
+               if (!g_unichar_isspace (gtk_text_iter_get_char (&prev)))
+               {
+                       break;
+               }
+
+               pos = prev;
+       }
+
+       while (!gtk_text_iter_is_start (&pos))
+       {
+               prev = pos;
+               gtk_text_iter_backward_visible_cursor_position (&prev);
+
+               if (g_unichar_isspace (gtk_text_iter_get_char (&prev)))
+               {
+                       break;
+               }
+
+               non_blank_found = TRUE;
+               pos = prev;
+       }
+
+       if (non_blank_found)
+       {
+               *iter = pos;
+       }
+}
+
+gboolean
+_ide_source_iter_starts_full_word (const GtkTextIter *iter)
+{
+       GtkTextIter prev = *iter;
+
+       if (gtk_text_iter_is_end (iter))
+       {
+               return FALSE;
+       }
+
+       if (!gtk_text_iter_backward_visible_cursor_position (&prev))
+       {
+               return !g_unichar_isspace (gtk_text_iter_get_char (iter));
+       }
+
+       return (g_unichar_isspace (gtk_text_iter_get_char (&prev)) &&
+               !g_unichar_isspace (gtk_text_iter_get_char (iter)));
+}
+
+gboolean
+_ide_source_iter_ends_full_word (const GtkTextIter *iter)
+{
+       GtkTextIter prev = *iter;
+
+       if (!gtk_text_iter_backward_visible_cursor_position (&prev))
+       {
+               return FALSE;
+       }
+
+       return (!g_unichar_isspace (gtk_text_iter_get_char (&prev)) &&
+               (gtk_text_iter_is_end (iter) ||
+                g_unichar_isspace (gtk_text_iter_get_char (iter))));
+}
+
+/* Extends the definition of a natural-language word used by Pango. The
+ * underscore is added to the possible characters of a natural-language word.
+ */
+void
+_ide_source_iter_forward_extra_natural_word_end (GtkTextIter *iter)
+{
+       GtkTextIter next_word_end = *iter;
+       GtkTextIter next_underscore_end = *iter;
+       GtkTextIter *limit = NULL;
+       gboolean found;
+
+       if (gtk_text_iter_forward_visible_word_end (&next_word_end))
+       {
+               limit = &next_word_end;
+       }
+
+       found = gtk_text_iter_forward_search (iter,
+                                             "_",
+                                             GTK_TEXT_SEARCH_VISIBLE_ONLY | GTK_TEXT_SEARCH_TEXT_ONLY,
+                                             NULL,
+                                             &next_underscore_end,
+                                             limit);
+
+       if (found)
+       {
+               *iter = next_underscore_end;
+       }
+       else
+       {
+               *iter = next_word_end;
+       }
+
+       while (TRUE)
+       {
+               if (gtk_text_iter_get_char (iter) == '_')
+               {
+                       gtk_text_iter_forward_visible_cursor_position (iter);
+               }
+               else if (gtk_text_iter_starts_word (iter))
+               {
+                       gtk_text_iter_forward_visible_word_end (iter);
+               }
+               else
+               {
+                       break;
+               }
+       }
+}
+
+/* Symmetric of iter_forward_extra_natural_word_end(). */
+void
+_ide_source_iter_backward_extra_natural_word_start (GtkTextIter *iter)
+{
+       GtkTextIter prev_word_start = *iter;
+       GtkTextIter prev_underscore_start = *iter;
+       GtkTextIter *limit = NULL;
+       gboolean found;
+
+       if (gtk_text_iter_backward_visible_word_start (&prev_word_start))
+       {
+               limit = &prev_word_start;
+       }
+
+       found = gtk_text_iter_backward_search (iter,
+                                              "_",
+                                              GTK_TEXT_SEARCH_VISIBLE_ONLY | GTK_TEXT_SEARCH_TEXT_ONLY,
+                                              &prev_underscore_start,
+                                              NULL,
+                                              limit);
+
+       if (found)
+       {
+               *iter = prev_underscore_start;
+       }
+       else
+       {
+               *iter = prev_word_start;
+       }
+
+       while (!gtk_text_iter_is_start (iter))
+       {
+               GtkTextIter prev = *iter;
+               gtk_text_iter_backward_visible_cursor_position (&prev);
+
+               if (gtk_text_iter_get_char (&prev) == '_')
+               {
+                       *iter = prev;
+               }
+               else if (gtk_text_iter_ends_word (iter))
+               {
+                       gtk_text_iter_backward_visible_word_start (iter);
+               }
+               else
+               {
+                       break;
+               }
+       }
+}
+
+gboolean
+_ide_source_iter_starts_extra_natural_word (const GtkTextIter *iter)
+{
+       gboolean starts_word;
+       GtkTextIter prev;
+
+       starts_word = gtk_text_iter_starts_word (iter);
+
+       prev = *iter;
+       if (!gtk_text_iter_backward_visible_cursor_position (&prev))
+       {
+               return starts_word || gtk_text_iter_get_char (iter) == '_';
+       }
+
+       if (starts_word)
+       {
+               return gtk_text_iter_get_char (&prev) != '_';
+       }
+
+       return (gtk_text_iter_get_char (iter) == '_' &&
+               gtk_text_iter_get_char (&prev) != '_' &&
+               !gtk_text_iter_ends_word (iter));
+}
+
+gboolean
+_ide_source_iter_ends_extra_natural_word (const GtkTextIter *iter)
+{
+       GtkTextIter prev;
+       gboolean ends_word;
+
+       prev = *iter;
+       if (!gtk_text_iter_backward_visible_cursor_position (&prev))
+       {
+               return FALSE;
+       }
+
+       ends_word = gtk_text_iter_ends_word (iter);
+
+       if (gtk_text_iter_is_end (iter))
+       {
+               return ends_word || gtk_text_iter_get_char (&prev) == '_';
+       }
+
+       if (ends_word)
+       {
+               return gtk_text_iter_get_char (iter) != '_';
+       }
+
+       return (gtk_text_iter_get_char (&prev) == '_' &&
+               gtk_text_iter_get_char (iter) != '_' &&
+               !gtk_text_iter_starts_word (iter));
+}
+
+/* Similar to gtk_text_iter_forward_visible_word_end, but with a custom
+ * definition of "word".
+ *
+ * It is normally the same word boundaries as in Vim. This function is the same
+ * as the 'e' command.
+ *
+ * With the custom word definition, a word can be:
+ * - a natural-language word as defined by Pango, plus the underscore. The
+ *   underscore is added because it is often used in programming languages.
+ * - a group of contiguous non-blank characters.
+ */
+gboolean
+_ide_source_iter_forward_visible_word_end (GtkTextIter *iter)
+{
+       GtkTextIter orig = *iter;
+       GtkTextIter farthest = *iter;
+       GtkTextIter next_word_end = *iter;
+       GtkTextIter word_start;
+
+       /* 'farthest' is the farthest position that this function can return. Example:
+        * "|---- aaaa"  ->  "----| aaaa"
+        */
+       _ide_source_iter_forward_full_word_end (&farthest);
+
+       /* Go to the next extra-natural word end. It can be farther than
+        * 'farthest':
+        * "|---- aaaa"  ->  "---- aaaa|"
+        *
+        * Or it can remain at the same place:
+        * "aaaa| ----"  ->  "aaaa| ----"
+        */
+       _ide_source_iter_forward_extra_natural_word_end (&next_word_end);
+
+       if (gtk_text_iter_compare (&farthest, &next_word_end) < 0 ||
+           gtk_text_iter_equal (iter, &next_word_end))
+       {
+               *iter = farthest;
+               goto end;
+       }
+
+       /* From 'next_word_end', go to the previous extra-natural word start.
+        *
+        * Example 1:
+        * iter:          "ab|cd"
+        * next_word_end: "abcd|" -> the good one
+        * word_start:    "|abcd"
+        *
+        * Example 2:
+        * iter:          "| abcd()"
+        * next_word_end: " abcd|()" -> the good one
+        * word_start:    " |abcd()"
+        *
+        * Example 3:
+        * iter:          "abcd|()efgh"
+        * next_word_end: "abcd()efgh|"
+        * word_start:    "abcd()|efgh" -> the good one, at the end of the word "()".
+        */
+       word_start = next_word_end;
+       _ide_source_iter_backward_extra_natural_word_start (&word_start);
+
+       /* Example 1 */
+       if (gtk_text_iter_compare (&word_start, iter) <= 0)
+       {
+               *iter = next_word_end;
+       }
+
+       /* Example 2 */
+       else if (_ide_source_iter_starts_full_word (&word_start))
+       {
+               *iter = next_word_end;
+       }
+
+       /* Example 3 */
+       else
+       {
+               *iter = word_start;
+       }
+
+end:
+       return !gtk_text_iter_equal (&orig, iter) && !gtk_text_iter_is_end (iter);
+}
+
+/* Symmetric of _ide_source_iter_forward_visible_word_end(). */
+gboolean
+_ide_source_iter_backward_visible_word_start (GtkTextIter *iter)
+{
+       GtkTextIter orig = *iter;
+       GtkTextIter farthest = *iter;
+       GtkTextIter prev_word_start = *iter;
+       GtkTextIter word_end;
+
+       /* 'farthest' is the farthest position that this function can return. Example:
+        * "aaaa ----|"  ->  "aaaa |----"
+        */
+       _ide_source_iter_backward_full_word_start (&farthest);
+
+       /* Go to the previous extra-natural word start. It can be farther than
+        * 'farthest':
+        * "aaaa ----|"  ->  "|aaaa ----"
+        *
+        * Or it can remain at the same place:
+        * "---- |aaaa"  ->  "---- |aaaa"
+        */
+       _ide_source_iter_backward_extra_natural_word_start (&prev_word_start);
+
+       if (gtk_text_iter_compare (&prev_word_start, &farthest) < 0 ||
+           gtk_text_iter_equal (iter, &prev_word_start))
+       {
+               *iter = farthest;
+               goto end;
+       }
+
+       /* From 'prev_word_start', go to the next extra-natural word end.
+        *
+        * Example 1:
+        * iter:            "ab|cd"
+        * prev_word_start: "|abcd" -> the good one
+        * word_end:        "abcd|"
+        *
+        * Example 2:
+        * iter:            "()abcd |"
+        * prev_word_start: "()|abcd " -> the good one
+        * word_end:        "()abcd| "
+        *
+        * Example 3:
+        * iter:            "abcd()|"
+        * prev_word_start: "|abcd()"
+        * word_end:        "abcd|()" -> the good one, at the start of the word "()".
+        */
+       word_end = prev_word_start;
+       _ide_source_iter_forward_extra_natural_word_end (&word_end);
+
+       /* Example 1 */
+       if (gtk_text_iter_compare (iter, &word_end) <= 0)
+       {
+               *iter = prev_word_start;
+       }
+
+       /* Example 2 */
+       else if (_ide_source_iter_ends_full_word (&word_end))
+       {
+               *iter = prev_word_start;
+       }
+
+       /* Example 3 */
+       else
+       {
+               *iter = word_end;
+       }
+
+end:
+       return !gtk_text_iter_equal (&orig, iter) && !gtk_text_iter_is_end (iter);
+}
+
+/* Similar to gtk_text_iter_forward_visible_word_ends(). */
+gboolean
+_ide_source_iter_forward_visible_word_ends (GtkTextIter *iter,
+                                           gint         count)
+{
+       GtkTextIter orig = *iter;
+       gint i;
+
+       if (count < 0)
+       {
+               return _ide_source_iter_backward_visible_word_starts (iter, -count);
+       }
+
+       for (i = 0; i < count; i++)
+       {
+               if (!_ide_source_iter_forward_visible_word_end (iter))
+               {
+                       break;
+               }
+       }
+
+       return !gtk_text_iter_equal (&orig, iter) && !gtk_text_iter_is_end (iter);
+}
+
+/* Similar to gtk_text_iter_backward_visible_word_starts(). */
+gboolean
+_ide_source_iter_backward_visible_word_starts (GtkTextIter *iter,
+                                              gint         count)
+{
+       GtkTextIter orig = *iter;
+       gint i;
+
+       if (count < 0)
+       {
+               return _ide_source_iter_forward_visible_word_ends (iter, -count);
+       }
+
+       for (i = 0; i < count; i++)
+       {
+               if (!_ide_source_iter_backward_visible_word_start (iter))
+               {
+                       break;
+               }
+       }
+
+       return !gtk_text_iter_equal (&orig, iter) && !gtk_text_iter_is_end (iter);
+}
+
+gboolean
+_ide_source_iter_starts_word (const GtkTextIter *iter)
+{
+       if (_ide_source_iter_starts_full_word (iter) ||
+           _ide_source_iter_starts_extra_natural_word (iter))
+       {
+               return TRUE;
+       }
+
+       /* Example: "abcd|()", at the start of the word "()". */
+       return (!_ide_source_iter_ends_full_word (iter) &&
+               _ide_source_iter_ends_extra_natural_word (iter));
+}
+
+gboolean
+_ide_source_iter_ends_word (const GtkTextIter *iter)
+{
+       if (_ide_source_iter_ends_full_word (iter) ||
+           _ide_source_iter_ends_extra_natural_word (iter))
+       {
+               return TRUE;
+       }
+
+       /* Example: "abcd()|efgh", at the end of the word "()". */
+       return (!_ide_source_iter_starts_full_word (iter) &&
+               _ide_source_iter_starts_extra_natural_word (iter));
+}
+
+gboolean
+_ide_source_iter_inside_word (const GtkTextIter *iter)
+{
+       GtkTextIter prev_word_start;
+       GtkTextIter word_end;
+
+       if (_ide_source_iter_starts_word (iter))
+       {
+               return TRUE;
+       }
+
+       prev_word_start = *iter;
+       if (!_ide_source_iter_backward_visible_word_start (&prev_word_start))
+       {
+               return FALSE;
+       }
+
+       word_end = prev_word_start;
+       _ide_source_iter_forward_visible_word_end (&word_end);
+
+       return (gtk_text_iter_compare (&prev_word_start, iter) <= 0 &&
+               gtk_text_iter_compare (iter, &word_end) < 0);
+}
+
+/* Used for the GtkTextView::extend-selection signal. */
+void
+_ide_source_iter_extend_selection_word (const GtkTextIter *location,
+                                       GtkTextIter       *start,
+                                       GtkTextIter       *end)
+{
+       /* Exactly the same algorithm as in GTK+, but with our custom word
+        * boundaries.
+        */
+       *start = *location;
+       *end = *location;
+
+       if (_ide_source_iter_inside_word (start))
+       {
+               if (!_ide_source_iter_starts_word (start))
+               {
+                       _ide_source_iter_backward_visible_word_start (start);
+               }
+
+               if (!_ide_source_iter_ends_word (end))
+               {
+                       _ide_source_iter_forward_visible_word_end (end);
+               }
+       }
+       else
+       {
+               GtkTextIter tmp;
+
+               tmp = *start;
+               if (_ide_source_iter_backward_visible_word_start (&tmp))
+               {
+                       _ide_source_iter_forward_visible_word_end (&tmp);
+               }
+
+               if (gtk_text_iter_get_line (&tmp) == gtk_text_iter_get_line (start))
+               {
+                       *start = tmp;
+               }
+               else
+               {
+                       gtk_text_iter_set_line_offset (start, 0);
+               }
+
+               tmp = *end;
+               if (!_ide_source_iter_forward_visible_word_end (&tmp))
+               {
+                       gtk_text_iter_forward_to_end (&tmp);
+               }
+
+               if (_ide_source_iter_ends_word (&tmp))
+               {
+                       _ide_source_iter_backward_visible_word_start (&tmp);
+               }
+
+               if (gtk_text_iter_get_line (&tmp) == gtk_text_iter_get_line (end))
+               {
+                       *end = tmp;
+               }
+               else
+               {
+                       gtk_text_iter_forward_to_line_end (end);
+               }
+       }
+}
diff --git a/src/libide/code/ide-source-iter.h b/src/libide/code/ide-source-iter.h
new file mode 100644
index 000000000..d2ded4386
--- /dev/null
+++ b/src/libide/code/ide-source-iter.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */
+/* ide-source-iter.h
+ * This file is part of GtkSourceView
+ *
+ * Copyright 2014 - Sébastien Wilmet <swilmet gnome org>
+ *
+ * GtkSourceView is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * GtkSourceView 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+/* Semi-public functions. */
+
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_forward_visible_word_end          (GtkTextIter       *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_forward_visible_word_ends         (GtkTextIter       *iter,
+                                                             gint               count);
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_backward_visible_word_start       (GtkTextIter       *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_backward_visible_word_starts      (GtkTextIter       *iter,
+                                                             gint               count);
+IDE_AVAILABLE_IN_3_32
+void     _ide_source_iter_extend_selection_word             (const GtkTextIter *location,
+                                                             GtkTextIter       *start,
+                                                             GtkTextIter       *end);
+IDE_AVAILABLE_IN_3_32
+void     _ide_source_iter_forward_full_word_end             (GtkTextIter       *iter);
+IDE_AVAILABLE_IN_3_32
+void     _ide_source_iter_backward_full_word_start          (GtkTextIter       *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_starts_full_word                  (const GtkTextIter *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_ends_full_word                    (const GtkTextIter *iter);
+IDE_AVAILABLE_IN_3_32
+void     _ide_source_iter_forward_extra_natural_word_end    (GtkTextIter       *iter);
+IDE_AVAILABLE_IN_3_32
+void     _ide_source_iter_backward_extra_natural_word_start (GtkTextIter       *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_starts_extra_natural_word         (const GtkTextIter *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_ends_extra_natural_word           (const GtkTextIter *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_starts_word                       (const GtkTextIter *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_ends_word                         (const GtkTextIter *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean _ide_source_iter_inside_word                       (const GtkTextIter *iter);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-source-style-scheme.c b/src/libide/code/ide-source-style-scheme.c
new file mode 100644
index 000000000..b68fd10dc
--- /dev/null
+++ b/src/libide/code/ide-source-style-scheme.c
@@ -0,0 +1,117 @@
+/* ide-source-style-scheme.c
+ *
+ * Copyright 2016-2019 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 "ide-source-style-scheme"
+
+#include "config.h"
+
+#include <string.h>
+
+#include "ide-source-style-scheme.h"
+
+gboolean
+ide_source_style_scheme_apply_style (GtkSourceStyleScheme *style_scheme,
+                                     const gchar          *style_name,
+                                     GtkTextTag           *tag)
+{
+  g_autofree gchar *foreground = NULL;
+  g_autofree gchar *background = NULL;
+  g_autofree gchar *underline_color = NULL;
+  GdkRGBA underline_rgba;
+  GtkSourceStyle *style;
+  const gchar *colon;
+  PangoUnderline pango_underline;
+  gboolean foreground_set = FALSE;
+  gboolean background_set = FALSE;
+  gboolean bold = FALSE;
+  gboolean bold_set = FALSE;
+  gboolean underline_set = FALSE;
+  gboolean underline_color_set = FALSE;
+  gboolean italic = FALSE;
+  gboolean italic_set = FALSE;
+
+  g_return_val_if_fail (!style_scheme || GTK_SOURCE_IS_STYLE_SCHEME (style_scheme), FALSE);
+  g_return_val_if_fail (style_name != NULL, FALSE);
+
+  g_object_set (tag,
+                "foreground-set", FALSE,
+                "background-set", FALSE,
+                "weight-set", FALSE,
+                "underline-set", FALSE,
+                "underline-rgba-set", FALSE,
+                "style-set", FALSE,
+                NULL);
+
+  if (style_scheme == NULL)
+    return FALSE;
+
+  style = gtk_source_style_scheme_get_style (style_scheme, style_name);
+
+  if (style == NULL && (colon = strchr (style_name, ':')))
+    {
+      gchar defname[64];
+
+      g_snprintf (defname, sizeof defname, "def%s", colon);
+
+      style = gtk_source_style_scheme_get_style (style_scheme, defname);
+    }
+
+  if (style == NULL)
+    return FALSE;
+
+  g_object_get (style,
+                "background", &background,
+                "background-set", &background_set,
+                "foreground", &foreground,
+                "foreground-set", &foreground_set,
+                "bold", &bold,
+                "bold-set", &bold_set,
+                "pango-underline", &pango_underline,
+                "underline-set", &underline_set,
+                "underline-color", &underline_color,
+                "underline-color-set", &underline_color_set,
+                "italic", &italic,
+                "italic-set", &italic_set,
+                NULL);
+
+  if (background_set)
+    g_object_set (tag, "background", background, NULL);
+
+  if (foreground_set)
+    g_object_set (tag, "foreground", foreground, NULL);
+
+  if (bold_set && bold)
+    g_object_set (tag, "weight", PANGO_WEIGHT_BOLD, NULL);
+
+  if (italic_set && italic)
+    g_object_set (tag, "style", PANGO_STYLE_ITALIC, NULL);
+
+  if (underline_set)
+    g_object_set (tag, "underline", pango_underline, NULL);
+
+  if (underline_color_set && underline_color != NULL)
+    {
+      gdk_rgba_parse (&underline_rgba, underline_color);
+      g_object_set (tag,
+                    "underline-rgba", &underline_rgba,
+                    NULL);
+    }
+  return TRUE;
+}
diff --git a/src/libide/code/ide-source-style-scheme.h b/src/libide/code/ide-source-style-scheme.h
new file mode 100644
index 000000000..cce1de880
--- /dev/null
+++ b/src/libide/code/ide-source-style-scheme.h
@@ -0,0 +1,37 @@
+/* ide-source-style-scheme.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <gtksourceview/gtksource.h>
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+gboolean ide_source_style_scheme_apply_style (GtkSourceStyleScheme *style_scheme,
+                                              const gchar          *style,
+                                              GtkTextTag           *tag);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-spaces-style.h b/src/libide/code/ide-spaces-style.h
new file mode 100644
index 000000000..51cb50c9e
--- /dev/null
+++ b/src/libide/code/ide-spaces-style.h
@@ -0,0 +1,43 @@
+/* ide-spaces-style.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_SPACES_STYLE_IGNORE              = 0,
+  IDE_SPACES_STYLE_BEFORE_LEFT_PAREN   = 1 << 0,
+  IDE_SPACES_STYLE_BEFORE_LEFT_BRACKET = 1 << 1,
+  IDE_SPACES_STYLE_BEFORE_LEFT_BRACE   = 1 << 2,
+  IDE_SPACES_STYLE_BEFORE_LEFT_ANGLE   = 1 << 3,
+  IDE_SPACES_STYLE_BEFORE_COLON        = 1 << 4,
+  IDE_SPACES_STYLE_BEFORE_COMMA        = 1 << 5,
+  IDE_SPACES_STYLE_BEFORE_SEMICOLON    = 1 << 6,
+} IdeSpacesStyle;
+
+G_END_DECLS
diff --git a/src/libide/code/ide-symbol-node.c b/src/libide/code/ide-symbol-node.c
new file mode 100644
index 000000000..05ad3f150
--- /dev/null
+++ b/src/libide/code/ide-symbol-node.c
@@ -0,0 +1,272 @@
+/* ide-symbol-node.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-symbol-node"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+
+#include "ide-code-enums.h"
+#include "ide-symbol.h"
+#include "ide-symbol-node.h"
+
+typedef struct
+{
+  gchar          *name;
+  IdeSymbolFlags  flags;
+  IdeSymbolKind   kind;
+  guint           use_markup : 1;
+} IdeSymbolNodePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeSymbolNode, ide_symbol_node, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_FLAGS,
+  PROP_KIND,
+  PROP_NAME,
+  PROP_USE_MARKUP,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_symbol_node_real_get_location_async (IdeSymbolNode       *self,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_symbol_node_get_location_async);
+  ide_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Unsupported operation on symbol node");
+}
+
+static IdeLocation *
+ide_symbol_node_real_get_location_finish (IdeSymbolNode  *self,
+                                          GAsyncResult   *result,
+                                          GError        **error)
+{
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+ide_symbol_node_finalize (GObject *object)
+{
+  IdeSymbolNode *self = (IdeSymbolNode *)object;
+  IdeSymbolNodePrivate *priv = ide_symbol_node_get_instance_private (self);
+
+  g_clear_pointer (&priv->name, g_free);
+
+  G_OBJECT_CLASS (ide_symbol_node_parent_class)->finalize (object);
+}
+
+static void
+ide_symbol_node_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeSymbolNode *self = IDE_SYMBOL_NODE (object);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_value_set_string (value, ide_symbol_node_get_name (self));
+      break;
+
+    case PROP_KIND:
+      g_value_set_enum (value, ide_symbol_node_get_kind (self));
+      break;
+
+    case PROP_FLAGS:
+      g_value_set_flags (value, ide_symbol_node_get_flags (self));
+      break;
+
+    case PROP_USE_MARKUP:
+      g_value_set_boolean (value, ide_symbol_node_get_use_markup (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_symbol_node_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeSymbolNode *self = IDE_SYMBOL_NODE (object);
+  IdeSymbolNodePrivate *priv = ide_symbol_node_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_NAME:
+      g_free (priv->name);
+      priv->name = g_value_dup_string (value);
+      break;
+
+    case PROP_KIND:
+      priv->kind = g_value_get_enum (value);
+      break;
+
+    case PROP_FLAGS:
+      priv->flags = g_value_get_flags (value);
+      break;
+
+    case PROP_USE_MARKUP:
+      priv->use_markup = g_value_get_boolean (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_symbol_node_class_init (IdeSymbolNodeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  klass->get_location_async = ide_symbol_node_real_get_location_async;
+  klass->get_location_finish = ide_symbol_node_real_get_location_finish;
+
+  object_class->finalize = ide_symbol_node_finalize;
+  object_class->get_property = ide_symbol_node_get_property;
+  object_class->set_property = ide_symbol_node_set_property;
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "Name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_KIND] =
+    g_param_spec_enum ("kind",
+                       "Kind",
+                       "Kind",
+                       IDE_TYPE_SYMBOL_KIND,
+                       IDE_SYMBOL_KIND_NONE,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FLAGS] =
+    g_param_spec_flags ("flags",
+                        "Flags",
+                        "Flags",
+                        IDE_TYPE_SYMBOL_FLAGS,
+                        IDE_SYMBOL_FLAGS_NONE,
+                        (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_USE_MARKUP] =
+    g_param_spec_boolean ("use-markup",
+                          "use-markup",
+                          "Use markup",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_symbol_node_init (IdeSymbolNode *self)
+{
+}
+
+const gchar *
+ide_symbol_node_get_name (IdeSymbolNode *self)
+{
+  IdeSymbolNodePrivate *priv = ide_symbol_node_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SYMBOL_NODE (self), NULL);
+
+  return priv->name;
+}
+
+IdeSymbolFlags
+ide_symbol_node_get_flags (IdeSymbolNode *self)
+{
+  IdeSymbolNodePrivate *priv = ide_symbol_node_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SYMBOL_NODE (self), IDE_SYMBOL_FLAGS_NONE);
+
+  return priv->flags;
+}
+
+IdeSymbolKind
+ide_symbol_node_get_kind (IdeSymbolNode *self)
+{
+  IdeSymbolNodePrivate *priv = ide_symbol_node_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SYMBOL_NODE (self), IDE_SYMBOL_KIND_NONE);
+
+  return priv->kind;
+}
+
+gboolean
+ide_symbol_node_get_use_markup (IdeSymbolNode *self)
+{
+  IdeSymbolNodePrivate *priv = ide_symbol_node_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SYMBOL_NODE (self), FALSE);
+
+  return priv->use_markup;
+}
+
+void
+ide_symbol_node_get_location_async (IdeSymbolNode       *self,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_SYMBOL_NODE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_SYMBOL_NODE_GET_CLASS (self)->get_location_async (self, cancellable, callback, user_data);
+}
+
+/**
+ * ide_symbol_node_get_location_finish:
+ *
+ * Completes the request to gets the location for the symbol node.
+ *
+ * Returns: (transfer full) (nullable): An #IdeLocation or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_symbol_node_get_location_finish (IdeSymbolNode  *self,
+                                     GAsyncResult   *result,
+                                     GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_SYMBOL_NODE (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_SYMBOL_NODE_GET_CLASS (self)->get_location_finish (self, result, error);
+}
diff --git a/src/libide/code/ide-symbol-node.h b/src/libide/code/ide-symbol-node.h
new file mode 100644
index 000000000..e5004c2e1
--- /dev/null
+++ b/src/libide/code/ide-symbol-node.h
@@ -0,0 +1,73 @@
+/* ide-symbol-node.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+#include "ide-symbol.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SYMBOL_NODE (ide_symbol_node_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeSymbolNode, ide_symbol_node, IDE, SYMBOL_NODE, GObject)
+
+struct _IdeSymbolNodeClass
+{
+  GObjectClass parent;
+
+  void         (*get_location_async)  (IdeSymbolNode        *self,
+                                       GCancellable         *cancellable,
+                                       GAsyncReadyCallback   callback,
+                                       gpointer              user_data);
+  IdeLocation *(*get_location_finish) (IdeSymbolNode        *self,
+                                       GAsyncResult         *result,
+                                       GError             **error);
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeSymbolKind   ide_symbol_node_get_kind            (IdeSymbolNode        *self);
+IDE_AVAILABLE_IN_3_32
+IdeSymbolFlags  ide_symbol_node_get_flags           (IdeSymbolNode        *self);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_symbol_node_get_name            (IdeSymbolNode        *self);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_symbol_node_get_use_markup      (IdeSymbolNode        *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_symbol_node_get_location_async  (IdeSymbolNode        *self,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+IdeLocation    *ide_symbol_node_get_location_finish (IdeSymbolNode        *self,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-symbol-resolver.c b/src/libide/code/ide-symbol-resolver.c
new file mode 100644
index 000000000..d5c830d7a
--- /dev/null
+++ b/src/libide/code/ide-symbol-resolver.c
@@ -0,0 +1,361 @@
+/* ide-symbol-resolver.c
+ *
+ * Copyright 2015-2019 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 "ide-symbol-resolver"
+
+#include "config.h"
+
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#include "ide-symbol-resolver.h"
+
+G_DEFINE_INTERFACE (IdeSymbolResolver, ide_symbol_resolver, IDE_TYPE_OBJECT)
+
+static void
+ide_symbol_resolver_real_get_symbol_tree_async (IdeSymbolResolver   *self,
+                                                GFile               *file,
+                                                GBytes              *contents,
+                                                GCancellable        *cancellable,
+                                                GAsyncReadyCallback  callback,
+                                                gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_SYMBOL_RESOLVER (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_symbol_resolver_get_symbol_tree_async);
+  ide_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Symbol tree is not supported on this symbol resolver");
+}
+
+static IdeSymbolTree *
+ide_symbol_resolver_real_get_symbol_tree_finish (IdeSymbolResolver  *self,
+                                                 GAsyncResult       *result,
+                                                 GError            **error)
+{
+  g_assert (IDE_IS_SYMBOL_RESOLVER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+ide_symbol_resolver_real_find_references_async (IdeSymbolResolver   *self,
+                                                IdeLocation         *location,
+                                                const gchar         *language_id,
+                                                GCancellable        *cancellable,
+                                                GAsyncReadyCallback  callback,
+                                                gpointer             user_data)
+{
+  g_assert (IDE_IS_SYMBOL_RESOLVER (self));
+  g_assert (location != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_task_report_new_error (self,
+                             callback,
+                             user_data,
+                             ide_symbol_resolver_real_find_references_async,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Finding references is not supported for this language");
+}
+
+static GPtrArray *
+ide_symbol_resolver_real_find_references_finish (IdeSymbolResolver  *self,
+                                                 GAsyncResult       *result,
+                                                 GError            **error)
+{
+  g_assert (IDE_IS_SYMBOL_RESOLVER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+ide_symbol_resolver_real_find_nearest_scope_async (IdeSymbolResolver   *self,
+                                                   IdeLocation         *location,
+                                                   GCancellable        *cancellable,
+                                                   GAsyncReadyCallback  callback,
+                                                   gpointer             user_data)
+{
+  g_assert (IDE_IS_SYMBOL_RESOLVER (self));
+  g_assert (location != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_task_report_new_error (self,
+                             callback,
+                             user_data,
+                             ide_symbol_resolver_real_find_nearest_scope_async,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Finding nearest scope is not supported for this language");
+}
+
+static IdeSymbol *
+ide_symbol_resolver_real_find_nearest_scope_finish (IdeSymbolResolver  *self,
+                                                    GAsyncResult       *result,
+                                                    GError            **error)
+{
+  g_assert (IDE_IS_SYMBOL_RESOLVER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+ide_symbol_resolver_default_init (IdeSymbolResolverInterface *iface)
+{
+  iface->get_symbol_tree_async = ide_symbol_resolver_real_get_symbol_tree_async;
+  iface->get_symbol_tree_finish = ide_symbol_resolver_real_get_symbol_tree_finish;
+  iface->find_references_async = ide_symbol_resolver_real_find_references_async;
+  iface->find_references_finish = ide_symbol_resolver_real_find_references_finish;
+  iface->find_nearest_scope_async = ide_symbol_resolver_real_find_nearest_scope_async;
+  iface->find_nearest_scope_finish = ide_symbol_resolver_real_find_nearest_scope_finish;
+}
+
+/**
+ * ide_symbol_resolver_lookup_symbol_async:
+ * @self: An #IdeSymbolResolver.
+ * @location: An #IdeLocation.
+ * @cancellable: (allow-none): a #GCancellable or %NULL.
+ * @callback: A callback to execute upon completion.
+ * @user_data: user data for @callback.
+ *
+ * Asynchronously requests that @self determine the symbol existing at the source location
+ * denoted by @self. @callback should call ide_symbol_resolver_lookup_symbol_finish() to
+ * retrieve the result.
+ *
+ * Since: 3.32
+ */
+void
+ide_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *self,
+                                         IdeLocation         *location,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_SYMBOL_RESOLVER (self));
+  g_return_if_fail (location != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_SYMBOL_RESOLVER_GET_IFACE (self)->lookup_symbol_async (self, location, cancellable, callback, 
user_data);
+}
+
+/**
+ * ide_symbol_resolver_lookup_symbol_finish:
+ * @self: An #IdeSymbolResolver.
+ * @result: a #GAsyncResult provided to the callback.
+ * @error: (out): A location for an @error or %NULL.
+ *
+ * Completes an asynchronous call to lookup a symbol using
+ * ide_symbol_resolver_lookup_symbol_async().
+ *
+ * Returns: (transfer full) (nullable): An #IdeSymbol if successful; otherwise %NULL.
+ *
+ * Since: 3.32
+ */
+IdeSymbol *
+ide_symbol_resolver_lookup_symbol_finish (IdeSymbolResolver  *self,
+                                          GAsyncResult       *result,
+                                          GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_SYMBOL_RESOLVER (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_SYMBOL_RESOLVER_GET_IFACE (self)->lookup_symbol_finish (self, result, error);
+}
+
+/**
+ * ide_symbol_resolver_get_symbol_tree_async:
+ * @self: An #IdeSymbolResolver
+ * @file: a #GFile
+ * @contents: (nullable): a #GBytes or %NULL
+ * @cancellable: (allow-none): a #GCancellable or %NULL.
+ * @callback: (allow-none): a callback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * Asynchronously fetch an up to date symbol tree for @file.
+ *
+ * Since: 3.32
+ */
+void
+ide_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *self,
+                                           GFile               *file,
+                                           GBytes              *contents,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_SYMBOL_RESOLVER (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  IDE_SYMBOL_RESOLVER_GET_IFACE (self)->get_symbol_tree_async (self, file, contents, cancellable, callback, 
user_data);
+}
+
+/**
+ * ide_symbol_resolver_get_symbol_tree_finish:
+ *
+ * Completes an asynchronous request to get the symbol tree for the
+ * requested file.
+ *
+ * Returns: (nullable) (transfer full): An #IdeSymbolTree; otherwise
+ *   %NULL and @error is set.
+ *
+ * Since: 3.32
+ */
+IdeSymbolTree *
+ide_symbol_resolver_get_symbol_tree_finish (IdeSymbolResolver  *self,
+                                            GAsyncResult       *result,
+                                            GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_SYMBOL_RESOLVER (self), NULL);
+  g_return_val_if_fail (!result || G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_SYMBOL_RESOLVER_GET_IFACE (self)->get_symbol_tree_finish (self, result, error);
+}
+
+void
+ide_symbol_resolver_load (IdeSymbolResolver *self)
+{
+  g_return_if_fail (IDE_IS_SYMBOL_RESOLVER (self));
+
+  if (IDE_SYMBOL_RESOLVER_GET_IFACE (self)->load)
+    IDE_SYMBOL_RESOLVER_GET_IFACE (self)->load (self);
+}
+
+void
+ide_symbol_resolver_unload (IdeSymbolResolver *self)
+{
+  g_return_if_fail (IDE_IS_SYMBOL_RESOLVER (self));
+
+  if (IDE_SYMBOL_RESOLVER_GET_IFACE (self)->unload)
+    IDE_SYMBOL_RESOLVER_GET_IFACE (self)->unload (self);
+}
+
+/**
+ * ide_symbol_resolver_find_references_async:
+ * @self: a #IdeSymbolResolver
+ * @location: an #IdeLocation
+ * @language_id: (nullable): a language identifier or %NULL
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute
+ * @user_data: user data for @callback
+ *
+ */
+void
+ide_symbol_resolver_find_references_async (IdeSymbolResolver   *self,
+                                           IdeLocation         *location,
+                                           const gchar         *language_id,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_SYMBOL_RESOLVER (self));
+  g_return_if_fail (location != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_SYMBOL_RESOLVER_GET_IFACE (self)->find_references_async (self, location, language_id, cancellable, 
callback, user_data);
+}
+
+/**
+ * ide_symbol_resolver_find_references_finish:
+ * @self: a #IdeSymbolResolver
+ * @result: a #GAsyncResult
+ * @error: a #GError or %NULL
+ *
+ * Completes an asynchronous request to ide_symbol_resolver_find_references_async().
+ *
+ * Returns: (transfer full) (element-type IdeRange): a #GPtrArray
+ *   of #IdeRange if successful; otherwise %NULL and @error is set.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_symbol_resolver_find_references_finish (IdeSymbolResolver  *self,
+                                            GAsyncResult       *result,
+                                            GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_SYMBOL_RESOLVER (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_SYMBOL_RESOLVER_GET_IFACE (self)->find_references_finish (self, result, error);
+}
+
+/**
+ * ide_symbol_resolver_find_nearest_scope_async:
+ * @self: a #IdeSymbolResolver
+ * @location: an #IdeLocation
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: (scope async) (closure user_data): an async callback
+ * @user_data: user data for @callback
+ *
+ * This function asynchronously requests to locate the containing
+ * scope for a given source location.
+ *
+ * See ide_symbol_resolver_find_nearest_scope_finish() for how to
+ * complete the operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_symbol_resolver_find_nearest_scope_async (IdeSymbolResolver   *self,
+                                              IdeLocation         *location,
+                                              GCancellable        *cancellable,
+                                              GAsyncReadyCallback  callback,
+                                              gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_SYMBOL_RESOLVER (self));
+  g_return_if_fail (location != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_SYMBOL_RESOLVER_GET_IFACE (self)->find_nearest_scope_async (self, location, cancellable, callback, 
user_data);
+}
+
+/**
+ * ide_symbol_resolver_find_nearest_scope_finish:
+ * @self: a #IdeSymbolResolver
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError or %NULL
+ *
+ * This function completes an asynchronous operation to locate the containing
+ * scope for a given source location.
+ *
+ * See ide_symbol_resolver_find_nearest_scope_async() for more information.
+ *
+ * Returns: (transfer full) (nullable): An #IdeSymbol or %NULL
+ *
+ * Since: 3.32
+ */
+IdeSymbol *
+ide_symbol_resolver_find_nearest_scope_finish (IdeSymbolResolver  *self,
+                                               GAsyncResult       *result,
+                                               GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_SYMBOL_RESOLVER (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_SYMBOL_RESOLVER_GET_IFACE (self)->find_nearest_scope_finish (self, result, error);
+}
diff --git a/src/libide/code/ide-symbol-resolver.h b/src/libide/code/ide-symbol-resolver.h
new file mode 100644
index 000000000..9b27a2062
--- /dev/null
+++ b/src/libide/code/ide-symbol-resolver.h
@@ -0,0 +1,127 @@
+/* ide-symbol-resolver.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SYMBOL_RESOLVER (ide_symbol_resolver_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeSymbolResolver, ide_symbol_resolver, IDE, SYMBOL_RESOLVER, IdeObject)
+
+struct _IdeSymbolResolverInterface
+{
+  GTypeInterface parent_interface;
+
+  void           (*load)                     (IdeSymbolResolver    *self);
+  void           (*unload)                   (IdeSymbolResolver    *self);
+  void           (*lookup_symbol_async)      (IdeSymbolResolver    *self,
+                                              IdeLocation          *location,
+                                              GCancellable         *cancellable,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+  IdeSymbol     *(*lookup_symbol_finish)     (IdeSymbolResolver    *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
+  void           (*get_symbol_tree_async)    (IdeSymbolResolver    *self,
+                                              GFile                *file,
+                                              GBytes               *contents,
+                                              GCancellable         *cancellable,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+  IdeSymbolTree *(*get_symbol_tree_finish)   (IdeSymbolResolver    *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
+  void           (*find_references_async)    (IdeSymbolResolver    *self,
+                                              IdeLocation          *location,
+                                              const gchar          *language_id,
+                                              GCancellable         *cancellable,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+  GPtrArray     *(*find_references_finish)   (IdeSymbolResolver    *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
+  void           (*find_nearest_scope_async) (IdeSymbolResolver    *self,
+                                              IdeLocation          *location,
+                                              GCancellable         *cancellable,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+  IdeSymbol     *(*find_nearest_scope_finish) (IdeSymbolResolver    *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void           ide_symbol_resolver_load                      (IdeSymbolResolver    *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_symbol_resolver_unload                    (IdeSymbolResolver    *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_symbol_resolver_lookup_symbol_async       (IdeSymbolResolver    *self,
+                                                              IdeLocation          *location,
+                                                              GCancellable         *cancellable,
+                                                              GAsyncReadyCallback   callback,
+                                                              gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+IdeSymbol     *ide_symbol_resolver_lookup_symbol_finish      (IdeSymbolResolver    *self,
+                                                              GAsyncResult         *result,
+                                                              GError              **error);
+IDE_AVAILABLE_IN_3_32
+void           ide_symbol_resolver_get_symbol_tree_async     (IdeSymbolResolver    *self,
+                                                              GFile                *file,
+                                                              GBytes               *contents,
+                                                              GCancellable         *cancellable,
+                                                              GAsyncReadyCallback   callback,
+                                                              gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+IdeSymbolTree *ide_symbol_resolver_get_symbol_tree_finish    (IdeSymbolResolver    *self,
+                                                              GAsyncResult         *result,
+                                                              GError              **error);
+IDE_AVAILABLE_IN_3_32
+void           ide_symbol_resolver_find_references_async     (IdeSymbolResolver    *self,
+                                                              IdeLocation          *location,
+                                                              const gchar          *language_id,
+                                                              GCancellable         *cancellable,
+                                                              GAsyncReadyCallback   callback,
+                                                              gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GPtrArray     *ide_symbol_resolver_find_references_finish    (IdeSymbolResolver    *self,
+                                                              GAsyncResult         *result,
+                                                              GError              **error);
+IDE_AVAILABLE_IN_3_32
+void           ide_symbol_resolver_find_nearest_scope_async  (IdeSymbolResolver    *self,
+                                                              IdeLocation          *location,
+                                                              GCancellable         *cancellable,
+                                                              GAsyncReadyCallback   callback,
+                                                              gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+IdeSymbol     *ide_symbol_resolver_find_nearest_scope_finish (IdeSymbolResolver    *self,
+                                                              GAsyncResult         *result,
+                                                              GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-symbol-tree.c b/src/libide/code/ide-symbol-tree.c
new file mode 100644
index 000000000..c96dc6987
--- /dev/null
+++ b/src/libide/code/ide-symbol-tree.c
@@ -0,0 +1,78 @@
+/* ide-symbol-tree.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-symbol-tree"
+
+#include "config.h"
+
+#include "ide-symbol-node.h"
+#include "ide-symbol-tree.h"
+
+G_DEFINE_INTERFACE (IdeSymbolTree, ide_symbol_tree, G_TYPE_OBJECT)
+
+static void
+ide_symbol_tree_default_init (IdeSymbolTreeInterface *iface)
+{
+}
+
+/**
+ * ide_symbol_tree_get_n_children:
+ * @self: An @IdeSymbolTree
+ * @node: (allow-none): An #IdeSymbolNode or %NULL.
+ *
+ * Get the number of children of @node. If @node is NULL, the root node
+ * is assumed.
+ *
+ * Returns: An unsigned integer containing the number of children.
+ *
+ * Since: 3.32
+ */
+guint
+ide_symbol_tree_get_n_children (IdeSymbolTree *self,
+                                IdeSymbolNode *node)
+{
+  g_return_val_if_fail (IDE_IS_SYMBOL_TREE (self), 0);
+  g_return_val_if_fail (!node || IDE_IS_SYMBOL_NODE (node), 0);
+
+  return IDE_SYMBOL_TREE_GET_IFACE (self)->get_n_children (self, node);
+}
+
+/**
+ * ide_symbol_tree_get_nth_child:
+ * @self: An #IdeSymbolTree.
+ * @node: (allow-none): an #IdeSymboNode
+ * @nth: the nth child to retrieve.
+ *
+ * Gets the @nth child node of @node.
+ *
+ * Returns: (transfer full) (nullable): an #IdeSymbolNode or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeSymbolNode *
+ide_symbol_tree_get_nth_child (IdeSymbolTree *self,
+                               IdeSymbolNode *node,
+                               guint          nth)
+{
+  g_return_val_if_fail (IDE_IS_SYMBOL_TREE (self), NULL);
+  g_return_val_if_fail (!node || IDE_IS_SYMBOL_NODE (node), NULL);
+
+  return IDE_SYMBOL_TREE_GET_IFACE (self)->get_nth_child (self, node, nth);
+}
diff --git a/src/libide/code/ide-symbol-tree.h b/src/libide/code/ide-symbol-tree.h
new file mode 100644
index 000000000..9eea8ab9a
--- /dev/null
+++ b/src/libide/code/ide-symbol-tree.h
@@ -0,0 +1,57 @@
+/* ide-symbol-tree.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SYMBOL_TREE (ide_symbol_tree_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeSymbolTree, ide_symbol_tree, IDE, SYMBOL_TREE, GObject)
+
+struct _IdeSymbolTreeInterface
+{
+  GTypeInterface parent;
+
+  guint          (*get_n_children) (IdeSymbolTree *self,
+                                    IdeSymbolNode *node);
+  IdeSymbolNode *(*get_nth_child)  (IdeSymbolTree *self,
+                                    IdeSymbolNode *node,
+                                    guint          nth);
+};
+
+IDE_AVAILABLE_IN_3_32
+guint          ide_symbol_tree_get_n_children (IdeSymbolTree *self,
+                                               IdeSymbolNode *node);
+IDE_AVAILABLE_IN_3_32
+IdeSymbolNode *ide_symbol_tree_get_nth_child  (IdeSymbolTree *self,
+                                               IdeSymbolNode *node,
+                                               guint          nth);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-symbol.c b/src/libide/code/ide-symbol.c
new file mode 100644
index 000000000..b1dc0b407
--- /dev/null
+++ b/src/libide/code/ide-symbol.c
@@ -0,0 +1,533 @@
+/* ide-symbol.c
+ *
+ * Copyright 2015-2019 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 "ide-symbol"
+
+#include "config.h"
+
+#include "ide-code-enums.h"
+#include "ide-location.h"
+#include "ide-symbol.h"
+
+typedef struct
+{
+  IdeSymbolKind   kind;
+  IdeSymbolFlags  flags;
+  gchar          *name;
+  IdeLocation    *location;
+  IdeLocation    *header_location;
+} IdeSymbolPrivate;
+
+enum {
+  PROP_0,
+  PROP_KIND,
+  PROP_FLAGS,
+  PROP_NAME,
+  PROP_LOCATION,
+  PROP_HEADER_LOCATION,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeSymbol, ide_symbol, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_symbol_finalize (GObject *object)
+{
+  IdeSymbol *self = (IdeSymbol *)object;
+  IdeSymbolPrivate *priv = ide_symbol_get_instance_private (self);
+
+  g_clear_pointer (&priv->name, g_free);
+  g_clear_object (&priv->location);
+  g_clear_object (&priv->header_location);
+
+  G_OBJECT_CLASS (ide_symbol_parent_class)->finalize (object);
+}
+
+static void
+ide_symbol_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  IdeSymbol *self = IDE_SYMBOL (object);
+
+  switch (prop_id)
+    {
+    case PROP_KIND:
+      g_value_set_enum (value, ide_symbol_get_kind (self));
+      break;
+
+    case PROP_FLAGS:
+      g_value_set_flags (value, ide_symbol_get_flags (self));
+      break;
+
+    case PROP_NAME:
+      g_value_set_string (value, ide_symbol_get_name (self));
+      break;
+
+    case PROP_LOCATION:
+      g_value_set_object (value, ide_symbol_get_location (self));
+      break;
+
+    case PROP_HEADER_LOCATION:
+      g_value_set_object (value, ide_symbol_get_header_location (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_symbol_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  IdeSymbol *self = IDE_SYMBOL (object);
+  IdeSymbolPrivate *priv = ide_symbol_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_KIND:
+      priv->kind = g_value_get_enum (value);
+      break;
+
+    case PROP_FLAGS:
+      priv->flags = g_value_get_flags (value);
+      break;
+
+    case PROP_NAME:
+      priv->name = g_value_dup_string (value);
+      break;
+
+    case PROP_LOCATION:
+      priv->location = g_value_dup_object (value);
+      break;
+
+    case PROP_HEADER_LOCATION:
+      priv->header_location = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_symbol_class_init (IdeSymbolClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_symbol_finalize;
+  object_class->get_property = ide_symbol_get_property;
+  object_class->set_property = ide_symbol_set_property;
+
+  properties [PROP_KIND] =
+    g_param_spec_enum ("kind",
+                       "Kind",
+                       "The kind of symbol",
+                       IDE_TYPE_SYMBOL_KIND,
+                       IDE_SYMBOL_KIND_NONE,
+                       (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FLAGS] =
+    g_param_spec_flags ("flags",
+                        "Flags",
+                        "The symbol flags",
+                        IDE_TYPE_SYMBOL_FLAGS,
+                        IDE_SYMBOL_FLAGS_NONE,
+                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "The name of the symbol",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LOCATION] =
+    g_param_spec_object ("location",
+                         "Location",
+                         "The location for the symbol",
+                         IDE_TYPE_LOCATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HEADER_LOCATION] =
+    g_param_spec_object ("header-location",
+                         "Header Location",
+                         "The header location for the symbol",
+                         IDE_TYPE_LOCATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_symbol_init (IdeSymbol *self)
+{
+}
+
+IdeSymbolKind
+ide_symbol_get_kind (IdeSymbol *self)
+{
+  IdeSymbolPrivate *priv = ide_symbol_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SYMBOL (self), 0);
+
+  return priv->kind;
+}
+
+IdeSymbolFlags
+ide_symbol_get_flags (IdeSymbol *self)
+{
+  IdeSymbolPrivate *priv = ide_symbol_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SYMBOL (self), 0);
+
+  return priv->flags;
+}
+
+const gchar *
+ide_symbol_get_name (IdeSymbol *self)
+{
+  IdeSymbolPrivate *priv = ide_symbol_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SYMBOL (self), NULL);
+
+  return priv->name;
+}
+
+/**
+ * ide_symbol_get_location:
+ * @self: a #IdeSymbol
+ *
+ * Gets the location, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeLocation or %NULL
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_symbol_get_location (IdeSymbol *self)
+{
+  IdeSymbolPrivate *priv = ide_symbol_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SYMBOL (self), NULL);
+
+  return priv->location;
+}
+
+/**
+ * ide_symbol_get_header_location:
+ * @self: a #IdeSymbol
+ *
+ * Gets the header location, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeLocation or %NULL
+ *
+ * Since: 3.32
+ */
+IdeLocation *
+ide_symbol_get_header_location (IdeSymbol *self)
+{
+  IdeSymbolPrivate *priv = ide_symbol_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SYMBOL (self), NULL);
+
+  return priv->header_location;
+}
+
+const gchar *
+ide_symbol_kind_get_icon_name (IdeSymbolKind kind)
+{
+  const gchar *icon_name = NULL;
+
+  switch (kind)
+    {
+    case IDE_SYMBOL_KIND_ALIAS:
+      icon_name = "lang-typedef-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_CLASS:
+      icon_name = "lang-class-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_ENUM:
+      icon_name = "lang-enum-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_ENUM_VALUE:
+      icon_name = "lang-enum-value-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_FUNCTION:
+      icon_name = "lang-function-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_PACKAGE:
+      icon_name = "lang-include-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_MACRO:
+      icon_name = "lang-define-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_METHOD:
+      icon_name = "lang-method-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_NAMESPACE:
+      icon_name = "lang-namespace-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_STRUCT:
+      icon_name = "lang-struct-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_FIELD:
+      icon_name = "lang-struct-field-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_SCALAR:
+    case IDE_SYMBOL_KIND_VARIABLE:
+      icon_name = "lang-variable-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UNION:
+      icon_name = "lang-union-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_ARRAY:
+    case IDE_SYMBOL_KIND_BOOLEAN:
+    case IDE_SYMBOL_KIND_CONSTANT:
+    case IDE_SYMBOL_KIND_CONSTRUCTOR:
+    case IDE_SYMBOL_KIND_FILE:
+    case IDE_SYMBOL_KIND_HEADER:
+    case IDE_SYMBOL_KIND_INTERFACE:
+    case IDE_SYMBOL_KIND_MODULE:
+    case IDE_SYMBOL_KIND_NUMBER:
+    case IDE_SYMBOL_KIND_NONE:
+    case IDE_SYMBOL_KIND_PROPERTY:
+    case IDE_SYMBOL_KIND_STRING:
+    case IDE_SYMBOL_KIND_TEMPLATE:
+    case IDE_SYMBOL_KIND_KEYWORD:
+      icon_name = NULL;
+      break;
+
+    case IDE_SYMBOL_KIND_UI_ATTRIBUTES:
+      icon_name = "ui-attributes-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_CHILD:
+      icon_name = "ui-child-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_ITEM:
+      icon_name = "ui-item-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_MENU:
+      icon_name = "ui-menu-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_OBJECT:
+      icon_name = "ui-object-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_PACKING:
+      icon_name = "ui-packing-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_PROPERTY:
+      icon_name = "ui-property-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_SECTION:
+      icon_name = "ui-section-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_SIGNAL:
+      icon_name = "ui-signal-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_STYLE:
+      icon_name = "ui-style-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_SUBMENU:
+      icon_name = "ui-submenu-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_TEMPLATE:
+      icon_name = "ui-template-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_XML_ATTRIBUTE:
+      icon_name = "xml-attribute-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_XML_CDATA:
+      icon_name = "xml-cdata-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_XML_COMMENT:
+      icon_name = "xml-comment-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_XML_DECLARATION:
+      icon_name = "xml-declaration-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_XML_ELEMENT:
+      icon_name = "xml-element-symbolic";
+      break;
+
+    case IDE_SYMBOL_KIND_UI_MENU_ATTRIBUTE:
+    case IDE_SYMBOL_KIND_UI_STYLE_CLASS:
+      icon_name = NULL;
+      break;
+
+    default:
+      icon_name = NULL;
+      break;
+    }
+
+  return icon_name;
+}
+
+/**
+ * ide_symbol_to_variant:
+ * @self: a #IdeSymbol
+ *
+ * This converts the symbol to a #GVariant that is suitable for passing
+ * across an IPC boundary.
+ *
+ * This function will never return a floating reference.
+ *
+ * Returns: (transfer full): a #GVariant
+ *
+ * Since: 3.32
+ */
+GVariant *
+ide_symbol_to_variant (IdeSymbol *self)
+{
+  IdeSymbolPrivate *priv = ide_symbol_get_instance_private (self);
+  GVariantBuilder builder;
+
+  g_return_val_if_fail (self != NULL, NULL);
+
+  g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
+
+  g_variant_builder_add_parsed (&builder, "{%s,<%i>}", "kind", priv->kind);
+  g_variant_builder_add_parsed (&builder, "{%s,<%i>}", "flags", priv->flags);
+  g_variant_builder_add_parsed (&builder, "{%s,<%s>}", "name", priv->name);
+
+  if (priv->location)
+    {
+      g_autoptr(GVariant) v = ide_location_to_variant (priv->location);
+      g_variant_builder_add_parsed (&builder, "{%s,%v}", "location", v);
+    }
+
+  if (priv->header_location)
+    {
+      g_autoptr(GVariant) v = ide_location_to_variant (priv->header_location);
+      g_variant_builder_add_parsed (&builder, "{%s,%v}", "header-location", v);
+    }
+
+  return g_variant_take_ref (g_variant_builder_end (&builder));
+}
+
+IdeSymbol *
+ide_symbol_new_from_variant (GVariant *variant)
+{
+  g_autoptr(GVariant) unboxed = NULL;
+  g_autoptr(GVariant) vdecl = NULL;
+  g_autoptr(GVariant) vdef = NULL;
+  g_autoptr(GVariant) vcanon = NULL;
+  g_autoptr(IdeLocation) decl = NULL;
+  g_autoptr(IdeLocation) def = NULL;
+  g_autoptr(IdeLocation) canon = NULL;
+  const gchar *name;
+  IdeSymbolKind kind;
+  IdeSymbolFlags flags;
+  IdeSymbol *self;
+  GVariantDict dict;
+
+  if (variant == NULL)
+    return NULL;
+
+  if (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARIANT))
+    variant = unboxed = g_variant_get_variant (variant);
+
+  if (!g_variant_is_of_type (variant, G_VARIANT_TYPE_VARDICT))
+    return NULL;
+
+  g_variant_dict_init (&dict, variant);
+
+  if (!g_variant_dict_lookup (&dict, "kind", "i", &kind))
+    kind = 0;
+
+  if (!g_variant_dict_lookup (&dict, "flags", "i", &flags))
+    flags = 0;
+
+  if (!g_variant_dict_lookup (&dict, "name", "&s", &name))
+    name = NULL;
+
+  vdef = g_variant_dict_lookup_value (&dict, "location", NULL);
+  vdecl = g_variant_dict_lookup_value (&dict, "header-location", NULL);
+
+  decl = ide_location_new_from_variant (vdecl);
+  def = ide_location_new_from_variant (vdef);
+
+  self = ide_symbol_new (name, kind, flags, decl, def);
+
+  g_variant_dict_clear (&dict);
+
+  return self;
+}
+
+/**
+ * ide_symbol_new:
+ *
+ * Returns: (transfer full): an #IdeSymbol
+ *
+ * Since: 3.32
+ */
+IdeSymbol *
+ide_symbol_new (const gchar    *name,
+                IdeSymbolKind   kind,
+                IdeSymbolFlags  flags,
+                IdeLocation    *location,
+                IdeLocation    *header_location)
+{
+  g_return_val_if_fail (!location || IDE_IS_LOCATION (location), NULL);
+  g_return_val_if_fail (!header_location || IDE_IS_LOCATION (header_location), NULL);
+
+  return g_object_new (IDE_TYPE_SYMBOL,
+                       "name", name,
+                       "kind", kind,
+                       "flags", flags,
+                       "location", location,
+                       "header-location", header_location,
+                       NULL);
+}
diff --git a/src/libide/code/ide-symbol.h b/src/libide/code/ide-symbol.h
new file mode 100644
index 000000000..e8f02360d
--- /dev/null
+++ b/src/libide/code/ide-symbol.h
@@ -0,0 +1,129 @@
+/* ide-symbol.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_SYMBOL_KIND_NONE,
+  IDE_SYMBOL_KIND_ALIAS,
+  IDE_SYMBOL_KIND_ARRAY,
+  IDE_SYMBOL_KIND_BOOLEAN,
+  IDE_SYMBOL_KIND_CLASS,
+  IDE_SYMBOL_KIND_CONSTANT,
+  IDE_SYMBOL_KIND_CONSTRUCTOR,
+  IDE_SYMBOL_KIND_ENUM,
+  IDE_SYMBOL_KIND_ENUM_VALUE,
+  IDE_SYMBOL_KIND_FIELD,
+  IDE_SYMBOL_KIND_FILE,
+  IDE_SYMBOL_KIND_FUNCTION,
+  IDE_SYMBOL_KIND_HEADER,
+  IDE_SYMBOL_KIND_INTERFACE,
+  IDE_SYMBOL_KIND_MACRO,
+  IDE_SYMBOL_KIND_METHOD,
+  IDE_SYMBOL_KIND_MODULE,
+  IDE_SYMBOL_KIND_NAMESPACE,
+  IDE_SYMBOL_KIND_NUMBER,
+  IDE_SYMBOL_KIND_PACKAGE,
+  IDE_SYMBOL_KIND_PROPERTY,
+  IDE_SYMBOL_KIND_SCALAR,
+  IDE_SYMBOL_KIND_STRING,
+  IDE_SYMBOL_KIND_STRUCT,
+  IDE_SYMBOL_KIND_TEMPLATE,
+  IDE_SYMBOL_KIND_UNION,
+  IDE_SYMBOL_KIND_VARIABLE,
+  IDE_SYMBOL_KIND_KEYWORD,
+  IDE_SYMBOL_KIND_UI_ATTRIBUTES,
+  IDE_SYMBOL_KIND_UI_CHILD,
+  IDE_SYMBOL_KIND_UI_ITEM,
+  IDE_SYMBOL_KIND_UI_MENU,
+  IDE_SYMBOL_KIND_UI_MENU_ATTRIBUTE,
+  IDE_SYMBOL_KIND_UI_OBJECT,
+  IDE_SYMBOL_KIND_UI_PACKING,
+  IDE_SYMBOL_KIND_UI_PROPERTY,
+  IDE_SYMBOL_KIND_UI_SECTION,
+  IDE_SYMBOL_KIND_UI_SIGNAL,
+  IDE_SYMBOL_KIND_UI_STYLE,
+  IDE_SYMBOL_KIND_UI_STYLE_CLASS,
+  IDE_SYMBOL_KIND_UI_SUBMENU,
+  IDE_SYMBOL_KIND_UI_TEMPLATE,
+  IDE_SYMBOL_KIND_XML_ATTRIBUTE,
+  IDE_SYMBOL_KIND_XML_DECLARATION,
+  IDE_SYMBOL_KIND_XML_ELEMENT,
+  IDE_SYMBOL_KIND_XML_COMMENT,
+  IDE_SYMBOL_KIND_XML_CDATA,
+} IdeSymbolKind;
+
+typedef enum
+{
+  IDE_SYMBOL_FLAGS_NONE          = 0,
+  IDE_SYMBOL_FLAGS_IS_STATIC     = 1 << 0,
+  IDE_SYMBOL_FLAGS_IS_MEMBER     = 1 << 1,
+  IDE_SYMBOL_FLAGS_IS_DEPRECATED = 1 << 2,
+  IDE_SYMBOL_FLAGS_IS_DEFINITION = 1 << 3
+} IdeSymbolFlags;
+
+#define IDE_TYPE_SYMBOL (ide_symbol_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeSymbol, ide_symbol, IDE, SYMBOL, GObject)
+
+struct _IdeSymbolClass
+{
+  GObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeSymbol      *ide_symbol_new                 (const gchar     *name,
+                                                IdeSymbolKind    kind,
+                                                IdeSymbolFlags   flags,
+                                                IdeLocation     *location,
+                                                IdeLocation     *header_location);
+IDE_AVAILABLE_IN_3_32
+IdeSymbolKind   ide_symbol_get_kind            (IdeSymbol       *self);
+IDE_AVAILABLE_IN_3_32
+IdeSymbolFlags  ide_symbol_get_flags           (IdeSymbol       *self);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_symbol_get_name            (IdeSymbol       *self);
+IDE_AVAILABLE_IN_3_32
+IdeLocation    *ide_symbol_get_location        (IdeSymbol       *self);
+IDE_AVAILABLE_IN_3_32
+IdeLocation    *ide_symbol_get_header_location (IdeSymbol       *self);
+IDE_AVAILABLE_IN_3_32
+IdeSymbol      *ide_symbol_new_from_variant    (GVariant        *variant);
+IDE_AVAILABLE_IN_3_32
+GVariant       *ide_symbol_to_variant          (IdeSymbol       *self);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_symbol_kind_get_icon_name  (IdeSymbolKind    kind);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-text-edit-private.h b/src/libide/code/ide-text-edit-private.h
new file mode 100644
index 000000000..0a9e1d106
--- /dev/null
+++ b/src/libide/code/ide-text-edit-private.h
@@ -0,0 +1,32 @@
+/* ide-text-edit-private.h
+ *
+ * Copyright 2016-2019 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 "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+void _ide_text_edit_apply   (IdeTextEdit *self,
+                             IdeBuffer   *buffer);
+void _ide_text_edit_prepare (IdeTextEdit *self,
+                             IdeBuffer   *buffer);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-text-edit.c b/src/libide/code/ide-text-edit.c
new file mode 100644
index 000000000..5259a2220
--- /dev/null
+++ b/src/libide/code/ide-text-edit.c
@@ -0,0 +1,347 @@
+/* ide-text-edit.c
+ *
+ * Copyright 2015-2019 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 "ide-text-edit"
+
+#include "config.h"
+
+#include "ide-buffer.h"
+#include "ide-text-edit.h"
+#include "ide-text-edit-private.h"
+#include "ide-location.h"
+#include "ide-range.h"
+
+typedef struct
+{
+  IdeRange       *range;
+  gchar          *text;
+
+  /* No references, cleared in apply */
+  GtkTextMark    *begin_mark;
+  GtkTextMark    *end_mark;
+} IdeTextEditPrivate;
+
+enum {
+  PROP_0,
+  PROP_RANGE,
+  PROP_TEXT,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTextEdit, ide_text_edit, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_text_edit_finalize (GObject *object)
+{
+  IdeTextEdit *self = (IdeTextEdit *)object;
+  IdeTextEditPrivate *priv = ide_text_edit_get_instance_private (self);
+
+  g_clear_object (&priv->range);
+  g_clear_pointer (&priv->text, g_free);
+
+  G_OBJECT_CLASS (ide_text_edit_parent_class)->finalize (object);
+}
+
+static void
+ide_text_edit_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  IdeTextEdit *self = IDE_TEXT_EDIT (object);
+
+  switch (prop_id)
+    {
+    case PROP_RANGE:
+      g_value_set_object (value, ide_text_edit_get_range (self));
+      break;
+
+    case PROP_TEXT:
+      g_value_set_string (value, ide_text_edit_get_text (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_text_edit_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeTextEdit *self = IDE_TEXT_EDIT (object);
+
+  switch (prop_id)
+    {
+    case PROP_RANGE:
+      ide_text_edit_set_range (self, g_value_get_object (value));
+      break;
+
+    case PROP_TEXT:
+      ide_text_edit_set_text (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_text_edit_class_init (IdeTextEditClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_text_edit_finalize;
+  object_class->get_property = ide_text_edit_get_property;
+  object_class->set_property = ide_text_edit_set_property;
+
+  properties [PROP_RANGE] =
+    g_param_spec_object ("range",
+                         "Range",
+                         "The range for the text edit",
+                         IDE_TYPE_RANGE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TEXT] =
+    g_param_spec_string ("text",
+                         "Text",
+                         "The text to replace",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_text_edit_init (IdeTextEdit *self)
+{
+}
+
+/**
+ * ide_text_edit_get_text:
+ * @self: a #IdeTextEdit
+ *
+ * Gets the text for the edit.
+ *
+ * Returns: (nullable): the text to replace, or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_text_edit_get_text (IdeTextEdit *self)
+{
+  IdeTextEditPrivate *priv = ide_text_edit_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEXT_EDIT (self), NULL);
+
+  return priv->text;
+}
+
+/**
+ * ide_text_edit_get_range:
+ * @self: a #IdeTextEdit
+ *
+ * Gets the range for the edit.
+ *
+ * Returns: (transfer none) (nullable): the range for the replacement, or %NULL
+ *
+ * Since: 3.32
+ */
+IdeRange *
+ide_text_edit_get_range (IdeTextEdit *self)
+{
+  IdeTextEditPrivate *priv = ide_text_edit_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEXT_EDIT (self), NULL);
+
+  return priv->range;
+}
+
+void
+_ide_text_edit_apply (IdeTextEdit *self,
+                      IdeBuffer   *buffer)
+{
+  IdeTextEditPrivate *priv = ide_text_edit_get_instance_private (self);
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_TEXT_EDIT (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &begin, priv->begin_mark);
+  gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &end, priv->end_mark);
+  gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &begin, &end);
+  gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &begin, priv->text, -1);
+  gtk_text_buffer_delete_mark (GTK_TEXT_BUFFER (buffer), priv->begin_mark);
+  gtk_text_buffer_delete_mark (GTK_TEXT_BUFFER (buffer), priv->end_mark);
+}
+
+void
+_ide_text_edit_prepare (IdeTextEdit *self,
+                        IdeBuffer   *buffer)
+{
+  IdeTextEditPrivate *priv = ide_text_edit_get_instance_private (self);
+  IdeLocation *begin;
+  IdeLocation *end;
+  GtkTextIter begin_iter;
+  GtkTextIter end_iter;
+
+  g_assert (IDE_IS_TEXT_EDIT (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  begin = ide_range_get_begin (priv->range);
+  end = ide_range_get_end (priv->range);
+
+  ide_buffer_get_iter_at_location (buffer, &begin_iter, begin);
+  ide_buffer_get_iter_at_location (buffer, &end_iter, end);
+
+  priv->begin_mark = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer),
+                                                  NULL,
+                                                  &begin_iter,
+                                                  TRUE);
+
+  priv->end_mark = gtk_text_buffer_create_mark (GTK_TEXT_BUFFER (buffer),
+                                                NULL,
+                                                &end_iter,
+                                                FALSE);
+}
+
+IdeTextEdit *
+ide_text_edit_new (IdeRange    *range,
+                   const gchar *text)
+{
+  g_return_val_if_fail (IDE_IS_RANGE (range), NULL);
+
+  return g_object_new (IDE_TYPE_TEXT_EDIT,
+                       "range", range,
+                       "text", text,
+                       NULL);
+}
+
+/**
+ * ide_text_edit_to_variant:
+ * @self: a #IdeTextEdit
+ *
+ * Creates a #GVariant to represent a text_edit.
+ *
+ * This function will never return a floating variant.
+ *
+ * Returns: (transfer full): a #GVariant
+ *
+ * Since: 3.32
+ */
+GVariant *
+ide_text_edit_to_variant (IdeTextEdit *self)
+{
+  IdeTextEditPrivate *priv = ide_text_edit_get_instance_private (self);
+  GVariantDict dict;
+  g_autoptr(GVariant) vrange = NULL;
+
+  g_return_val_if_fail (self != NULL, NULL);
+
+  g_variant_dict_init (&dict, NULL);
+
+  g_variant_dict_insert (&dict, "text", "s", priv->text ?: "");
+
+  if ((vrange = ide_range_to_variant (priv->range)))
+    g_variant_dict_insert_value (&dict, "range", vrange);
+
+  return g_variant_take_ref (g_variant_dict_end (&dict));
+}
+
+/**
+ * ide_text_edit_new_from_variant:
+ * @variant: (nullable): a #GVariant
+ *
+ * Creates a new #IdeTextEdit from the variant.
+ *
+ * If @variant is %NULL, %NULL is returned.
+ *
+ * Returns: (transfer full) (nullable): an #IdeTextEdit or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTextEdit *
+ide_text_edit_new_from_variant (GVariant *variant)
+{
+  g_autoptr(GVariant) unboxed = NULL;
+  g_autoptr(GVariant) vrange = NULL;
+  g_autoptr(IdeRange) range = NULL;
+  GVariantDict dict;
+  const gchar *text;
+  IdeTextEdit *self = NULL;
+
+  if (variant == NULL)
+    return NULL;
+
+  if (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARIANT))
+    variant = unboxed = g_variant_get_variant (variant);
+
+  g_variant_dict_init (&dict, variant);
+
+  if (!g_variant_dict_lookup (&dict, "text", "&s", &text))
+    text = "";
+
+  if ((vrange = g_variant_dict_lookup_value (&dict, "range", NULL)))
+    {
+      if (!(range = ide_range_new_from_variant (vrange)))
+        goto failed;
+    }
+
+  self = ide_text_edit_new (range, text);
+
+failed:
+
+  g_variant_dict_clear (&dict);
+
+  return self;
+}
+
+void
+ide_text_edit_set_text (IdeTextEdit *self,
+                        const gchar *text)
+{
+  IdeTextEditPrivate *priv = ide_text_edit_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEXT_EDIT (self));
+
+  if (!ide_str_equal0 (priv->text, text))
+    {
+      g_free (priv->text);
+      priv->text = g_strdup (text);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TEXT]);
+    }
+}
+
+void
+ide_text_edit_set_range (IdeTextEdit *self,
+                         IdeRange    *range)
+{
+  IdeTextEditPrivate *priv = ide_text_edit_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEXT_EDIT (self));
+
+  if (g_set_object (&priv->range, range))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RANGE]);
+}
diff --git a/src/libide/code/ide-text-edit.h b/src/libide/code/ide-text-edit.h
new file mode 100644
index 000000000..eb7a3287d
--- /dev/null
+++ b/src/libide/code/ide-text-edit.h
@@ -0,0 +1,64 @@
+/* ide-text-edit.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEXT_EDIT (ide_text_edit_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTextEdit, ide_text_edit, IDE, TEXT_EDIT, IdeObject)
+
+struct _IdeTextEditClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeTextEdit *ide_text_edit_new              (IdeRange    *range,
+                                             const gchar *text);
+IDE_AVAILABLE_IN_3_32
+IdeTextEdit *ide_text_edit_new_from_variant (GVariant    *variant);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_text_edit_get_text         (IdeTextEdit *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_text_edit_set_text         (IdeTextEdit *self,
+                                             const gchar *text);
+IDE_AVAILABLE_IN_3_32
+IdeRange    *ide_text_edit_get_range        (IdeTextEdit *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_text_edit_set_range        (IdeTextEdit *self,
+                                             IdeRange    *range);
+IDE_AVAILABLE_IN_3_32
+GVariant    *ide_text_edit_to_variant       (IdeTextEdit *self);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-text-iter.c b/src/libide/code/ide-text-iter.c
new file mode 100644
index 000000000..541b7df43
--- /dev/null
+++ b/src/libide/code/ide-text-iter.c
@@ -0,0 +1,1001 @@
+/* ide-text-iter.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-text-iter"
+
+#include "config.h"
+
+#include <gtk/gtk.h>
+#include <gtksourceview/gtksource.h>
+#include <string.h>
+
+#include "ide-text-iter.h"
+
+typedef enum
+{
+  SENTENCE_OK,
+  SENTENCE_PARA,
+  SENTENCE_FAILED,
+} SentenceStatus;
+
+enum
+{
+  CLASS_0,
+  CLASS_NEWLINE,
+  CLASS_SPACE,
+  CLASS_SPECIAL,
+  CLASS_WORD,
+};
+
+static int
+ide_text_word_classify (gunichar ch)
+{
+  switch (ch)
+    {
+    case ' ':
+    case '\t':
+    case '\n':
+      return CLASS_SPACE;
+
+    case '"': case '\'':
+    case '(': case ')':
+    case '{': case '}':
+    case '[': case ']':
+    case '<': case '>':
+    case '-': case '+': case '*': case '/':
+    case '!': case '@': case '#': case '$': case '%':
+    case '^': case '&': case ':': case ';': case '?':
+    case '|': case '=': case '\\': case '.': case ',':
+      return CLASS_SPECIAL;
+
+    case '_':
+    default:
+      return CLASS_WORD;
+    }
+}
+
+static int
+ide_text_word_classify_newline_stop (gunichar ch)
+{
+  switch (ch)
+    {
+    case ' ':
+    case '\t':
+      return CLASS_SPACE;
+
+    case '\n':
+      return CLASS_NEWLINE;
+
+    case '"': case '\'':
+    case '(': case ')':
+    case '{': case '}':
+    case '[': case ']':
+    case '<': case '>':
+    case '-': case '+': case '*': case '/':
+    case '!': case '@': case '#': case '$': case '%':
+    case '^': case '&': case ':': case ';': case '?':
+    case '|': case '=': case '\\': case '.': case ',':
+      return CLASS_SPECIAL;
+
+    case '_':
+    default:
+      return CLASS_WORD;
+    }
+}
+
+static int
+ide_text_WORD_classify (gunichar ch)
+{
+  if (g_unichar_isspace (ch))
+    return CLASS_SPACE;
+  return CLASS_WORD;
+}
+
+static int
+ide_text_WORD_classify_newline_stop (gunichar ch)
+{
+  if (ch == '\n')
+    return CLASS_NEWLINE;
+
+  if (g_unichar_isspace (ch))
+    return CLASS_SPACE;
+  return CLASS_WORD;
+}
+
+static gboolean
+ide_text_iter_line_is_empty (GtkTextIter *iter)
+{
+  return gtk_text_iter_starts_line (iter) && gtk_text_iter_ends_line (iter);
+}
+
+/**
+ * ide_text_iter_backward_paragraph_start:
+ * @iter: a #GtkTextIter
+ *
+ * Searches backwards until we find the beginning of a paragraph.
+ *
+ * Returns: %TRUE if we are not at the beginning of the buffer; otherwise %FALSE.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_text_iter_backward_paragraph_start (GtkTextIter *iter)
+{
+  g_return_val_if_fail (iter, FALSE);
+
+  /* Work our way past the current empty lines */
+  if (ide_text_iter_line_is_empty (iter))
+    while (ide_text_iter_line_is_empty (iter))
+      if (!gtk_text_iter_backward_line (iter))
+        return FALSE;
+
+  /* Now find first line that is empty */
+  while (!ide_text_iter_line_is_empty (iter))
+    if (!gtk_text_iter_backward_line (iter))
+      return FALSE;
+
+  return TRUE;
+}
+
+/**
+ * ide_text_iter_forward_paragraph_end:
+ * @iter: a #GtkTextIter
+ *
+ * Searches forward until the end of a paragraph has been hit.
+ *
+ * Returns: %TRUE if we are not at the end of the buffer; otherwise %FALSE.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_text_iter_forward_paragraph_end (GtkTextIter *iter)
+{
+  g_return_val_if_fail (iter, FALSE);
+
+  /* Work our way past the current empty lines */
+  if (ide_text_iter_line_is_empty (iter))
+    while (ide_text_iter_line_is_empty (iter))
+      if (!gtk_text_iter_forward_line (iter))
+        return FALSE;
+
+  /* Now find first line that is empty */
+  while (!ide_text_iter_line_is_empty (iter))
+    if (!gtk_text_iter_forward_line (iter))
+      return FALSE;
+
+  return TRUE;
+}
+
+static gboolean
+sentence_end_chars (gunichar ch,
+                    gpointer user_data)
+{
+  switch (ch)
+    {
+    case '!':
+    case '.':
+    case '?':
+      return TRUE;
+
+    default:
+      return FALSE;
+    }
+}
+
+static SentenceStatus
+ide_text_iter_backward_sentence_end (GtkTextIter *iter)
+{
+  GtkTextIter end_bounds;
+  GtkTextIter start_bounds;
+  gboolean found_para;
+
+  g_return_val_if_fail (iter, FALSE);
+
+  end_bounds = *iter;
+  start_bounds = *iter;
+  found_para = ide_text_iter_backward_paragraph_start (&start_bounds);
+
+  if (!found_para)
+    gtk_text_buffer_get_start_iter (gtk_text_iter_get_buffer (iter), &start_bounds);
+
+  while ((gtk_text_iter_compare (iter, &start_bounds) > 0) && gtk_text_iter_backward_char (iter))
+    {
+      if (gtk_text_iter_backward_find_char (iter, sentence_end_chars, NULL, &end_bounds))
+        {
+          GtkTextIter copy = *iter;
+
+          while (gtk_text_iter_forward_char (&copy) && (gtk_text_iter_compare (&copy, &end_bounds) < 0))
+            {
+              gunichar ch;
+
+              ch = gtk_text_iter_get_char (&copy);
+
+              switch (ch)
+                {
+                case ']':
+                case ')':
+                case '"':
+                case '\'':
+                  continue;
+
+                case ' ':
+                case '\n':
+                  *iter = copy;
+                  return SENTENCE_OK;
+
+                default:
+                  break;
+                }
+            }
+        }
+    }
+
+  *iter = start_bounds;
+
+  if (found_para)
+    return SENTENCE_PARA;
+
+  return SENTENCE_FAILED;
+}
+
+gboolean
+ide_text_iter_forward_sentence_end (GtkTextIter *iter)
+{
+  GtkTextIter end_bounds;
+  gboolean found_para;
+
+  g_return_val_if_fail (iter, FALSE);
+
+  end_bounds = *iter;
+  found_para = ide_text_iter_forward_paragraph_end (&end_bounds);
+
+  if (!found_para)
+    gtk_text_buffer_get_end_iter (gtk_text_iter_get_buffer (iter), &end_bounds);
+
+  while ((gtk_text_iter_compare (iter, &end_bounds) < 0) && gtk_text_iter_forward_char (iter))
+    {
+      if (gtk_text_iter_forward_find_char (iter, sentence_end_chars, NULL, &end_bounds))
+        {
+          GtkTextIter copy = *iter;
+
+          while (gtk_text_iter_forward_char (&copy) && (gtk_text_iter_compare (&copy, &end_bounds) < 0))
+            {
+              gunichar ch;
+              gboolean invalid = FALSE;
+
+              ch = gtk_text_iter_get_char (&copy);
+
+              switch (ch)
+                {
+                case ']':
+                case ')':
+                case '"':
+                case '\'':
+                  continue;
+
+                case ' ':
+                case '\n':
+                  *iter = copy;
+                  return SENTENCE_OK;
+
+                default:
+                  invalid = TRUE;
+                  break;
+                }
+
+              if (invalid)
+                break;
+            }
+        }
+    }
+
+  *iter = end_bounds;
+
+  if (found_para)
+    return SENTENCE_PARA;
+
+  return SENTENCE_FAILED;
+}
+
+gboolean
+ide_text_iter_backward_sentence_start (GtkTextIter *iter)
+{
+  GtkTextIter tmp;
+  SentenceStatus status;
+
+  g_return_val_if_fail (iter, FALSE);
+
+  tmp = *iter;
+  status = ide_text_iter_backward_sentence_end (&tmp);
+
+  switch (status)
+    {
+    case SENTENCE_PARA:
+    case SENTENCE_OK:
+      {
+        GtkTextIter copy = tmp;
+
+        /*
+         * try to work forward to first non-whitespace char.
+         * if we land where we started, discard the walk.
+         */
+        while (g_unichar_isspace (gtk_text_iter_get_char (&copy)))
+          if (!gtk_text_iter_forward_char (&copy))
+            break;
+        if (gtk_text_iter_compare (&copy, iter) < 0)
+          tmp = copy;
+        *iter = tmp;
+
+        return TRUE;
+      }
+
+    case SENTENCE_FAILED:
+    default:
+      gtk_text_buffer_get_start_iter (gtk_text_iter_get_buffer (iter), iter);
+      return FALSE;
+    }
+}
+
+static gboolean
+ide_text_iter_forward_classified_start (GtkTextIter  *iter,
+                                         gint        (*classify) (gunichar))
+{
+  gint begin_class;
+  gint cur_class;
+  gunichar ch;
+
+  g_assert (iter);
+
+  ch = gtk_text_iter_get_char (iter);
+  begin_class = classify (ch);
+
+  /* Move to the first non-whitespace character if necessary. */
+  if (begin_class == CLASS_SPACE)
+    {
+      for (;;)
+        {
+          if (!gtk_text_iter_forward_char (iter))
+            return FALSE;
+
+          ch = gtk_text_iter_get_char (iter);
+          cur_class = classify (ch);
+          if (cur_class != CLASS_SPACE)
+            return TRUE;
+        }
+    }
+
+  /* move to first character not at same class level. */
+  while (gtk_text_iter_forward_char (iter))
+    {
+      ch = gtk_text_iter_get_char (iter);
+      cur_class = classify (ch);
+
+      if (cur_class == CLASS_SPACE)
+        {
+          begin_class = CLASS_0;
+          continue;
+        }
+
+      if (cur_class != begin_class || cur_class == CLASS_NEWLINE)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+gboolean
+ide_text_iter_forward_word_start (GtkTextIter *iter,
+                                   gboolean     newline_stop)
+{
+  if (newline_stop)
+    return ide_text_iter_forward_classified_start (iter, ide_text_word_classify_newline_stop);
+  else
+    return ide_text_iter_forward_classified_start (iter, ide_text_word_classify);
+}
+
+gboolean
+ide_text_iter_forward_WORD_start (GtkTextIter *iter,
+                                   gboolean     newline_stop)
+{
+  if (newline_stop)
+    return ide_text_iter_forward_classified_start (iter, ide_text_WORD_classify_newline_stop);
+  else
+    return ide_text_iter_forward_classified_start (iter, ide_text_WORD_classify);
+}
+
+static gboolean
+ide_text_iter_forward_classified_end (GtkTextIter  *iter,
+                                       gint        (*classify) (gunichar))
+{
+  gunichar ch;
+  gint begin_class;
+  gint cur_class;
+
+  g_assert (iter);
+
+  if (!gtk_text_iter_forward_char (iter))
+    return FALSE;
+
+  /* If we are on space, walk to the start of the next word. */
+  ch = gtk_text_iter_get_char (iter);
+  if (classify (ch) == CLASS_SPACE)
+    if (!ide_text_iter_forward_classified_start (iter, classify))
+      return FALSE;
+
+  ch = gtk_text_iter_get_char (iter);
+  begin_class = classify (ch);
+
+  if (begin_class == CLASS_NEWLINE)
+    {
+      gtk_text_iter_backward_char (iter);
+      return TRUE;
+    }
+
+  for (;;)
+    {
+      if (!gtk_text_iter_forward_char (iter))
+        return FALSE;
+
+      ch = gtk_text_iter_get_char (iter);
+      cur_class = classify (ch);
+
+      if (cur_class != begin_class || cur_class == CLASS_NEWLINE)
+        {
+          gtk_text_iter_backward_char (iter);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+gboolean
+ide_text_iter_forward_word_end (GtkTextIter *iter,
+                                 gboolean     newline_stop)
+{
+  if (newline_stop)
+    return ide_text_iter_forward_classified_end (iter, ide_text_word_classify_newline_stop);
+  else
+    return ide_text_iter_forward_classified_end (iter, ide_text_word_classify);
+}
+
+gboolean
+ide_text_iter_forward_WORD_end (GtkTextIter *iter,
+                                 gboolean     newline_stop)
+{
+  if (newline_stop)
+    return ide_text_iter_forward_classified_end (iter, ide_text_WORD_classify_newline_stop);
+  else
+    return ide_text_iter_forward_classified_end (iter, ide_text_WORD_classify);
+}
+
+static gboolean
+ide_text_iter_backward_classified_end (GtkTextIter  *iter,
+                                        gint        (*classify) (gunichar))
+{
+  gunichar ch;
+  gint begin_class;
+  gint cur_class;
+
+  g_assert (iter);
+
+  ch = gtk_text_iter_get_char (iter);
+  begin_class = classify (ch);
+
+  if (begin_class == CLASS_NEWLINE)
+  {
+    gtk_text_iter_forward_char (iter);
+    return TRUE;
+  }
+
+  for (;;)
+    {
+      if (!gtk_text_iter_backward_char (iter))
+        return FALSE;
+
+      ch = gtk_text_iter_get_char (iter);
+      cur_class = classify (ch);
+
+      if (cur_class == CLASS_NEWLINE)
+      {
+        gtk_text_iter_forward_char (iter);
+        return TRUE;
+      }
+
+      /* reset begin_class if we hit space, we can take anything after that */
+      if (cur_class == CLASS_SPACE)
+        begin_class = CLASS_SPACE;
+
+      if (cur_class != begin_class && cur_class != CLASS_SPACE)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+gboolean
+ide_text_iter_backward_word_end (GtkTextIter *iter,
+                                  gboolean     newline_stop)
+{
+  if (newline_stop)
+    return ide_text_iter_backward_classified_end (iter, ide_text_word_classify_newline_stop);
+  else
+    return ide_text_iter_backward_classified_end (iter, ide_text_word_classify);
+}
+
+gboolean
+ide_text_iter_backward_WORD_end (GtkTextIter *iter,
+                                  gboolean     newline_stop)
+{
+  if (newline_stop)
+    return ide_text_iter_backward_classified_end (iter, ide_text_WORD_classify_newline_stop);
+  else
+    return ide_text_iter_backward_classified_end (iter, ide_text_WORD_classify);
+}
+
+static gboolean
+ide_text_iter_backward_classified_start (GtkTextIter  *iter,
+                                          gint        (*classify) (gunichar))
+{
+  gunichar ch;
+  gint begin_class;
+  gint cur_class;
+
+  g_assert (iter);
+
+  if (!gtk_text_iter_backward_char (iter))
+    return FALSE;
+
+  /* If we are on space, walk to the end of the previous word. */
+  ch = gtk_text_iter_get_char (iter);
+  if (classify (ch) == CLASS_SPACE)
+    if (!ide_text_iter_backward_classified_end (iter, classify))
+      return FALSE;
+
+  ch = gtk_text_iter_get_char (iter);
+  begin_class = classify (ch);
+  if (begin_class == CLASS_NEWLINE)
+  {
+    gtk_text_iter_forward_char (iter);
+    return TRUE;
+  }
+
+  for (;;)
+    {
+      if (!gtk_text_iter_backward_char (iter))
+        return FALSE;
+
+      ch = gtk_text_iter_get_char (iter);
+      cur_class = classify (ch);
+
+      if (cur_class != begin_class || cur_class == CLASS_NEWLINE)
+        {
+          gtk_text_iter_forward_char (iter);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+gboolean
+ide_text_iter_backward_word_start (GtkTextIter *iter,
+                                    gboolean     newline_stop)
+{
+  if (newline_stop)
+    return ide_text_iter_backward_classified_start (iter, ide_text_word_classify_newline_stop);
+  else
+    return ide_text_iter_backward_classified_start (iter, ide_text_word_classify);
+}
+
+gboolean
+ide_text_iter_backward_WORD_start (GtkTextIter *iter,
+                                    gboolean     newline_stop)
+{
+  if (newline_stop)
+    return ide_text_iter_backward_classified_start (iter, ide_text_WORD_classify_newline_stop);
+  else
+    return ide_text_iter_backward_classified_start (iter, ide_text_WORD_classify);
+}
+
+static gboolean
+matches_pred (GtkTextIter              *iter,
+              IdeTextIterCharPredicate  pred,
+              gpointer                  user_data)
+{
+  gint ch;
+
+  ch = gtk_text_iter_get_char (iter);
+
+  return (*pred) (iter, ch, user_data);
+}
+
+/**
+ * ide_text_iter_forward_find_char:
+ * @pred: (scope call): a callback to locate the char.
+ *
+ * Similar to gtk_text_iter_forward_find_char but
+ * lets us acces to the iter in the predicate.
+ *
+ * Returns: %TRUE if found
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_text_iter_forward_find_char (GtkTextIter              *iter,
+                                  IdeTextIterCharPredicate  pred,
+                                  gpointer                  user_data,
+                                  const GtkTextIter        *limit)
+{
+  g_return_val_if_fail (iter != NULL, FALSE);
+  g_return_val_if_fail (pred != NULL, FALSE);
+
+  if (limit && gtk_text_iter_compare (iter, limit) >= 0)
+    return FALSE;
+
+  while ((limit == NULL ||
+         !gtk_text_iter_equal (limit, iter)) &&
+         gtk_text_iter_forward_char (iter))
+    {
+      if (matches_pred (iter, pred, user_data))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+/* Similar to gtk_text_iter_backward_find_char but
+ * lets us acces to the iter in the predicate
+ */
+/**
+ * ide_text_iter_backward_find_char:
+ * @pred: (scope call):
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_text_iter_backward_find_char (GtkTextIter              *iter,
+                                   IdeTextIterCharPredicate  pred,
+                                   gpointer                  user_data,
+                                   const GtkTextIter        *limit)
+{
+  g_return_val_if_fail (iter != NULL, FALSE);
+  g_return_val_if_fail (pred != NULL, FALSE);
+
+  if (limit && gtk_text_iter_compare (iter, limit) <= 0)
+    return FALSE;
+
+  while ((limit == NULL ||
+         !gtk_text_iter_equal (limit, iter)) &&
+         gtk_text_iter_backward_char (iter))
+    {
+      if (matches_pred (iter, pred, user_data))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_text_iter_in_string:
+ * @iter: a #GtkTextIter indicating the position to check for.
+ * @str: A C type string.
+ * @str_start: (out): a #GtkTextIter returning the str start iter (if found).
+ * @str_end: (out): a #GtkTextIter returning the str end iter (if found).
+ * @include_str_bounds: %TRUE if we take into account the str limits as possible @iter positions.
+ *
+ * Check if @iter position in the buffer is part of @str.
+ *
+ * Returns: %TRUE if case of succes, %FALSE otherwise.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_text_iter_in_string (GtkTextIter *iter,
+                          const gchar *str,
+                          GtkTextIter *str_start,
+                          GtkTextIter *str_end,
+                          gboolean     include_str_bounds)
+{
+  gint len;
+  gint cursor_offset;
+  gint slice_left_pos;
+  gint slice_right_pos;
+  gint slice_len;
+  gint cursor_pos;
+  gint str_pos;
+  gint end_iter_offset;
+  gint res_offset;
+  guint count;
+  g_autofree gchar *slice = NULL;
+  const gchar *slice_ptr;
+  const gchar *str_ptr;
+  GtkTextIter slice_left = *iter;
+  GtkTextIter slice_right = *iter;
+  GtkTextIter end_iter;
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (!ide_str_empty0 (str), FALSE);
+
+  len = g_utf8_strlen (str, -1);
+  cursor_offset = gtk_text_iter_get_offset (iter);
+  slice_left_pos = MAX(0, cursor_offset - len);
+  gtk_text_iter_set_offset (&slice_left, slice_left_pos);
+
+  cursor_pos = cursor_offset - slice_left_pos;
+
+  gtk_text_buffer_get_end_iter (gtk_text_iter_get_buffer (iter), &end_iter);
+  end_iter_offset = gtk_text_iter_get_offset (&end_iter);
+
+  slice_right_pos = MIN(end_iter_offset, cursor_offset + len);
+  gtk_text_iter_set_offset (&slice_right, slice_right_pos);
+
+  slice = gtk_text_iter_get_slice (&slice_left, &slice_right);
+  slice_len = slice_right_pos - slice_left_pos;
+
+  slice_ptr = slice;
+  for (count = 0; count < slice_len - len + 1; count++)
+    {
+      str_ptr = strstr (slice_ptr, str);
+      if (str_ptr == NULL)
+        {
+          ret = FALSE;
+          break;
+        }
+
+      str_pos = g_utf8_pointer_to_offset (slice, str_ptr);
+
+      if ((!include_str_bounds && (str_pos < cursor_pos && cursor_pos < str_pos + len)) ||
+          (include_str_bounds && (str_pos <= cursor_pos && cursor_pos <= str_pos + len)))
+        {
+          ret = TRUE;
+          break;
+        }
+
+      slice_ptr = g_utf8_next_char (slice_ptr);
+    }
+
+  if (ret)
+    {
+      res_offset = slice_left_pos + str_pos + count;
+
+      if (str_start != NULL)
+        {
+          *str_start = *iter;
+          gtk_text_iter_set_offset (str_start, res_offset);
+        }
+
+      if (str_end != NULL)
+        {
+          *str_end = *iter;
+          gtk_text_iter_set_offset (str_end, res_offset + len);
+        }
+    }
+
+  return ret;
+}
+
+/**
+ * ide_text_iter_find_chars_backward:
+ * @iter: a #GtkTextIter indicating the start position to check for.
+ * @limit: (nullable): a #GtkTextIter indicating the limit of the search.
+ * @end: (out) (nullable): a #GtkTextIter returning the str end iter (if found).
+ * @str: A C type string.
+ * @only_at_start: %TRUE if the searched @str string should be constrained to start @iter position.
+ *
+ * Search backward for a @str string, starting at @iter position till @limit if there's one.
+ * In case of succes, @iter is updated to @str start position.
+ *
+ * Notice that for @str to be found, @iter need to be at least on the @str last char
+ *
+ * Returns: %TRUE if case of succes, %FALSE otherwise.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_text_iter_find_chars_backward (GtkTextIter *iter,
+                                    GtkTextIter *limit,
+                                    GtkTextIter *end,
+                                    const gchar *str,
+                                    gboolean     only_at_start)
+{
+  const gchar *base_str;
+  const gchar *str_limit;
+  GtkTextIter base_cursor;
+
+  g_return_val_if_fail (!ide_str_empty0 (str), FALSE);
+
+  if (!gtk_text_iter_backward_char (iter))
+    return FALSE;
+
+  str_limit = str;
+  base_str = str = str + strlen (str) - 1;
+  base_cursor = *iter;
+  do
+    {
+      *iter = base_cursor;
+      do
+        {
+          if (gtk_text_iter_get_char (iter) != g_utf8_get_char (str))
+            {
+              if (only_at_start)
+                return FALSE;
+              else
+                break;
+            }
+
+          str = g_utf8_find_prev_char (str_limit, str);
+          if (str == NULL)
+            {
+              if (end)
+                {
+                  *end = base_cursor;
+                  gtk_text_iter_forward_char (end);
+                }
+
+              return TRUE;
+            }
+
+        } while ((gtk_text_iter_backward_char (iter)));
+
+      if (gtk_text_iter_is_start (iter))
+        return FALSE;
+      else
+        str = base_str;
+
+    } while (gtk_text_iter_backward_char (&base_cursor));
+
+  return FALSE;
+}
+
+/**
+ * ide_text_iter_find_chars_forward:
+ * @iter: a #GtkTextIter indicating the start position to check for.
+ * @limit: (nullable): a #GtkTextIter indicating the limit of the search.
+ * @end: (out) (nullable): a #GtkTextIter returning the str end iter (if found).
+ * @str: A C type string.
+ * @only_at_start: %TRUE if the searched @str string should be constrained to start @iter position.
+ *
+ * Search forward for a @str string, starting at @iter position till @limit if there's one.
+ * In case of succes, @iter is updated to the found @str start position,
+ * otherwise, its position is undefined.
+ *
+ * Returns: %TRUE if case of succes, %FALSE otherwise.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_text_iter_find_chars_forward (GtkTextIter *iter,
+                                   GtkTextIter *limit,
+                                   GtkTextIter *end,
+                                   const gchar *str,
+                                   gboolean     only_at_start)
+{
+  const gchar *base_str;
+  const gchar *str_limit;
+  GtkTextIter base_cursor;
+  GtkTextIter real_limit;
+  gint str_char_len;
+  gint real_limit_offset;
+
+  g_return_val_if_fail (!ide_str_empty0 (str), FALSE);
+
+  if (limit == NULL)
+    {
+      real_limit = *iter;
+      gtk_text_iter_forward_to_end (&real_limit);
+    }
+  else
+    real_limit = *limit;
+
+  str_char_len = g_utf8_strlen (str, -1);
+  real_limit_offset = gtk_text_iter_get_offset (&real_limit) - str_char_len;
+  if (real_limit_offset < 0)
+    return FALSE;
+
+  gtk_text_iter_set_offset (&real_limit, real_limit_offset);
+  if (gtk_text_iter_compare(iter, &real_limit) > 0)
+    return FALSE;
+
+  str_limit = str + strlen (str);
+  base_str = str;
+  base_cursor = *iter;
+  do
+    {
+      *iter = base_cursor;
+      do
+        {
+          if (gtk_text_iter_get_char (iter) != g_utf8_get_char (str))
+            {
+              if (only_at_start)
+                return FALSE;
+              else
+                break;
+            }
+
+          str = g_utf8_find_next_char (str, str_limit);
+          if (str == NULL)
+            {
+              if (end)
+                {
+                  *end = *iter;
+                  gtk_text_iter_forward_char (end);
+                }
+
+              *iter = base_cursor;
+              return TRUE;
+            }
+
+        } while ((gtk_text_iter_forward_char (iter)));
+
+    } while (gtk_text_iter_compare(&base_cursor, &real_limit) < 0 &&
+             (str = base_str) &&
+             gtk_text_iter_forward_char (&base_cursor));
+
+  return FALSE;
+}
+
+static inline gboolean
+is_symbol_char (gunichar ch)
+{
+  return g_unichar_isalnum (ch) || (ch == '_');
+}
+
+gchar *
+ide_text_iter_current_symbol (const GtkTextIter *iter,
+                               GtkTextIter       *out_begin)
+{
+  GtkTextBuffer *buffer;
+  GtkTextIter end = *iter;
+  GtkTextIter begin = *iter;
+  gunichar ch = 0;
+
+  do
+    {
+      if (!gtk_text_iter_backward_char (&begin))
+        break;
+      ch = gtk_text_iter_get_char (&begin);
+    }
+  while (is_symbol_char (ch));
+
+  if (ch && !is_symbol_char (ch))
+    gtk_text_iter_forward_char (&begin);
+
+  buffer = gtk_text_iter_get_buffer (iter);
+
+  if (GTK_SOURCE_IS_BUFFER (buffer))
+    {
+      GtkSourceBuffer *gsb = GTK_SOURCE_BUFFER (buffer);
+
+      if (gtk_source_buffer_iter_has_context_class (gsb, &begin, "comment") ||
+          gtk_source_buffer_iter_has_context_class (gsb, &begin, "string") ||
+          gtk_source_buffer_iter_has_context_class (gsb, &end, "comment") ||
+          gtk_source_buffer_iter_has_context_class (gsb, &end, "string"))
+        return NULL;
+    }
+
+  if (gtk_text_iter_equal (&begin, &end))
+    return NULL;
+
+  if (out_begin != NULL)
+    *out_begin = begin;
+
+  return gtk_text_iter_get_slice (&begin, &end);
+}
diff --git a/src/libide/code/ide-text-iter.h b/src/libide/code/ide-text-iter.h
new file mode 100644
index 000000000..151cb6644
--- /dev/null
+++ b/src/libide/code/ide-text-iter.h
@@ -0,0 +1,102 @@
+/* ide-text-iter.h
+ *
+ * Copyright 2015-2019 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+/* Semi-public API */
+
+typedef gboolean (* IdeTextIterCharPredicate)    (GtkTextIter              *iter,
+                                                  gunichar                  ch,
+                                                  gpointer                  user_data);
+
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_forward_find_char        (GtkTextIter              *iter,
+                                                  IdeTextIterCharPredicate  pred,
+                                                  gpointer                  user_data,
+                                                  const GtkTextIter        *limit);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_backward_find_char       (GtkTextIter              *iter,
+                                                  IdeTextIterCharPredicate  pred,
+                                                  gpointer                  user_data,
+                                                  const GtkTextIter        *limit);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_forward_word_start       (GtkTextIter              *iter,
+                                                  gboolean                  newline_stop);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_forward_WORD_start       (GtkTextIter              *iter,
+                                                  gboolean                  newline_stop);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_forward_word_end         (GtkTextIter              *iter,
+                                                  gboolean                  newline_stop);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_forward_WORD_end         (GtkTextIter              *iter,
+                                                  gboolean                  newline_stop);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_backward_paragraph_start (GtkTextIter              *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_forward_paragraph_end    (GtkTextIter              *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_backward_sentence_start  (GtkTextIter              *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_forward_sentence_end     (GtkTextIter              *iter);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_backward_WORD_start      (GtkTextIter              *iter,
+                                                  gboolean                  newline_stop);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_backward_word_start      (GtkTextIter              *iter,
+                                                  gboolean                  newline_stop);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_backward_WORD_end        (GtkTextIter              *iter,
+                                                  gboolean                  newline_stop);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_backward_word_end        (GtkTextIter              *iter,
+                                                  gboolean                  newline_stop);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_in_string                (GtkTextIter              *iter,
+                                                  const gchar              *str,
+                                                  GtkTextIter              *str_start,
+                                                  GtkTextIter              *str_end,
+                                                  gboolean                  include_str_bounds);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_find_chars_backward      (GtkTextIter              *iter,
+                                                  GtkTextIter              *limit,
+                                                  GtkTextIter              *end,
+                                                  const gchar              *str,
+                                                  gboolean                  only_at_start);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_text_iter_find_chars_forward       (GtkTextIter              *iter,
+                                                  GtkTextIter              *limit,
+                                                  GtkTextIter              *end,
+                                                  const gchar              *str,
+                                                  gboolean                  only_at_start);
+IDE_AVAILABLE_IN_3_32
+gchar   *ide_text_iter_current_symbol           (const GtkTextIter        *iter,
+                                                  GtkTextIter              *out_begin);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-unsaved-file-private.h b/src/libide/code/ide-unsaved-file-private.h
new file mode 100644
index 000000000..99d8ba3e0
--- /dev/null
+++ b/src/libide/code/ide-unsaved-file-private.h
@@ -0,0 +1,32 @@
+/* ide-unsaved-file-private.h
+ *
+ * Copyright 2018-2019 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 "ide-unsaved-file.h"
+
+G_BEGIN_DECLS
+
+IdeUnsavedFile *_ide_unsaved_file_new (GFile       *file,
+                                       GBytes      *content,
+                                       const gchar *temp_path,
+                                       gint64       sequence);
+
+G_END_DECLS
diff --git a/src/libide/code/ide-unsaved-file.c b/src/libide/code/ide-unsaved-file.c
new file mode 100644
index 000000000..c371e51b3
--- /dev/null
+++ b/src/libide/code/ide-unsaved-file.c
@@ -0,0 +1,178 @@
+/* ide-unsaved-file.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-unsaved-file"
+
+#include "config.h"
+
+#include "ide-buffer.h"
+#include "ide-buffer-private.h"
+#include "ide-unsaved-file.h"
+#include "ide-unsaved-file-private.h"
+
+/*
+ * This type is meant to be created and then immutable after that.
+ * So you can create it from the main thread, and then pass it to
+ * any other thread to do the work.
+ */
+
+G_DEFINE_BOXED_TYPE (IdeUnsavedFile, ide_unsaved_file, ide_unsaved_file_ref, ide_unsaved_file_unref)
+
+struct _IdeUnsavedFile
+{
+  volatile gint  ref_count;
+  GBytes        *content;
+  GFile         *file;
+  gchar         *temp_path;
+  gint64         sequence;
+};
+
+IdeUnsavedFile *
+_ide_unsaved_file_new (GFile       *file,
+                       GBytes      *content,
+                       const gchar *temp_path,
+                       gint64       sequence)
+{
+  IdeUnsavedFile *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+  g_return_val_if_fail (content, NULL);
+
+  ret = g_slice_new0 (IdeUnsavedFile);
+  ret->ref_count = 1;
+  ret->file = g_object_ref (file);
+  ret->content = g_bytes_ref (content);
+  ret->sequence = sequence;
+  ret->temp_path = g_strdup (temp_path);
+
+  return ret;
+}
+
+const gchar *
+ide_unsaved_file_get_temp_path (IdeUnsavedFile *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  return self->temp_path;
+}
+
+gboolean
+ide_unsaved_file_persist (IdeUnsavedFile  *self,
+                          GCancellable    *cancellable,
+                          GError         **error)
+{
+  g_autoptr(GFile) file = NULL;
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (self != NULL, FALSE);
+  g_return_val_if_fail (self->ref_count > 0, FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  IDE_TRACE_MSG ("Saving draft to \"%s\"", self->temp_path);
+
+  file = g_file_new_for_path (self->temp_path);
+  ret = g_file_replace_contents (file,
+                                 g_bytes_get_data (self->content, NULL),
+                                 g_bytes_get_size (self->content),
+                                 NULL,
+                                 FALSE,
+                                 G_FILE_CREATE_REPLACE_DESTINATION,
+                                 NULL,
+                                 cancellable,
+                                 error);
+
+  IDE_RETURN (ret);
+}
+
+gint64
+ide_unsaved_file_get_sequence (IdeUnsavedFile *self)
+{
+  g_return_val_if_fail (self != NULL, -1);
+  g_return_val_if_fail (self->ref_count > 0, -1);
+
+  return self->sequence;
+}
+
+IdeUnsavedFile *
+ide_unsaved_file_ref (IdeUnsavedFile *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  g_atomic_int_inc (&self->ref_count);
+
+  return self;
+}
+
+void
+ide_unsaved_file_unref (IdeUnsavedFile *self)
+{
+  g_return_if_fail (self != NULL);
+  g_return_if_fail (self->ref_count > 0);
+
+  if (g_atomic_int_dec_and_test (&self->ref_count))
+    {
+      g_clear_pointer (&self->temp_path, g_free);
+      g_clear_pointer (&self->content, g_bytes_unref);
+      g_clear_object (&self->file);
+      g_slice_free (IdeUnsavedFile, self);
+    }
+}
+
+/**
+ * ide_unsaved_file_get_content:
+ * @self: an #IdeUnsavedFile.
+ *
+ * Gets the contents of the unsaved file.
+ *
+ * Returns: (transfer none): a #GBytes containing the unsaved file content.
+ *
+ * Since: 3.32
+ */
+GBytes *
+ide_unsaved_file_get_content (IdeUnsavedFile *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  return self->content;
+}
+
+/**
+ * ide_unsaved_file_get_file:
+ *
+ * Retrieves the underlying file represented by @self.
+ *
+ * Returns: (transfer none): a #GFile.
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_unsaved_file_get_file (IdeUnsavedFile *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  return self->file;
+}
diff --git a/src/libide/code/ide-unsaved-file.h b/src/libide/code/ide-unsaved-file.h
new file mode 100644
index 000000000..9e72bc65a
--- /dev/null
+++ b/src/libide/code/ide-unsaved-file.h
@@ -0,0 +1,54 @@
+/* ide-unsaved-file.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+GType           ide_unsaved_file_get_type      (void);
+IDE_AVAILABLE_IN_3_32
+IdeUnsavedFile *ide_unsaved_file_ref           (IdeUnsavedFile  *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_unsaved_file_unref         (IdeUnsavedFile  *self);
+IDE_AVAILABLE_IN_3_32
+GBytes         *ide_unsaved_file_get_content   (IdeUnsavedFile  *self);
+IDE_AVAILABLE_IN_3_32
+GFile          *ide_unsaved_file_get_file      (IdeUnsavedFile  *self);
+IDE_AVAILABLE_IN_3_32
+gint64          ide_unsaved_file_get_sequence  (IdeUnsavedFile  *self);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_unsaved_file_get_temp_path (IdeUnsavedFile  *self);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_unsaved_file_persist       (IdeUnsavedFile  *self,
+                                                GCancellable    *cancellable,
+                                                GError         **error);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeUnsavedFile, ide_unsaved_file_unref)
+
+G_END_DECLS
diff --git a/src/libide/code/ide-unsaved-files.c b/src/libide/code/ide-unsaved-files.c
new file mode 100644
index 000000000..19b4ae9a9
--- /dev/null
+++ b/src/libide/code/ide-unsaved-files.c
@@ -0,0 +1,1022 @@
+/* ide-unsaved-files.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-unsaved-files"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <glib/gstdio.h>
+#include <string.h>
+
+#include <libide-io.h>
+#include <libide-threading.h>
+
+#include "ide-unsaved-file.h"
+#include "ide-unsaved-file-private.h"
+#include "ide-unsaved-files.h"
+
+typedef struct
+{
+  gint64           sequence;
+  GFile           *file;
+  GBytes          *content;
+  gchar           *temp_path;
+  gint             temp_fd;
+  IdeUnsavedFiles *backptr;
+} UnsavedFile;
+
+struct _IdeUnsavedFiles
+{
+  IdeObject  parent_instance;
+  GMutex     mutex;
+  GPtrArray *unsaved_files;
+  gint64     sequence;
+  gchar     *project_id;
+};
+
+typedef struct
+{
+  GPtrArray *unsaved_files;
+  gchar     *drafts_directory;
+} AsyncState;
+
+G_DEFINE_TYPE (IdeUnsavedFiles, ide_unsaved_files, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_PROJECT_ID,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void ide_unsaved_files_update_locked (IdeUnsavedFiles *self,
+                                             GFile           *file,
+                                             GBytes          *content);
+
+static gchar *
+get_drafts_directory (IdeUnsavedFiles *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_UNSAVED_FILES (self));
+
+  return g_build_filename (g_get_user_data_dir (),
+                           ide_get_program_name (),
+                           "drafts",
+                           self->project_id,
+                           NULL);
+}
+
+static void
+async_state_free (gpointer data)
+{
+  AsyncState *state = data;
+
+  if (state != NULL)
+    {
+      g_clear_pointer (&state->drafts_directory, g_free);
+      g_clear_pointer (&state->unsaved_files, g_ptr_array_unref);
+      g_slice_free (AsyncState, state);
+    }
+}
+
+static void
+unsaved_file_free (gpointer data)
+{
+  UnsavedFile *uf = data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  if (uf != NULL)
+    {
+      g_clear_object (&uf->file);
+      g_clear_pointer (&uf->content, g_bytes_unref);
+
+      if (uf->temp_path != NULL)
+        {
+           g_unlink (uf->temp_path);
+           g_clear_pointer (&uf->temp_path, g_free);
+        }
+
+      if (uf->temp_fd != -1)
+        {
+          g_close (uf->temp_fd, NULL);
+          uf->temp_fd = -1;
+        }
+
+      g_slice_free (UnsavedFile, uf);
+    }
+}
+
+static UnsavedFile *
+unsaved_file_copy (const UnsavedFile *uf)
+{
+  UnsavedFile *copy;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (uf != NULL);
+
+  copy = g_slice_new0 (UnsavedFile);
+  copy->file = g_object_ref (uf->file);
+  copy->content = g_bytes_ref (uf->content);
+
+  return copy;
+}
+
+static gboolean
+unsaved_file_save (UnsavedFile  *uf,
+                   const gchar  *path,
+                   GError      **error)
+{
+  g_autoptr(GFile) file = NULL;
+
+  g_assert (uf != NULL);
+  g_assert (uf->content != NULL);
+  g_assert (path != NULL);
+
+  /*
+   * These files can be accessed by third-party programs. So we need to ensure
+   * those programs see either the old version of the file or the new version
+   * of the file. g_file_replace_contents() conveniently provides the atomic
+   * rename() for us.
+   */
+
+  file = g_file_new_for_path (path);
+
+  return g_file_replace_contents (file,
+                                  g_bytes_get_data (uf->content, NULL),
+                                  g_bytes_get_size (uf->content),
+                                  NULL,
+                                  FALSE,
+                                  G_FILE_CREATE_REPLACE_DESTINATION,
+                                  NULL,
+                                  NULL,
+                                  error);
+}
+
+static gchar *
+hash_uri (const gchar *uri)
+{
+  GChecksum *checksum;
+  gchar *ret;
+
+  g_assert (uri != NULL);
+
+  checksum = g_checksum_new (G_CHECKSUM_SHA1);
+  g_checksum_update (checksum, (guchar *)uri, strlen (uri));
+  ret = g_strdup (g_checksum_get_string (checksum));
+  g_checksum_free (checksum);
+
+  return ret;
+}
+
+static gchar *
+get_buffers_dir (IdeContext *context)
+{
+  g_assert (IDE_IS_CONTEXT (context));
+
+  return ide_context_cache_filename (context, "buffers", NULL);
+}
+
+static void
+ide_unsaved_files_save_worker (IdeTask      *task,
+                               gpointer      source_object,
+                               gpointer      task_data,
+                               GCancellable *cancellable)
+{
+  g_autofree gchar *manifest_path = NULL;
+  g_autoptr(GString) manifest = NULL;
+  g_autoptr(GError) write_error = NULL;
+  AsyncState *state = task_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_UNSAVED_FILES (source_object));
+  g_assert (state != NULL);
+  g_assert (state->drafts_directory != NULL);
+  g_assert (state->unsaved_files != NULL);
+
+  /* ensure that the directory exists */
+  if (g_mkdir_with_parents (state->drafts_directory, 0700) != 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 g_io_error_from_errno (errno),
+                                 "Failed to create drafts directory");
+      IDE_EXIT;
+    }
+
+  manifest = g_string_new (NULL);
+  manifest_path = g_build_filename (state->drafts_directory, "manifest", NULL);
+
+  for (guint i = 0; i < state->unsaved_files->len; i++)
+    {
+      UnsavedFile *uf = g_ptr_array_index (state->unsaved_files, i);
+      g_autoptr(GError) error = NULL;
+      g_autofree gchar *path = NULL;
+      g_autofree gchar *uri = NULL;
+      g_autofree gchar *hash = NULL;
+
+      uri = g_file_get_uri (uf->file);
+
+      IDE_TRACE_MSG ("saving draft for unsaved file \"%s\"", uri);
+
+      g_string_append_printf (manifest, "%s\n", uri);
+
+      hash = hash_uri (uri);
+      path = g_build_filename (state->drafts_directory, hash, NULL);
+
+      if (!unsaved_file_save (uf, path, &error))
+        ide_object_warning (source_object,
+                            /* translators: %s is replaced with the error message */
+                            _("Failed to save draft: %s"),
+                            error->message);
+    }
+
+  if (!g_file_set_contents (manifest_path, manifest->str, manifest->len, &write_error))
+    ide_task_return_error (task, g_steal_pointer (&write_error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static AsyncState *
+async_state_new (IdeUnsavedFiles *files)
+{
+  AsyncState *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_UNSAVED_FILES (files));
+
+  state = g_slice_new0 (AsyncState);
+  state->unsaved_files = g_ptr_array_new_with_free_func (unsaved_file_free);
+  state->drafts_directory = get_drafts_directory (files);
+
+  return state;
+}
+
+void
+ide_unsaved_files_save_async (IdeUnsavedFiles     *self,
+                              GCancellable        *cancellable,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  AsyncState *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_UNSAVED_FILES (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  state = async_state_new (self);
+
+  g_assert (state != NULL);
+  g_assert (state->unsaved_files != NULL);
+  g_assert (state->drafts_directory != NULL);
+
+  g_mutex_lock (&self->mutex);
+
+  for (guint i = 0; i < self->unsaved_files->len; i++)
+    {
+      UnsavedFile *uf = g_ptr_array_index (self->unsaved_files, i);
+      UnsavedFile *uf_copy = unsaved_file_copy (uf);
+
+      g_ptr_array_add (state->unsaved_files, uf_copy);
+    }
+
+  g_mutex_unlock (&self->mutex);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_unsaved_files_save_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_task_data (task, state, async_state_free);
+  ide_task_run_in_thread (task, ide_unsaved_files_save_worker);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_unsaved_files_save_finish (IdeUnsavedFiles  *files,
+                               GAsyncResult     *result,
+                               GError          **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_UNSAVED_FILES (files), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_unsaved_files_restore_worker (IdeTask      *task,
+                                  gpointer      source_object,
+                                  gpointer      task_data,
+                                  GCancellable *cancellable)
+{
+  AsyncState *state = task_data;
+  g_autofree gchar *manifest_contents = NULL;
+  g_autofree gchar *manifest_path = NULL;
+  g_autoptr(GError) read_error = NULL;
+  IdeLineReader reader;
+  gchar *line;
+  gsize line_len;
+  gsize len;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_UNSAVED_FILES (source_object));
+  g_assert (state != NULL);
+
+  manifest_path = g_build_filename (state->drafts_directory, "manifest", NULL);
+
+  g_debug ("Loading drafts manifest %s", manifest_path);
+
+  if (!g_file_test (manifest_path, G_FILE_TEST_IS_REGULAR))
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  if (!g_file_get_contents (manifest_path, &manifest_contents, &len, &read_error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&read_error));
+      return;
+    }
+
+  if (len > G_MAXSSIZE)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NO_SPACE,
+                                 "File is too large to load");
+      return;
+    }
+
+  ide_line_reader_init (&reader, manifest_contents, len);
+
+  while (NULL != (line = ide_line_reader_next (&reader, &line_len)))
+    {
+      g_autoptr(GFile) file = NULL;
+      g_autoptr(GError) error = NULL;
+      g_autofree gchar *contents = NULL;
+      g_autofree gchar *hash = NULL;
+      g_autofree gchar *path = NULL;
+      UnsavedFile *unsaved;
+      gsize data_len = 0;
+
+      line[line_len] = '\0';
+
+      if (ide_str_empty0 (line))
+        continue;
+
+      file = g_file_new_for_uri (line);
+      if (file == NULL || !g_file_query_exists (file, NULL))
+        continue;
+
+      hash = hash_uri (line);
+      path = g_build_filename (state->drafts_directory, hash, NULL);
+
+      g_debug ("Loading draft for \"%s\" from \"%s\"", line, path);
+
+      if (!g_file_get_contents (path, &contents, &data_len, &error))
+        {
+          ide_object_warning (source_object,
+                              /* translators: the first %s is the path, th second is the error message */
+                              "Failed to load draft for %s: %s",
+                              line, error->message);
+          continue;
+        }
+
+      unsaved = g_slice_new0 (UnsavedFile);
+      unsaved->file = g_object_ref (file);
+      unsaved->content = g_bytes_new_take (g_steal_pointer (&contents), data_len);
+
+      g_ptr_array_add (state->unsaved_files, g_steal_pointer (&unsaved));
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+void
+ide_unsaved_files_restore_async (IdeUnsavedFiles     *files,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  AsyncState *state;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_UNSAVED_FILES (files));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (callback != NULL);
+
+  state = async_state_new (files);
+
+  task = ide_task_new (files, cancellable, callback, user_data);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_task_data (task, state, async_state_free);
+  ide_task_run_in_thread (task, ide_unsaved_files_restore_worker);
+}
+
+gboolean
+ide_unsaved_files_restore_finish (IdeUnsavedFiles  *self,
+                                  GAsyncResult     *result,
+                                  GError          **error)
+{
+  AsyncState *state;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_UNSAVED_FILES (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  state = ide_task_get_task_data (IDE_TASK (result));
+  g_assert (state != NULL);
+  g_assert (state->unsaved_files != NULL);
+
+  g_mutex_lock (&self->mutex);
+
+  for (guint i = 0; i < state->unsaved_files->len; i++)
+    {
+      const UnsavedFile *uf = g_ptr_array_index (state->unsaved_files, i);
+      ide_unsaved_files_update_locked (self, uf->file, uf->content);
+    }
+
+  g_mutex_unlock (&self->mutex);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_unsaved_files_move_to_front_locked (IdeUnsavedFiles *self,
+                                        guint            index)
+{
+  UnsavedFile *new_front;
+  UnsavedFile *old_front;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_UNSAVED_FILES (self));
+
+  if (index == 0)
+    return;
+
+  new_front = g_ptr_array_index (self->unsaved_files, index);
+  old_front = g_ptr_array_index (self->unsaved_files, 0);
+
+  /*
+   * We could shift all these items down, but it probably isn't worth
+   * the effort. We will just move-to-front after a miss and ping
+   * pong the old item back to the front.
+   */
+  self->unsaved_files->pdata[0] = new_front;
+  self->unsaved_files->pdata[index] = old_front;
+}
+
+static void
+ide_unsaved_files_remove_draft_locked (IdeUnsavedFiles *self,
+                                       GFile           *file)
+{
+  g_autofree gchar *drafts_directory = NULL;
+  g_autofree gchar *uri = NULL;
+  g_autofree gchar *hash = NULL;
+  g_autofree gchar *path = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_UNSAVED_FILES (self));
+  g_assert (G_IS_FILE (file));
+
+  drafts_directory = get_drafts_directory (self);
+  uri = g_file_get_uri (file);
+  hash = hash_uri (uri);
+  path = g_build_filename (drafts_directory, hash, NULL);
+
+  g_debug ("Removing draft for \"%s\"", uri);
+
+  g_unlink (path);
+
+  IDE_EXIT;
+}
+
+void
+ide_unsaved_files_remove (IdeUnsavedFiles *self,
+                          GFile           *file)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_UNSAVED_FILES (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  g_mutex_lock (&self->mutex);
+
+  for (guint i = 0; i < self->unsaved_files->len; i++)
+    {
+      const UnsavedFile *unsaved = g_ptr_array_index (self->unsaved_files, i);
+
+      if (g_file_equal (file, unsaved->file))
+        {
+          ide_unsaved_files_remove_draft_locked (self, file);
+          g_ptr_array_remove_index_fast (self->unsaved_files, i);
+          break;
+        }
+    }
+
+  g_mutex_unlock (&self->mutex);
+
+  IDE_EXIT;
+}
+
+static void
+setup_tempfile (IdeContext  *context,
+                GFile       *file,
+                gint        *temp_fd,
+                gchar      **temp_path_out)
+{
+  g_autofree gchar *tmpdir = NULL;
+  g_autofree gchar *name = NULL;
+  g_autofree gchar *shortname = NULL;
+  g_autofree gchar *tmpl_path = NULL;
+  const gchar *suffix;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONTEXT (context));
+  g_assert (G_IS_FILE (file));
+  g_assert (temp_fd != NULL);
+  g_assert (temp_path_out != NULL);
+
+  *temp_fd = -1;
+  *temp_path_out = NULL;
+
+  /* Get the suffix for the filename so that we can add it as the suffix to
+   * our temporary file. That increases the chance that anything sniffing
+   * content-type will work correctly.
+   */
+  name = g_file_get_basename (file);
+  suffix = strrchr (name, '.') ?: "";
+
+  /*
+   * We want to create our tempfile within a custom directory. It turns out
+   * that g_mkstemp_full() does not do directory checks in the template, so
+   * we can pass our own directory to be used instead of $TMPDIR. We need to
+   * control the directory so that we can ensure we have one that is available
+   * to both the flatpak runtime and the host system.
+   */
+  tmpdir = get_buffers_dir (context);
+  shortname = g_strdup_printf ("buffer-XXXXXX%s", suffix);
+  tmpl_path = g_build_filename (tmpdir, shortname, NULL);
+
+  /* Ensure the directory exists */
+  if (!g_file_test (tmpdir, G_FILE_TEST_IS_DIR))
+    g_mkdir_with_parents (tmpdir, 0750);
+
+  /* Now try to open our custom tempfile in the directory we control. */
+  *temp_fd = g_mkstemp_full (tmpl_path, O_RDWR, 0664);
+  if (*temp_fd != -1)
+    *temp_path_out = g_steal_pointer (&tmpl_path);
+}
+
+static void
+ide_unsaved_files_update_locked (IdeUnsavedFiles *self,
+                                 GFile           *file,
+                                 GBytes          *content)
+{
+  UnsavedFile *unsaved;
+  IdeContext *context;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_UNSAVED_FILES (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  if (content == NULL)
+    {
+      ide_unsaved_files_remove (self, file);
+      return;
+    }
+
+  self->sequence++;
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  for (guint i = 0; i < self->unsaved_files->len; i++)
+    {
+      unsaved = g_ptr_array_index (self->unsaved_files, i);
+
+      if (g_file_equal (file, unsaved->file))
+        {
+          if (content != unsaved->content)
+            {
+              g_clear_pointer (&unsaved->content, g_bytes_unref);
+              unsaved->content = g_bytes_ref (content);
+              unsaved->sequence = self->sequence;
+            }
+
+          /*
+           * A file that get's updated is the most likely to get updated on
+           * the next attempt. Therefore, we will simply move this entry to
+           * the beginning of the array to increase its chances of being the
+           * first entry we check.
+           */
+          if (i > 0)
+            ide_unsaved_files_move_to_front_locked (self, i);
+
+          return;
+        }
+    }
+
+  unsaved = g_slice_new0 (UnsavedFile);
+  unsaved->file = g_object_ref (file);
+  unsaved->content = g_bytes_ref (content);
+  unsaved->sequence = self->sequence;
+  setup_tempfile (context, file, &unsaved->temp_fd, &unsaved->temp_path);
+
+  g_ptr_array_add (self->unsaved_files, unsaved);
+}
+
+void
+ide_unsaved_files_update (IdeUnsavedFiles *self,
+                          GFile           *file,
+                          GBytes          *content)
+{
+  g_assert (IDE_IS_UNSAVED_FILES (self));
+  g_assert (G_IS_FILE (file));
+
+  g_mutex_lock (&self->mutex);
+  ide_unsaved_files_update_locked (self, file, content);
+  g_mutex_unlock (&self->mutex);
+}
+
+/**
+ * ide_unsaved_files_to_array:
+ * @self: an #IdeUnsavedFiles
+ *
+ * This retrieves all of the unsaved file buffers known to the context.
+ * These are handy if you need to pass modified state to parsers such as
+ * clang.
+ *
+ * Call g_ptr_array_unref() on the resulting #GPtrArray when no longer in use.
+ *
+ * If you would like to hold onto an unsaved file instance, call
+ * ide_unsaved_file_ref() to increment its reference count.
+ *
+ * Returns: (transfer full) (element-type Ide.UnsavedFile): a #GPtrArray
+ *   containing #IdeUnsavedFile elements.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_unsaved_files_to_array (IdeUnsavedFiles *self)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_UNSAVED_FILES (self), NULL);
+
+  ar = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_unsaved_file_unref);
+
+  g_mutex_lock (&self->mutex);
+
+  for (guint i = 0; i < self->unsaved_files->len; i++)
+    {
+      const UnsavedFile *uf = g_ptr_array_index (self->unsaved_files, i);
+      g_autoptr(IdeUnsavedFile) item = NULL;
+
+      item = _ide_unsaved_file_new (uf->file,
+                                    uf->content,
+                                    uf->temp_path,
+                                    uf->sequence);
+      g_ptr_array_add (ar, g_steal_pointer (&item));
+    }
+
+  g_mutex_unlock (&self->mutex);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ar);
+}
+
+gboolean
+ide_unsaved_files_contains (IdeUnsavedFiles *self,
+                            GFile           *file)
+{
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_UNSAVED_FILES (self), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+
+  g_mutex_lock (&self->mutex);
+
+  for (guint i = 0; i < self->unsaved_files->len; i++)
+    {
+      UnsavedFile *uf = g_ptr_array_index (self->unsaved_files, i);
+
+      if (g_file_equal (uf->file, file))
+        {
+          ret = TRUE;
+          break;
+        }
+    }
+
+  g_mutex_unlock (&self->mutex);
+
+  return ret;
+}
+
+/**
+ * ide_unsaved_files_get_unsaved_file:
+ *
+ * Retrieves the unsaved file content for a particular file. If no unsaved
+ * file content is registered, %NULL is returned.
+ *
+ * Returns: (nullable) (transfer full): An #IdeUnsavedFile or %NULL.
+ *
+ * Thread safety: you may call this from any thread, as long as you
+ *   hold a reference to @self.
+ *
+ * Since: 3.32
+ */
+IdeUnsavedFile *
+ide_unsaved_files_get_unsaved_file (IdeUnsavedFiles *self,
+                                    GFile           *file)
+{
+  IdeUnsavedFile *ret = NULL;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_UNSAVED_FILES (self), NULL);
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    g_autofree gchar *path = g_file_get_path (file);
+    IDE_TRACE_MSG ("%s", path);
+  }
+#endif
+
+  g_mutex_lock (&self->mutex);
+
+  for (guint i = 0; i < self->unsaved_files->len; i++)
+    {
+      const UnsavedFile *uf = g_ptr_array_index (self->unsaved_files, i);
+
+      if (g_file_equal (uf->file, file))
+        {
+          ret = _ide_unsaved_file_new (uf->file, uf->content, uf->temp_path, uf->sequence);
+          break;
+        }
+    }
+
+  g_mutex_unlock (&self->mutex);
+
+  IDE_RETURN (ret);
+}
+
+gint64
+ide_unsaved_files_get_sequence (IdeUnsavedFiles *self)
+{
+  gint64 ret;
+
+  g_return_val_if_fail (IDE_IS_UNSAVED_FILES (self), -1);
+
+  g_mutex_lock (&self->mutex);
+  ret = self->sequence;
+  g_mutex_unlock (&self->mutex);
+
+  return ret;
+}
+
+static void
+ide_unsaved_files_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeUnsavedFiles *self = IDE_UNSAVED_FILES (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      g_value_set_string (value, self->project_id);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_unsaved_files_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeUnsavedFiles *self = IDE_UNSAVED_FILES (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      self->project_id = g_value_dup_string (value);
+      g_assert (self->project_id != NULL);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_unsaved_files_finalize (GObject *object)
+{
+  IdeUnsavedFiles *self = (IdeUnsavedFiles *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_pointer (&self->unsaved_files, g_ptr_array_unref);
+  g_clear_pointer (&self->project_id, g_free);
+  g_mutex_clear (&self->mutex);
+
+  G_OBJECT_CLASS (ide_unsaved_files_parent_class)->finalize (object);
+}
+
+static void
+ide_unsaved_files_class_init (IdeUnsavedFilesClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_unsaved_files_finalize;
+  object_class->get_property = ide_unsaved_files_get_property;
+  object_class->set_property = ide_unsaved_files_set_property;
+
+  properties [PROP_PROJECT_ID] =
+    g_param_spec_string ("project-id",
+                         "Project Id",
+                         "The identifier for the project",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_unsaved_files_init (IdeUnsavedFiles *self)
+{
+  g_mutex_init (&self->mutex);
+  self->unsaved_files = g_ptr_array_new_with_free_func (unsaved_file_free);
+}
+
+void
+ide_unsaved_files_clear (IdeUnsavedFiles *self)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_UNSAVED_FILES (self));
+
+  ar = ide_unsaved_files_to_array (self);
+
+  IDE_PTR_ARRAY_SET_FREE_FUNC (ar, ide_unsaved_file_unref);
+
+  g_mutex_lock (&self->mutex);
+
+  for (guint i = 0; i < ar->len; i++)
+    {
+      IdeUnsavedFile *uf = g_ptr_array_index (ar, i);
+      GFile *file = ide_unsaved_file_get_file (uf);
+
+      ide_unsaved_files_remove (self, file);
+    }
+
+  g_mutex_unlock (&self->mutex);
+}
+
+static void
+ide_unsaved_files_reap_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  DzlDirectoryReaper *reaper = (DzlDirectoryReaper *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (DZL_IS_DIRECTORY_REAPER (reaper));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!dzl_directory_reaper_execute_finish (reaper, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+void
+ide_unsaved_files_reap_async (IdeUnsavedFiles     *self,
+                              GCancellable        *cancellable,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(DzlDirectoryReaper) reaper = NULL;
+  g_autoptr(GFile) buffersdir = NULL;
+  g_autofree gchar *path = NULL;
+  IdeContext *context;
+
+  g_return_if_fail (IDE_IS_UNSAVED_FILES (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_unsaved_files_reap_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  g_return_if_fail (context != NULL);
+
+  reaper = dzl_directory_reaper_new ();
+  path = get_buffers_dir (context);
+  buffersdir = g_file_new_for_path (path);
+
+  dzl_directory_reaper_add_directory (reaper, buffersdir, G_TIME_SPAN_DAY);
+
+  /* Now cleanup the old files */
+  dzl_directory_reaper_execute_async (reaper,
+                                      cancellable,
+                                      ide_unsaved_files_reap_cb,
+                                      g_steal_pointer (&task));
+}
+
+gboolean
+ide_unsaved_files_reap_finish (IdeUnsavedFiles  *self,
+                               GAsyncResult     *result,
+                               GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_UNSAVED_FILES (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_unsaved_files_from_context:
+ * @context: an #IdeContext
+ *
+ * Gets the unsaved files object for @context.
+ *
+ * Returns: (transfer none): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeUnsavedFiles *
+ide_unsaved_files_from_context (IdeContext *context)
+{
+  IdeUnsavedFiles *self;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  ide_object_lock (IDE_OBJECT (context));
+  self = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_UNSAVED_FILES);
+  if (self == NULL)
+    {
+      g_autofree gchar *project_id = ide_context_dup_project_id (context);
+      self = g_object_new (IDE_TYPE_UNSAVED_FILES,
+                           "project-id", project_id,
+                           NULL);
+      ide_object_append (IDE_OBJECT (context), IDE_OBJECT (self));
+    }
+  ide_object_unlock (IDE_OBJECT (context));
+
+  /* Looks unsafe because we get a full ref back */
+  g_object_unref (self);
+
+  return self;
+}
diff --git a/src/libide/code/ide-unsaved-files.h b/src/libide/code/ide-unsaved-files.h
new file mode 100644
index 000000000..0759d7cb5
--- /dev/null
+++ b/src/libide/code/ide-unsaved-files.h
@@ -0,0 +1,87 @@
+/* ide-unsaved-files.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_CODE_INSIDE) && !defined (IDE_CODE_COMPILATION)
+# error "Only <libide-code.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-code-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_UNSAVED_FILES (ide_unsaved_files_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeUnsavedFiles, ide_unsaved_files, IDE, UNSAVED_FILES, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeUnsavedFiles *ide_unsaved_files_from_context     (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_update           (IdeUnsavedFiles      *self,
+                                                     GFile                *file,
+                                                     GBytes               *content);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_remove           (IdeUnsavedFiles      *self,
+                                                     GFile                *file);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_save_async       (IdeUnsavedFiles      *files,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_unsaved_files_save_finish      (IdeUnsavedFiles      *files,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_restore_async    (IdeUnsavedFiles      *files,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_unsaved_files_restore_finish   (IdeUnsavedFiles      *files,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+IDE_AVAILABLE_IN_3_32
+GPtrArray       *ide_unsaved_files_to_array         (IdeUnsavedFiles      *self);
+IDE_AVAILABLE_IN_3_32
+gint64           ide_unsaved_files_get_sequence     (IdeUnsavedFiles      *files);
+IDE_AVAILABLE_IN_3_32
+IdeUnsavedFile  *ide_unsaved_files_get_unsaved_file (IdeUnsavedFiles      *self,
+                                                    GFile                *file);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_clear            (IdeUnsavedFiles      *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_unsaved_files_contains         (IdeUnsavedFiles      *self,
+                                                    GFile                *file);
+IDE_AVAILABLE_IN_3_32
+void             ide_unsaved_files_reap_async       (IdeUnsavedFiles      *self,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_unsaved_files_reap_finish      (IdeUnsavedFiles      *self,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/code/libide-code.gresource.xml b/src/libide/code/libide-code.gresource.xml
new file mode 100644
index 000000000..22ebf3183
--- /dev/null
+++ b/src/libide/code/libide-code.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/builder/file-settings">
+    <file>defaults.ini</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/code/libide-code.h b/src/libide/code/libide-code.h
new file mode 100644
index 000000000..e35570a3c
--- /dev/null
+++ b/src/libide/code/libide-code.h
@@ -0,0 +1,70 @@
+/* libide-code.h
+ *
+ * Copyright 2018-2019 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-core.h>
+#include <libide-io.h>
+#include <libide-threading.h>
+
+G_BEGIN_DECLS
+
+#define IDE_CODE_INSIDE
+
+#include "ide-code-enums.h"
+#include "ide-code-types.h"
+
+#include "ide-buffer.h"
+#include "ide-buffer-addin.h"
+#include "ide-buffer-change-monitor.h"
+#include "ide-buffer-manager.h"
+#include "ide-code-index-entries.h"
+#include "ide-code-index-entry.h"
+#include "ide-code-indexer.h"
+#include "ide-diagnostic.h"
+#include "ide-diagnostic-provider.h"
+#include "ide-diagnostics.h"
+#include "ide-diagnostics-manager.h"
+#include "ide-file-settings.h"
+#include "ide-formatter-options.h"
+#include "ide-formatter.h"
+#include "ide-highlight-engine.h"
+#include "ide-highlight-index.h"
+#include "ide-highlighter.h"
+#include "ide-indent-style.h"
+#include "ide-language.h"
+#include "ide-location.h"
+#include "ide-range.h"
+#include "ide-rename-provider.h"
+#include "ide-source-iter.h"
+#include "ide-source-style-scheme.h"
+#include "ide-spaces-style.h"
+#include "ide-symbol-node.h"
+#include "ide-symbol-resolver.h"
+#include "ide-symbol-tree.h"
+#include "ide-symbol.h"
+#include "ide-text-edit.h"
+#include "ide-text-iter.h"
+#include "ide-unsaved-file.h"
+#include "ide-unsaved-files.h"
+
+#undef IDE_CODE_INSIDE
+
+G_END_DECLS
diff --git a/src/libide/code/meson.build b/src/libide/code/meson.build
new file mode 100644
index 000000000..14e7979b2
--- /dev/null
+++ b/src/libide/code/meson.build
@@ -0,0 +1,189 @@
+libide_code_header_dir = join_paths(libide_header_dir, 'code')
+libide_code_header_subdir = join_paths(libide_header_subdir, 'code')
+
+libide_code_generated_sources = []
+libide_code_generated_headers = []
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_code_private_headers = [
+  'ide-buffer-private.h',
+  'ide-doc-seq-private.h',
+  'ide-gsettings-file-settings.h',
+  'ide-language-defaults.h',
+  'ide-text-edit-private.h',
+  'ide-unsaved-file-private.h',
+]
+
+libide_code_public_headers = [
+  'ide-buffer-addin.h',
+  'ide-buffer-change-monitor.h',
+  'ide-buffer.h',
+  'ide-buffer-manager.h',
+  'ide-code-index-entries.h',
+  'ide-code-index-entry.h',
+  'ide-code-indexer.h',
+  'ide-code-types.h',
+  'ide-diagnostic.h',
+  'ide-diagnostic-provider.h',
+  'ide-diagnostics.h',
+  'ide-diagnostics-manager.h',
+  'ide-file-settings.h',
+  'ide-file-settings.defs',
+  'ide-formatter.h',
+  'ide-formatter-options.h',
+  'ide-highlight-engine.h',
+  'ide-highlighter.h',
+  'ide-highlight-index.h',
+  'ide-indent-style.h',
+  'ide-language.h',
+  'ide-location.h',
+  'ide-range.h',
+  'ide-rename-provider.h',
+  'ide-source-iter.h',
+  'ide-source-style-scheme.h',
+  'ide-spaces-style.h',
+  'ide-symbol.h',
+  'ide-symbol-node.h',
+  'ide-symbol-resolver.h',
+  'ide-symbol-tree.h',
+  'ide-text-edit.h',
+  'ide-text-iter.h',
+  'ide-unsaved-file.h',
+  'ide-unsaved-files.h',
+  'libide-code.h',
+]
+
+libide_code_enum_headers = [
+  'ide-buffer.h',
+  'ide-buffer-manager.h',
+  'ide-diagnostic.h',
+  'ide-indent-style.h',
+  'ide-spaces-style.h',
+  'ide-symbol.h',
+]
+
+install_headers(libide_code_public_headers, subdir: libide_code_header_subdir)
+
+#
+# Sources
+#
+
+libide_code_private_sources = [
+  'ide-doc-seq.c',
+  'ide-gsettings-file-settings.c',
+  'ide-language-defaults.c',
+]
+
+libide_code_public_sources = [
+  'ide-buffer-addin.c',
+  'ide-buffer.c',
+  'ide-buffer-change-monitor.c',
+  'ide-buffer-manager.c',
+  'ide-code-global.c',
+  'ide-code-index-entries.c',
+  'ide-code-index-entry.c',
+  'ide-code-indexer.c',
+  'ide-diagnostic.c',
+  'ide-diagnostic-provider.c',
+  'ide-diagnostics.c',
+  'ide-diagnostics-manager.c',
+  'ide-file-settings.c',
+  'ide-formatter.c',
+  'ide-formatter-options.c',
+  'ide-highlight-engine.c',
+  'ide-highlighter.c',
+  'ide-highlight-index.c',
+  'ide-language.c',
+  'ide-location.c',
+  'ide-range.c',
+  'ide-rename-provider.c',
+  'ide-source-iter.c',
+  'ide-source-style-scheme.c',
+  'ide-symbol.c',
+  'ide-symbol-node.c',
+  'ide-symbol-resolver.c',
+  'ide-symbol-tree.c',
+  'ide-text-edit.c',
+  'ide-text-iter.c',
+  'ide-unsaved-file.c',
+  'ide-unsaved-files.c',
+]
+
+#
+# Enum generation
+#
+
+libide_code_enums = gnome.mkenums_simple('ide-code-enums',
+     body_prefix: '#include "config.h"',
+   header_prefix: '#include <libide-core.h>',
+       decorator: '_IDE_EXTERN',
+         sources: libide_code_enum_headers,
+  install_header: true,
+     install_dir: libide_code_header_dir,
+)
+libide_code_generated_sources += [libide_code_enums[0]]
+libide_code_generated_headers += [libide_code_enums[1]]
+
+#
+# Generated Resource Files
+#
+
+libide_code_resources = gnome.compile_resources(
+  'ide-code-resources',
+  'libide-code.gresource.xml',
+  c_name: 'ide_code',
+)
+libide_code_generated_headers += [libide_code_resources[1]]
+libide_code_generated_sources += libide_code_resources[0]
+
+
+#
+# Dependencies
+#
+
+libide_code_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libgtksource_dep,
+  libdazzle_dep,
+  libtemplate_glib_dep,
+
+  libide_core_dep,
+  libide_plugins_dep,
+  libide_io_dep,
+  libide_threading_dep,
+]
+
+#
+# Library Definitions
+#
+
+
+libide_code = static_library('ide-code-' + libide_api_version,
+                             libide_code_public_sources,
+                             libide_code_private_sources,
+                             libide_code_generated_sources,
+                             libide_code_generated_headers,
+   dependencies: libide_code_deps,
+         c_args: libide_args + release_args + ['-DIDE_CODE_COMPILATION'],
+)
+
+libide_code_dep = declare_dependency(
+              sources: libide_code_private_headers + libide_code_generated_headers,
+         dependencies: libide_code_deps,
+           link_whole: libide_code,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_code_public_sources)
+gnome_builder_public_headers += files(libide_code_public_headers)
+gnome_builder_private_sources += files(libide_code_private_sources)
+gnome_builder_private_headers += files(libide_code_private_headers)
+gnome_builder_generated_headers += libide_code_generated_headers
+gnome_builder_generated_sources += libide_code_generated_sources
+gnome_builder_include_subdirs += libide_code_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-code.h', '-DIDE_CODE_COMPILATION']
diff --git a/src/libide/ide-build-ident.h.in b/src/libide/core/ide-build-ident.h.in
similarity index 100%
rename from src/libide/ide-build-ident.h.in
rename to src/libide/core/ide-build-ident.h.in
diff --git a/src/libide/core/ide-context-addin.c b/src/libide/core/ide-context-addin.c
new file mode 100644
index 000000000..3f3071ffc
--- /dev/null
+++ b/src/libide/core/ide-context-addin.c
@@ -0,0 +1,207 @@
+/* ide-context-addin.c
+ *
+ * Copyright 2018-2019 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 "ide-context-addin"
+
+#include "config.h"
+
+#include "ide-context-addin.h"
+
+G_DEFINE_INTERFACE (IdeContextAddin, ide_context_addin, G_TYPE_OBJECT)
+
+enum {
+  PROJECT_LOADED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+ide_context_addin_real_load_project_async (IdeContextAddin     *addin,
+                                           IdeContext          *context,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  g_autoptr(GTask) task = g_task_new (addin, cancellable, callback, user_data);
+  g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_context_addin_real_load_project_finish (IdeContextAddin  *addin,
+                                            GAsyncResult     *result,
+                                            GError          **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_context_addin_default_init (IdeContextAddinInterface *iface)
+{
+  iface->load_project_async = ide_context_addin_real_load_project_async;
+  iface->load_project_finish = ide_context_addin_real_load_project_finish;
+
+  /**
+   * IdeContextAddin::project-loaded:
+   * @self: an #IdeContextAddin
+   * @context: an #IdeContext
+   *
+   * The "project-loaded" signal is emitted after a project has been loaded
+   * in the #IdeContext.
+   *
+   * You might use this to setup any runtime features that rely on the project
+   * being successfully loaded first. Every addin's
+   * ide_context_addin_load_project_async() will have been called and completed
+   * before this signal is emitted.
+   *
+   * Since: 3.32
+   */
+  signals [PROJECT_LOADED] =
+    g_signal_new ("project-loaded",
+                  G_TYPE_FROM_INTERFACE (iface),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeContextAddinInterface, project_loaded),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_CONTEXT);
+  g_signal_set_va_marshaller (signals [PROJECT_LOADED],
+                              G_TYPE_FROM_INTERFACE (iface),
+                              g_cclosure_marshal_VOID__OBJECTv);
+}
+
+/**
+ * ide_context_addin_load_project_async:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Requests to load a project with the #IdeContextAddin.
+ *
+ * This function is called when the #IdeContext requests loading a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_load_project_async (IdeContextAddin     *self,
+                                      IdeContext          *context,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_CONTEXT_ADDIN_GET_IFACE (self)->load_project_async (self, context, cancellable, callback, user_data);
+}
+
+/**
+ * ide_context_addin_load_project_finish:
+ * @self: an #IdeContextAddin
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes a request to load a project with the #IdeContextAddin.
+ *
+ * This function will be called from the callback provided to
+ * ide_context_addin_load_project_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_context_addin_load_project_finish (IdeContextAddin  *self,
+                                       GAsyncResult     *result,
+                                       GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_CONTEXT_ADDIN_GET_IFACE (self)->load_project_finish (self, result, error);
+}
+
+/**
+ * ide_context_addin_load:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ *
+ * Requests that the #IdeContextAddin loads any necessary runtime features.
+ *
+ * This is called when the #IdeContext is created. If you would rather wait
+ * until a project is loaded, then use #IdeContextAddin::project-loaded to
+ * load runtime features.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_load (IdeContextAddin *self,
+                        IdeContext      *context)
+{
+  g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  if (IDE_CONTEXT_ADDIN_GET_IFACE (self)->load)
+    IDE_CONTEXT_ADDIN_GET_IFACE (self)->load (self, context);
+}
+
+/**
+ * ide_context_addin_unload:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ *
+ * Requests that the #IdeContextAddin unloads any previously loaded
+ * resources.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_unload (IdeContextAddin *self,
+                          IdeContext      *context)
+{
+  g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  if (IDE_CONTEXT_ADDIN_GET_IFACE (self)->unload)
+    IDE_CONTEXT_ADDIN_GET_IFACE (self)->unload (self, context);
+}
+
+/**
+ * ide_context_addin_project_loaded:
+ * @self: an #IdeContextAddin
+ * @context: an #IdeContext
+ *
+ * Emits the #IdeContextAddin::project-loaded signal.
+ *
+ * This is called when the context has completed loading a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_addin_project_loaded (IdeContextAddin *self,
+                                  IdeContext      *context)
+{
+  g_return_if_fail (IDE_IS_CONTEXT_ADDIN (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  g_signal_emit (self, signals [PROJECT_LOADED], 0, context);
+}
diff --git a/src/libide/core/ide-context-addin.h b/src/libide/core/ide-context-addin.h
new file mode 100644
index 000000000..ea4e3b575
--- /dev/null
+++ b/src/libide/core/ide-context-addin.h
@@ -0,0 +1,73 @@
+/* ide-context-addin.h
+ *
+ * Copyright 2018-2019 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 "ide-context.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONTEXT_ADDIN (ide_context_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeContextAddin, ide_context_addin, IDE, CONTEXT_ADDIN, GObject)
+
+struct _IdeContextAddinInterface
+{
+  GTypeInterface parent_iface;
+
+  void     (*load)                (IdeContextAddin      *self,
+                                   IdeContext           *context);
+  void     (*unload)              (IdeContextAddin      *self,
+                                   IdeContext           *context);
+  void     (*load_project_async)  (IdeContextAddin      *self,
+                                   IdeContext           *context,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data);
+  gboolean (*load_project_finish) (IdeContextAddin      *self,
+                                   GAsyncResult         *result,
+                                   GError              **error);
+  void     (*project_loaded)      (IdeContextAddin      *self,
+                                   IdeContext           *context);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_context_addin_load_project_async  (IdeContextAddin      *self,
+                                                IdeContext           *context,
+                                                GCancellable         *cancellable,
+                                                GAsyncReadyCallback   callback,
+                                                gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_context_addin_load_project_finish (IdeContextAddin      *self,
+                                                GAsyncResult         *result,
+                                                GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_context_addin_load                (IdeContextAddin      *self,
+                                                IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void     ide_context_addin_unload              (IdeContextAddin      *self,
+                                                IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void     ide_context_addin_project_loaded      (IdeContextAddin      *self,
+                                                IdeContext           *context);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-context-private.h b/src/libide/core/ide-context-private.h
new file mode 100644
index 000000000..e554b9952
--- /dev/null
+++ b/src/libide/core/ide-context-private.h
@@ -0,0 +1,29 @@
+/* ide-context-private.h
+ *
+ * Copyright 2018-2019 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 "ide-context.h"
+
+G_BEGIN_DECLS
+
+void _ide_context_set_has_project (IdeContext *self);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-context.c b/src/libide/core/ide-context.c
new file mode 100644
index 000000000..4f6cc3144
--- /dev/null
+++ b/src/libide/core/ide-context.c
@@ -0,0 +1,855 @@
+/* ide-context.c
+ *
+ * Copyright 2014-2019 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 "ide-context"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
+#include "ide-context.h"
+#include "ide-context-private.h"
+#include "ide-context-addin.h"
+#include "ide-macros.h"
+#include "ide-notifications.h"
+
+/**
+ * SECTION:ide-context
+ * @title: IdeContext
+ * @short_description: the root object for a project
+ *
+ * The #IdeContext object is the root object for a project. Everything
+ * in a project is contained by this object.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeContext
+{
+  IdeObject         parent_instance;
+  PeasExtensionSet *addins;
+  gchar            *project_id;
+  gchar            *title;
+  GFile            *workdir;
+  guint             project_loaded : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_PROJECT_ID,
+  PROP_TITLE,
+  PROP_WORKDIR,
+  N_PROPS
+};
+
+enum {
+  LOG,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE (IdeContext, ide_context, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_context_addin_load_project_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeContextAddin *addin = (IdeContextAddin *)object;
+  g_autoptr(IdeContext) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_CONTEXT_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_CONTEXT (self));
+
+  if (ide_context_addin_load_project_finish (addin, result, &error))
+    ide_context_addin_project_loaded (addin, self);
+  else
+    g_warning ("%s context addin failed to load project: %s",
+               G_OBJECT_TYPE_NAME (addin), error->message);
+}
+
+static void
+ide_context_addin_added_cb (PeasExtensionSet *set,
+                            PeasPluginInfo   *plugin_info,
+                            PeasExtension    *exten,
+                            gpointer          user_data)
+{
+  IdeContextAddin *addin = (IdeContextAddin *)exten;
+  IdeContext *self = user_data;
+  g_autoptr(GCancellable) cancellable = NULL;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_CONTEXT_ADDIN (addin));
+
+  /* Ignore any request during shutdown */
+  cancellable = ide_object_ref_cancellable (IDE_OBJECT (self));
+  if (g_cancellable_is_cancelled (cancellable))
+    return;
+
+  ide_context_addin_load (addin, self);
+
+  if (self->project_loaded)
+    ide_context_addin_load_project_async (addin,
+                                          self,
+                                          cancellable,
+                                          ide_context_addin_load_project_cb,
+                                          g_object_ref (self));
+}
+
+static void
+ide_context_addin_removed_cb (PeasExtensionSet *set,
+                              PeasPluginInfo   *plugin_info,
+                              PeasExtension    *exten,
+                              gpointer          user_data)
+{
+  IdeContextAddin *addin = (IdeContextAddin *)exten;
+  IdeContext *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_CONTEXT_ADDIN (addin));
+
+  ide_context_addin_unload (addin, self);
+}
+
+static void
+ide_context_real_log (IdeContext     *self,
+                      GLogLevelFlags  level,
+                      const gchar    *domain,
+                      const gchar    *message)
+{
+  g_log (domain, level, "%s", message);
+}
+
+static gchar *
+ide_context_repr (IdeObject *object)
+{
+  IdeContext *self = IDE_CONTEXT (object);
+
+  return g_strdup_printf ("%s workdir=\"%s\" has_project=%d",
+                          G_OBJECT_TYPE_NAME (self),
+                          g_file_peek_path (self->workdir),
+                          self->project_loaded);
+}
+
+static void
+ide_context_constructed (GObject *object)
+{
+  IdeContext *self = (IdeContext *)object;
+
+  g_assert (IDE_IS_OBJECT (object));
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_CONTEXT_ADDIN,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_context_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_context_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_context_addin_added_cb,
+                              self);
+
+  G_OBJECT_CLASS (ide_context_parent_class)->constructed (object);
+}
+
+static void
+ide_context_destroy (IdeObject *object)
+{
+  IdeContext *self = (IdeContext *)object;
+
+  g_assert (IDE_IS_OBJECT (object));
+
+  g_clear_object (&self->addins);
+
+  IDE_OBJECT_CLASS (ide_context_parent_class)->destroy (object);
+}
+
+static void
+ide_context_finalize (GObject *object)
+{
+  IdeContext *self = (IdeContext *)object;
+
+  g_clear_object (&self->workdir);
+  g_clear_pointer (&self->project_id, g_free);
+  g_clear_pointer (&self->title, g_free);
+
+  G_OBJECT_CLASS (ide_context_parent_class)->finalize (object);
+}
+
+static void
+ide_context_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  IdeContext *self = IDE_CONTEXT (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      g_value_take_string (value, ide_context_dup_project_id (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_take_string (value, ide_context_dup_title (self));
+      break;
+
+    case PROP_WORKDIR:
+      g_value_take_object (value, ide_context_ref_workdir (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_context_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  IdeContext *self = IDE_CONTEXT (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      ide_context_set_project_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_context_set_title (self, g_value_get_string (value));
+      break;
+
+    case PROP_WORKDIR:
+      ide_context_set_workdir (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_context_class_init (IdeContextClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_context_constructed;
+  object_class->finalize = ide_context_finalize;
+  object_class->get_property = ide_context_get_property;
+  object_class->set_property = ide_context_set_property;
+
+  i_object_class->destroy = ide_context_destroy;
+  i_object_class->repr = ide_context_repr;
+
+  /**
+   * IdeContext:project-id:
+   *
+   * The "project-id" property is the identifier to use when creating
+   * files and folders for this project. It has a mutated form of either
+   * the directory or some other discoverable trait of the project.
+   *
+   * It has also been modified to remove spaces and other unsafe
+   * characters for file-systems.
+   *
+   * This may change during runtime, but usually only once when the
+   * project has been initialize loaded.
+   *
+   * Before any project has loaded, this is "empty" to allow flexibility
+   * for non-project use.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROJECT_ID] =
+    g_param_spec_string ("project-id",
+                         "Project Id",
+                         "The project identifier used when creating files and folders",
+                         "empty",
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeContext:title:
+   *
+   * The "title" property is a descriptive name for the project.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the project",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeContext:workdir:
+   *
+   * The "workdir" property is the best guess at the working directory for the
+   * context. This may be discovered using a common parent if multiple files
+   * are opened without a project.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_WORKDIR] =
+    g_param_spec_object ("workdir",
+                         "Working Directory",
+                         "The working directory for the project",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeContext::log:
+   * @self: an #IdeContext
+   * @severity: the log severity
+   * @domain: the log domain
+   * @message: the log message
+   *
+   * This signal is emitted when a log item has been added for the context.
+   *
+   * Since: 3.32
+   */
+  signals [LOG] =
+    g_signal_new_class_handler ("log",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_context_real_log),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE,
+                                3,
+                                G_TYPE_UINT,
+                                G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
+                                G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static void
+ide_context_init (IdeContext *self)
+{
+  g_autoptr(IdeNotifications) notifs = NULL;
+
+  self->workdir = g_file_new_for_path (g_get_home_dir ());
+  self->project_id = g_strdup ("empty");
+  self->title = g_strdup (_("Untitled"));
+
+  notifs = ide_notifications_new ();
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (notifs));
+}
+
+/**
+ * ide_context_new:
+ *
+ * Creates a new #IdeContext.
+ *
+ * This only creates the context object. After creating the object you need
+ * to set a number of properties and then initialize asynchronously using
+ * g_async_initable_init_async().
+ *
+ * Returns: (transfer full): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_context_new (void)
+{
+  return ide_object_new (IDE_TYPE_CONTEXT, NULL);
+}
+
+static void
+ide_context_peek_child_typed_cb (IdeObject *object,
+                                 gpointer   user_data)
+{
+  struct {
+    IdeObject *ret;
+    GType      type;
+  } *lookup = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  if (lookup->ret != NULL)
+    return;
+
+  /* Take a borrowed instance, we're in the main thread so
+   * we can ensure it's not fully destroyed.
+   */
+  if (G_TYPE_CHECK_INSTANCE_TYPE (object, lookup->type))
+    lookup->ret = object;
+}
+
+/**
+ * ide_context_peek_child_typed:
+ * @self: a #IdeContext
+ * @type: the #GType of the child
+ *
+ * Looks for the first child matching @type, and returns it. No reference is
+ * taken to the child, so you should avoid using this except as used by
+ * compatability functions.
+ *
+ * This may only be called from the main thread or you risk the objects
+ * being finalized before your caller has a chance to reference them.
+ *
+ * Returns: (transfer none) (type IdeObject) (nullable): an #IdeObject that
+ *   matches @type if successful; otherwise %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_context_peek_child_typed (IdeContext *self,
+                              GType       type)
+{
+  struct {
+    IdeObject *ret;
+    GType      type;
+  } lookup = { NULL, type };
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ide_object_foreach (IDE_OBJECT (self), (GFunc)ide_context_peek_child_typed_cb, &lookup);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return lookup.ret;
+}
+
+/**
+ * ide_context_dup_project_id:
+ * @self: a #IdeContext
+ *
+ * Copies the project-id and returns it to the caller.
+ *
+ * Returns: (transfer full): a project-id as a string
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_dup_project_id (IdeContext *self)
+{
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_strdup (self->project_id);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  g_return_val_if_fail (ret != NULL, NULL);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_set_project_id:
+ * @self: a #IdeContext
+ *
+ * Sets the project-id for the context.
+ *
+ * Generally, this should only be done once after loading a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_set_project_id (IdeContext  *self,
+                            const gchar *project_id)
+{
+  g_return_if_fail (IDE_IS_CONTEXT (self));
+
+  if (ide_str_empty0 (project_id))
+    project_id = "empty";
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (!ide_str_equal0 (self->project_id, project_id))
+    {
+      g_free (self->project_id);
+      self->project_id = g_strdup (project_id);
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROJECT_ID]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_context_ref_workdir:
+ * @self: a #IdeContext
+ *
+ * Gets the working-directory of the context and increments the
+ * reference count by one.
+ *
+ * Returns: (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_context_ref_workdir (IdeContext *self)
+{
+  GFile *ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_object_ref (self->workdir);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_set_workdir:
+ * @self: a #IdeContext
+ * @workdir: a #GFile
+ *
+ * Sets the working directory for the project.
+ *
+ * This should generally only be set once after checking out the project.
+ *
+ * In future releases, changes may be made to change this in support of
+ * git-worktrees or similar workflows.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_set_workdir (IdeContext *self,
+                         GFile      *workdir)
+{
+  g_return_if_fail (IDE_IS_CONTEXT (self));
+  g_return_if_fail (G_IS_FILE (workdir));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (g_set_object (&self->workdir, workdir))
+    ide_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WORKDIR]);
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_context_cache_file:
+ * @self: a #IdeContext
+ * @first_part: The first part of the path
+ *
+ * Like ide_context_cache_filename() but returns a #GFile.
+ *
+ * Returns: (transfer full): a #GFile for the cache file
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_context_cache_file (IdeContext  *self,
+                        const gchar *first_part,
+                        ...)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+  g_autofree gchar *path = NULL;
+  g_autofree gchar *project_id = NULL;
+  const gchar *part = first_part;
+  va_list args;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+  g_return_val_if_fail (first_part != NULL, NULL);
+
+  project_id = ide_context_dup_project_id (self);
+
+  ar = g_ptr_array_new ();
+  g_ptr_array_add (ar, (gchar *)g_get_user_cache_dir ());
+  g_ptr_array_add (ar, (gchar *)ide_get_program_name ());
+  g_ptr_array_add (ar, (gchar *)"projects");
+  g_ptr_array_add (ar, (gchar *)project_id);
+
+  va_start (args, first_part);
+  do
+    {
+      g_ptr_array_add (ar, (gchar *)part);
+      part = va_arg (args, const gchar *);
+    }
+  while (part != NULL);
+  va_end (args);
+
+  g_ptr_array_add (ar, NULL);
+
+  path = g_build_filenamev ((gchar **)ar->pdata);
+
+  return g_file_new_for_path (path);
+}
+
+/**
+ * ide_context_cache_filename:
+ * @self: a #IdeContext
+ * @first_part: the first part of the filename
+ *
+ * Creates a new filename that will be located in the projects cache directory.
+ * This makes it convenient to remove files when a project is deleted as all
+ * cache files will share a unified parent directory.
+ *
+ * The file will be located in a directory similar to
+ * ~/.cache/gnome-builder/project_name. This may change based on the value
+ * of g_get_user_cache_dir().
+ *
+ * Returns: (transfer full): A new string containing the cache filename
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_cache_filename (IdeContext  *self,
+                            const gchar *first_part,
+                            ...)
+{
+  g_autofree gchar *project_id = NULL;
+  g_autofree gchar *base = NULL;
+  va_list args;
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+  g_return_val_if_fail (first_part != NULL, NULL);
+
+  project_id = ide_context_dup_project_id (self);
+
+  g_return_val_if_fail (project_id != NULL, NULL);
+
+  base = g_build_filename (g_get_user_cache_dir (),
+                           ide_get_program_name (),
+                           "projects",
+                           project_id,
+                           first_part,
+                           NULL);
+
+  va_start (args, first_part);
+  ret = g_build_filename_valist (base, &args);
+  va_end (args);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_build_file:
+ * @self: a #IdeContext
+ * @path: (nullable): a path to the file
+ *
+ * Creates a new #GFile for the path.
+ *
+ * - If @path is %NULL, #IdeContext:workdir is returned.
+ * - If @path is absolute, a new #GFile to the absolute path is returned.
+ * - Otherwise, a #GFile child of #IdeContext:workdir is returned.
+ *
+ * Returns: (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_context_build_file (IdeContext  *self,
+                        const gchar *path)
+{
+  g_autoptr(GFile) ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  if (path == NULL)
+    ret = g_file_dup (self->workdir);
+  else if (g_path_is_absolute (path))
+    ret = g_file_new_for_path (path);
+  else
+    ret = g_file_get_child (self->workdir, path);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_build_filename:
+ * @self: a #IdeContext
+ * @first_part: first path part
+ *
+ * Creates a new path that starts from the working directory of the
+ * loaded project.
+ *
+ * Returns: (transfer full): a string containing the new path
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_build_filename (IdeContext  *self,
+                            const gchar *first_part,
+                            ...)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  const gchar *part = first_part;
+  const gchar *base;
+  va_list args;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+  g_return_val_if_fail (first_part != NULL, NULL);
+
+  workdir = ide_context_ref_workdir (self);
+  base = g_file_peek_path (workdir);
+
+  ar = g_ptr_array_new ();
+
+  /* If first part is absolute, just use that as our root */
+  if (!g_path_is_absolute (first_part))
+    g_ptr_array_add (ar, (gchar *)base);
+
+  va_start (args, first_part);
+  do
+    {
+      g_ptr_array_add (ar, (gchar *)part);
+      part = va_arg (args, const gchar *);
+    }
+  while (part != NULL);
+  va_end (args);
+
+  g_ptr_array_add (ar, NULL);
+
+  return g_build_filenamev ((gchar **)ar->pdata);
+}
+
+/**
+ * ide_context_ref_project_settings:
+ * @self: a #IdeContext
+ *
+ * Gets an org.gnome.builder.project #GSettings.
+ *
+ * This creates a new #GSettings instance for the project.
+ *
+ * Returns: (transfer full): a #GSettings
+ *
+ * Since: 3.32
+ */
+GSettings *
+ide_context_ref_project_settings (IdeContext *self)
+{
+  g_autofree gchar *path = NULL;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  path = g_strdup_printf ("/org/gnome/builder/projects/%s/", self->project_id);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_settings_new_with_path ("org.gnome.builder.project", path);
+}
+
+/**
+ * ide_context_dup_title:
+ * @self: a #IdeContext
+ *
+ * Returns: (transfer full): a string containing the title
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_context_dup_title (IdeContext *self)
+{
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_strdup (self->title);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_context_set_title:
+ * @self: an #IdeContext
+ * @title: (nullable): the title for the project or %NULL
+ *
+ * Sets the #IdeContext:title property. This is used by various
+ * components to show the user the name of the project. This may
+ * include the omnibar and the window title.
+ *
+ * Since: 3.32
+ */
+void
+ide_context_set_title (IdeContext  *self,
+                       const gchar *title)
+{
+  g_return_if_fail (IDE_IS_CONTEXT (self));
+
+  if (ide_str_empty0 (title))
+    title = _("Untitled");
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (!ide_str_equal0 (self->title, title))
+    {
+      g_free (self->title);
+      self->title = g_strdup (title);
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_TITLE]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+void
+ide_context_log (IdeContext     *self,
+                 GLogLevelFlags  level,
+                 const gchar    *domain,
+                 const gchar    *message)
+{
+  g_assert (IDE_IS_CONTEXT (self));
+
+  g_signal_emit (self, signals [LOG], 0, level, domain, message);
+}
+
+/**
+ * ide_context_has_project:
+ * @self: a #IdeContext
+ *
+ * Checks to see if a project has been loaded in @context.
+ *
+ * Returns: %TRUE if a project has been, or is currently, loading.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_context_has_project (IdeContext *self)
+{
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = self->project_loaded;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+void
+_ide_context_set_has_project (IdeContext *self)
+{
+  g_return_if_fail (IDE_IS_CONTEXT (self));
+
+  ide_object_lock (IDE_OBJECT (self));
+  self->project_loaded = TRUE;
+  ide_object_unlock (IDE_OBJECT (self));
+}
diff --git a/src/libide/core/ide-context.h b/src/libide/core/ide-context.h
new file mode 100644
index 000000000..065ac4563
--- /dev/null
+++ b/src/libide/core/ide-context.h
@@ -0,0 +1,91 @@
+/* ide-context.h
+ *
+ * Copyright 2014-2019 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
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONTEXT (ide_context_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeContext, ide_context, IDE, CONTEXT, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeContext *ide_context_new                  (void);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_context_has_project          (IdeContext     *self);
+IDE_AVAILABLE_IN_3_32
+gpointer    ide_context_peek_child_typed     (IdeContext     *self,
+                                              GType           type);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_context_dup_project_id       (IdeContext     *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_context_set_project_id       (IdeContext     *self,
+                                              const gchar    *project_id);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_context_dup_title            (IdeContext     *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_context_set_title            (IdeContext     *self,
+                                              const gchar    *title);
+IDE_AVAILABLE_IN_3_32
+GFile      *ide_context_ref_workdir          (IdeContext     *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_context_set_workdir          (IdeContext     *self,
+                                              GFile          *workdir);
+IDE_AVAILABLE_IN_3_32
+GFile      *ide_context_build_file           (IdeContext     *self,
+                                              const gchar    *path);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_context_build_filename       (IdeContext     *self,
+                                              const gchar    *first_part,
+                                              ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+GFile      *ide_context_cache_file           (IdeContext     *self,
+                                              const gchar    *first_part,
+                                              ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_context_cache_filename       (IdeContext     *self,
+                                              const gchar    *first_part,
+                                              ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+GSettings  *ide_context_ref_project_settings (IdeContext     *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext *ide_object_ref_context           (IdeObject      *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext *ide_object_get_context           (IdeObject      *object);
+IDE_AVAILABLE_IN_3_32
+void        ide_object_set_context           (IdeObject      *object,
+                                              IdeContext     *context);
+IDE_AVAILABLE_IN_3_32
+void        ide_context_log                  (IdeContext     *self,
+                                              GLogLevelFlags  level,
+                                              const gchar    *domain,
+                                              const gchar    *message);
+
+#define ide_context_warning(instance, format, ...) \
+  ide_object_log(instance, G_LOG_LEVEL_WARNING, G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__)
+
+G_END_DECLS
diff --git a/src/libide/ide-debug.h.in b/src/libide/core/ide-debug.h.in
similarity index 100%
rename from src/libide/ide-debug.h.in
rename to src/libide/core/ide-debug.h.in
diff --git a/src/libide/core/ide-global.c b/src/libide/core/ide-global.c
new file mode 100644
index 000000000..46133cc56
--- /dev/null
+++ b/src/libide/core/ide-global.c
@@ -0,0 +1,234 @@
+/* ide-global.c
+ *
+ * Copyright 2014-2019 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 "ide-global"
+
+#include "config.h"
+
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/user.h>
+#include <sys/utsname.h>
+#include <unistd.h>
+
+#include "../../gconstructor.h"
+
+#include "ide-macros.h"
+#include "ide-global.h"
+
+static GThread *main_thread;
+static const gchar *application_id = "org.gnome.Builder";
+static IdeProcessKind kind = IDE_PROCESS_KIND_HOST;
+
+#if defined (G_HAS_CONSTRUCTORS)
+# ifdef G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA
+#  pragma G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS(ide_init_ctor)
+# endif
+G_DEFINE_CONSTRUCTOR(ide_init_ctor)
+#else
+# error Your platform/compiler is missing constructor support
+#endif
+
+static void
+ide_init_ctor (void)
+{
+  main_thread = g_thread_self ();
+
+  if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS))
+    kind = IDE_PROCESS_KIND_FLATPAK;
+}
+
+/**
+ * ide_get_main_thread
+ *
+ * Gets #GThread of the main thread.
+ *
+ * Generally this is used by macros to determine what thread they code is
+ * currently running within.
+ *
+ * Returns: (transfer none): a #GThread
+ *
+ * Since: 3.32
+ */
+GThread *
+ide_get_main_thread (void)
+{
+  return main_thread;
+}
+
+/**
+ * ide_get_process_kind:
+ *
+ * Gets the kind of process we're running as.
+ *
+ * Returns: an #IdeProcessKind
+ *
+ * Since: 3.32
+ */
+IdeProcessKind
+ide_get_process_kind (void)
+{
+  return kind;
+}
+
+const gchar *
+ide_get_application_id (void)
+{
+  return application_id;
+}
+
+/**
+ * ide_set_application_id:
+ * @app_id: the application id
+ *
+ * Sets the application id that will be used.
+ *
+ * This must be set at application startup before any GApplication
+ * has connected to the D-Bus.
+ *
+ * The default is "org.gnome.Builder".
+ *
+ * Since: 3.32
+ */
+void
+ide_set_application_id (const gchar *app_id)
+{
+  g_return_if_fail (app_id != NULL);
+
+  application_id = g_intern_string (app_id);
+}
+
+const gchar *
+ide_get_program_name (void)
+{
+  return "gnome-builder";
+}
+
+gchar *
+ide_create_host_triplet (const gchar *arch,
+                         const gchar *kernel,
+                         const gchar *system)
+{
+  if (arch == NULL || kernel == NULL)
+    return g_strdup (ide_get_system_type ());
+  else if (system == NULL)
+    return g_strdup_printf ("%s-%s", arch, kernel);
+  else
+    return g_strdup_printf ("%s-%s-%s", arch, kernel, system);
+}
+
+const gchar *
+ide_get_system_type (void)
+{
+  static gchar *system_type;
+  g_autofree gchar *os_lower = NULL;
+  const gchar *machine = NULL;
+  struct utsname u;
+
+  if (system_type != NULL)
+    return system_type;
+
+  if (uname (&u) < 0)
+    return g_strdup ("unknown");
+
+  os_lower = g_utf8_strdown (u.sysname, -1);
+
+  /* config.sub doesn't accept amd64-OS */
+  machine = strcmp (u.machine, "amd64") ? u.machine : "x86_64";
+
+  /*
+   * TODO: Clearly we want to discover "gnu", but that should be just fine
+   *       for a default until we try to actually run on something non-gnu.
+   *       Which seems unlikely at the moment. If you run FreeBSD, you can
+   *       probably fix this for me :-) And while you're at it, make the
+   *       uname() call more portable.
+   */
+
+#ifdef __GLIBC__
+  system_type = g_strdup_printf ("%s-%s-%s", machine, os_lower, "gnu");
+#else
+  system_type = g_strdup_printf ("%s-%s", machine, os_lower);
+#endif
+
+  return system_type;
+}
+
+gchar *
+ide_get_system_arch (void)
+{
+  struct utsname u;
+  const char *machine;
+
+  if (uname (&u) < 0)
+    return g_strdup ("unknown");
+
+  /* config.sub doesn't accept amd64-OS */
+  machine = strcmp (u.machine, "amd64") ? u.machine : "x86_64";
+
+  return g_strdup (machine);
+}
+
+gsize
+ide_get_system_page_size (void)
+{
+  return sysconf (_SC_PAGE_SIZE);
+}
+
+static gchar *
+get_base_path (const gchar *name)
+{
+  g_autoptr(GKeyFile) keyfile = g_key_file_new ();
+
+  if (g_key_file_load_from_file (keyfile, "/.flatpak-info", 0, NULL))
+    return g_key_file_get_string (keyfile, "Instance", name, NULL);
+
+  return NULL;
+}
+
+/**
+ * ide_get_relocatable_path:
+ * @path: a relocatable path
+ *
+ * Gets the path to a resource that may be relocatable at runtime.
+ *
+ * Returns: (transfer full): a new string containing the path
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_get_relocatable_path (const gchar *path)
+{
+  static gchar *base_path;
+
+  if G_UNLIKELY (base_path == NULL)
+    base_path = get_base_path ("app-path");
+
+  return g_build_filename (base_path, path, NULL);
+}
+
+const gchar *
+ide_gettext (const gchar *message)
+{
+  if (message != NULL)
+    return g_dgettext (GETTEXT_PACKAGE, message);
+  return NULL;
+}
diff --git a/src/libide/core/ide-global.h b/src/libide/core/ide-global.h
new file mode 100644
index 000000000..28adbdfb8
--- /dev/null
+++ b/src/libide/core/ide-global.h
@@ -0,0 +1,66 @@
+/* ide-global.h
+ *
+ * Copyright 2014-2019 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
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_PROCESS_KIND_HOST    = 0,
+  IDE_PROCESS_KIND_FLATPAK = 1,
+} IdeProcessKind;
+
+#define ide_is_flatpak() (ide_get_process_kind() == IDE_PROCESS_KIND_FLATPAK)
+
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_gettext             (const gchar *message);
+IDE_AVAILABLE_IN_3_32
+GThread        *ide_get_main_thread     (void);
+IDE_AVAILABLE_IN_3_32
+IdeProcessKind  ide_get_process_kind    (void);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_get_application_id  (void);
+IDE_AVAILABLE_IN_3_32
+void            ide_set_application_id  (const gchar *app_id);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_get_program_name    (void);
+IDE_AVAILABLE_IN_3_32
+gchar          *ide_get_system_arch     (void);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_get_system_type     (void);
+IDE_AVAILABLE_IN_3_32
+gchar          *ide_create_host_triplet (const gchar *arch,
+                                         const gchar *kernel,
+                                         const gchar *system);
+IDE_AVAILABLE_IN_3_32
+gsize          ide_get_system_page_size (void) G_GNUC_CONST;
+IDE_AVAILABLE_IN_3_32
+gchar         *ide_get_relocatable_path (const gchar *path);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-log.c b/src/libide/core/ide-log.c
new file mode 100644
index 000000000..69a992df6
--- /dev/null
+++ b/src/libide/core/ide-log.c
@@ -0,0 +1,380 @@
+/* ide-log.c
+ *
+ * Copyright 2015-2019 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 "ide-log"
+
+#include "config.h"
+
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#ifdef __linux__
+# include <sys/types.h>
+# include <sys/syscall.h>
+#endif
+
+#include <glib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "ide-debug.h"
+#include "ide-log.h"
+#include "ide-macros.h"
+
+/**
+ * SECTION:ide-log
+ * @title: Logging
+ * @short_description: Standard logging facilities for Builder
+ *
+ * This module manages the logging facilities in Builder. It involves
+ * formatting the standard output and error logs as well as filtering
+ * logs based on their #GLogLevelFlags.
+ *
+ * Generally speaking, you want to continue using the GLib logging API
+ * such as g_debug(), g_warning(), g_message(), or g_error(). These functions
+ * will redirect their logging information to this module who will format
+ * the log message appropriately.
+ *
+ * If you are writing code for Builder that is in C, you want to ensure you
+ * set the %G_LOG_DOMAIN define at the top of your file (after the license)
+ * as such:
+ *
+ * ## Logging from C
+ *
+ * |[
+ * #define G_LOG_DOMAIN "my-module"
+ * ...
+ * static void
+ * some_function (void)
+ * {
+ *   g_debug ("Use normal logging facilities");
+ * }
+ * ]|
+ *
+ * ## Logging from Python
+ *
+ * If you are writing an extension to Builder from Python, you may use the
+ * helper functions provided by our Ide python module.
+ *
+ * |[<!-- Language="py" -->
+ * from gi.repository import Ide
+ *
+ * Ide.warning("This is a warning")
+ * Ide.debug("This is a debug")
+ * Ide.error("This is a fatal error")
+ * ]|
+ *
+ * Since: 3.32
+ */
+
+typedef const gchar *(*IdeLogLevelStrFunc) (GLogLevelFlags log_level);
+
+static GPtrArray          *channels;
+static GLogFunc            last_handler;
+static int                 log_verbosity;
+static IdeLogLevelStrFunc  log_level_str_func;
+static gchar              *domains;
+static gboolean            has_domains;
+
+G_LOCK_DEFINE (channels_lock);
+
+/**
+ * ide_log_get_thread:
+ *
+ * Retrieves task id for the current thread. This is only supported on Linux.
+ * On other platforms, the current thread pointer is retrieved.
+ *
+ * Returns: The task id.
+ *
+ * Since: 3.32
+ */
+static inline gint
+ide_log_get_thread (void)
+{
+#ifdef __linux__
+  return (gint) syscall (SYS_gettid);
+#else
+  return GPOINTER_TO_INT (g_thread_self ());
+#endif /* __linux__ */
+}
+
+/**
+ * ide_log_level_str:
+ * @log_level: a #GLogLevelFlags.
+ *
+ * Retrieves the log level as a string.
+ *
+ * Returns: A string which shouldn't be modified or freed.
+ * Side effects: None.
+ *
+ * Since: 3.32
+ */
+static const gchar *
+ide_log_level_str (GLogLevelFlags log_level)
+{
+  switch (((gulong)log_level & G_LOG_LEVEL_MASK))
+    {
+    case G_LOG_LEVEL_ERROR:    return "   ERROR";
+    case G_LOG_LEVEL_CRITICAL: return "CRITICAL";
+    case G_LOG_LEVEL_WARNING:  return " WARNING";
+    case G_LOG_LEVEL_MESSAGE:  return " MESSAGE";
+    case G_LOG_LEVEL_INFO:     return "    INFO";
+    case G_LOG_LEVEL_DEBUG:    return "   DEBUG";
+    case IDE_LOG_LEVEL_TRACE:  return "   TRACE";
+
+    default:
+      return " UNKNOWN";
+    }
+}
+
+static const gchar *
+ide_log_level_str_with_color (GLogLevelFlags log_level)
+{
+  switch (((gulong)log_level & G_LOG_LEVEL_MASK))
+    {
+    case G_LOG_LEVEL_ERROR:    return "   \033[1;31mERROR\033[0m";
+    case G_LOG_LEVEL_CRITICAL: return "\033[1;35mCRITICAL\033[0m";
+    case G_LOG_LEVEL_WARNING:  return " \033[1;33mWARNING\033[0m";
+    case G_LOG_LEVEL_MESSAGE:  return " \033[1;32mMESSAGE\033[0m";
+    case G_LOG_LEVEL_INFO:     return "    \033[1;32mINFO\033[0m";
+    case G_LOG_LEVEL_DEBUG:    return "   \033[1;32mDEBUG\033[0m";
+    case IDE_LOG_LEVEL_TRACE:  return "   \033[1;36mTRACE\033[0m";
+
+    default:
+      return " UNKNOWN";
+    }
+}
+
+/**
+ * ide_log_write_to_channel:
+ * @channel: a #GIOChannel.
+ * @message: A string log message.
+ *
+ * Writes @message to @channel and flushes the channel.
+ *
+ * Since: 3.32
+ */
+static void
+ide_log_write_to_channel (GIOChannel  *channel,
+                          const gchar *message)
+{
+  g_io_channel_write_chars (channel, message, -1, NULL, NULL);
+  g_io_channel_flush (channel, NULL);
+}
+
+/**
+ * ide_log_handler:
+ * @log_domain: A string containing the log section.
+ * @log_level: a #GLogLevelFlags.
+ * @message: The string message.
+ * @user_data: User data supplied to g_log_set_default_handler().
+ *
+ * Default log handler that will dispatch log messages to configured logging
+ * destinations.
+ *
+ * Since: 3.32
+ */
+static void
+ide_log_handler (const gchar    *log_domain,
+                 GLogLevelFlags  log_level,
+                 const gchar    *message,
+                 gpointer        user_data)
+{
+  GTimeVal tv;
+  struct tm tt;
+  time_t t;
+  const gchar *level;
+  gchar ftime[32];
+  gchar *buffer;
+  gboolean is_debug_level;
+
+  if (G_LIKELY (channels->len))
+    {
+      is_debug_level = (log_level == G_LOG_LEVEL_DEBUG || log_level == IDE_LOG_LEVEL_TRACE);
+      if (is_debug_level &&
+          has_domains &&
+          (log_domain == NULL || strstr (domains, log_domain) == NULL))
+        return;
+
+      switch ((int)log_level)
+        {
+        case G_LOG_LEVEL_MESSAGE:
+          if (log_verbosity < 1)
+            return;
+          break;
+
+        case G_LOG_LEVEL_INFO:
+          if (log_verbosity < 2)
+            return;
+          break;
+
+        case G_LOG_LEVEL_DEBUG:
+          if (log_verbosity < 3)
+            return;
+          break;
+
+        case IDE_LOG_LEVEL_TRACE:
+          if (log_verbosity < 4)
+            return;
+          break;
+
+        default:
+          break;
+        }
+
+      level = log_level_str_func (log_level);
+      g_get_current_time (&tv);
+      t = (time_t) tv.tv_sec;
+      tt = *localtime (&t);
+      strftime (ftime, sizeof (ftime), "%H:%M:%S", &tt);
+      buffer = g_strdup_printf ("%s.%04ld  %40s[% 5d]: %s: %s\n",
+                                ftime,
+                                tv.tv_usec / 1000,
+                                log_domain,
+                                ide_log_get_thread (),
+                                level,
+                                message);
+      G_LOCK (channels_lock);
+      g_ptr_array_foreach (channels, (GFunc) ide_log_write_to_channel, buffer);
+      G_UNLOCK (channels_lock);
+      g_free (buffer);
+    }
+}
+
+/**
+ * ide_log_init:
+ * @stdout_: Indicates logging should be written to stdout.
+ * @filename: An optional file in which to store logs.
+ *
+ * Initializes the logging subsystem. This should be called from
+ * the application entry point only. Secondary calls to this function
+ * will do nothing.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_init (gboolean     stdout_,
+              const gchar *filename)
+{
+  static gsize initialized = FALSE;
+  GIOChannel *channel;
+
+  if (g_once_init_enter (&initialized))
+    {
+      log_level_str_func = ide_log_level_str;
+      channels = g_ptr_array_new ();
+      if (filename)
+        {
+          channel = g_io_channel_new_file (filename, "a", NULL);
+          g_ptr_array_add (channels, channel);
+        }
+      if (stdout_)
+        {
+          channel = g_io_channel_unix_new (STDOUT_FILENO);
+          g_ptr_array_add (channels, channel);
+          if ((filename == NULL) && isatty (STDOUT_FILENO))
+            log_level_str_func = ide_log_level_str_with_color;
+        }
+
+      domains = g_strdup (g_getenv ("G_MESSAGES_DEBUG"));
+      if (!ide_str_empty0 (domains) && strcmp (domains, "all") != 0)
+        has_domains = TRUE;
+
+      g_log_set_default_handler (ide_log_handler, NULL);
+      g_once_init_leave (&initialized, TRUE);
+    }
+}
+
+/**
+ * ide_log_shutdown:
+ *
+ * Cleans up after the logging subsystem and restores the original
+ * log handler.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_shutdown (void)
+{
+  if (last_handler)
+    {
+      g_log_set_default_handler (last_handler, NULL);
+      last_handler = NULL;
+    }
+
+  g_clear_pointer (&domains, g_free);
+}
+
+/**
+ * ide_log_increase_verbosity:
+ *
+ * Increases the amount of logging that will occur. By default, only
+ * warning and above will be displayed.
+ *
+ * Calling this once will cause %G_LOG_LEVEL_MESSAGE to be displayed.
+ * Calling this twice will cause %G_LOG_LEVEL_INFO to be displayed.
+ * Calling this thrice will cause %G_LOG_LEVEL_DEBUG to be displayed.
+ * Calling this four times will cause %IDE_LOG_LEVEL_TRACE to be displayed.
+ *
+ * Note that many DEBUG and TRACE level log messages are only compiled into
+ * debug builds, and therefore will not be available in release builds.
+ *
+ * This method is meant to be called for every -v provided on the command
+ * line.
+ *
+ * Calling this method more than four times is acceptable.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_increase_verbosity (void)
+{
+  log_verbosity++;
+}
+
+/**
+ * ide_log_get_verbosity:
+ *
+ * Retrieves the log verbosity, which is the number of times -v was
+ * provided on the command line.
+ *
+ * Since: 3.32
+ */
+gint
+ide_log_get_verbosity (void)
+{
+  return log_verbosity;
+}
+
+/**
+ * ide_log_set_verbosity:
+ *
+ * Sets the explicit verbosity. Generally you want to use
+ * ide_log_increase_verbosity() instead of this function.
+ *
+ * Since: 3.32
+ */
+void
+ide_log_set_verbosity (gint level)
+{
+  log_verbosity = level;
+}
diff --git a/src/libide/core/ide-log.h b/src/libide/core/ide-log.h
new file mode 100644
index 000000000..c8052dca4
--- /dev/null
+++ b/src/libide/core/ide-log.h
@@ -0,0 +1,45 @@
+/* ide-log.h
+ *
+ * Copyright 2015-2019 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
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+void ide_log_init               (gboolean     stdout_,
+                                 const gchar *filename);
+IDE_AVAILABLE_IN_3_32
+void ide_log_increase_verbosity (void);
+IDE_AVAILABLE_IN_3_32
+gint ide_log_get_verbosity      (void);
+IDE_AVAILABLE_IN_3_32
+void ide_log_set_verbosity      (gint         level);
+IDE_AVAILABLE_IN_3_32
+void ide_log_shutdown           (void);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-macros.h b/src/libide/core/ide-macros.h
new file mode 100644
index 000000000..6519df04b
--- /dev/null
+++ b/src/libide/core/ide-macros.h
@@ -0,0 +1,249 @@
+/* ide-macros.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#ifndef __GI_SCANNER__
+
+#include <glib.h>
+
+#include "ide-global.h"
+#include "ide-object.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define ide_str_empty0(str)       (!(str) || !*(str))
+#define ide_str_equal0(str1,str2) (g_strcmp0(str1,str2)==0)
+#define ide_strv_empty0(strv)     (((strv) == NULL) || ((strv)[0] == NULL))
+#define ide_set_string(ptr,str)   (ide_take_string((ptr), g_strdup(str)))
+
+#define ide_clear_param(pptr, pval) \
+  G_STMT_START { if (pptr) { *(pptr) = pval; }; } G_STMT_END
+
+#define IDE_IS_MAIN_THREAD() (g_thread_self() == ide_get_main_thread())
+
+#define IDE_PTR_ARRAY_CLEAR_FREE_FUNC(ar)                       \
+  IDE_PTR_ARRAY_SET_FREE_FUNC(ar, NULL)
+#define IDE_PTR_ARRAY_SET_FREE_FUNC(ar, func)                   \
+  G_STMT_START {                                                \
+    if ((ar) != NULL)                                           \
+      g_ptr_array_set_free_func ((ar), (GDestroyNotify)(func)); \
+  } G_STMT_END
+#define IDE_PTR_ARRAY_STEAL_FULL(arptr)        \
+  ({ IDE_PTR_ARRAY_CLEAR_FREE_FUNC (*(arptr)); \
+     g_steal_pointer ((arptr)); })
+
+static inline void
+_g_object_unref0 (gpointer instance)
+{
+  if (instance)
+    g_object_unref (instance);
+}
+
+static inline gboolean
+ide_take_string (gchar **ptr,
+                 gchar  *str)
+{
+  if (*ptr != str)
+    {
+      g_free (*ptr);
+      *ptr = str;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static inline void
+ide_clear_string (gchar **ptr)
+{
+  g_free (*ptr);
+  *ptr = NULL;
+}
+
+static inline GList *
+_g_list_insert_before_link (GList *list,
+                            GList *sibling,
+                            GList *link_)
+{
+  g_return_val_if_fail (link_ != NULL, list);
+
+  if (!list)
+    {
+      g_return_val_if_fail (sibling == NULL, list);
+      return link_;
+    }
+  else if (sibling)
+    {
+      link_->prev = sibling->prev;
+      link_->next = sibling;
+      sibling->prev = link_;
+      if (link_->prev)
+        {
+          link_->prev->next = link_;
+          return list;
+        }
+      else
+        {
+          g_return_val_if_fail (sibling == list, link_);
+          return link_;
+        }
+    }
+  else
+    {
+      GList *last;
+
+      last = list;
+      while (last->next)
+        last = last->next;
+
+      last->next = link_;
+      last->next->prev = last;
+      last->next->next = NULL;
+
+      return list;
+    }
+}
+
+static inline void
+_g_queue_insert_before_link (GQueue *queue,
+                             GList  *sibling,
+                             GList  *link_)
+{
+  g_return_if_fail (queue != NULL);
+  g_return_if_fail (link_ != NULL);
+
+  if G_UNLIKELY (sibling == NULL)
+    {
+      /* We don't use g_list_insert_before_link() with a NULL sibling because it
+       * would be a O(n) operation and we would need to update manually the tail
+       * pointer.
+       */
+      g_queue_push_tail_link (queue, link_);
+    }
+  else
+    {
+      queue->head = _g_list_insert_before_link (queue->head, sibling, link_);
+      queue->length++;
+    }
+}
+
+static inline void
+_g_queue_insert_after_link (GQueue *queue,
+                            GList  *sibling,
+                            GList  *link_)
+{
+  g_return_if_fail (queue != NULL);
+  g_return_if_fail (link_ != NULL);
+
+  if (sibling == NULL)
+    g_queue_push_head_link (queue, link_);
+  else
+    _g_queue_insert_before_link (queue, sibling->next, link_);
+}
+
+static inline GPtrArray *
+_g_ptr_array_copy_objects (GPtrArray *ar)
+{
+  if (ar != NULL)
+    {
+      GPtrArray *copy = g_ptr_array_new_full (ar->len, g_object_unref);
+      for (guint i = 0; i < ar->len; i++)
+        g_ptr_array_add (copy, g_object_ref (g_ptr_array_index (ar, i)));
+      return g_steal_pointer (&copy);
+    }
+
+  return NULL;
+}
+
+static void
+ide_object_unref_and_destroy (IdeObject *object)
+{
+  if (object != NULL)
+    {
+      if (!ide_object_in_destruction (object))
+        ide_object_destroy (object);
+      g_object_unref (object);
+    }
+}
+
+typedef GPtrArray IdeObjectArray;
+
+static inline void
+ide_clear_and_destroy_object (gpointer pptr)
+{
+  IdeObject **ptr = pptr;
+
+  if (ptr && *ptr)
+    {
+      if (!ide_object_in_destruction (*ptr))
+        ide_object_destroy (*ptr);
+      g_clear_object (ptr);
+    }
+}
+
+static inline GPtrArray *
+ide_object_array_new (void)
+{
+  return g_ptr_array_new_with_free_func ((GDestroyNotify)ide_object_unref_and_destroy);
+}
+
+static inline gpointer
+ide_object_array_steal_index (IdeObjectArray *array,
+                              guint           position)
+{
+  gpointer ret = g_ptr_array_index (array, position);
+  g_ptr_array_index (array, position) = NULL;
+  g_ptr_array_remove_index (array, position);
+  return ret;
+}
+
+static inline gpointer
+ide_object_array_index (IdeObjectArray *array,
+                        guint           position)
+{
+  return g_ptr_array_index (array, position);
+}
+
+static inline void
+ide_object_array_add (IdeObjectArray *ar,
+                      gpointer        instance)
+{
+  g_ptr_array_add (ar, g_object_ref (IDE_OBJECT (instance)));
+}
+
+static inline void
+ide_object_array_unref (IdeObjectArray *ar)
+{
+  g_ptr_array_unref (ar);
+}
+
+#define IDE_OBJECT_ARRAY_STEAL_FULL(ar) IDE_PTR_ARRAY_STEAL_FULL(ar)
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeObjectArray, g_ptr_array_unref)
+
+G_END_DECLS
+
+#endif /* __GI_SCANNER__ */
diff --git a/src/libide/core/ide-notification.c b/src/libide/core/ide-notification.c
new file mode 100644
index 000000000..f41c4ed29
--- /dev/null
+++ b/src/libide/core/ide-notification.c
@@ -0,0 +1,1187 @@
+/* ide-notification.c
+ *
+ * Copyright 2018-2019 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 "ide-notification"
+
+#include "config.h"
+
+#include "ide-macros.h"
+#include "ide-notification.h"
+#include "ide-notifications.h"
+
+typedef struct
+{
+  gchar    *id;
+  gchar    *title;
+  gchar    *body;
+  GIcon    *icon;
+  gchar    *default_action;
+  GVariant *default_target;
+  GArray   *buttons;
+  gdouble   progress;
+  gint      priority;
+  guint     has_progress : 1;
+  guint     progress_is_imprecise : 1;
+  guint     urgent : 1;
+} IdeNotificationPrivate;
+
+typedef struct
+{
+  gchar    *label;
+  GIcon    *icon;
+  gchar    *action;
+  GVariant *target;
+} Button;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeNotification, ide_notification, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_BODY,
+  PROP_HAS_PROGRESS,
+  PROP_ICON,
+  PROP_ICON_NAME,
+  PROP_ID,
+  PROP_PRIORITY,
+  PROP_PROGRESS,
+  PROP_PROGRESS_IS_IMPRECISE,
+  PROP_TITLE,
+  PROP_URGENT,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+clear_button (Button *button)
+{
+  g_clear_pointer (&button->label, g_free);
+  g_clear_pointer (&button->action, g_free);
+  g_clear_pointer (&button->target, g_variant_unref);
+  g_clear_object (&button->icon);
+}
+
+static void
+ide_notification_destroy (IdeObject *object)
+{
+  IdeNotification *self = (IdeNotification *)object;
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_pointer (&priv->body, g_free);
+  g_clear_pointer (&priv->default_action, g_free);
+  g_clear_pointer (&priv->default_target, g_variant_unref);
+  g_clear_pointer (&priv->buttons, g_array_unref);
+  g_clear_object (&priv->icon);
+
+  IDE_OBJECT_CLASS (ide_notification_parent_class)->destroy (object);
+}
+
+static void
+ide_notification_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  IdeNotification *self = IDE_NOTIFICATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_BODY:
+      g_value_take_string (value, ide_notification_dup_body (self));
+      break;
+
+    case PROP_HAS_PROGRESS:
+      g_value_set_boolean (value, ide_notification_get_has_progress (self));
+      break;
+
+    case PROP_ICON:
+      g_value_take_object (value, ide_notification_ref_icon (self));
+      break;
+
+    case PROP_ID:
+      g_value_take_string (value, ide_notification_dup_id (self));
+      break;
+
+    case PROP_PRIORITY:
+      g_value_set_int (value, ide_notification_get_priority (self));
+      break;
+
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_notification_get_progress (self));
+      break;
+
+    case PROP_PROGRESS_IS_IMPRECISE:
+      g_value_set_boolean (value, ide_notification_get_progress_is_imprecise (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_take_string (value, ide_notification_dup_title (self));
+      break;
+
+    case PROP_URGENT:
+      g_value_set_boolean (value, ide_notification_get_urgent (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  IdeNotification *self = IDE_NOTIFICATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_BODY:
+      ide_notification_set_body (self, g_value_get_string (value));
+      break;
+
+    case PROP_HAS_PROGRESS:
+      ide_notification_set_has_progress (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_ICON:
+      ide_notification_set_icon (self, g_value_get_object (value));
+      break;
+
+    case PROP_ICON_NAME:
+      ide_notification_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ID:
+      ide_notification_set_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_PRIORITY:
+      ide_notification_set_priority (self, g_value_get_int (value));
+      break;
+
+    case PROP_PROGRESS:
+      ide_notification_set_progress (self, g_value_get_double (value));
+      break;
+
+    case PROP_PROGRESS_IS_IMPRECISE:
+      ide_notification_set_progress_is_imprecise (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_TITLE:
+      ide_notification_set_title (self, g_value_get_string (value));
+      break;
+
+    case PROP_URGENT:
+      ide_notification_set_urgent (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_class_init (IdeNotificationClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *ide_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_notification_get_property;
+  object_class->set_property = ide_notification_set_property;
+
+  ide_object_class->destroy = ide_notification_destroy;
+
+  /**
+   * IdeNotification:body:
+   *
+   * The "body" property is the main body of text for the notification.
+   * Not all notifications need this, but more complex notifications might.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_BODY] =
+    g_param_spec_string ("body",
+                         "Body",
+                         "The body of the notification",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:has-progress:
+   *
+   * The "has-progress" property denotes the notification will receive
+   * updates to the #IdeNotification:progress property.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_PROGRESS] =
+    g_param_spec_boolean ("has-progress",
+                          "Has Progress",
+                          "If the notification supports progress updates",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:icon:
+   *
+   * The "icon" property is an optional icon that may be shown next to
+   * the notification title and body under certain senarios.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ICON] =
+    g_param_spec_object ("icon",
+                         "Icon",
+                         "The icon for the notification, if any",
+                         G_TYPE_ICON,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:icon-name:
+   *
+   * The "icon-name" property is a helper to make setting #IdeNotification:icon
+   * more convenient.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "An icon-name to use to set IdeNotification:icon",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:id:
+   *
+   * The "id" property is an optional identifier that can be used to locate
+   * the notification later.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "An optional identifier for the notification",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:priority:
+   *
+   * The "priority" property is used to sort the notification in order of
+   * importance when displaying to the user.
+   *
+   * You may also use the #IdeNotification:urgent property to raise the
+   * importance of a message to the user.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority",
+                      "Priority",
+                      "The priority of the notification",
+                      G_MININT, G_MAXINT, 0,
+                      (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:progress:
+   *
+   * The "progress" property is a value between 0.0 and 1.0 describing the progress of
+   * the operation for which the notification represents.
+   *
+   * This property is ignored if #IdeNotification:has-progress is unset.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "The progress for the notification, if any",
+                         0.0, 1.0, 0.0,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:progress-is-imprecise:
+   *
+   * The "progress-is-imprecise" property indicates that the notification has
+   * progress, but it is imprecise.
+   *
+   * The UI may show a bouncing progress bar if set.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS_IS_IMPRECISE] =
+    g_param_spec_boolean ("progress-is-imprecise",
+                          "Progress is Imprecise",
+                          "If the notification supports progress, but is imprecise",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:title:
+   *
+   * The "title" property is the main text to show the user. It may be
+   * displayed more prominently such as in the titlebar.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the notification",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotification:urgent:
+   *
+   * If the notification is urgent. These notifications will be displayed with
+   * higher priority than those without the urgent property set.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_URGENT] =
+    g_param_spec_boolean ("urgent",
+                          "Urgent",
+                          "If it is urgent the user see the notification",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_notification_init (IdeNotification *self)
+{
+}
+
+/**
+ * ide_notification_new:
+ *
+ * Creates a new #IdeNotification.
+ *
+ * To "send" the notification, you should attach it to the #IdeNotifications
+ * object which can be found under the root #IdeObject. To simplify this,
+ * the ide_notification_attach() function is provided to locate the
+ * #IdeNotifications object using any #IdeObject you have access to.
+ *
+ * ```
+ * IdeNotification *notif = ide_notification_new ();
+ * setup_notification (notify);
+ * ide_notification_attach (notif, IDE_OBJECT (some_object));
+ * ```
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notification_new (void)
+{
+  return g_object_new (IDE_TYPE_NOTIFICATION, NULL);
+}
+
+/**
+ * ide_notification_attach:
+ * @self: an #IdeNotifications
+ * @object: an #IdeObject
+ *
+ * This function will locate the #IdeNotifications object starting from
+ * @object and attach @self as a child to that object.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_attach (IdeNotification *self,
+                         IdeObject       *object)
+{
+  g_autoptr(IdeObject) root = NULL;
+  g_autoptr(IdeObject) child = NULL;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (IDE_IS_OBJECT (object));
+
+  root = ide_object_ref_root (object);
+  child = ide_object_get_child_typed (root, IDE_TYPE_NOTIFICATIONS);
+
+  if (child != NULL)
+    ide_notifications_add_notification (IDE_NOTIFICATIONS (child), self);
+  else
+    g_warning ("Failed to locate IdeNotifications from %s", G_OBJECT_TYPE_NAME (object));
+}
+
+/**
+ * ide_notification_dup_id:
+ *
+ * Copies the id of the notification and returns it to the caller after locking
+ * the object. A copy is used to avoid thread-races.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_notification_dup_id (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_strdup (priv->id);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_notification_set_id:
+ * @self: an #IdeNotification
+ * @id: (nullable): a string containing the id, or %NULL
+ *
+ * Sets the #IdeNotification:id property.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_id (IdeNotification *self,
+                         const gchar     *id)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (!ide_str_equal0 (priv->id, id))
+    {
+      g_free (priv->id);
+      priv->id = g_strdup (id);
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_ID]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_dup_title:
+ *
+ * Copies the current title and returns it to the caller after locking the
+ * object. A copy is used to avoid thread-races.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_notification_dup_title (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_strdup (priv->title);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_notification_set_title:
+ * @self: an #IdeNotification
+ * @title: (nullable): a string containing the title text, or %NULL
+ *
+ * Sets the #IdeNotification:title property.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_title (IdeNotification *self,
+                            const gchar     *title)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (!ide_str_equal0 (priv->title, title))
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_TITLE]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_dup_body:
+ *
+ * Copies the current body and returns it to the caller after locking the
+ * object. A copy is used to avoid thread-races.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_notification_dup_body (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = g_strdup (priv->body);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_notification_set_body:
+ * @self: an #IdeNotification
+ * @body: (nullable): a string containing the body text, or %NULL
+ *
+ * Sets the #IdeNotification:body property.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_body (IdeNotification *self,
+                           const gchar     *body)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (!ide_str_equal0 (priv->body, body))
+    {
+      g_free (priv->body);
+      priv->body = g_strdup (body);
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_BODY]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_ref_icon:
+ *
+ * Gets the icon for the notification, and returns a new reference
+ * to the #GIcon.
+ *
+ * Returns: (transfer full) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_notification_ref_icon (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  GIcon *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  g_set_object (&ret, priv->icon);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+void
+ide_notification_set_icon (IdeNotification *self,
+                           GIcon           *icon)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (!icon || G_IS_ICON (icon));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (g_set_object (&priv->icon, icon))
+    ide_object_notify_by_pspec (self, properties [PROP_ICON]);
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+void
+ide_notification_set_icon_name (IdeNotification *self,
+                                const gchar     *icon_name)
+{
+  g_autoptr(GIcon) icon = NULL;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (!icon || G_IS_ICON (icon));
+
+  if (icon_name != NULL)
+    icon = g_themed_icon_new (icon_name);
+  ide_notification_set_icon (self, icon);
+}
+
+gint
+ide_notification_get_priority (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gint ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), 0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = priv->priority;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+void
+ide_notification_set_priority (IdeNotification *self,
+                               gint             priority)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->priority != priority)
+    {
+      priv->priority = priority;
+      ide_object_notify_by_pspec (self, properties [PROP_PRIORITY]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+gboolean
+ide_notification_get_urgent (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = priv->urgent;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+void
+ide_notification_set_urgent (IdeNotification *self,
+                             gboolean         urgent)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  urgent = !!urgent;
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->urgent != urgent)
+    {
+      priv->urgent = urgent;
+      ide_object_notify_by_pspec (self, properties [PROP_URGENT]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+guint
+ide_notification_get_n_buttons (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  guint ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->buttons != NULL)
+    ret = priv->buttons->len;
+  else
+    ret = 0;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+/**
+ * ide_notification_get_button:
+ * @self: an #IdeNotification
+ * @label: (out) (optional): a location for the button label
+ * @icon: (out) (optional): a location for the button icon
+ * @action: (out) (optional): a location for the button action name
+ * @target: (out) (optional): a location for the button action target
+ *
+ * Gets the button indexed by @button, and stores information about the
+ * button into the various out parameters @label, @icon, @action, and @target.
+ *
+ * Caller should check for the number of buttons using
+ * ide_notification_get_n_buttons() to determine the numerical range of
+ * indexes to provide for @button.
+ *
+ * To avoid racing with threads modifying notifications, the caller can
+ * hold a recursive lock across the function calls using ide_object_lock()
+ * and ide_object_unlock().
+ *
+ * Returns: %TRUE if @button was found; otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notification_get_button (IdeNotification  *self,
+                             guint             button,
+                             gchar           **label,
+                             GIcon           **icon,
+                             gchar           **action,
+                             GVariant        **target)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->buttons != NULL)
+    {
+      if (button < priv->buttons->len)
+        {
+          Button *b = &g_array_index (priv->buttons, Button, button);
+
+          if (label)
+            *label = g_strdup (b->label);
+          if (icon)
+            g_set_object (icon, b->icon);
+          if (action)
+            *action = g_strdup (b->action);
+          if (target)
+            *target = b->target ? g_variant_ref (b->target) : NULL;
+          ret = TRUE;
+        }
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+/**
+ * ide_notification_add_button:
+ * @self: an #IdeNotification
+ * @label: the label for the button
+ * @icon: (nullable): an optional icon for the button
+ * @detailed_action: a detailed action name (See #GAction)
+ *
+ * Adds a new button that may be displayed with the notification.
+ *
+ * See also: ide_notification_add_button_with_target_value().
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_add_button (IdeNotification *self,
+                             const gchar     *label,
+                             GIcon           *icon,
+                             const gchar     *detailed_action)
+{
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) target_value = NULL;
+  g_autofree gchar *action_name = NULL;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (label || icon);
+  g_return_if_fail (!icon || G_IS_ICON (icon));
+  g_return_if_fail (detailed_action != NULL);
+
+  if (!g_action_parse_detailed_name (detailed_action, &action_name, &target_value, &error))
+    g_warning ("Failed to parse detailed_action: %s", error->message);
+  else
+    ide_notification_add_button_with_target_value (self, label, icon, action_name, target_value);
+}
+
+/**
+ * ide_notification_add_button_with_target_value:
+ * @self: an #IdeNotification
+ * @label: the label for the button
+ * @icon: (nullable): an optional icon for the button
+ * @action: an action name (See #GAction)
+ * @target: (nullable): an optional #GVariant for the action target
+ *
+ * Adds a new button, used the parsed #GVariant format for the action
+ * target.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_add_button_with_target_value (IdeNotification *self,
+                                               const gchar     *label,
+                                               GIcon           *icon,
+                                               const gchar     *action,
+                                               GVariant        *target)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  Button b = {0};
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (label || icon);
+  g_return_if_fail (action != NULL);
+
+  b.label = g_strdup (label);
+  g_set_object (&b.icon, icon);
+  b.action = g_strdup (action);
+  b.target = target ? g_variant_ref (target) : NULL;
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->buttons == NULL)
+    {
+      priv->buttons = g_array_new (FALSE, FALSE, sizeof b);
+      g_array_set_clear_func (priv->buttons, (GDestroyNotify)clear_button);
+    }
+  g_array_append_val (priv->buttons, b);
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+gboolean
+ide_notification_get_default_action (IdeNotification  *self,
+                                     gchar           **action,
+                                     GVariant        **target)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->default_action != NULL)
+    {
+      if (action)
+        *action = g_strdup (priv->default_action);
+      if (target)
+        *target = priv->default_target ? g_variant_ref (priv->default_target) : NULL;
+      ret = TRUE;
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+void
+ide_notification_set_default_action (IdeNotification *self,
+                                     const gchar     *detailed_action)
+{
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) target_value = NULL;
+  g_autofree gchar *action_name = NULL;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (detailed_action != NULL);
+
+  if (!g_action_parse_detailed_name (detailed_action, &action_name, &target_value, &error))
+    g_warning ("Failed to parse detailed_action: %s", error->message);
+  else
+    ide_notification_set_default_action_and_target_value (self, action_name, target_value);
+}
+
+void
+ide_notification_set_default_action_and_target_value (IdeNotification *self,
+                                                      const gchar     *action,
+                                                      GVariant        *target)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+  g_return_if_fail (action != NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+
+  if (!ide_str_equal0 (priv->default_action, action))
+    {
+      g_free (priv->default_action);
+      priv->default_action = g_strdup (action);
+    }
+
+  if (priv->default_target != NULL &&
+      target != NULL &&
+      g_variant_equal (priv->default_target, target))
+    goto unlock;
+
+  g_clear_pointer (&priv->default_target, g_variant_unref);
+  priv->default_target = target ? g_variant_ref (target) : NULL;
+
+unlock:
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+gint
+ide_notification_compare (IdeNotification *a,
+                          IdeNotification *b)
+{
+  IdeNotificationPrivate *a_priv = ide_notification_get_instance_private (a);
+  IdeNotificationPrivate *b_priv = ide_notification_get_instance_private (b);
+
+  if (a_priv->urgent)
+    {
+      if (!b_priv->urgent)
+        return -1;
+    }
+
+  if (b_priv->urgent)
+    {
+      if (!a_priv->urgent)
+        return 1;
+    }
+
+  return a_priv->priority - b_priv->priority;
+}
+
+/**
+ * ide_notification_get_progress:
+ * @self: a #IdeNotification
+ *
+ * Gets the progress for the notification.
+ *
+ * Returns: a value between 0.0 and 1.0
+ *
+ * Since: 3.32
+ */
+gdouble
+ide_notification_get_progress (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gdouble ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), 0.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = priv->progress;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+/**
+ * ide_notification_set_progress:
+ * @self: a #IdeNotification
+ * @progress: a value between 0.0 and 1.0
+ *
+ * Sets the progress for the notification.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_progress (IdeNotification *self,
+                               gdouble          progress)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  progress = CLAMP (progress, 0.0, 1.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->progress != progress)
+    {
+      priv->progress = progress;
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROGRESS]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_get_has_progress:
+ * @self: a #IdeNotification
+ *
+ * Gets if the notification supports progress updates.
+ *
+ * Returns: %TRUE if progress updates are supported.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notification_get_has_progress (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), 0.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = priv->has_progress;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+/**
+ * ide_notification_set_has_progress:
+ * @self: a #IdeNotification
+ * @has_progress: if @notification supports progress
+ *
+ * Set to %TRUE if the notification supports progress updates.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_set_has_progress (IdeNotification *self,
+                                   gboolean         has_progress)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  has_progress = !!has_progress;
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->has_progress != has_progress)
+    {
+      priv->has_progress = has_progress;
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_HAS_PROGRESS]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+gboolean
+ide_notification_get_progress_is_imprecise (IdeNotification *self)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = priv->progress_is_imprecise;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
+
+void
+ide_notification_set_progress_is_imprecise (IdeNotification *self,
+                                            gboolean         progress_is_imprecise)
+{
+  IdeNotificationPrivate *priv = ide_notification_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  progress_is_imprecise = !!progress_is_imprecise;
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (priv->progress_is_imprecise != progress_is_imprecise)
+    {
+      priv->progress_is_imprecise = progress_is_imprecise;
+      ide_object_notify_by_pspec (IDE_OBJECT (self), properties [PROP_PROGRESS_IS_IMPRECISE]);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_notification_withdraw:
+ * @self: a #IdeNotification
+ *
+ * Withdraws the notification by removing it from the #IdeObject parent it
+ * belongs to.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_withdraw (IdeNotification *self)
+{
+  g_autoptr(IdeObject) parent = NULL;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  g_object_ref (self);
+  ide_object_lock (IDE_OBJECT (self));
+
+  if ((parent = ide_object_ref_parent (IDE_OBJECT (self))))
+    ide_object_remove (parent, IDE_OBJECT (self));
+
+  ide_object_unlock (IDE_OBJECT (self));
+  g_object_unref (self);
+}
+
+static gboolean
+do_withdrawal (gpointer data)
+{
+  ide_notification_withdraw (data);
+  return FALSE;
+}
+
+/**
+ * ide_notification_withdraw_in_seconds:
+ * @self: a #IdeNotification
+ * @seconds: number of seconds to withdraw after, or less than zero for a
+ *   sensible default.
+ *
+ * Withdraws @self from it's #IdeObject parent after @seconds have passed.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_withdraw_in_seconds (IdeNotification *self,
+                                      gint             seconds)
+{
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  if (seconds < 0)
+    seconds = 15;
+
+  g_timeout_add_seconds_full (G_PRIORITY_DEFAULT,
+                              seconds,
+                              do_withdrawal,
+                              g_object_ref (self),
+                              g_object_unref);
+}
+
+/**
+ * ide_notification_file_progress_callback:
+ *
+ * This function is a #GFileProgressCallback helper that will update the
+ * #IdeNotification:fraction property. @user_data must be an #IdeNotification.
+ *
+ * Remember to make sure to unref the #IdeNotification instance with
+ * g_object_unref() during the #GDestroyNotify.
+ *
+ * Since: 3.32
+ */
+void
+ide_notification_file_progress_callback (goffset  current_num_bytes,
+                                         goffset  total_num_bytes,
+                                         gpointer user_data)
+{
+  IdeNotification *self = user_data;
+  gdouble fraction = 0.0;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  if (total_num_bytes)
+    fraction = (gdouble)current_num_bytes / (gdouble)total_num_bytes;
+
+  ide_notification_set_progress (self, fraction);
+}
+
+void
+ide_notification_flatpak_progress_callback (const char *status,
+                                            guint       notification,
+                                            gboolean    estimating,
+                                            gpointer    user_data)
+{
+  IdeNotification *self = user_data;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION (self));
+
+  ide_notification_set_body (self, status);
+  ide_notification_set_progress (self, (gdouble)notification / 100.0);
+}
diff --git a/src/libide/core/ide-notification.h b/src/libide/core/ide-notification.h
new file mode 100644
index 000000000..fdb763a67
--- /dev/null
+++ b/src/libide/core/ide-notification.h
@@ -0,0 +1,143 @@
+/* ide-notification.h
+ *
+ * Copyright 2018-2019 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 "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATION (ide_notification_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeNotification, ide_notification, IDE, NOTIFICATION, IdeObject)
+
+struct _IdeNotificationClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private */
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeNotification *ide_notification_new                                 (void);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_attach                              (IdeNotification  *self,
+                                                                       IdeObject        *object);
+IDE_AVAILABLE_IN_3_32
+gchar           *ide_notification_dup_id                              (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_id                              (IdeNotification  *self,
+                                                                       const gchar      *id);
+IDE_AVAILABLE_IN_3_32
+gchar           *ide_notification_dup_title                           (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_title                           (IdeNotification  *self,
+                                                                       const gchar      *title);
+IDE_AVAILABLE_IN_3_32
+GIcon           *ide_notification_ref_icon                            (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_icon                            (IdeNotification  *self,
+                                                                       GIcon            *icon);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_icon_name                       (IdeNotification  *self,
+                                                                       const gchar      *icon_name);
+IDE_AVAILABLE_IN_3_32
+gchar           *ide_notification_dup_body                            (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_body                            (IdeNotification  *self,
+                                                                       const gchar      *body);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_notification_get_has_progress                    (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_has_progress                    (IdeNotification  *self,
+                                                                       gboolean          has_progress);
+IDE_AVAILABLE_IN_3_32
+gint             ide_notification_get_priority                        (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_priority                        (IdeNotification  *self,
+                                                                       gint              priority);
+IDE_AVAILABLE_IN_3_32
+gdouble          ide_notification_get_progress                        (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_progress                        (IdeNotification  *self,
+                                                                       gdouble           progress);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_notification_get_progress_is_imprecise           (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_progress_is_imprecise           (IdeNotification  *self,
+                                                                       gboolean          
progress_is_imprecise);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_notification_get_urgent                          (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_urgent                          (IdeNotification  *self,
+                                                                       gboolean          urgent);
+IDE_AVAILABLE_IN_3_32
+guint            ide_notification_get_n_buttons                       (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_notification_get_button                          (IdeNotification  *self,
+                                                                       guint             button,
+                                                                       gchar           **label,
+                                                                       GIcon           **icon,
+                                                                       gchar           **action,
+                                                                       GVariant        **target);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_add_button                          (IdeNotification  *self,
+                                                                       const gchar      *label,
+                                                                       GIcon            *icon,
+                                                                       const gchar      *detailed_action);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_add_button_with_target_value        (IdeNotification  *self,
+                                                                       const gchar      *label,
+                                                                       GIcon            *icon,
+                                                                       const gchar      *action,
+                                                                       GVariant         *target);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_notification_get_default_action                  (IdeNotification  *self,
+                                                                       gchar           **action,
+                                                                       GVariant        **target);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_default_action                  (IdeNotification  *self,
+                                                                       const gchar      *detailed_action);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_set_default_action_and_target_value (IdeNotification  *self,
+                                                                       const gchar      *action,
+                                                                       GVariant         *target);
+IDE_AVAILABLE_IN_3_32
+gint             ide_notification_compare                             (IdeNotification  *a,
+                                                                       IdeNotification  *b);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_withdraw                            (IdeNotification  *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_withdraw_in_seconds                 (IdeNotification  *self,
+                                                                       gint              seconds);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_file_progress_callback              (goffset           current_num_bytes,
+                                                                       goffset           total_num_bytes,
+                                                                       gpointer          user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_notification_flatpak_progress_callback           (const char       *status,
+                                                                       guint             notification,
+                                                                       gboolean          estimating,
+                                                                       gpointer          user_data);
+
+
+G_END_DECLS
diff --git a/src/libide/core/ide-notifications.c b/src/libide/core/ide-notifications.c
new file mode 100644
index 000000000..9c09da832
--- /dev/null
+++ b/src/libide/core/ide-notifications.c
@@ -0,0 +1,516 @@
+/* ide-notifications.c
+ *
+ * Copyright 2018-2019 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 "ide-notifications"
+
+#include "config.h"
+
+#include "ide-macros.h"
+#include "ide-notifications.h"
+
+struct _IdeNotifications
+{
+  IdeObject parent_instance;
+};
+
+typedef struct
+{
+  gdouble progress;
+  guint   total;
+  guint   imprecise;
+} Progress;
+
+typedef struct
+{
+  const gchar     *id;
+  IdeNotification *notif;
+} Find;
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeNotifications, ide_notifications, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_HAS_PROGRESS,
+  PROP_PROGRESS,
+  PROP_PROGRESS_IS_IMPRECISE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_notifications_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeNotifications *self = IDE_NOTIFICATIONS (object);
+
+  switch (prop_id)
+    {
+    case PROP_HAS_PROGRESS:
+      g_value_set_boolean (value, ide_notifications_get_has_progress (self));
+      break;
+
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_notifications_get_progress (self));
+      break;
+
+    case PROP_PROGRESS_IS_IMPRECISE:
+      g_value_set_boolean (value, ide_notifications_get_progress_is_imprecise (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notifications_child_notify_progress_cb (IdeNotifications *self,
+                                            GParamSpec       *pspec,
+                                            IdeNotification  *child)
+{
+  g_assert (IDE_IS_NOTIFICATIONS (self));
+  g_assert (IDE_IS_NOTIFICATION (child));
+
+  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
+}
+
+static void
+ide_notifications_add (IdeObject         *object,
+                       IdeObject         *sibling,
+                       IdeObject         *child,
+                       IdeObjectLocation  location)
+{
+  IdeNotifications *self = (IdeNotifications *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_NOTIFICATIONS (object));
+  g_assert (IDE_IS_OBJECT (child));
+
+  if (!IDE_IS_NOTIFICATION (child))
+    {
+      g_warning ("Attempt to add something other than an IdeNotification is not allowed");
+      return;
+    }
+
+  g_signal_connect_object (child,
+                           "notify::progress",
+                           G_CALLBACK (ide_notifications_child_notify_progress_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  IDE_OBJECT_CLASS (ide_notifications_parent_class)->add (object, sibling, child, location);
+
+  g_list_model_items_changed (G_LIST_MODEL (object), ide_object_get_position (child), 0, 1);
+  ide_object_notify_by_pspec (self, properties [PROP_HAS_PROGRESS]);
+  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS_IS_IMPRECISE]);
+  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
+}
+
+static void
+ide_notifications_remove (IdeObject *object,
+                          IdeObject *child)
+{
+  IdeNotifications *self = (IdeNotifications *)object;
+  guint position;
+
+  g_assert (IDE_IS_NOTIFICATIONS (self));
+  g_assert (IDE_IS_OBJECT (child));
+
+  g_signal_handlers_disconnect_by_func (child,
+                                        G_CALLBACK (ide_notifications_child_notify_progress_cb),
+                                        self);
+
+  position = ide_object_get_position (child);
+
+  IDE_OBJECT_CLASS (ide_notifications_parent_class)->remove (object, child);
+
+  g_list_model_items_changed (G_LIST_MODEL (object), position, 1, 0);
+  ide_object_notify_by_pspec (self, properties [PROP_HAS_PROGRESS]);
+  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS_IS_IMPRECISE]);
+  ide_object_notify_by_pspec (self, properties [PROP_PROGRESS]);
+}
+
+static void
+ide_notifications_class_init (IdeNotificationsClass *klass)
+{
+  GObjectClass *g_object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *object_class = IDE_OBJECT_CLASS (klass);
+
+  g_object_class->get_property = ide_notifications_get_property;
+
+  object_class->add = ide_notifications_add;
+  object_class->remove = ide_notifications_remove;
+
+  /**
+   * IdeNotifications:has-progress:
+   *
+   * The "has-progress" property denotes if any of the notifications
+   * have progress supported.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_PROGRESS] =
+    g_param_spec_boolean ("has-progress",
+                          "Has Progress",
+                          "If any of the notifications have progress",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotifications:progress:
+   *
+   * The "progress" property is the combination of all of the notifications
+   * currently monitored. It is updated when child notifications progress
+   * changes.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "The combined process of all child notifications",
+                         0.0, 1.0, 0.0,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeNotifications:progress-is-imprecise:
+   *
+   * The "progress-is-imprecise" property indicates that all progress-bearing
+   * notifications are imprecise.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS_IS_IMPRECISE] =
+    g_param_spec_boolean ("progress-is-imprecise",
+                          "Progress is Imprecise",
+                          "If all of the notifications have imprecise progress",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (g_object_class, N_PROPS, properties);
+}
+
+static void
+ide_notifications_init (IdeNotifications *self)
+{
+#if 0
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(IdeNotification) notif2 = NULL;
+  g_autoptr(IdeNotification) notif3 = NULL;
+  g_autoptr(IdeNotification) notif4 = NULL;
+  g_autoptr(IdeNotification) notif5 = NULL;
+  g_autoptr(IdeNotification) notif6 = NULL;
+  g_autoptr(IdeNotification) notif7 = NULL;
+  g_autoptr(GIcon) icon1 = NULL;
+  g_autoptr(GIcon) icon2 = NULL;
+  g_autoptr(GIcon) icon4 = NULL;
+  g_autoptr(GIcon) icon5 = NULL;
+  g_autoptr(GIcon) icon6 = NULL;
+  g_autoptr(GIcon) icon7 = NULL;
+
+  notif = ide_notification_new ();
+  ide_notification_set_title (notif, "Builder ready.");
+  ide_notification_set_has_progress (notif, FALSE);
+  ide_notification_add_button (notif, "Foo", (icon1 = g_icon_new_for_string 
("media-playback-pause-symbolic", NULL)), "debugger.pause");
+  ide_notifications_add_notification (self, notif);
+
+  notif2 = ide_notification_new ();
+  ide_notification_set_title (notif2, "Downloading libdazzle…");
+  ide_notification_set_has_progress (notif2, TRUE);
+  ide_notification_set_progress (notif2, .75);
+  ide_notification_set_default_action (notif2, "win.close");
+  ide_notification_add_button (notif2, "Foo", (icon2 = g_icon_new_for_string ("process-stop-symbolic", 
NULL)), "build-manager.stop");
+  ide_notifications_add_notification (self, notif2);
+
+  notif3 = ide_notification_new ();
+  ide_notification_set_title (notif3, "SDK Not Installed");
+  ide_notification_set_body (notif3, "The org.gnome.Calculator.json build profile requires the 
org.gnome.Platform runtime. Install it to allow this project to be built.");
+  ide_notification_set_has_progress (notif3, FALSE);
+  ide_notification_set_progress (notif3, 0);
+  ide_notification_add_button (notif3, "Download and Install", NULL, "win.close");
+  ide_notification_set_default_action (notif3, "win.close");
+  ide_notification_set_urgent (notif3, TRUE);
+  ide_notifications_add_notification (self, notif3);
+
+  notif4 = ide_notification_new ();
+  ide_notification_set_title (notif4, "Code Analytics Unavailable");
+  ide_notification_set_body (notif4, "Code highlighting, error detection, and macros are not fully 
available, due to this project not being built recently. Rebuild to fully enable these features.");
+  ide_notification_set_has_progress (notif4, FALSE);
+  ide_notification_set_progress (notif4, 0);
+  ide_notification_set_default_action (notif4, "win.close");
+  ide_notifications_add_notification (self, notif4);
+
+  notif5 = ide_notification_new ();
+  ide_notification_set_title (notif5, "Running Partial Build");
+  ide_notification_set_body (notif5, "Diagnostics and autocompletion may be limited until complete.");
+  ide_notification_set_has_progress (notif5, TRUE);
+  ide_notification_set_progress_is_imprecise (notif5, TRUE);
+  ide_notification_set_progress (notif5, 0);
+  ide_notification_add_button (notif5, NULL, (icon5 = g_icon_new_for_string ("process-stop-symbolic", 
NULL)), "win.close");
+  ide_notifications_add_notification (self, notif5);
+
+  notif6 = ide_notification_new ();
+  ide_notification_set_title (notif6, "Indexing Source Code");
+  ide_notification_set_body (notif6, "Search, diagnostics, and autocompletion may be limited until 
complete.");
+  ide_notification_set_has_progress (notif6, TRUE);
+  ide_notification_set_progress (notif6, 0);
+  ide_notification_set_progress_is_imprecise (notif6, TRUE);
+  ide_notification_add_button (notif6, NULL, (icon6 = g_icon_new_for_string 
("media-playback-pause-symbolic", NULL)), "win.close");
+  ide_notifications_add_notification (self, notif6);
+
+  notif7 = ide_notification_new ();
+  ide_notification_set_title (notif7, "Downloading org.gnome.Platform");
+  ide_notification_set_body (notif7, "3 minutes remaining");
+  ide_notification_set_has_progress (notif7, TRUE);
+  ide_notification_set_progress (notif7, 0);
+  ide_notification_add_button (notif7, NULL, (icon7 = g_icon_new_for_string ("process-stop-symbolic", 
NULL)), "win.close");
+  ide_notifications_add_notification (self, notif7);
+
+  ide_notification_withdraw_in_seconds (notif, 10);
+  ide_notification_withdraw_in_seconds (notif2, 12);
+  ide_notification_withdraw_in_seconds (notif3, 14);
+  ide_notification_withdraw_in_seconds (notif4, 16);
+  ide_notification_withdraw_in_seconds (notif5, 18);
+#endif
+}
+
+/**
+ * ide_notifications_new:
+ *
+ * Create a new #IdeNotifications.
+ *
+ * Usually, creating this is not necessary, as the #IdeContext root
+ * #IdeObject will create it automatically.
+ *
+ * Returns: (transfer full): a newly created #IdeNotifications
+ *
+ * Since: 3.32
+ */
+IdeNotifications *
+ide_notifications_new (void)
+{
+  return g_object_new (IDE_TYPE_NOTIFICATIONS, NULL);
+}
+
+/**
+ * ide_notifications_add_notification:
+ * @self: an #IdeNotifications
+ * @notification: an #IdeNotification
+ *
+ * Adds @notification as a child of @self, sorting it by priority
+ * and urgency.
+ *
+ * Since: 3.32
+ */
+void
+ide_notifications_add_notification (IdeNotifications *self,
+                                    IdeNotification  *notification)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_NOTIFICATIONS (self));
+  g_return_if_fail (IDE_IS_NOTIFICATION (notification));
+
+  ide_object_insert_sorted (IDE_OBJECT (self),
+                            IDE_OBJECT (notification),
+                            (GCompareDataFunc)ide_notification_compare,
+                            NULL);
+}
+
+static GType
+ide_notifications_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_NOTIFICATION;
+}
+
+static guint
+ide_notifications_get_n_items (GListModel *model)
+{
+  return ide_object_get_n_children (IDE_OBJECT (model));
+}
+
+static gpointer
+ide_notifications_get_item (GListModel *model,
+                            guint       position)
+{
+  return ide_object_get_nth_child (IDE_OBJECT (model), position);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_notifications_get_item_type;
+  iface->get_n_items = ide_notifications_get_n_items;
+  iface->get_item = ide_notifications_get_item;
+}
+
+static void
+collect_progress_cb (gpointer item,
+                     gpointer user_data)
+{
+  IdeNotification *notif = item;
+  Progress *prog = user_data;
+
+  g_assert (IDE_IS_NOTIFICATION (notif));
+  g_assert (prog != NULL);
+
+  if (ide_notification_get_has_progress (notif))
+    {
+      if (ide_notification_get_progress_is_imprecise (notif))
+        prog->imprecise++;
+      else
+        prog->progress += ide_notification_get_progress (notif);
+
+      prog->total++;
+    }
+}
+
+/**
+ * ide_notifications_get_progress:
+ * @self: a #IdeNotifications
+ *
+ * Gets the combined progress of the notifications contained in this
+ * #IdeNotifications object.
+ *
+ * Returns: A double between 0.0 and 1.0
+ *
+ * Since: 3.32
+ */
+gdouble
+ide_notifications_get_progress (IdeNotifications *self)
+{
+  Progress prog = {0};
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  if (prog.total > 0)
+    {
+      if (prog.imprecise != prog.total)
+        return prog.progress / (gdouble)(prog.total - prog.imprecise);
+      else
+        return prog.progress / (gdouble)prog.total;
+    }
+
+  return 0.0;
+}
+
+/**
+ * ide_notifications_get_has_progress:
+ * @self: a #IdeNotifications
+ *
+ * Gets if any of the notification support progress updates.
+ *
+ * Returns: %TRUE if any notification has progress
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notifications_get_has_progress (IdeNotifications *self)
+{
+  Progress prog = {0};
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return prog.total > 0;
+}
+
+/**
+ * ide_notifications_get_progress_is_imprecise:
+ * @self: a #IdeNotifications
+ *
+ * Checks if all of the notifications with progress are imprecise.
+ *
+ * Returns: %TRUE if all progress-supporting notifications are imprecise.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_notifications_get_progress_is_imprecise (IdeNotifications *self)
+{
+  Progress prog = {0};
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), 0.0);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ide_object_foreach (IDE_OBJECT (self), collect_progress_cb, &prog);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  if (prog.total > 0)
+    return prog.imprecise == prog.total;
+
+  return FALSE;
+}
+
+static void
+find_by_id (gpointer item,
+            gpointer user_data)
+{
+  IdeNotification *notif = item;
+  Find *find = user_data;
+  g_autofree gchar *id = NULL;
+
+  if (find->notif)
+    return;
+
+  id = ide_notification_dup_id (notif);
+
+  if (ide_str_equal0 (find->id, id))
+    find->notif = g_object_ref (notif);
+}
+
+/**
+ * ide_notifications_find_by_id:
+ * @self: a #IdeNotifications
+ * @id: the id of the notification
+ *
+ * Finds the first #IdeNotification registered with @self with
+ * #IdeNotification:id of @id.
+ *
+ * Returns: (transfer full) (nullable): an #IdeNotification or %NULL
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notifications_find_by_id (IdeNotifications *self,
+                              const gchar      *id)
+{
+  Find find = { id, NULL };
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATIONS (self), NULL);
+  g_return_val_if_fail (id != NULL, NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ide_object_foreach (IDE_OBJECT (self), find_by_id, &find);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&find.notif);
+}
diff --git a/src/libide/core/ide-notifications.h b/src/libide/core/ide-notifications.h
new file mode 100644
index 000000000..fc482cfe4
--- /dev/null
+++ b/src/libide/core/ide-notifications.h
@@ -0,0 +1,48 @@
+/* ide-notifications.h
+ *
+ * Copyright 2018-2019 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 "ide-object.h"
+#include "ide-notification.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATIONS (ide_notifications_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeNotifications, ide_notifications, IDE, NOTIFICATIONS, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeNotifications *ide_notifications_new                       (void);
+IDE_AVAILABLE_IN_3_32
+void              ide_notifications_add_notification          (IdeNotifications *self,
+                                                               IdeNotification  *notification);
+IDE_AVAILABLE_IN_3_32
+gdouble           ide_notifications_get_progress              (IdeNotifications *self);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_notifications_get_has_progress          (IdeNotifications *self);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_notifications_get_progress_is_imprecise (IdeNotifications *self);
+IDE_AVAILABLE_IN_3_32
+IdeNotification  *ide_notifications_find_by_id                (IdeNotifications *self,
+                                                               const gchar      *id);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-object-box.c b/src/libide/core/ide-object-box.c
new file mode 100644
index 000000000..3a3ad2383
--- /dev/null
+++ b/src/libide/core/ide-object-box.c
@@ -0,0 +1,289 @@
+/* ide-object-box.c
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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 "ide-object-box"
+
+#include "config.h"
+
+#include "ide-object-box.h"
+#include "ide-macros.h"
+
+struct _IdeObjectBox
+{
+  IdeObject  parent_instance;
+  GObject   *object;
+  guint      propagate_disposal : 1;
+};
+
+G_DEFINE_TYPE (IdeObjectBox, ide_object_box, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_OBJECT,
+  PROP_PROPAGATE_DISPOSAL,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_object_box_set_object (IdeObjectBox *self,
+                           GObject      *object)
+{
+  g_return_if_fail (IDE_IS_OBJECT_BOX (self));
+  g_return_if_fail (G_IS_OBJECT (object));
+  g_return_if_fail (g_object_get_data (object, "IDE_OBJECT_BOX") == NULL);
+
+  self->object = g_object_ref (object);
+  g_object_set_data (self->object, "IDE_OBJECT_BOX", self);
+}
+
+/**
+ * ide_object_box_new:
+ *
+ * Create a new #IdeObjectBox.
+ *
+ * Returns: (transfer full): a newly created #IdeObjectBox
+ *
+ * Since: 3.32
+ */
+IdeObjectBox *
+ide_object_box_new (GObject *object)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+
+  return g_object_new (IDE_TYPE_OBJECT_BOX,
+                       "object", object,
+                       NULL);
+}
+
+static gchar *
+ide_object_box_repr (IdeObject *object)
+{
+  g_autoptr(GObject) obj = ide_object_box_ref_object (IDE_OBJECT_BOX (object));
+
+  if (obj != NULL)
+    return g_strdup_printf ("%s object=\"%s\"",
+                            G_OBJECT_TYPE_NAME (object),
+                            G_OBJECT_TYPE_NAME (obj));
+  else
+    return IDE_OBJECT_CLASS (ide_object_box_parent_class)->repr (object);
+}
+
+static void
+ide_object_box_destroy (IdeObject *object)
+{
+  IdeObjectBox *self = (IdeObjectBox *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_OBJECT (self));
+
+  g_object_ref (self);
+
+  /* Clear the backpointer before any disposal to the object, since that
+   * will possibly result in the object calling back into this peer object.
+   */
+  if (self->object)
+    {
+      g_object_set_data (G_OBJECT (self->object), "IDE_OBJECT_BOX", NULL);
+      if (self->propagate_disposal)
+        g_object_run_dispose (G_OBJECT (self->object));
+    }
+
+  IDE_OBJECT_CLASS (ide_object_box_parent_class)->destroy (object);
+
+  g_clear_object (&self->object);
+
+  g_object_unref (self);
+}
+
+static void
+ide_object_box_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeObjectBox *self = IDE_OBJECT_BOX (object);
+
+  switch (prop_id)
+    {
+    case PROP_OBJECT:
+      g_value_take_object (value, ide_object_box_ref_object (self));
+      break;
+
+    case PROP_PROPAGATE_DISPOSAL:
+      g_value_set_boolean (value, self->propagate_disposal);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_object_box_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeObjectBox *self = IDE_OBJECT_BOX (object);
+
+  switch (prop_id)
+    {
+    case PROP_OBJECT:
+      ide_object_box_set_object (self, g_value_get_object (value));
+      break;
+
+    case PROP_PROPAGATE_DISPOSAL:
+      self->propagate_disposal = g_value_get_boolean (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_object_box_class_init (IdeObjectBoxClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_object_box_get_property;
+  object_class->set_property = ide_object_box_set_property;
+
+  i_object_class->destroy = ide_object_box_destroy;
+  i_object_class->repr = ide_object_box_repr;
+
+  /**
+   * IdeObjectBox:object:
+   *
+   * The "object" property contains the object that is boxed and
+   * placed onto the object graph using this box.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_OBJECT] =
+    g_param_spec_object ("object",
+                         "Object",
+                         "The boxed object",
+                         G_TYPE_OBJECT,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeObjectBox:propagate-disposal:
+   *
+   * The "propagate-disposal" property denotes if the #IdeObject:object
+   * property contents should have g_object_run_dispose() called when the
+   * #IdeObjectBox is destroyed.
+   *
+   * This is useful when you want to force disposal of an external object
+   * when @self is removed from the object tree.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROPAGATE_DISPOSAL] =
+    g_param_spec_boolean ("propagate-disposal",
+                          "Propagate Disposal",
+                          "If the object should be disposed when the box is destroyed",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_object_box_init (IdeObjectBox *self)
+{
+  self->propagate_disposal = TRUE;
+}
+
+/**
+ * ide_object_box_ref_object:
+ * @self: an #IdeObjectBox
+ *
+ * Gets the boxed object.
+ *
+ * Returns: (transfer full) (nullable) (type GObject): a #GObject or %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_box_ref_object (IdeObjectBox *self)
+{
+  GObject *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_OBJECT_BOX (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = self->object ? g_object_ref (self->object) : NULL;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_box_from_object:
+ * @object: a #GObject
+ *
+ * Gets the #IdeObjectBox that contains @object, if any.
+ *
+ * This function may only be called from the main thread.
+ *
+ * Returns: (transfer none): an #IdeObjectBox
+ *
+ * Since: 3.32
+ */
+IdeObjectBox *
+ide_object_box_from_object (GObject *object)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (G_IS_OBJECT (object), NULL);
+
+  return g_object_get_data (G_OBJECT (object), "IDE_OBJECT_BOX");
+}
+
+/**
+ * ide_object_box_contains:
+ * @self: a #IdeObjectBox
+ * @instance: (type GObject) (nullable): a #GObject or %NULL
+ *
+ * Checks if @self contains @instance.
+ *
+ * Returns: %TRUE if #IdeObjectBox:object matches @instance
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_object_box_contains (IdeObjectBox *self,
+                         gpointer      instance)
+{
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_OBJECT_BOX (self), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = (instance == (gpointer)self->object);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return ret;
+}
diff --git a/src/libide/core/ide-object-box.h b/src/libide/core/ide-object-box.h
new file mode 100644
index 000000000..eb81085fb
--- /dev/null
+++ b/src/libide/core/ide-object-box.h
@@ -0,0 +1,46 @@
+/* ide-object-box.h
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-object.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_OBJECT_BOX (ide_object_box_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeObjectBox, ide_object_box, IDE, OBJECT_BOX, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeObjectBox *ide_object_box_new         (GObject      *object);
+IDE_AVAILABLE_IN_3_32
+gpointer      ide_object_box_ref_object  (IdeObjectBox *self);
+IDE_AVAILABLE_IN_3_32
+IdeObjectBox *ide_object_box_from_object (GObject      *object);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_object_box_contains    (IdeObjectBox *self,
+                                          gpointer      instance);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-object-notify.c b/src/libide/core/ide-object-notify.c
new file mode 100644
index 000000000..42cfe92ed
--- /dev/null
+++ b/src/libide/core/ide-object-notify.c
@@ -0,0 +1,114 @@
+/* ide-object-notify.c
+ *
+ * Copyright 2018-2019 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 "ide-object-notify"
+
+#include "config.h"
+
+#include "ide-object.h"
+#include "ide-macros.h"
+
+typedef struct
+{
+  GObject    *object;
+  GParamSpec *pspec;
+} NotifyInMain;
+
+static gboolean
+ide_object_notify_in_main_cb (gpointer data)
+{
+  NotifyInMain *notify = data;
+
+  g_assert (notify != NULL);
+  g_assert (G_IS_OBJECT (notify->object));
+  g_assert (notify->pspec != NULL);
+
+  g_object_notify_by_pspec (notify->object, notify->pspec);
+
+  g_object_unref (notify->object);
+  g_param_spec_unref (notify->pspec);
+  g_slice_free (NotifyInMain, notify);
+
+  return G_SOURCE_REMOVE;
+}
+
+/**
+ * ide_object_notify_by_pspec:
+ * @instance: a #IdeObjectNotify
+ * @pspec: a #GParamSpec
+ *
+ * Like g_object_notify_by_pspec() if the caller is in the main-thread.
+ * Otherwise, the request is deferred to the main thread.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_notify_by_pspec (gpointer    instance,
+                            GParamSpec *pspec)
+{
+  NotifyInMain *notify;
+
+  g_return_if_fail (G_IS_OBJECT (instance));
+  g_return_if_fail (G_IS_PARAM_SPEC (pspec));
+
+  if G_LIKELY (IDE_IS_MAIN_THREAD ())
+    {
+      g_object_notify_by_pspec (instance, pspec);
+      return;
+    }
+
+  notify = g_slice_new0 (NotifyInMain);
+  notify->pspec = g_param_spec_ref (pspec);
+  notify->object = g_object_ref (instance);
+
+  g_timeout_add (0, ide_object_notify_in_main_cb, g_steal_pointer (&notify));
+}
+
+/**
+ * ide_object_notify_in_main:
+ * @instance: (type GObject.Object): a #GObject
+ * @pspec: a #GParamSpec
+ *
+ * This helper will perform a g_object_notify_by_pspec() with the
+ * added requirement that it is run from the applications main thread.
+ *
+ * You may want to do this when modifying state from a thread, but only
+ * notify from the Gtk+ thread.
+ *
+ * This will *always* return to the default main context, and never
+ * emit ::notify immediately.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_notify_in_main (gpointer    instance,
+                           GParamSpec *pspec)
+{
+  NotifyInMain *notify;
+
+  g_return_if_fail (G_IS_OBJECT (instance));
+  g_return_if_fail (G_IS_PARAM_SPEC (pspec));
+
+  notify = g_slice_new0 (NotifyInMain);
+  notify->pspec = g_param_spec_ref (pspec);
+  notify->object = g_object_ref (instance);
+
+  g_timeout_add (0, ide_object_notify_in_main_cb, g_steal_pointer (&notify));
+}
diff --git a/src/libide/core/ide-object.c b/src/libide/core/ide-object.c
new file mode 100644
index 000000000..ffabce957
--- /dev/null
+++ b/src/libide/core/ide-object.c
@@ -0,0 +1,1367 @@
+/* ide-object.c
+ *
+ * Copyright 2014-2019 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 "ide-object"
+
+#include "config.h"
+
+#include "ide-context.h"
+#include "ide-object.h"
+#include "ide-macros.h"
+
+/**
+ * SECTION:ide-object
+ * @title: IdeObject
+ * @short_description: Base object with support for object trees
+ *
+ * #IdeObject is a specialized #GObject for use in Builder. It provides a
+ * hierarchy of objects using a specialized tree similar to a DOM. You can
+ * insert/append/prepend objects to a parent node, and track their lifetime
+ * as part of the tree.
+ *
+ * When an object is removed from the tree, it can automatically be destroyed
+ * via the #IdeObject::destroy signal. This is useful as it may cause the
+ * children of that object to be removed, recursively destroying the objects
+ * descendants. This behavior is ideal when you want a large amount of objects
+ * to be reclaimed once an ancestor is no longer necessary.
+ *
+ * #IdeObject's may also have a #GCancellable associated with them. The
+ * cancellable is created on demand when ide_object_ref_cancellable() is
+ * called. When the object is destroyed, the #GCancellable::cancel signal
+ * is emitted. This allows automatic cleanup of asynchronous operations
+ * when used properly.
+ *
+ * Since: 3.32
+ */
+
+typedef struct
+{
+  GRecMutex     mutex;
+  GCancellable *cancellable;
+  IdeObject    *parent;
+  GQueue        children;
+  GList         link;
+  guint         in_destruction : 1;
+  guint         destroyed : 1;
+} IdeObjectPrivate;
+
+typedef struct
+{
+  GType      type;
+  IdeObject *child;
+} GetChildTyped;
+
+typedef struct
+{
+  GType      type;
+  GPtrArray *array;
+} GetChildrenTyped;
+
+enum {
+  PROP_0,
+  PROP_CANCELLABLE,
+  PROP_PARENT,
+  N_PROPS
+};
+
+enum {
+  DESTROY,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeObject, ide_object, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static inline void
+ide_object_private_lock (IdeObjectPrivate *priv)
+{
+  g_rec_mutex_lock (&priv->mutex);
+}
+
+static inline void
+ide_object_private_unlock (IdeObjectPrivate *priv)
+{
+  g_rec_mutex_unlock (&priv->mutex);
+}
+
+static gboolean
+check_disposition (IdeObject        *child,
+                   IdeObject        *parent,
+                   IdeObjectPrivate *sibling_priv)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (child);
+
+  if (priv->parent != NULL)
+    {
+      g_critical ("Attempt to add %s to %s, but it already has a parent",
+                  G_OBJECT_TYPE_NAME (child),
+                  G_OBJECT_TYPE_NAME (parent));
+      return FALSE;
+    }
+
+  if (sibling_priv && sibling_priv->parent != parent)
+    {
+      g_critical ("Attempt to add child relative to sibling of another parent");
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+static gchar *
+ide_object_real_repr (IdeObject *self)
+{
+  return g_strdup (G_OBJECT_TYPE_NAME (self));
+}
+
+static void
+ide_object_real_add (IdeObject         *self,
+                     IdeObject         *sibling,
+                     IdeObject         *child,
+                     IdeObjectLocation  location)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObjectPrivate *child_priv = ide_object_get_instance_private (child);
+  IdeObjectPrivate *sibling_priv = ide_object_get_instance_private (sibling);
+
+  g_assert (IDE_IS_OBJECT (self));
+  g_assert (IDE_IS_OBJECT (child));
+  g_assert (!sibling || IDE_IS_OBJECT (sibling));
+
+  if (location == IDE_OBJECT_BEFORE_SIBLING ||
+      location == IDE_OBJECT_AFTER_SIBLING)
+    g_return_if_fail (IDE_IS_OBJECT (sibling));
+
+  ide_object_private_lock (priv);
+  ide_object_private_lock (child_priv);
+
+  if (sibling)
+    ide_object_private_lock (sibling_priv);
+
+  if (!check_disposition (child, self, NULL))
+    goto unlock;
+
+  switch (location)
+    {
+    case IDE_OBJECT_START:
+      g_queue_push_head_link (&priv->children, &child_priv->link);
+      break;
+
+    case IDE_OBJECT_END:
+      g_queue_push_tail_link (&priv->children, &child_priv->link);
+      break;
+
+    case IDE_OBJECT_BEFORE_SIBLING:
+      _g_queue_insert_before_link (&priv->children, &sibling_priv->link, &child_priv->link);
+      break;
+
+    case IDE_OBJECT_AFTER_SIBLING:
+      _g_queue_insert_after_link (&priv->children, &sibling_priv->link, &child_priv->link);
+      break;
+
+    default:
+      g_critical ("Invalid location to add object child");
+      goto unlock;
+    }
+
+  child_priv->parent = self;
+  g_object_ref (child);
+
+  if (IDE_OBJECT_GET_CLASS (child)->parent_set)
+    IDE_OBJECT_GET_CLASS (child)->parent_set (child, self);
+
+unlock:
+  if (sibling)
+    ide_object_private_unlock (sibling_priv);
+  ide_object_private_unlock (child_priv);
+  ide_object_private_unlock (priv);
+}
+
+static void
+ide_object_real_remove (IdeObject *self,
+                        IdeObject *child)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObjectPrivate *child_priv = ide_object_get_instance_private (child);
+
+  g_assert (IDE_IS_OBJECT (self));
+  g_assert (IDE_IS_OBJECT (child));
+
+  ide_object_private_lock (priv);
+  ide_object_private_lock (child_priv);
+
+  g_assert (child_priv->parent == self);
+
+  if (child_priv->parent != self)
+    {
+      g_critical ("Attempt to remove child object from incorrect parent");
+      ide_object_private_unlock (child_priv);
+      ide_object_private_unlock (priv);
+      return;
+    }
+
+  g_queue_unlink (&priv->children, &child_priv->link);
+  child_priv->parent = NULL;
+
+  if (IDE_OBJECT_GET_CLASS (child)->parent_set)
+    IDE_OBJECT_GET_CLASS (child)->parent_set (child, NULL);
+
+  ide_object_private_unlock (child_priv);
+  ide_object_private_unlock (priv);
+
+  g_object_unref (child);
+}
+
+static void
+ide_object_real_destroy (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObject *hold = NULL;
+
+  g_assert (IDE_IS_OBJECT (self));
+
+  /* We already hold the instance lock, for destroy */
+
+  g_cancellable_cancel (priv->cancellable);
+
+  if (priv->parent != NULL)
+    {
+      hold = g_object_ref (self);
+      ide_object_remove (priv->parent, self);
+    }
+
+  g_assert (priv->parent == NULL);
+  g_assert (priv->link.prev == NULL);
+  g_assert (priv->link.next == NULL);
+
+  while (priv->children.head != NULL)
+    {
+      IdeObject *child = priv->children.head->data;
+
+      ide_object_destroy (child);
+    }
+
+  g_assert (priv->children.tail == NULL);
+  g_assert (priv->children.head == NULL);
+  g_assert (priv->children.length == 0);
+
+  g_assert (priv->parent == NULL);
+  g_assert (priv->link.prev == NULL);
+  g_assert (priv->link.next == NULL);
+
+  priv->destroyed = TRUE;
+
+  if (hold != NULL)
+    g_object_unref (hold);
+}
+
+static gboolean
+ide_object_destroy_in_main_cb (IdeObject *object)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_OBJECT (object));
+
+  ide_object_destroy (object);
+
+  return G_SOURCE_REMOVE;
+}
+
+void
+ide_object_destroy (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_OBJECT (self));
+
+  g_object_ref (self);
+  ide_object_private_lock (priv);
+
+  /* If we are not on the main thread, we want to detach from the
+   * object tree and then dispatch the rest of the destroy to the
+   * main thread (so no threaded cleanup can occur).
+   */
+
+  if (IDE_IS_MAIN_THREAD ())
+    {
+      g_cancellable_cancel (priv->cancellable);
+      if (!priv->in_destruction && !priv->destroyed)
+        g_object_run_dispose (G_OBJECT (self));
+    }
+  else
+    {
+      g_autoptr(IdeObject) parent = NULL;
+
+      if ((parent = ide_object_ref_parent (self)))
+        ide_object_remove (parent, self);
+
+      g_idle_add_full (G_PRIORITY_LOW + 1000,
+                       (GSourceFunc)ide_object_destroy_in_main_cb,
+                       g_object_ref (self),
+                       g_object_unref);
+    }
+
+  ide_object_private_unlock (priv);
+  g_object_unref (self);
+}
+
+static gboolean
+ide_object_dispose_from_main_cb (gpointer user_data)
+{
+  IdeObject *self = user_data;
+  g_object_run_dispose (G_OBJECT (self));
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_object_dispose (GObject *object)
+{
+  IdeObject *self = (IdeObject *)object;
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  if (!IDE_IS_MAIN_THREAD ())
+    {
+      /* We are not on the main thread and might lose our last reference count.
+       * Pass this object to the main thread for disposal. This usually only
+       * happens when an object was temporarily created/destroyed on a thread.
+       */
+      g_idle_add_full (G_PRIORITY_LOW + 1000,
+                       ide_object_dispose_from_main_cb,
+                       g_object_ref (self),
+                       g_object_unref);
+      return;
+    }
+
+  g_assert (IDE_IS_OBJECT (object));
+
+  ide_object_private_lock (priv);
+
+  if (!priv->in_destruction)
+    {
+      priv->in_destruction = TRUE;
+      g_signal_emit (self, signals [DESTROY], 0);
+      priv->in_destruction = FALSE;
+    }
+
+  ide_object_private_unlock (priv);
+
+  G_OBJECT_CLASS (ide_object_parent_class)->dispose (object);
+}
+
+static void
+ide_object_finalize (GObject *object)
+{
+  IdeObject *self = (IdeObject *)object;
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  if (!IDE_IS_MAIN_THREAD ())
+    {
+      g_critical ("Attempt to finalize %s on a thread which is not allowed. Leaking instead.",
+                  G_OBJECT_TYPE_NAME (object));
+      return;
+    }
+
+  g_assert (priv->parent == NULL);
+  g_assert (priv->children.length == 0);
+  g_assert (priv->children.head == NULL);
+  g_assert (priv->children.tail == NULL);
+  g_assert (priv->link.prev == NULL);
+  g_assert (priv->link.next == NULL);
+
+  g_clear_object (&priv->cancellable);
+  g_rec_mutex_clear (&priv->mutex);
+
+  G_OBJECT_CLASS (ide_object_parent_class)->finalize (object);
+}
+
+static void
+ide_object_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  IdeObject *self = IDE_OBJECT (object);
+
+  switch (prop_id)
+    {
+    case PROP_PARENT:
+      g_value_take_object (value, ide_object_ref_parent (self));
+      break;
+
+    case PROP_CANCELLABLE:
+      g_value_take_object (value, ide_object_ref_cancellable (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_object_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  IdeObject *self = IDE_OBJECT (object);
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_CANCELLABLE:
+      priv->cancellable = g_value_dup_object (value);
+      break;
+
+    case PROP_PARENT:
+      {
+        IdeObject *parent = g_value_get_object (value);
+        if (parent != NULL)
+          ide_object_append (parent, IDE_OBJECT (self));
+      }
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_object_class_init (IdeObjectClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_object_dispose;
+  object_class->finalize = ide_object_finalize;
+  object_class->get_property = ide_object_get_property;
+  object_class->set_property = ide_object_set_property;
+
+  klass->add = ide_object_real_add;
+  klass->remove = ide_object_real_remove;
+  klass->destroy = ide_object_real_destroy;
+  klass->repr = ide_object_real_repr;
+
+  /**
+   * IdeObject:parent:
+   *
+   * The parent #IdeObject, if any.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PARENT] =
+    g_param_spec_object ("parent",
+                         "Parent",
+                         "The parent IdeObject",
+                         IDE_TYPE_OBJECT,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeObject:cancellable:
+   *
+   * The "cancellable" property is a #GCancellable that can be used by operations
+   * that will be cancelled when the #IdeObject::destroy signal is emitted on @self.
+   *
+   * This is convenient when you want operations to automatically be cancelled when
+   * part of teh object tree is segmented.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CANCELLABLE] =
+    g_param_spec_object ("cancellable",
+                         "Cancellable",
+                         "A GCancellable for the object to use in operations",
+                         G_TYPE_CANCELLABLE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeObject::destroy:
+   *
+   * The "destroy" signal is emitted when the object should destroy itself
+   * and cleanup any state that is no longer necessary. This happens when
+   * the object has been removed from the because it was requested to be
+   * destroyed, or because a parent object is being destroyed.
+   *
+   * If you do not want to receive the "destroy" signal, then you must
+   * manually remove the object from the tree using ide_object_remove()
+   * while holding a reference to the object.
+   *
+   * Since: 3.32
+   */
+  signals [DESTROY] =
+    g_signal_new ("destroy",
+                  G_TYPE_FROM_CLASS (klass),
+                  (G_SIGNAL_RUN_CLEANUP | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS),
+                  G_STRUCT_OFFSET (IdeObjectClass, destroy),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [DESTROY],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+}
+
+static void
+ide_object_init (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  priv->link.data = self;
+
+  g_rec_mutex_init (&priv->mutex);
+}
+
+/**
+ * ide_object_new:
+ * @type: a #GType of an #IdeObject derived object
+ * @parent: (nullable): an optional #IdeObject parent
+ *
+ * This is a convenience function for creating an #IdeObject and appending it
+ * to a parent.
+ *
+ * This function may only be called from the main-thread, as calling from any
+ * other thread would potentially risk being disposed before returning.
+ *
+ * Returns: (transfer full) (type IdeObject): a new #IdeObject
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_new (GType      type,
+                IdeObject *parent)
+{
+  IdeObject *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+  g_return_val_if_fail (!parent || IDE_IS_OBJECT (parent), NULL);
+
+  ret = g_object_new (type, NULL);
+  if (parent != NULL)
+    ide_object_append (parent, ret);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_get_n_children:
+ * @self: a #IdeObject
+ *
+ * Gets the number of children for an object.
+ *
+ * Returns: the number of children
+ *
+ * Since: 3.32
+ */
+guint
+ide_object_get_n_children (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  guint ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), 0);
+
+  ide_object_private_lock (priv);
+  ret = priv->children.length;
+  ide_object_private_unlock (priv);
+
+  return ret;
+}
+
+/**
+ * ide_object_get_nth_child:
+ * @self: a #IdeObject
+ * @nth: position of child to fetch
+ *
+ * Gets the @nth child of @self.
+ *
+ * A full reference to the child is returned.
+ *
+ * Returns: (transfer full) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_get_nth_child (IdeObject *self,
+                          guint      nth)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObject *ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), 0);
+
+  ide_object_private_lock (priv);
+  ret = g_list_nth_data (priv->children.head, nth);
+  if (ret != NULL)
+    g_object_ref (ret);
+  ide_object_private_unlock (priv);
+
+  g_return_val_if_fail (!ret || IDE_IS_OBJECT (ret), NULL);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_get_position:
+ * @self: a #IdeObject
+ *
+ * Gets the position of @self within the parent node.
+ *
+ * Returns: the position, starting from 0
+ *
+ * Since: 3.32
+ */
+guint
+ide_object_get_position (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  guint ret = 0;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), 0);
+
+  ide_object_private_lock (priv);
+
+  if (priv->parent != NULL)
+    {
+      IdeObjectPrivate *parent_priv = ide_object_get_instance_private (priv->parent);
+      ret = g_list_position (parent_priv->children.head, &priv->link);
+    }
+
+  ide_object_private_unlock (priv);
+
+  return ret;
+}
+
+/**
+ * ide_object_lock:
+ * @self: a #IdeObject
+ *
+ * Acquires the lock for @self. This can be useful when you need to do
+ * multi-threaded work with @self and want to ensure exclusivity.
+ *
+ * Call ide_object_unlock() to release the lock.
+ *
+ * The synchronization used is a #GRecMutex.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_lock (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_OBJECT (self));
+
+  ide_object_private_lock (priv);
+}
+
+/**
+ * ide_object_unlock:
+ * @self: a #IdeObject
+ *
+ * Releases a previously acuiqred lock from ide_object_lock().
+ *
+ * The synchronization used is a #GRecMutex.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_unlock (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_OBJECT (self));
+
+  ide_object_private_unlock (priv);
+}
+
+/**
+ * ide_object_ref_cancellable:
+ * @self: a #IdeObject
+ *
+ * Gets a #GCancellable for the object.
+ *
+ * Returns: (transfer none) (not nullable): a #GCancellable
+ *
+ * Since: 3.32
+ */
+GCancellable *
+ide_object_ref_cancellable (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  GCancellable *ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  ide_object_private_lock (priv);
+  if (priv->cancellable == NULL)
+    priv->cancellable = g_cancellable_new ();
+  ret = g_object_ref (priv->cancellable);
+  ide_object_private_unlock (priv);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_get_parent:
+ * @self: a #IdeObject
+ *
+ * Gets the parent #IdeObject, if any.
+ *
+ * This function may only be called from the main thread.
+ *
+ * Returns: (transfer none) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_get_parent (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObject *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  ide_object_private_lock (priv);
+  ret = priv->parent;
+  ide_object_private_unlock (priv);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_ref_parent:
+ * @self: a #IdeObject
+ *
+ * Gets the parent #IdeObject, if any.
+ *
+ * Returns: (transfer full) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_ref_parent (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  IdeObject *ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  ide_object_private_lock (priv);
+  ret = priv->parent ? g_object_ref (priv->parent) : NULL;
+  ide_object_private_unlock (priv);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_is_root:
+ * @self: a #IdeObject
+ *
+ * Checks if @self is root, meaning it has no parent.
+ *
+ * Returns: %TRUE if @self has no parent
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_object_is_root (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), FALSE);
+
+  ide_object_private_lock (priv);
+  ret = priv->parent == NULL;
+  ide_object_private_unlock (priv);
+
+  return ret;
+}
+
+/**
+ * ide_object_add:
+ * @self: an #IdeObject
+ * @sibling: (nullable): an #IdeObject or %NULL
+ * @child: an #IdeObject
+ * @location: location for child
+ *
+ * Adds @child to @self, with location dependent on @location.
+ *
+ * Generally, it is simpler to use the helper functions such as
+ * ide_object_append(), ide_object_prepend(), ide_object_insert_before(),
+ * or ide_object_insert_after().
+ *
+ * This function is primarily meant for consumers that don't know the
+ * relative position they need until runtime.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_add (IdeObject         *self,
+                IdeObject         *sibling,
+                IdeObject         *child,
+                IdeObjectLocation  location)
+{
+  g_return_if_fail (IDE_IS_OBJECT (self));
+  g_return_if_fail (IDE_IS_OBJECT (child));
+
+  if (location == IDE_OBJECT_BEFORE_SIBLING ||
+      location == IDE_OBJECT_AFTER_SIBLING)
+    g_return_if_fail (IDE_IS_OBJECT (sibling));
+  else
+    g_return_if_fail (sibling == NULL);
+
+  IDE_OBJECT_GET_CLASS (self)->add (self, sibling, child, location);
+}
+
+/**
+ * ide_object_remove:
+ * @self: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Removes @child from @self.
+ *
+ * If @child is a borrowed reference, it may be finalized before this
+ * function returns.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_remove (IdeObject *self,
+                   IdeObject *child)
+{
+  g_return_if_fail (IDE_IS_OBJECT (self));
+  g_return_if_fail (IDE_IS_OBJECT (child));
+
+  IDE_OBJECT_GET_CLASS (self)->remove (self, child);
+}
+
+/**
+ * ide_object_append:
+ * @self: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child as the last child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_append (IdeObject *self,
+                   IdeObject *child)
+{
+  ide_object_add (self, NULL, child, IDE_OBJECT_END);
+}
+
+/**
+ * ide_object_prepend:
+ * @self: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child as the first child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_prepend (IdeObject *self,
+                    IdeObject *child)
+{
+  ide_object_add (self, NULL, child, IDE_OBJECT_START);
+}
+
+/**
+ * ide_object_insert_before:
+ * @self: an #IdeObject
+ * @sibling: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child into @self's children, directly before @sibling.
+ *
+ * @sibling MUST BE a child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_insert_before (IdeObject *self,
+                          IdeObject *sibling,
+                          IdeObject *child)
+{
+  ide_object_add (self, sibling, child, IDE_OBJECT_BEFORE_SIBLING);
+}
+
+/**
+ * ide_object_insert_after:
+ * @self: an #IdeObject
+ * @sibling: an #IdeObject
+ * @child: an #IdeObject
+ *
+ * Inserts @child into @self's children, directly after @sibling.
+ *
+ * @sibling MUST BE a child of @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_insert_after (IdeObject *self,
+                         IdeObject *sibling,
+                         IdeObject *child)
+{
+  ide_object_add (self, sibling, child, IDE_OBJECT_AFTER_SIBLING);
+}
+
+/**
+ * ide_object_insert_sorted:
+ * @self: a #IdeObject
+ * @child: an #IdeObject
+ * @func: (scope call): a #GCompareDataFunc that can be used to locate the
+ *    proper sibling
+ * @user_data: user data for @func
+ *
+ * Locates the proper sibling for @child by using @func amongst @self's
+ * children #IdeObject. Those objects must already be sorted.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_insert_sorted (IdeObject        *self,
+                          IdeObject        *child,
+                          GCompareDataFunc  func,
+                          gpointer          user_data)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_OBJECT (self));
+  g_return_if_fail (IDE_IS_OBJECT (child));
+  g_return_if_fail (func != NULL);
+
+  ide_object_lock (self);
+
+  if (priv->children.length == 0)
+    {
+      ide_object_prepend (self, child);
+      goto unlock;
+    }
+
+  g_assert (priv->children.head != NULL);
+  g_assert (priv->children.tail != NULL);
+
+  for (GList *iter = priv->children.tail; iter; iter = iter->prev)
+    {
+      IdeObject *other = iter->data;
+
+      g_assert (IDE_IS_OBJECT (other));
+
+      if (func (child, other, user_data) <= 0)
+        {
+          ide_object_insert_after (self, other, child);
+          goto unlock;
+        }
+    }
+
+  ide_object_append (self, child);
+
+unlock:
+  ide_object_unlock (self);
+}
+
+/**
+ * ide_object_foreach:
+ * @self: a #IdeObject
+ * @callback: (scope call): a #GFunc to call for each child
+ * @user_data: closure data for @callback
+ *
+ * Calls @callback for each child of @self.
+ *
+ * @callback is allowed to remove children from @self, but only as long as they are
+ * the child passed to callback (or child itself). See g_queue_foreach() for more
+ * details about what is allowed.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_foreach (IdeObject *self,
+                    GFunc      callback,
+                    gpointer   user_data)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_OBJECT (self));
+  g_return_if_fail (callback != NULL);
+
+  ide_object_private_lock (priv);
+  g_queue_foreach (&priv->children, callback, user_data);
+  ide_object_private_unlock (priv);
+}
+
+static void
+get_child_typed_cb (gpointer data,
+                    gpointer user_data)
+{
+  IdeObject *child = data;
+  GetChildTyped *q = user_data;
+
+  if (q->child != NULL)
+    return;
+
+  if (G_TYPE_CHECK_INSTANCE_TYPE (child, q->type))
+    q->child = g_object_ref (child);
+}
+
+/**
+ * ide_object_get_child_typed:
+ * @self: a #IdeObject
+ * @type: the #GType of the child to match
+ *
+ * Finds the first child of @self that is of @type.
+ *
+ * Returns: (transfer full) (type IdeObject) (nullable): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_get_child_typed (IdeObject *self,
+                            GType      type)
+{
+  GetChildTyped q = { type, NULL };
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+
+  ide_object_foreach (self, get_child_typed_cb, &q);
+
+  return g_steal_pointer (&q.child);
+}
+
+static void
+get_children_typed_cb (gpointer data,
+                       gpointer user_data)
+{
+  IdeObject *child = data;
+  GetChildrenTyped *q = user_data;
+
+  if (G_TYPE_CHECK_INSTANCE_TYPE (child, q->type))
+    g_ptr_array_add (q->array, g_object_ref (child));
+}
+
+/**
+ * ide_object_get_children_typed:
+ * @self: a #IdeObject
+ * @type: a #GType
+ *
+ * Gets all children matching @type.
+ *
+ * Returns: (transfer full) (element-type IdeObject): a #GPtrArray of
+ *   #IdeObject matching @type.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_object_get_children_typed (IdeObject *self,
+                               GType      type)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+  GetChildrenTyped q;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+
+  ar = g_ptr_array_new ();
+
+  q.type = type;
+  q.array = ar;
+
+  ide_object_foreach (self, get_children_typed_cb, &q);
+
+  return g_steal_pointer (&ar);
+}
+
+/**
+ * ide_object_ref_root:
+ * @self: a #IdeObject
+ *
+ * Finds and returns the toplevel object in the tree.
+ *
+ * Returns: (transfer full): an #IdeObject
+ *
+ * Since: 3.32
+ */
+IdeObject *
+ide_object_ref_root (IdeObject *self)
+{
+  IdeObject *cur;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  cur = g_object_ref (self);
+
+  while (!ide_object_is_root (cur))
+    {
+      IdeObject *tmp = cur;
+      cur = ide_object_ref_parent (tmp);
+      g_object_unref (tmp);
+    }
+
+  return g_steal_pointer (&cur);
+}
+
+static void
+ide_object_async_init_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  GAsyncInitable *initable = (GAsyncInitable *)object;
+  g_autoptr(IdeObject) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_ASYNC_INITABLE (initable));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_OBJECT (self));
+
+  if (!g_async_initable_init_finish (initable, result, &error))
+    {
+      g_warning ("Failed to initialize %s: %s",
+                 G_OBJECT_TYPE_NAME (initable),
+                 error->message);
+      ide_object_destroy (IDE_OBJECT (initable));
+    }
+}
+
+/**
+ * ide_object_ensure_child_typed:
+ * @self: a #IdeObject
+ * @type: the #GType of the child
+ *
+ * Like ide_object_get_child_typed() except that it creates an object of
+ * @type if it is missing.
+ *
+ * Returns: (transfer full) (nullable) (type IdeObject): an #IdeObject or %NULL
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_object_ensure_child_typed (IdeObject *self,
+                               GType      type)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  g_autoptr(IdeObject) ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_OBJECT), NULL);
+  g_return_val_if_fail (!ide_object_in_destruction (self), NULL);
+
+  ide_object_private_lock (priv);
+  if (!(ret = ide_object_get_child_typed (self, type)))
+    {
+      g_autoptr(GError) error = NULL;
+
+      ret = ide_object_new (type, self);
+
+      if (G_IS_INITABLE (ret))
+        {
+          if (!g_initable_init (G_INITABLE (ret), NULL, &error))
+            g_warning ("Failed to initialize %s: %s",
+                       G_OBJECT_TYPE_NAME (ret), error->message);
+        }
+      else if (G_IS_ASYNC_INITABLE (ret))
+        {
+          g_async_initable_init_async (G_ASYNC_INITABLE (ret),
+                                       G_PRIORITY_DEFAULT,
+                                       priv->cancellable,
+                                       ide_object_async_init_cb,
+                                       g_object_ref (self));
+        }
+    }
+  ide_object_private_unlock (priv);
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_object_destroyed:
+ * @self: a #IdeObject
+ *
+ * This function sets *object_pointer to NULL if object_pointer != NULL. It's
+ * intended to be used as a callback connected to the "destroy" signal of a
+ * object. You connect ide_object_destroyed() as a signal handler, and pass the
+ * address of your object variable as user data. Then when the object is
+ * destroyed, the variable will be set to NULL. Useful for example to avoid
+ * multiple copies of the same dialog.
+ *
+ * Since: 3.32
+ */
+void
+ide_object_destroyed (IdeObject **object_pointer)
+{
+  if (object_pointer != NULL)
+    *object_pointer = NULL;
+}
+
+/* compat for now to ease porting */
+void
+ide_object_set_context (IdeObject  *object,
+                        IdeContext *context)
+{
+  ide_object_append (IDE_OBJECT (context), object);
+}
+
+static gboolean dummy (gpointer p) { return G_SOURCE_REMOVE; }
+
+/**
+ * ide_object_get_context:
+ * @object: a #IdeObject
+ *
+ * Gets the #IdeContext for the object.
+ *
+ * Returns: (transfer none) (nullable): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_object_get_context (IdeObject *object)
+{
+  g_autoptr(IdeObject) root = ide_object_ref_root (object);
+  IdeContext *ret = NULL;
+  GSource *source;
+
+  if (IDE_IS_CONTEXT (root))
+    ret = IDE_CONTEXT (root);
+
+  /* We can just return a borrowed instance if in main thread,
+   * otherwise we need to queue the object to the main loop.
+   */
+  if (IDE_IS_MAIN_THREAD ())
+    return ret;
+
+  source = g_idle_source_new ();
+  g_source_set_name (source, "context-release");
+  g_source_set_callback (source, dummy, g_steal_pointer (&root), g_object_unref);
+  g_source_attach (source, g_main_context_get_thread_default ());
+  g_source_unref (source);
+
+  return ret;
+}
+
+/**
+ * ide_object_ref_context:
+ * @self: a #IdeContext
+ *
+ * Gets the root #IdeContext for the object, if any.
+ *
+ * Returns: (transfer full) (nullable): an #IdeContext or %NULL
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_object_ref_context (IdeObject *self)
+{
+  g_autoptr(IdeObject) root = NULL;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  if ((root = ide_object_ref_root (self)) && IDE_IS_CONTEXT (root))
+    return IDE_CONTEXT (g_steal_pointer (&root));
+
+  return NULL;
+}
+
+gboolean
+ide_object_in_destruction (IdeObject *self)
+{
+  IdeObjectPrivate *priv = ide_object_get_instance_private (self);
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), FALSE);
+
+  ide_object_lock (self);
+  ret = priv->in_destruction || priv->destroyed;
+  ide_object_unlock (self);
+
+  return ret;
+}
+
+/**
+ * ide_object_repr:
+ * @self: a #IdeObject
+ *
+ * This function is similar to Python's `repr()` which gives a string
+ * representation for the object. It is useful when debugging Builder
+ * or when writing plugins.
+ *
+ * Returns: (transfer full): a string containing the string representation
+ *   of the #IdeObject
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_object_repr (IdeObject *self)
+{
+  g_autofree gchar *str = NULL;
+
+  g_return_val_if_fail (IDE_IS_OBJECT (self), NULL);
+
+  str = IDE_OBJECT_GET_CLASS (self)->repr (self);
+
+  return g_strdup_printf ("<%s at %p>", str, self);
+}
+
+gboolean
+ide_object_set_error_if_destroyed (IdeObject  *self,
+                                   GError    **error)
+{
+  g_return_val_if_fail (IDE_IS_OBJECT (self), FALSE);
+
+  if (ide_object_in_destruction (self))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_CANCELLED,
+                   "The object was destroyed");
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+void
+ide_object_log (gpointer        instance,
+                GLogLevelFlags  level,
+                const gchar    *domain,
+                const gchar    *format,
+                ...)
+{
+  g_autoptr(IdeObject) root = NULL;
+  va_list args;
+
+  g_assert (IDE_IS_OBJECT (instance));
+
+  root = ide_object_ref_root (instance);
+
+  if (IDE_IS_CONTEXT (root))
+    {
+      g_autofree gchar *message = NULL;
+
+      va_start (args, format);
+      message = g_strdup_vprintf (format, args);
+      ide_context_log (IDE_CONTEXT (root), level, domain, message);
+      va_end (args);
+    }
+}
diff --git a/src/libide/core/ide-object.h b/src/libide/core/ide-object.h
new file mode 100644
index 000000000..a0ec78c78
--- /dev/null
+++ b/src/libide/core/ide-object.h
@@ -0,0 +1,156 @@
+/* ide-object.h
+ *
+ * Copyright 2014-2019 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
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <gio/gio.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_OBJECT (ide_object_get_type())
+
+typedef enum
+{
+  IDE_OBJECT_START,
+  IDE_OBJECT_END,
+  IDE_OBJECT_BEFORE_SIBLING,
+  IDE_OBJECT_AFTER_SIBLING,
+} IdeObjectLocation;
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeObject, ide_object, IDE, OBJECT, GObject)
+
+struct _IdeObjectClass
+{
+  GObjectClass parent_class;
+
+  void     (*destroy)    (IdeObject         *self);
+  void     (*add)        (IdeObject         *self,
+                          IdeObject         *sibling,
+                          IdeObject         *child,
+                          IdeObjectLocation  location);
+  void     (*remove)     (IdeObject         *self,
+                          IdeObject         *child);
+  void     (*parent_set) (IdeObject         *self,
+                          IdeObject         *parent);
+  gchar  *(*repr)        (IdeObject         *self);
+
+  /*< private */
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+gpointer      ide_object_new                    (GType               type,
+                                                 IdeObject          *parent) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+GCancellable *ide_object_ref_cancellable        (IdeObject          *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+IdeObject    *ide_object_get_parent             (IdeObject          *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+IdeObject    *ide_object_ref_parent             (IdeObject          *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+IdeObject    *ide_object_ref_root               (IdeObject          *self) G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_object_is_root                (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_lock                   (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_unlock                 (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_add                    (IdeObject          *self,
+                                                 IdeObject          *sibling,
+                                                 IdeObject          *child,
+                                                 IdeObjectLocation   location);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_append                 (IdeObject          *self,
+                                                 IdeObject          *child);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_prepend                (IdeObject          *self,
+                                                 IdeObject          *child);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_insert_before          (IdeObject          *self,
+                                                 IdeObject          *sibling,
+                                                 IdeObject          *child);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_insert_after           (IdeObject          *self,
+                                                 IdeObject          *sibling,
+                                                 IdeObject          *child);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_insert_sorted          (IdeObject          *self,
+                                                 IdeObject          *child,
+                                                 GCompareDataFunc    func,
+                                                 gpointer            user_data);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_remove                 (IdeObject          *self,
+                                                 IdeObject          *child);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_foreach                (IdeObject          *self,
+                                                 GFunc               callback,
+                                                 gpointer            user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_object_set_error_if_destroyed (IdeObject          *self,
+                                                 GError            **error);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_destroy                (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_destroyed              (IdeObject         **self);
+IDE_AVAILABLE_IN_3_32
+guint         ide_object_get_position           (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+guint         ide_object_get_n_children         (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+IdeObject    *ide_object_get_nth_child          (IdeObject          *self,
+                                                 guint               nth);
+IDE_AVAILABLE_IN_3_32
+gpointer      ide_object_get_child_typed        (IdeObject          *self,
+                                                 GType               type);
+IDE_AVAILABLE_IN_3_32
+GPtrArray    *ide_object_get_children_typed     (IdeObject          *self,
+                                                 GType               type);
+IDE_AVAILABLE_IN_3_32
+gpointer      ide_object_ensure_child_typed     (IdeObject          *self,
+                                                 GType               type);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_notify_in_main         (gpointer            instance,
+                                                 GParamSpec         *pspec);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_notify_by_pspec        (gpointer            instance,
+                                                 GParamSpec         *pspec);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_object_in_destruction         (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+gchar        *ide_object_repr                   (IdeObject          *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_object_log                    (gpointer            instance,
+                                                 GLogLevelFlags      level,
+                                                 const gchar        *domain,
+                                                 const gchar        *format,
+                                                 ...) G_GNUC_PRINTF (4, 5);
+
+#define ide_object_message(instance, format, ...) ide_object_log(instance, G_LOG_LEVEL_MESSAGE, 
G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__)
+#define ide_object_warning(instance, format, ...) ide_object_log(instance, G_LOG_LEVEL_WARNING, 
G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__)
+
+G_END_DECLS
diff --git a/src/libide/core/ide-settings.c b/src/libide/core/ide-settings.c
new file mode 100644
index 000000000..80b851da7
--- /dev/null
+++ b/src/libide/core/ide-settings.c
@@ -0,0 +1,589 @@
+/* ide-settings.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-settings"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <stdlib.h>
+
+#include "ide-settings.h"
+
+/**
+ * SECTION:ide-settings
+ * @title: IdeSettings
+ * @short_description: Settings with per-project overrides
+ *
+ * In Builder, we need support for settings at the user level (their chosen
+ * defaults) as well as defaults for a project. #IdeSettings attempts to
+ * simplify this by providing a layered approach to settings.
+ *
+ * If a setting has been set for the current project, it will be returned. If
+ * not, the users preference will be returned. Setting a preference via
+ * #IdeSettings will always modify the projects setting, not the users default
+ * settings.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeSettings
+{
+  GObject              parent_instance;
+
+  DzlSettingsSandwich *settings_sandwich;
+  gchar               *relative_path;
+  gchar               *schema_id;
+  gchar               *project_id;
+  guint                ignore_project_settings : 1;
+};
+
+G_DEFINE_TYPE (IdeSettings, ide_settings, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_RELATIVE_PATH,
+  PROP_SCHEMA_ID,
+  PROP_IGNORE_PROJECT_SETTINGS,
+  PROP_PROJECT_ID,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_settings_set_ignore_project_settings (IdeSettings *self,
+                                          gboolean     ignore_project_settings)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+
+  ignore_project_settings = !!ignore_project_settings;
+
+  if (ignore_project_settings != self->ignore_project_settings)
+    {
+      self->ignore_project_settings = ignore_project_settings;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IGNORE_PROJECT_SETTINGS]);
+    }
+}
+
+static void
+ide_settings_set_relative_path (IdeSettings *self,
+                                const gchar *relative_path)
+{
+  g_assert (IDE_IS_SETTINGS (self));
+  g_assert (relative_path != NULL);
+
+  if (*relative_path == '/')
+    relative_path++;
+
+  if (!ide_str_equal0 (relative_path, self->relative_path))
+    {
+      g_free (self->relative_path);
+      self->relative_path = g_strdup (relative_path);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RELATIVE_PATH]);
+    }
+}
+
+static void
+ide_settings_set_schema_id (IdeSettings *self,
+                            const gchar *schema_id)
+{
+  g_assert (IDE_IS_SETTINGS (self));
+  g_assert (schema_id != NULL);
+
+  if (!ide_str_equal0 (schema_id, self->schema_id))
+    {
+      g_free (self->schema_id);
+      self->schema_id = g_strdup (schema_id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SCHEMA_ID]);
+    }
+}
+
+static void
+ide_settings_constructed (GObject *object)
+{
+  IdeSettings *self = (IdeSettings *)object;
+  g_autofree gchar *full_path = NULL;
+  GSettings *settings;
+  gchar *path;
+
+  IDE_ENTRY;
+
+  G_OBJECT_CLASS (ide_settings_parent_class)->constructed (object);
+
+  if (self->schema_id == NULL)
+    {
+      g_error ("You must provide IdeSettings:schema-id");
+      abort ();
+    }
+
+  if (self->relative_path == NULL)
+    {
+      g_autoptr(GSettingsSchema) schema = NULL;
+      GSettingsSchemaSource *source;
+      const gchar *schema_path;
+
+      source = g_settings_schema_source_get_default ();
+      schema = g_settings_schema_source_lookup (source, self->schema_id, TRUE);
+
+      if (schema == NULL)
+        {
+          g_error ("Could not locate schema %s", self->schema_id);
+          abort ();
+        }
+
+      schema_path = g_settings_schema_get_path (schema);
+
+      if ((schema_path != NULL) && !g_str_has_prefix (schema_path, "/org/gnome/builder/"))
+        {
+          g_error ("Schema path MUST be under /org/gnome/builder/");
+          abort ();
+        }
+      else if (schema_path == NULL)
+        {
+          self->relative_path = g_strdup ("");
+        }
+      else
+        {
+          self->relative_path = g_strdup (schema_path + strlen ("/org/gnome/builder/"));
+        }
+    }
+
+  g_assert (self->relative_path != NULL);
+  g_assert (self->relative_path [0] != '/');
+  g_assert ((self->relative_path [0] == 0) || g_str_has_suffix (self->relative_path, "/"));
+
+  full_path = g_strdup_printf ("/org/gnome/builder/%s", self->relative_path);
+  self->settings_sandwich = dzl_settings_sandwich_new (self->schema_id, full_path);
+
+  /* Add our project relative settings */
+  if (self->ignore_project_settings == FALSE)
+    {
+      path = g_strdup_printf ("/org/gnome/builder/projects/%s/%s",
+                              self->project_id, self->relative_path);
+      settings = g_settings_new_with_path (self->schema_id, path);
+      dzl_settings_sandwich_append (self->settings_sandwich, settings);
+      g_clear_object (&settings);
+      g_free (path);
+    }
+
+  /* Add our application global (user defaults) settings */
+  settings = g_settings_new_with_path (self->schema_id, full_path);
+  dzl_settings_sandwich_append (self->settings_sandwich, settings);
+  g_clear_object (&settings);
+
+  IDE_EXIT;
+}
+
+static void
+ide_settings_finalize (GObject *object)
+{
+  IdeSettings *self = (IdeSettings *)object;
+
+  g_clear_object (&self->settings_sandwich);
+  g_clear_pointer (&self->relative_path, g_free);
+  g_clear_pointer (&self->schema_id, g_free);
+  g_clear_pointer (&self->project_id, g_free);
+
+  G_OBJECT_CLASS (ide_settings_parent_class)->finalize (object);
+}
+
+static void
+ide_settings_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  IdeSettings *self = IDE_SETTINGS (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      g_value_set_string (value, self->project_id);
+      break;
+
+    case PROP_SCHEMA_ID:
+      g_value_set_string (value, ide_settings_get_schema_id (self));
+      break;
+
+    case PROP_RELATIVE_PATH:
+      g_value_set_string (value, ide_settings_get_relative_path (self));
+      break;
+
+    case PROP_IGNORE_PROJECT_SETTINGS:
+      g_value_set_boolean (value, ide_settings_get_ignore_project_settings (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_settings_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  IdeSettings *self = IDE_SETTINGS (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_ID:
+      self->project_id = g_value_dup_string (value);
+      break;
+
+    case PROP_SCHEMA_ID:
+      ide_settings_set_schema_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_RELATIVE_PATH:
+      ide_settings_set_relative_path (self, g_value_get_string (value));
+      break;
+
+    case PROP_IGNORE_PROJECT_SETTINGS:
+      ide_settings_set_ignore_project_settings (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_settings_class_init (IdeSettingsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_settings_constructed;
+  object_class->finalize = ide_settings_finalize;
+  object_class->get_property = ide_settings_get_property;
+  object_class->set_property = ide_settings_set_property;
+
+  properties [PROP_PROJECT_ID] =
+    g_param_spec_string ("project-id",
+                         "Project Id",
+                         "The identifier for the project",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_IGNORE_PROJECT_SETTINGS] =
+    g_param_spec_boolean ("ignore-project-settings",
+                         "Ignore Project Settings",
+                         "If project settings should be ignored.",
+                         FALSE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RELATIVE_PATH] =
+    g_param_spec_string ("relative-path",
+                         "Relative Path",
+                         "Relative Path",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SCHEMA_ID] =
+    g_param_spec_string ("schema-id",
+                         "Schema ID",
+                         "Schema ID",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_STRING);
+}
+
+static void
+ide_settings_init (IdeSettings *self)
+{
+}
+
+IdeSettings *
+ide_settings_new (const gchar *project_id,
+                  const gchar *schema_id,
+                  const gchar *relative_path,
+                  gboolean     ignore_project_settings)
+{
+  IdeSettings *ret;
+
+  IDE_ENTRY;
+
+  g_assert (project_id != NULL);
+  g_assert (schema_id != NULL);
+  g_assert (relative_path != NULL);
+
+  ret = g_object_new (IDE_TYPE_SETTINGS,
+                      "project-id", project_id,
+                      "ignore-project-settings", ignore_project_settings,
+                      "relative-path", relative_path,
+                      "schema-id", schema_id,
+                      NULL);
+
+  IDE_RETURN (ret);
+}
+
+const gchar *
+ide_settings_get_schema_id (IdeSettings *self)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+
+  return self->schema_id;
+}
+
+const gchar *
+ide_settings_get_relative_path (IdeSettings *self)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+
+  return self->relative_path;
+}
+
+gboolean
+ide_settings_get_ignore_project_settings (IdeSettings *self)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), FALSE);
+
+  return self->ignore_project_settings;
+}
+
+GVariant *
+ide_settings_get_default_value (IdeSettings *self,
+                                const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  return dzl_settings_sandwich_get_default_value (self->settings_sandwich, key);
+}
+
+GVariant *
+ide_settings_get_user_value (IdeSettings *self,
+                             const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  return dzl_settings_sandwich_get_user_value (self->settings_sandwich, key);
+}
+
+GVariant *
+ide_settings_get_value (IdeSettings *self,
+                        const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  return dzl_settings_sandwich_get_value (self->settings_sandwich, key);
+}
+
+void
+ide_settings_set_value (IdeSettings *self,
+                        const gchar *key,
+                        GVariant    *value)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  return dzl_settings_sandwich_set_value (self->settings_sandwich, key, value);
+}
+
+gboolean
+ide_settings_get_boolean (IdeSettings *self,
+                          const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), FALSE);
+  g_return_val_if_fail (key != NULL, FALSE);
+
+  return dzl_settings_sandwich_get_boolean (self->settings_sandwich, key);
+}
+
+gdouble
+ide_settings_get_double (IdeSettings *self,
+                         const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), 0.0);
+  g_return_val_if_fail (key != NULL, 0.0);
+
+  return dzl_settings_sandwich_get_double (self->settings_sandwich, key);
+}
+
+gint
+ide_settings_get_int (IdeSettings *self,
+                      const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), 0);
+  g_return_val_if_fail (key != NULL, 0);
+
+  return dzl_settings_sandwich_get_int (self->settings_sandwich, key);
+}
+
+gchar *
+ide_settings_get_string (IdeSettings *self,
+                         const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  return dzl_settings_sandwich_get_string (self->settings_sandwich, key);
+}
+
+guint
+ide_settings_get_uint (IdeSettings *self,
+                       const gchar *key)
+{
+  g_return_val_if_fail (IDE_IS_SETTINGS (self), 0);
+  g_return_val_if_fail (key != NULL, 0);
+
+  return dzl_settings_sandwich_get_uint (self->settings_sandwich, key);
+}
+
+void
+ide_settings_set_boolean (IdeSettings *self,
+                          const gchar *key,
+                          gboolean     val)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  dzl_settings_sandwich_set_boolean (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_double (IdeSettings *self,
+                         const gchar *key,
+                         gdouble      val)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  dzl_settings_sandwich_set_double (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_int (IdeSettings *self,
+                      const gchar *key,
+                      gint         val)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  dzl_settings_sandwich_set_int (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_string (IdeSettings *self,
+                         const gchar *key,
+                         const gchar *val)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  dzl_settings_sandwich_set_string (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_set_uint (IdeSettings *self,
+                       const gchar *key,
+                       guint        val)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+
+  dzl_settings_sandwich_set_uint (self->settings_sandwich, key, val);
+}
+
+void
+ide_settings_bind (IdeSettings        *self,
+                   const gchar        *key,
+                   gpointer            object,
+                   const gchar        *property,
+                   GSettingsBindFlags  flags)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+  g_return_if_fail (G_IS_OBJECT (object));
+  g_return_if_fail (property != NULL);
+
+  dzl_settings_sandwich_bind (self->settings_sandwich, key, object, property, flags);
+}
+
+/**
+ * ide_settings_bind_with_mapping:
+ * @self: An #IdeSettings
+ * @key: The settings key
+ * @object: the object to bind to
+ * @property: the property of @object to bind to
+ * @flags: flags for the binding
+ * @get_mapping: (allow-none) (scope notified): variant to value mapping
+ * @set_mapping: (allow-none) (scope notified): value to variant mapping
+ * @user_data: user data for @get_mapping and @set_mapping
+ * @destroy: destroy function to cleanup @user_data.
+ *
+ * Like ide_settings_bind() but allows transforming to and from settings storage using
+ * @get_mapping and @set_mapping transformation functions.
+ *
+ * Call ide_settings_unbind() to unbind the mapping.
+ *
+ * Since: 3.32
+ */
+void
+ide_settings_bind_with_mapping (IdeSettings             *self,
+                                const gchar             *key,
+                                gpointer                 object,
+                                const gchar             *property,
+                                GSettingsBindFlags       flags,
+                                GSettingsBindGetMapping  get_mapping,
+                                GSettingsBindSetMapping  set_mapping,
+                                gpointer                 user_data,
+                                GDestroyNotify           destroy)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (key != NULL);
+  g_return_if_fail (G_IS_OBJECT (object));
+  g_return_if_fail (property != NULL);
+
+  dzl_settings_sandwich_bind_with_mapping (self->settings_sandwich, key, object, property, flags,
+                                           get_mapping, set_mapping, user_data, destroy);
+}
+
+void
+ide_settings_unbind (IdeSettings *self,
+                     const gchar *property)
+{
+  g_return_if_fail (IDE_IS_SETTINGS (self));
+  g_return_if_fail (property != NULL);
+
+  dzl_settings_sandwich_unbind (self->settings_sandwich, property);
+}
diff --git a/src/libide/core/ide-settings.h b/src/libide/core/ide-settings.h
new file mode 100644
index 000000000..ab61a0043
--- /dev/null
+++ b/src/libide/core/ide-settings.h
@@ -0,0 +1,111 @@
+/* ide-settings.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SETTINGS (ide_settings_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeSettings, ide_settings, IDE, SETTINGS, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeSettings *ide_settings_new                         (const gchar             *project_id,
+                                                       const gchar             *schema_id,
+                                                       const gchar             *relative_path,
+                                                       gboolean                 ignore_project_settings);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_settings_get_relative_path           (IdeSettings             *self);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_settings_get_schema_id               (IdeSettings             *self);
+IDE_AVAILABLE_IN_3_32
+gboolean     ide_settings_get_ignore_project_settings (IdeSettings             *self);
+IDE_AVAILABLE_IN_3_32
+GVariant    *ide_settings_get_default_value           (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+GVariant    *ide_settings_get_user_value              (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+GVariant    *ide_settings_get_value                   (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_value                   (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       GVariant                *value);
+IDE_AVAILABLE_IN_3_32
+gboolean     ide_settings_get_boolean                 (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+gdouble      ide_settings_get_double                  (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+gint         ide_settings_get_int                     (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+gchar       *ide_settings_get_string                  (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+guint        ide_settings_get_uint                    (IdeSettings             *self,
+                                                       const gchar             *key);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_boolean                 (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       gboolean                 val);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_double                  (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       gdouble                  val);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_int                     (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       gint                     val);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_string                  (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       const gchar             *val);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_set_uint                    (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       guint                    val);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_bind                        (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       gpointer                 object,
+                                                       const gchar             *property,
+                                                       GSettingsBindFlags       flags);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_bind_with_mapping           (IdeSettings             *self,
+                                                       const gchar             *key,
+                                                       gpointer                 object,
+                                                       const gchar             *property,
+                                                       GSettingsBindFlags       flags,
+                                                       GSettingsBindGetMapping  get_mapping,
+                                                       GSettingsBindSetMapping  set_mapping,
+                                                       gpointer                 user_data,
+                                                       GDestroyNotify           destroy);
+IDE_AVAILABLE_IN_3_32
+void         ide_settings_unbind                      (IdeSettings             *self,
+                                                       const gchar             *property);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-transfer-manager.c b/src/libide/core/ide-transfer-manager.c
new file mode 100644
index 000000000..66011bf71
--- /dev/null
+++ b/src/libide/core/ide-transfer-manager.c
@@ -0,0 +1,493 @@
+/* ide-transfer-manager.c
+ *
+ * Copyright 2016-2019 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 "ide-transfer-manager"
+
+#include "config.h"
+
+#include "ide-context.h"
+#include "ide-debug.h"
+#include "ide-macros.h"
+
+#include "ide-transfer.h"
+#include "ide-transfer-manager.h"
+
+struct _IdeTransferManager
+{
+  GObject    parent_instance;
+  GPtrArray *transfers;
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeTransferManager, ide_transfer_manager, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_HAS_ACTIVE,
+  PROP_PROGRESS,
+  N_PROPS
+};
+
+enum {
+  TRANSFER_COMPLETED,
+  TRANSFER_FAILED,
+  ALL_TRANSFERS_COMPLETED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+/**
+ * ide_transfer_manager_get_has_active:
+ *
+ * Gets if there are active transfers.
+ *
+ * Returns: %TRUE if there are active transfers.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_transfer_manager_get_has_active (IdeTransferManager *self)
+{
+  g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), FALSE);
+
+  for (guint i = 0; i < self->transfers->len; i++)
+    {
+      IdeTransfer *transfer = g_ptr_array_index (self->transfers, i);
+
+      if (ide_transfer_get_active (transfer))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_transfer_manager_finalize (GObject *object)
+{
+  IdeTransferManager *self = (IdeTransferManager *)object;
+
+  g_clear_pointer (&self->transfers, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (ide_transfer_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_transfer_manager_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  IdeTransferManager *self = IDE_TRANSFER_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_HAS_ACTIVE:
+      g_value_set_boolean (value, ide_transfer_manager_get_has_active (self));
+      break;
+
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_transfer_manager_get_progress (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_transfer_manager_class_init (IdeTransferManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_transfer_manager_finalize;
+  object_class->get_property = ide_transfer_manager_get_property;
+
+  /**
+   * IdeTransferManager:has-active:
+   *
+   * If there are transfers active, this will be set.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_ACTIVE] =
+    g_param_spec_boolean ("has-active",
+                          "Has Active",
+                          "Has Active",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTransferManager:progress:
+   *
+   * A double between and including 0.0 and 1.0 describing the progress of
+   * all tasks.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "Progress",
+                         0.0,
+                         1.0,
+                         0.0,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeTransferManager::all-transfers-completed:
+   *
+   * This signal is emitted when all of the transfers have completed or failed.
+   *
+   * Since: 3.32
+   */
+  signals [ALL_TRANSFERS_COMPLETED] =
+    g_signal_new ("all-transfers-completed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  /**
+   * IdeTransferManager::transfer-completed:
+   * @self: An #IdeTransferManager
+   * @transfer: An #IdeTransfer
+   *
+   * This signal is emitted when a transfer has completed successfully.
+   *
+   * Since: 3.32
+   */
+  signals [TRANSFER_COMPLETED] =
+    g_signal_new ("transfer-completed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 1, IDE_TYPE_TRANSFER);
+
+  /**
+   * IdeTransferManager::transfer-failed:
+   * @self: An #IdeTransferManager
+   * @transfer: An #IdeTransfer
+   * @reason: (in): The reason for the failure.
+   *
+   * This signal is emitted when a transfer has failed to complete
+   * successfully.
+   *
+   * Since: 3.32
+   */
+  signals [TRANSFER_FAILED] =
+    g_signal_new ("transfer-failed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 2, IDE_TYPE_TRANSFER, G_TYPE_ERROR);
+}
+
+static void
+ide_transfer_manager_init (IdeTransferManager *self)
+{
+  self->transfers = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+static void
+ide_transfer_manager_notify_progress (IdeTransferManager *self,
+                                      GParamSpec         *pspec,
+                                      IdeTransfer        *transfer)
+{
+  g_assert (IDE_IS_TRANSFER_MANAGER (self));
+  g_assert (IDE_IS_TRANSFER (transfer));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+}
+
+static gboolean
+ide_transfer_manager_append (IdeTransferManager *self,
+                             IdeTransfer        *transfer)
+{
+  guint position;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TRANSFER (transfer), FALSE);
+
+  for (guint i = 0; i < self->transfers->len; i++)
+    {
+      if (transfer == (IdeTransfer *)g_ptr_array_index (self->transfers, i))
+        IDE_RETURN (FALSE);
+    }
+
+  g_signal_connect_object (transfer,
+                           "notify::progress",
+                           G_CALLBACK (ide_transfer_manager_notify_progress),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  position = self->transfers->len;
+  g_ptr_array_add (self->transfers, g_object_ref (transfer));
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+
+  IDE_RETURN (TRUE);
+}
+
+void
+ide_transfer_manager_cancel_all (IdeTransferManager *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_TRANSFER_MANAGER (self));
+
+  for (guint i = 0; i < self->transfers->len; i++)
+    {
+      IdeTransfer *transfer = g_ptr_array_index (self->transfers, i);
+
+      ide_transfer_cancel (transfer);
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_transfer_manager_clear:
+ *
+ * Removes all transfers from the manager that are completed.
+ *
+ * Since: 3.32
+ */
+void
+ide_transfer_manager_clear (IdeTransferManager *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_TRANSFER_MANAGER (self));
+
+  for (guint i = self->transfers->len; i > 0; i--)
+    {
+      IdeTransfer *transfer = g_ptr_array_index (self->transfers, i - 1);
+
+      if (!ide_transfer_get_active (transfer))
+        {
+          g_ptr_array_remove_index (self->transfers, i - 1);
+          g_list_model_items_changed (G_LIST_MODEL (self), i - 1, 1, 0);
+        }
+    }
+
+  IDE_EXIT;
+}
+
+static GType
+ide_transfer_manager_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_TRANSFER;
+}
+
+static guint
+ide_transfer_manager_get_n_items (GListModel *model)
+{
+  IdeTransferManager *self = (IdeTransferManager *)model;
+
+  g_assert (IDE_IS_TRANSFER_MANAGER (self));
+
+  return self->transfers->len;
+}
+
+static gpointer
+ide_transfer_manager_get_item (GListModel *model,
+                               guint       position)
+{
+  IdeTransferManager *self = (IdeTransferManager *)model;
+
+  g_assert (IDE_IS_TRANSFER_MANAGER (self));
+
+  if G_UNLIKELY (position >= self->transfers->len)
+    return NULL;
+
+  return g_object_ref (g_ptr_array_index (self->transfers, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_transfer_manager_get_item_type;
+  iface->get_n_items = ide_transfer_manager_get_n_items;
+  iface->get_item = ide_transfer_manager_get_item;
+}
+
+gdouble
+ide_transfer_manager_get_progress (IdeTransferManager *self)
+{
+  gdouble total = 0.0;
+
+  g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), 0.0);
+
+  if (self->transfers->len > 0)
+    {
+      guint count = 0;
+
+      for (guint i = 0; i < self->transfers->len; i++)
+        {
+          IdeTransfer *transfer = g_ptr_array_index (self->transfers, i);
+          gdouble progress = ide_transfer_get_progress (transfer);
+
+          if (ide_transfer_get_completed (transfer) || ide_transfer_get_active (transfer))
+            {
+              total += MAX (0.0, MIN (1.0, progress));
+              count++;
+            }
+        }
+
+      if (count != 0)
+        total /= (gdouble)count;
+    }
+
+  return total;
+}
+
+static void
+ide_transfer_manager_execute_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  IdeTransfer *transfer = (IdeTransfer *)object;
+  IdeTransferManager *self;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER (transfer));
+  g_assert (G_IS_TASK (task));
+
+  self = g_task_get_source_object (task);
+
+  if (!ide_transfer_execute_finish (transfer, result, &error))
+    {
+      g_signal_emit (self, signals[TRANSFER_FAILED], 0, transfer, error);
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_GOTO (notify_properties);
+    }
+  else
+    {
+      g_signal_emit (self, signals[TRANSFER_COMPLETED], 0, transfer);
+      g_task_return_boolean (task, TRUE);
+    }
+
+  if (!ide_transfer_manager_get_has_active (self))
+    g_signal_emit (self, signals[ALL_TRANSFERS_COMPLETED], 0);
+
+notify_properties:
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_ACTIVE]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_transfer_manager_execute_async:
+ * @self: An #IdeTransferManager
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: (nullable): A callback or %NULL
+ * @user_data: user data for @callback
+ *
+ * This is a convenience function that will queue @transfer into the transfer
+ * manager and execute callback upon completion of the transfer. The success
+ * or failure #GError will be propagated to the caller via
+ * ide_transfer_manager_execute_finish().
+ *
+ * Since: 3.32
+ */
+void
+ide_transfer_manager_execute_async (IdeTransferManager  *self,
+                                    IdeTransfer         *transfer,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (!self || IDE_IS_TRANSFER_MANAGER (self));
+  g_return_if_fail (IDE_IS_TRANSFER (transfer));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (self == NULL)
+    self = ide_transfer_manager_get_default ();
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_transfer_manager_execute_async);
+
+  if (!ide_transfer_manager_append (self, transfer))
+    {
+      if (ide_transfer_get_active (transfer))
+        {
+          g_warning ("%s is already active, ignoring transfer request",
+                     G_OBJECT_TYPE_NAME (transfer));
+          IDE_EXIT;
+        }
+    }
+
+  ide_transfer_execute_async (transfer,
+                              cancellable,
+                              ide_transfer_manager_execute_cb,
+                              g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_transfer_manager_execute_finish (IdeTransferManager  *self,
+                                     GAsyncResult        *result,
+                                     GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_TRANSFER_MANAGER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+/**
+ * ide_transfer_manager_get_default:
+ *
+ * Gets the #IdeTransferManager singleton.
+ *
+ * Returns: (transfer none): an #IdeTransferManager
+ *
+ * Since: 3.32
+ */
+IdeTransferManager *
+ide_transfer_manager_get_default (void)
+{
+  static IdeTransferManager *instance;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (!instance || IDE_IS_TRANSFER_MANAGER (instance));
+
+  if (g_once_init_enter (&instance))
+    g_once_init_leave (&instance, g_object_new (IDE_TYPE_TRANSFER_MANAGER, NULL));
+
+  return instance;
+}
diff --git a/src/libide/core/ide-transfer-manager.h b/src/libide/core/ide-transfer-manager.h
new file mode 100644
index 000000000..a3adccb54
--- /dev/null
+++ b/src/libide/core/ide-transfer-manager.h
@@ -0,0 +1,58 @@
+/* ide-transfer-manager.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-transfer.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TRANSFER_MANAGER (ide_transfer_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTransferManager, ide_transfer_manager, IDE, TRANSFER_MANAGER, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeTransferManager *ide_transfer_manager_get_default    (void);
+IDE_AVAILABLE_IN_3_32
+gdouble             ide_transfer_manager_get_progress   (IdeTransferManager   *self);
+IDE_AVAILABLE_IN_3_32
+gboolean            ide_transfer_manager_get_has_active (IdeTransferManager   *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_transfer_manager_cancel_all     (IdeTransferManager   *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_transfer_manager_clear          (IdeTransferManager   *self);
+IDE_AVAILABLE_IN_3_32
+void                ide_transfer_manager_execute_async  (IdeTransferManager   *self,
+                                                         IdeTransfer          *transfer,
+                                                         GCancellable         *cancellable,
+                                                         GAsyncReadyCallback   callback,
+                                                         gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean            ide_transfer_manager_execute_finish (IdeTransferManager   *self,
+                                                         GAsyncResult         *result,
+                                                         GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-transfer.c b/src/libide/core/ide-transfer.c
new file mode 100644
index 000000000..a3c35b3e0
--- /dev/null
+++ b/src/libide/core/ide-transfer.c
@@ -0,0 +1,522 @@
+/* ide-transfer.c
+ *
+ * Copyright 2016-2019 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 "ide-transfer"
+
+#include "config.h"
+
+#include "ide-debug.h"
+#include "ide-macros.h"
+#include "ide-transfer.h"
+
+typedef struct
+{
+  gchar *icon_name;
+  gchar *status;
+  gchar *title;
+  GCancellable *cancellable;
+  gdouble progress;
+  guint active : 1;
+  guint completed : 1;
+} IdeTransferPrivate;
+
+enum {
+  PROP_0,
+  PROP_ACTIVE,
+  PROP_COMPLETED,
+  PROP_ICON_NAME,
+  PROP_PROGRESS,
+  PROP_STATUS,
+  PROP_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTransfer, ide_transfer, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_transfer_real_execute_async (IdeTransfer         *self,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_assert (IDE_IS_TRANSFER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_transfer_real_execute_finish (IdeTransfer   *self,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  g_assert (IDE_IS_TRANSFER (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_transfer_finalize (GObject *object)
+{
+  IdeTransfer *self = (IdeTransfer *)object;
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_clear_pointer (&priv->icon_name, g_free);
+  g_clear_pointer (&priv->status, g_free);
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_object (&priv->cancellable);
+
+  G_OBJECT_CLASS (ide_transfer_parent_class)->finalize (object);
+}
+
+static void
+ide_transfer_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  IdeTransfer *self = IDE_TRANSFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVE:
+      g_value_set_boolean (value, ide_transfer_get_active (self));
+      break;
+
+    case PROP_COMPLETED:
+      g_value_set_boolean (value, ide_transfer_get_completed (self));
+      break;
+
+    case PROP_ICON_NAME:
+      g_value_set_string (value, ide_transfer_get_icon_name (self));
+      break;
+
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_transfer_get_progress (self));
+      break;
+
+    case PROP_STATUS:
+      g_value_set_string (value, ide_transfer_get_status (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, ide_transfer_get_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_transfer_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  IdeTransfer *self = IDE_TRANSFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_ICON_NAME:
+      ide_transfer_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_PROGRESS:
+      ide_transfer_set_progress (self, g_value_get_double (value));
+      break;
+
+    case PROP_STATUS:
+      ide_transfer_set_status (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_transfer_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_transfer_class_init (IdeTransferClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_transfer_finalize;
+  object_class->get_property = ide_transfer_get_property;
+  object_class->set_property = ide_transfer_set_property;
+
+  klass->execute_async = ide_transfer_real_execute_async;
+  klass->execute_finish = ide_transfer_real_execute_finish;
+
+  properties [PROP_ACTIVE] =
+    g_param_spec_boolean ("active",
+                          "Active",
+                          "If the transfer is active",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_COMPLETED] =
+    g_param_spec_boolean ("completed",
+                          "Completed",
+                          "If the transfer has completed successfully",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The icon to display next to the transfer",
+                         "folder-download-symbolic",
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "The progress for the transfer between 0 adn 1",
+                         0.0,
+                         1.0,
+                         0.0,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_STATUS] =
+    g_param_spec_string ("status",
+                         "Status",
+                         "The status message for the transfer",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the transfer",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_transfer_init (IdeTransfer *self)
+{
+}
+
+static void
+ide_transfer_execute_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  IdeTransfer *self = (IdeTransfer *)object;
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER (self));
+  g_assert (G_IS_TASK (task));
+
+  priv->active = FALSE;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE]);
+
+  ide_transfer_set_progress (self, 1.0);
+
+  if (!IDE_TRANSFER_GET_CLASS (self)->execute_finish (self, result, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  priv->completed = TRUE;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPLETED]);
+
+  g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+void
+ide_transfer_execute_async (IdeTransfer         *self,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /*
+   * We already create our own wrapper task so that we can track completion
+   * cleanly from the subclass implementation. It also allows us to ensure
+   * that the subclasses execute_finish() is guaranteed to be called.
+   */
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_transfer_execute_async);
+
+  /*
+   * Wrap our own cancellable so that we can gracefully control
+   * the cancellation of the underlying transfer without affecting
+   * the callers cancellation state.
+   */
+  g_clear_object (&priv->cancellable);
+  priv->cancellable = g_cancellable_new ();
+
+  if (cancellable != NULL)
+    g_signal_connect_object (cancellable,
+                             "cancelled",
+                             G_CALLBACK (g_cancellable_cancel),
+                             priv->cancellable,
+                             G_CONNECT_SWAPPED);
+
+  priv->active = TRUE;
+  priv->completed = FALSE;
+
+  IDE_TRANSFER_GET_CLASS (self)->execute_async (self,
+                                                priv->cancellable,
+                                                ide_transfer_execute_cb,
+                                                g_steal_pointer (&task));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTIVE]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPLETED]);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_transfer_execute_finish (IdeTransfer   *self,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+const gchar *
+ide_transfer_get_icon_name (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+  return priv->icon_name ?: "folder-download-symbolic";
+}
+
+void
+ide_transfer_set_icon_name (IdeTransfer *self,
+                            const gchar *icon_name)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSFER (self));
+
+  if (g_strcmp0 (priv->icon_name, icon_name) != 0)
+    {
+      g_free (priv->icon_name);
+      priv->icon_name = g_strdup (icon_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+    }
+}
+
+gdouble
+ide_transfer_get_progress (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), 0.0);
+
+  return priv->progress;
+}
+
+void
+ide_transfer_set_progress (IdeTransfer *self,
+                           gdouble      progress)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSFER (self));
+
+  if (progress != priv->progress)
+    {
+      priv->progress = progress;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+    }
+}
+
+const gchar *
+ide_transfer_get_status (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+  return priv->status;
+}
+
+void
+ide_transfer_set_status (IdeTransfer *self,
+                         const gchar *status)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSFER (self));
+
+  if (g_strcmp0 (priv->status, status) != 0)
+    {
+      g_free (priv->status);
+      priv->status = g_strdup (status);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATUS]);
+    }
+}
+
+const gchar *
+ide_transfer_get_title (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+  return priv->title;
+}
+
+void
+ide_transfer_set_title (IdeTransfer *self,
+                        const gchar *title)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSFER (self));
+
+  if (g_strcmp0 (priv->title, title) != 0)
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+void
+ide_transfer_cancel (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSFER (self));
+
+  if (!g_cancellable_is_cancelled (priv->cancellable))
+    g_cancellable_cancel (priv->cancellable);
+}
+
+gboolean
+ide_transfer_get_completed (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), FALSE);
+
+  return priv->completed;
+}
+
+gboolean
+ide_transfer_get_active (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), FALSE);
+
+  return priv->active;
+}
+
+GQuark
+ide_transfer_error_quark (void)
+{
+  return g_quark_from_static_string ("ide-transfer-error-quark");
+}
+
+static void
+ide_transfer_notification_notify_completed (IdeTransfer     *self,
+                                            GParamSpec      *pspec,
+                                            IdeNotification *notif)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TRANSFER (self));
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  ide_notification_withdraw_in_seconds (notif, 10);
+}
+
+/**
+ * ide_transfer_create_notification:
+ * @self: a #IdeTransfer
+ *
+ * Creates a new #IdeNotification that is updated with the progress
+ * of the #IdeTransfer. This is useful when you need to bridge an
+ * #IdeTransfer into something that can be displayed to the user.
+ *
+ * If the transfer has completed, %NULL is returned.
+ *
+ * Returns: (transfer full) (nullable): an #IdeNotification or %NULL
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_transfer_create_notification (IdeTransfer *self)
+{
+  IdeTransferPrivate *priv = ide_transfer_get_instance_private (self);
+  g_autoptr(IdeNotification) notif = NULL;
+
+  g_return_val_if_fail (IDE_IS_TRANSFER (self), NULL);
+
+  if (priv->completed)
+    return NULL;
+
+  notif = ide_notification_new ();
+  ide_notification_set_has_progress (notif, TRUE);
+  g_object_bind_property (self, "title", notif, "title", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (self, "status", notif, "body", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (self, "progress", notif, "progress", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (self, "icon-name", notif, "icon-name", G_BINDING_SYNC_CREATE);
+
+  g_signal_connect_object (self,
+                           "notify::completed",
+                           G_CALLBACK (ide_transfer_notification_notify_completed),
+                           notif,
+                           0);
+
+  return g_steal_pointer (&notif);
+}
diff --git a/src/libide/core/ide-transfer.h b/src/libide/core/ide-transfer.h
new file mode 100644
index 000000000..380161773
--- /dev/null
+++ b/src/libide/core/ide-transfer.h
@@ -0,0 +1,101 @@
+/* ide-transfer.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include "ide-notification.h"
+#include "ide-object.h"
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TRANSFER  (ide_transfer_get_type())
+#define IDE_TRANSFER_ERROR (ide_transfer_error_quark())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTransfer, ide_transfer, IDE, TRANSFER, IdeObject)
+
+struct _IdeTransferClass
+{
+  IdeObjectClass parent_class;
+
+  void     (*execute_async)  (IdeTransfer          *self,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+  gboolean (*execute_finish) (IdeTransfer          *self,
+                              GAsyncResult         *result,
+                              GError              **error);
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+typedef enum
+{
+  IDE_TRANSFER_ERROR_UNKNOWN = 0,
+  IDE_TRANSFER_ERROR_CONNECTION_IS_METERED = 1,
+} IdeTransferError;
+
+IDE_AVAILABLE_IN_3_32
+GQuark           ide_transfer_error_quark         (void);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_cancel              (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_transfer_get_completed       (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_transfer_get_active          (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_transfer_get_icon_name       (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_set_icon_name       (IdeTransfer          *self,
+                                                   const gchar          *icon_name);
+IDE_AVAILABLE_IN_3_32
+gdouble          ide_transfer_get_progress        (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_set_progress        (IdeTransfer          *self,
+                                                   gdouble               progress);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_transfer_get_status          (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_set_status          (IdeTransfer          *self,
+                                                   const gchar          *status);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_transfer_get_title           (IdeTransfer          *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_set_title           (IdeTransfer          *self,
+                                                   const gchar          *title);
+IDE_AVAILABLE_IN_3_32
+void             ide_transfer_execute_async       (IdeTransfer          *self,
+                                                   GCancellable         *cancellable,
+                                                   GAsyncReadyCallback   callback,
+                                                   gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_transfer_execute_finish      (IdeTransfer          *self,
+                                                   GAsyncResult         *result,
+                                                   GError              **error);
+IDE_AVAILABLE_IN_3_32
+IdeNotification *ide_transfer_create_notification (IdeTransfer          *self);
+
+G_END_DECLS
diff --git a/src/libide/core/ide-version-macros.h b/src/libide/core/ide-version-macros.h
new file mode 100644
index 000000000..4e7af41a3
--- /dev/null
+++ b/src/libide/core/ide-version-macros.h
@@ -0,0 +1,160 @@
+/* ide-version-macros.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_CORE_INSIDE) && !defined (IDE_CORE_COMPILATION)
+# error "Only <libide-core.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+#include "ide-version.h"
+
+#ifndef _IDE_EXTERN
+#define _IDE_EXTERN extern
+#endif
+
+#ifdef IDE_DISABLE_DEPRECATION_WARNINGS
+#define IDE_DEPRECATED _IDE_EXTERN
+#define IDE_DEPRECATED_FOR(f) _IDE_EXTERN
+#define IDE_UNAVAILABLE(maj,min) _IDE_EXTERN
+#else
+#define IDE_DEPRECATED G_DEPRECATED _IDE_EXTERN
+#define IDE_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f) _IDE_EXTERN
+#define IDE_UNAVAILABLE(maj,min) G_UNAVAILABLE(maj,min) _IDE_EXTERN
+#endif
+
+#define IDE_VERSION_3_28 (G_ENCODE_VERSION (3, 28))
+#define IDE_VERSION_3_30 (G_ENCODE_VERSION (3, 30))
+#define IDE_VERSION_3_32 (G_ENCODE_VERSION (3, 32))
+
+#if (IDE_MINOR_VERSION == 99)
+# define IDE_VERSION_CUR_STABLE (G_ENCODE_VERSION (IDE_MAJOR_VERSION + 1, 0))
+#elif (IDE_MINOR_VERSION % 2)
+# define IDE_VERSION_CUR_STABLE (G_ENCODE_VERSION (IDE_MAJOR_VERSION, IDE_MINOR_VERSION + 1))
+#else
+# define IDE_VERSION_CUR_STABLE (G_ENCODE_VERSION (IDE_MAJOR_VERSION, IDE_MINOR_VERSION))
+#endif
+
+#if (IDE_MINOR_VERSION == 99)
+# define IDE_VERSION_PREV_STABLE (G_ENCODE_VERSION (IDE_MAJOR_VERSION + 1, 0))
+#elif (IDE_MINOR_VERSION % 2)
+# define IDE_VERSION_PREV_STABLE (G_ENCODE_VERSION (IDE_MAJOR_VERSION, IDE_MINOR_VERSION - 1))
+#else
+# define IDE_VERSION_PREV_STABLE (G_ENCODE_VERSION (IDE_MAJOR_VERSION, IDE_MINOR_VERSION - 2))
+#endif
+
+/**
+ * IDE_VERSION_MIN_REQUIRED:
+ *
+ * A macro that should be defined by the user prior to including
+ * the ide.h header.
+ *
+ * The definition should be one of the predefined IDE version
+ * macros: %IDE_VERSION_3_28, ...
+ *
+ * This macro defines the lower bound for the Builder API to use.
+ *
+ * If a function has been deprecated in a newer version of Builder,
+ * it is possible to use this symbol to avoid the compiler warnings
+ * without disabling warning for every deprecated function.
+ *
+ * Since: 3.32
+ */
+#ifndef IDE_VERSION_MIN_REQUIRED
+# define IDE_VERSION_MIN_REQUIRED (IDE_VERSION_CUR_STABLE)
+#endif
+
+/**
+ * IDE_VERSION_MAX_ALLOWED:
+ *
+ * A macro that should be defined by the user prior to including
+ * the ide.h header.
+
+ * The definition should be one of the predefined Builder version
+ * macros: %IDE_VERSION_1_0, %IDE_VERSION_1_2,...
+ *
+ * This macro defines the upper bound for the IDE API to use.
+ *
+ * If a function has been introduced in a newer version of Builder,
+ * it is possible to use this symbol to get compiler warnings when
+ * trying to use that function.
+ *
+ * Since: 3.32
+ */
+#ifndef IDE_VERSION_MAX_ALLOWED
+# if IDE_VERSION_MIN_REQUIRED > IDE_VERSION_PREV_STABLE
+#  define IDE_VERSION_MAX_ALLOWED (IDE_VERSION_MIN_REQUIRED)
+# else
+#  define IDE_VERSION_MAX_ALLOWED (IDE_VERSION_CUR_STABLE)
+# endif
+#endif
+
+#if IDE_VERSION_MAX_ALLOWED < IDE_VERSION_MIN_REQUIRED
+#error "IDE_VERSION_MAX_ALLOWED must be >= IDE_VERSION_MIN_REQUIRED"
+#endif
+#if IDE_VERSION_MIN_REQUIRED < IDE_VERSION_3_28
+#error "IDE_VERSION_MIN_REQUIRED must be >= IDE_VERSION_3_28"
+#endif
+
+#define IDE_AVAILABLE_IN_ALL                   _IDE_EXTERN
+
+#if IDE_VERSION_MIN_REQUIRED >= IDE_VERSION_3_28
+# define IDE_DEPRECATED_IN_3_28                IDE_DEPRECATED
+# define IDE_DEPRECATED_IN_3_28_FOR(f)         IDE_DEPRECATED_FOR(f)
+#else
+# define IDE_DEPRECATED_IN_3_28                _IDE_EXTERN
+# define IDE_DEPRECATED_IN_3_28_FOR(f)         _IDE_EXTERN
+#endif
+
+#if IDE_VERSION_MAX_ALLOWED < IDE_VERSION_3_28
+# define IDE_AVAILABLE_IN_3_28                 IDE_UNAVAILABLE(3, 28)
+#else
+# define IDE_AVAILABLE_IN_3_28                 _IDE_EXTERN
+#endif
+
+#if IDE_VERSION_MIN_REQUIRED >= IDE_VERSION_3_30
+# define IDE_DEPRECATED_IN_3_30                IDE_DEPRECATED
+# define IDE_DEPRECATED_IN_3_30_FOR(f)         IDE_DEPRECATED_FOR(f)
+#else
+# define IDE_DEPRECATED_IN_3_30                _IDE_EXTERN
+# define IDE_DEPRECATED_IN_3_30_FOR(f)         _IDE_EXTERN
+#endif
+
+#if IDE_VERSION_MAX_ALLOWED < IDE_VERSION_3_30
+# define IDE_AVAILABLE_IN_3_30                 IDE_UNAVAILABLE(3, 30)
+#else
+# define IDE_AVAILABLE_IN_3_30                 _IDE_EXTERN
+#endif
+
+#if IDE_VERSION_MIN_REQUIRED >= IDE_VERSION_3_32
+# define IDE_DEPRECATED_IN_3_32                IDE_DEPRECATED
+# define IDE_DEPRECATED_IN_3_32_FOR(f)         IDE_DEPRECATED_FOR(f)
+#else
+# define IDE_DEPRECATED_IN_3_32                _IDE_EXTERN
+# define IDE_DEPRECATED_IN_3_32_FOR(f)         _IDE_EXTERN
+#endif
+
+#if IDE_VERSION_MAX_ALLOWED < IDE_VERSION_3_32
+# define IDE_AVAILABLE_IN_3_32                 IDE_UNAVAILABLE(3, 32)
+#else
+# define IDE_AVAILABLE_IN_3_32                 _IDE_EXTERN
+#endif
diff --git a/src/libide/ide-version.h.in b/src/libide/core/ide-version.h.in
similarity index 100%
rename from src/libide/ide-version.h.in
rename to src/libide/core/ide-version.h.in
diff --git a/src/libide/core/libide-core.h b/src/libide/core/libide-core.h
new file mode 100644
index 000000000..42d4373ea
--- /dev/null
+++ b/src/libide/core/libide-core.h
@@ -0,0 +1,43 @@
+/* ide-core.h
+ *
+ * Copyright 2018-2019 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>
+
+#define IDE_CORE_INSIDE
+
+#include "ide-build-ident.h"
+#include "ide-context.h"
+#include "ide-debug.h"
+#include "ide-global.h"
+#include "ide-log.h"
+#include "ide-macros.h"
+#include "ide-notification.h"
+#include "ide-notifications.h"
+#include "ide-object.h"
+#include "ide-object-box.h"
+#include "ide-settings.h"
+#include "ide-transfer.h"
+#include "ide-transfer-manager.h"
+#include "ide-version.h"
+#include "ide-version-macros.h"
+
+#undef IDE_CORE_INSIDE
diff --git a/src/libide/core/meson.build b/src/libide/core/meson.build
new file mode 100644
index 000000000..155d9ea6c
--- /dev/null
+++ b/src/libide/core/meson.build
@@ -0,0 +1,124 @@
+libide_core_header_dir = join_paths(libide_header_dir, 'core')
+libide_core_header_subdir = join_paths(libide_header_subdir, 'core')
+libide_include_directories += include_directories('.')
+
+#
+# Versioning that all libide libraries (re)use
+#
+
+version_data = configuration_data()
+version_data.set('MAJOR_VERSION', MAJOR_VERSION)
+version_data.set('MINOR_VERSION', MINOR_VERSION)
+version_data.set('MICRO_VERSION', MICRO_VERSION)
+version_data.set('VERSION', meson.project_version())
+version_data.set_quoted('BUILD_CHANNEL', get_option('with_channel'))
+version_data.set_quoted('BUILD_TYPE', get_option('buildtype'))
+
+libide_core_version_h = configure_file(
+          input: 'ide-version.h.in',
+         output: 'ide-version.h',
+    install_dir: libide_core_header_dir,
+        install: true,
+  configuration: version_data)
+
+libide_core_generated_headers = [libide_core_version_h]
+
+libide_build_ident_h = vcs_tag(
+     fallback: meson.project_version(),
+        input: 'ide-build-ident.h.in',
+       output: 'ide-build-ident.h',
+)
+libide_core_generated_headers += [libide_build_ident_h]
+
+#
+# Debugging and Tracing Support
+#
+
+libide_core_conf = configuration_data()
+libide_core_conf.set10('ENABLE_TRACING', get_option('tracing'))
+libide_core_conf.set('BUGREPORT_URL', 'https://gitlab.gnome.org/GNOME/gnome-builder/issues')
+
+libide_debug_h = configure_file(
+         input: 'ide-debug.h.in',
+         output: 'ide-debug.h',
+  configuration: libide_core_conf,
+        install: true,
+    install_dir: libide_core_header_dir,
+)
+
+libide_core_generated_headers += [libide_debug_h]
+
+#
+# Public API Headers
+#
+
+libide_core_public_headers = [
+  'ide-context.h',
+  'ide-context-addin.h',
+  'ide-global.h',
+  'ide-log.h',
+  'ide-macros.h',
+  'ide-notification.h',
+  'ide-notifications.h',
+  'ide-object.h',
+  'ide-object-box.h',
+  'ide-settings.h',
+  'ide-transfer.h',
+  'ide-transfer-manager.h',
+  'ide-version-macros.h',
+  'libide-core.h',
+]
+
+install_headers(libide_core_public_headers, subdir: libide_core_header_subdir)
+
+#
+# Sources
+#
+
+libide_core_public_sources = [
+  'ide-context.c',
+  'ide-context-addin.c',
+  'ide-global.c',
+  'ide-log.c',
+  'ide-notification.c',
+  'ide-notifications.c',
+  'ide-object.c',
+  'ide-object-box.c',
+  'ide-object-notify.c',
+  'ide-settings.c',
+  'ide-transfer.c',
+  'ide-transfer-manager.c',
+]
+
+libide_core_sources = []
+libide_core_sources += libide_core_generated_headers
+libide_core_sources += libide_core_public_sources
+
+#
+# Library Definitions
+#
+
+libide_core_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+  libpeas_dep,
+]
+
+libide_core = static_library('ide-core-' + libide_api_version, libide_core_sources,
+   dependencies: libide_core_deps,
+         c_args: libide_args + release_args + ['-DIDE_CORE_COMPILATION'],
+)
+
+libide_core_dep = declare_dependency(
+              sources: libide_core_generated_headers,
+         dependencies: libide_core_deps,
+           link_whole: libide_core,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_core_public_sources)
+gnome_builder_public_headers += files(libide_core_public_headers)
+gnome_builder_generated_headers += libide_core_generated_headers
+gnome_builder_include_subdirs += libide_core_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-core.h', '-DIDE_CORE_COMPILATION']
diff --git a/src/libide/debugger/ide-debug-manager.c b/src/libide/debugger/ide-debug-manager.c
index 8fd5cc91f..41c2b86a0 100644
--- a/src/libide/debugger/ide-debug-manager.c
+++ b/src/libide/debugger/ide-debug-manager.c
@@ -1,6 +1,6 @@
 /* ide-debug-manager.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-debug-manager"
@@ -22,43 +24,40 @@
 
 #include <dazzle.h>
 #include <glib/gi18n.h>
+#include <libide-code.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
 #include <stdlib.h>
 #include <string.h>
 
-#include "ide-debug.h"
+#include "ide-buffer-private.h"
 
-#include "buffers/ide-buffer.h"
-#include "buffers/ide-buffer-manager.h"
-#include "buildsystem/ide-build-target.h"
-#include "debugger/ide-debug-manager.h"
-#include "debugger/ide-debugger.h"
-#include "debugger/ide-debugger-private.h"
-#include "files/ide-file.h"
-#include "plugins/ide-extension-util.h"
-#include "runner/ide-runner.h"
-#include "threading/ide-task.h"
+#include "ide-debug-manager.h"
+#include "ide-debugger.h"
+#include "ide-debugger-private.h"
 
 #define TAG_CURRENT_BKPT "debugger::current-breakpoint"
 
 struct _IdeDebugManager
 {
-  IdeObject           parent_instance;
+  IdeObject       parent_instance;
 
-  GHashTable         *breakpoints;
-  IdeDebugger        *debugger;
-  DzlSignalGroup     *debugger_signals;
-  IdeRunner          *runner;
-  GQueue              pending_breakpoints;
-  GPtrArray          *supported_languages;
+  GHashTable     *breakpoints;
+  IdeDebugger    *debugger;
+  DzlSignalGroup *debugger_signals;
+  IdeRunner      *runner;
+  GQueue          pending_breakpoints;
+  GPtrArray      *supported_languages;
 
-  guint               active : 1;
+  guint           active : 1;
 };
 
 typedef struct
 {
-  IdeDebugger *debugger;
-  IdeRunner   *runner;
-  gint         priority;
+  IdeDebugManager *self;
+  IdeDebugger     *debugger;
+  IdeRunner       *runner;
+  gint             priority;
 } DebuggerLookup;
 
 enum {
@@ -90,6 +89,28 @@ compare_language_id (gconstpointer a,
   return strcmp (*astr, *bstr);
 }
 
+static void
+ide_debug_manager_notify_buffer (IdeDebugManager        *self,
+                                 IdeDebuggerBreakpoints *breakpoints)
+{
+  g_autoptr(IdeContext) context = NULL;
+  IdeBufferManager *bufmgr;
+  IdeBuffer *buffer;
+  GFile *file;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DEBUG_MANAGER (self));
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINTS (breakpoints));
+
+  context = ide_object_ref_context (IDE_OBJECT (self));
+  bufmgr = ide_buffer_manager_from_context (context);
+  file = ide_debugger_breakpoints_get_file (breakpoints);
+  buffer = ide_buffer_manager_find_buffer (bufmgr, file);
+
+  if (buffer != NULL)
+    _ide_buffer_line_flags_changed (buffer);
+}
+
 /**
  * ide_debug_manager_supports_language:
  * @self: a #IdeDebugManager
@@ -104,7 +125,7 @@ compare_language_id (gconstpointer a,
  *
  * Returns: %TRUE if the language is supported; otherwise %FALSE.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 gboolean
 ide_debug_manager_supports_language (IdeDebugManager *self,
@@ -292,7 +313,7 @@ ide_debug_manager_breakpoint_added (IdeDebugManager       *self,
                                     IdeDebuggerBreakpoint *breakpoint,
                                     IdeDebugger           *debugger)
 {
-  IdeDebuggerBreakpoints *breakpoints;
+  g_autoptr(IdeDebuggerBreakpoints) breakpoints = NULL;
   g_autoptr(GFile) file = NULL;
   const gchar *path;
 
@@ -301,21 +322,13 @@ ide_debug_manager_breakpoint_added (IdeDebugManager       *self,
   g_assert (IDE_IS_DEBUGGER (debugger));
 
   /* If there is no file, then there is nothing to cache */
-  path = ide_debugger_breakpoint_get_file (breakpoint);
-  if (path == NULL)
+  if (!(path = ide_debugger_breakpoint_get_file (breakpoint)))
     return;
 
   file = g_file_new_for_path (path);
-  breakpoints = g_hash_table_lookup (self->breakpoints, file);
-  if (breakpoints == NULL)
-    {
-      breakpoints = g_object_new (IDE_TYPE_DEBUGGER_BREAKPOINTS,
-                                  "file", file,
-                                  NULL);
-      g_hash_table_insert (self->breakpoints, g_steal_pointer (&file), breakpoints);
-    }
-
+  breakpoints = ide_debug_manager_get_breakpoints_for_file (self, file);
   _ide_debugger_breakpoints_add (breakpoints, breakpoint);
+  ide_debug_manager_notify_buffer (self, breakpoints);
 }
 
 static void
@@ -382,7 +395,7 @@ ide_debug_manager_clear_stopped (IdeDebugManager *self)
   g_assert (IDE_IS_DEBUG_MANAGER (self));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  bufmgr = ide_context_get_buffer_manager (context);
+  bufmgr = ide_buffer_manager_from_context (context);
 
   n_items = g_list_model_get_n_items (G_LIST_MODEL (bufmgr));
 
@@ -502,17 +515,17 @@ ide_debug_manager_real_breakpoint_reached (IdeDebugManager       *self,
   if (path != NULL)
     {
       IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
-      IdeBufferManager *bufmgr = ide_context_get_buffer_manager (context);
-      g_autoptr(IdeFile) file = ide_file_new_for_path (context, path);
+      IdeBufferManager *bufmgr = ide_buffer_manager_from_context (context);
+      g_autoptr(GFile) file = ide_context_build_file (context, path);
       g_autoptr(IdeTask) task = NULL;
 
       task = ide_task_new (self, NULL, NULL, NULL);
+      ide_task_set_source_tag (task, ide_debug_manager_real_breakpoint_reached);
       ide_task_set_task_data (task, g_object_ref (breakpoint), g_object_unref);
 
       ide_buffer_manager_load_file_async (bufmgr,
                                           file,
-                                          FALSE,
-                                          IDE_WORKBENCH_OPEN_FLAGS_NONE,
+                                          IDE_BUFFER_OPEN_FLAGS_NONE,
                                           NULL,
                                           NULL,
                                           ide_debug_manager_load_file_cb,
@@ -530,8 +543,8 @@ ide_debug_manager_dispose (GObject *object)
 
   g_hash_table_remove_all (self->breakpoints);
   dzl_signal_group_set_target (self->debugger_signals, NULL);
-  g_clear_object (&self->debugger);
-  g_clear_object (&self->runner);
+  ide_clear_and_destroy_object (&self->debugger);
+  ide_clear_and_destroy_object (&self->runner);
 
   G_OBJECT_CLASS (ide_debug_manager_parent_class)->dispose (object);
 }
@@ -586,6 +599,8 @@ ide_debug_manager_class_init (IdeDebugManagerClass *klass)
    *
    * This can be used to determine if the controls should be made visible
    * in the workbench.
+   *
+   * Since: 3.32
    */
   properties [PROP_ACTIVE] =
     g_param_spec_boolean ("active",
@@ -611,7 +626,7 @@ ide_debug_manager_class_init (IdeDebugManagerClass *klass)
    * The "breakpoint-added" signal is emitted when a new breakpoint has
    * been registered by the debugger.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [BREAKPOINT_ADDED] =
     g_signal_new_class_handler ("breakpoint-added",
@@ -629,7 +644,7 @@ ide_debug_manager_class_init (IdeDebugManagerClass *klass)
    * The "breakpoint-removed" signal is emitted when a new breakpoint has been
    * removed by the debugger.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [BREAKPOINT_REMOVED] =
     g_signal_new_class_handler ("breakpoint-removed",
@@ -652,7 +667,7 @@ ide_debug_manager_class_init (IdeDebugManagerClass *klass)
    *
    * See also: #IdeDebugManager:debugger
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [BREAKPOINT_REACHED] =
     g_signal_new_class_handler ("breakpoint-reached",
@@ -742,6 +757,8 @@ debugger_lookup (PeasExtensionSet *set,
   g_assert (IDE_IS_DEBUGGER (debugger));
   g_assert (lookup != NULL);
 
+  ide_object_append (IDE_OBJECT (lookup->self), IDE_OBJECT (debugger));
+
   if (ide_debugger_supports_runner (debugger, lookup->runner, &priority))
     {
       IdeBuildTarget *build_target = ide_runner_get_build_target (lookup->runner);
@@ -751,15 +768,20 @@ debugger_lookup (PeasExtensionSet *set,
           g_autofree gchar *language = ide_build_target_get_language (build_target);
 
           if (!debugger_supports_language (plugin_info, language))
-            return;
+            goto failure;
         }
 
       if (lookup->debugger == NULL || priority < lookup->priority)
         {
-          g_set_object (&lookup->debugger, debugger);
+          ide_clear_and_destroy_object (&lookup->debugger);
+          lookup->debugger = g_object_ref (debugger);
           lookup->priority = priority;
+          return;
         }
     }
+
+failure:
+  ide_object_destroy (IDE_OBJECT (debugger));
 }
 
 /**
@@ -771,28 +793,27 @@ debugger_lookup (PeasExtensionSet *set,
  * supports the runner.
  *
  * Returns: (transfer full) (nullable): An #IdeDebugger or %NULL
+ *
+ * Since: 3.32
  */
 IdeDebugger *
 ide_debug_manager_find_debugger (IdeDebugManager *self,
                                  IdeRunner       *runner)
 {
   g_autoptr(PeasExtensionSet) set = NULL;
-  IdeContext *context;
   DebuggerLookup lookup;
 
   g_return_val_if_fail (IDE_IS_DEBUG_MANAGER (self), NULL);
   g_return_val_if_fail (IDE_IS_RUNNER (runner), NULL);
 
-  context = ide_object_get_context (IDE_OBJECT (runner));
-
+  lookup.self = self;
   lookup.debugger = NULL;
   lookup.runner = runner;
   lookup.priority = G_MAXINT;
 
-  set = ide_extension_set_new (peas_engine_get_default (),
-                               IDE_TYPE_DEBUGGER,
-                               "context", context,
-                               NULL);
+  set = peas_extension_set_new (peas_engine_get_default (),
+                                IDE_TYPE_DEBUGGER,
+                                NULL);
 
   peas_extension_set_foreach (set, debugger_lookup, &lookup);
 
@@ -937,6 +958,9 @@ ide_debug_manager_runner_exited (IdeDebugManager *self,
   ide_debug_manager_clear_stopped (self);
 
   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DEBUGGER]);
+
+  ide_clear_and_destroy_object (&debugger);
+  ide_clear_and_destroy_object (&hold_runner);
 }
 
 /**
@@ -948,6 +972,8 @@ ide_debug_manager_runner_exited (IdeDebugManager *self,
  * Attempts to start a runner using a discovered debugger backend.
  *
  * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
  */
 gboolean
 ide_debug_manager_start (IdeDebugManager  *self,
@@ -961,6 +987,7 @@ ide_debug_manager_start (IdeDebugManager  *self,
 
   g_return_val_if_fail (IDE_IS_DEBUG_MANAGER (self), FALSE);
   g_return_val_if_fail (IDE_IS_RUNNER (runner), FALSE);
+  g_return_val_if_fail (self->debugger == NULL, FALSE);
 
   debugger = ide_debug_manager_find_debugger (self, runner);
 
@@ -970,7 +997,7 @@ ide_debug_manager_start (IdeDebugManager  *self,
       g_set_error (error,
                    G_IO_ERROR,
                    G_IO_ERROR_NOT_SUPPORTED,
-                   _("A suitable debugger could not be found."));
+                   _("A suitable debugger was not found."));
       IDE_GOTO (failure);
     }
 
@@ -1013,10 +1040,10 @@ ide_debug_manager_stop (IdeDebugManager *self)
   if (self->runner != NULL)
     {
       ide_runner_force_quit (self->runner);
-      g_clear_object (&self->runner);
+      ide_clear_and_destroy_object (&self->runner);
     }
 
-  g_clear_object (&self->debugger);
+  ide_clear_and_destroy_object (&self->debugger);
   ide_debug_manager_reset_breakpoints (self);
 
   g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DEBUGGER]);
@@ -1037,6 +1064,8 @@ ide_debug_manager_get_active (IdeDebugManager *self)
  * Gets the debugger instance, if it is loaded.
  *
  * Returns: (transfer none) (nullable): An #IdeDebugger or %NULL
+ *
+ * Since: 3.32
  */
 IdeDebugger *
 ide_debug_manager_get_debugger (IdeDebugManager *self)
@@ -1060,6 +1089,8 @@ ide_debug_manager_get_debugger (IdeDebugManager *self)
  * propagate to the debugger when the debugger has been successfully spawned.
  *
  * Returns: (transfer full): An #IdeDebuggerBreakpoints
+ *
+ * Since: 3.32
  */
 IdeDebuggerBreakpoints *
 ide_debug_manager_get_breakpoints_for_file (IdeDebugManager *self,
@@ -1093,7 +1124,7 @@ ide_debug_manager_get_breakpoints_for_file (IdeDebugManager *self,
  * not an active debugger, then it is done by caching the breakpoint
  * until the debugger is next started.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 _ide_debug_manager_add_breakpoint (IdeDebugManager       *self,
@@ -1114,9 +1145,7 @@ _ide_debug_manager_add_breakpoint (IdeDebugManager       *self,
       IDE_EXIT;
     }
 
-  path = ide_debugger_breakpoint_get_file (breakpoint);
-
-  if (path == NULL)
+  if (!(path = ide_debugger_breakpoint_get_file (breakpoint)))
     {
       /* We don't know where this breakpoint is because it's either an
        * address, function, expression, etc. So we just need to queue
@@ -1129,6 +1158,7 @@ _ide_debug_manager_add_breakpoint (IdeDebugManager       *self,
   file = g_file_new_for_path (path);
   breakpoints = ide_debug_manager_get_breakpoints_for_file (self, file);
   _ide_debugger_breakpoints_add (breakpoints, breakpoint);
+  ide_debug_manager_notify_buffer (self, breakpoints);
 
   IDE_EXIT;
 }
@@ -1142,7 +1172,7 @@ _ide_debug_manager_add_breakpoint (IdeDebugManager       *self,
  * is done by notifying the debugger to remove the breakpoint. If there is
  * not an active debugger, then it is done by removing the cached breakpoint.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 _ide_debug_manager_remove_breakpoint (IdeDebugManager       *self,
@@ -1177,3 +1207,27 @@ _ide_debug_manager_remove_breakpoint (IdeDebugManager       *self,
 
   IDE_EXIT;
 }
+
+/**
+ * ide_debug_manager_from_context:
+ * @context: an #IdeContext
+ *
+ * Gets the #IdeDebugManager for a context.
+ *
+ * Returns: (transfer none): an #IdeDebugManager
+ *
+ * Since: 3.32
+ */
+IdeDebugManager *
+ide_debug_manager_from_context (IdeContext *context)
+{
+  IdeDebugManager *ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  /* Returns a borrowed reference, instead of full */
+  ret = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_DEBUG_MANAGER);
+  g_object_unref (ret);
+
+  return ret;
+}
diff --git a/src/libide/debugger/ide-debug-manager.h b/src/libide/debugger/ide-debug-manager.h
index 22467a980..62f11bd7b 100644
--- a/src/libide/debugger/ide-debug-manager.h
+++ b/src/libide/debugger/ide-debug-manager.h
@@ -1,6 +1,6 @@
 /* ide-debug-manager.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,40 +14,45 @@
  *
  * 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 "ide-version-macros.h"
-
-#include "ide-object.h"
+#include <libide-core.h>
+#include <libide-foundry.h>
 
-#include "debugger/ide-debugger-breakpoints.h"
+#include "ide-debugger.h"
+#include "ide-debugger-breakpoints.h"
+#include "ide-debugger-types.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUG_MANAGER (ide_debug_manager_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeDebugManager, ide_debug_manager, IDE, DEBUG_MANAGER, IdeObject)
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
+IdeDebugManager        *ide_debug_manager_from_context             (IdeContext             *context);
+IDE_AVAILABLE_IN_3_32
 IdeDebugger            *ide_debug_manager_get_debugger             (IdeDebugManager        *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                ide_debug_manager_get_active               (IdeDebugManager        *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                ide_debug_manager_start                    (IdeDebugManager        *self,
                                                                     IdeRunner              *runner,
                                                                     GError                **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debug_manager_stop                     (IdeDebugManager        *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerBreakpoints *ide_debug_manager_get_breakpoints_for_file (IdeDebugManager        *self,
                                                                     GFile                  *file);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                ide_debug_manager_supports_language        (IdeDebugManager        *self,
                                                                     const gchar            *language_id);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebugger            *ide_debug_manager_find_debugger            (IdeDebugManager        *self,
                                                                     IdeRunner              *runner);
 
diff --git a/src/libide/debugger/ide-debugger-actions.c b/src/libide/debugger/ide-debugger-actions.c
index b635c32ad..99e7194ea 100644
--- a/src/libide/debugger/ide-debugger-actions.c
+++ b/src/libide/debugger/ide-debugger-actions.c
@@ -1,6 +1,6 @@
 /* ide-debugger-actions.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-debugger-actions"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-private.h"
+#include "ide-debugger-private.h"
 
 typedef struct _IdeDebuggerActionEntry IdeDebuggerActionEntry;
 
diff --git a/src/libide/debugger/ide-debugger-address-map-private.h 
b/src/libide/debugger/ide-debugger-address-map-private.h
new file mode 100644
index 000000000..341729e70
--- /dev/null
+++ b/src/libide/debugger/ide-debugger-address-map-private.h
@@ -0,0 +1,57 @@
+/* ide-debugger-address-map-private.h
+ *
+ * Copyright 2016-2019 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 "ide-debugger-types.h"
+
+G_BEGIN_DECLS
+
+typedef struct _IdeDebuggerAddressMap IdeDebuggerAddressMap;
+
+typedef struct
+{
+  /*
+   * The file on disk that is mapped and the offset within the file.
+   */
+  const gchar *filename;
+  guint64 offset;
+
+  /*
+   * The range within the processes address space. We only support up to 64-bit
+   * address space for local and remote debugging.
+   */
+  IdeDebuggerAddress start;
+  IdeDebuggerAddress end;
+
+} IdeDebuggerAddressMapEntry;
+
+IdeDebuggerAddressMap            *ide_debugger_address_map_new    (void);
+void                              ide_debugger_address_map_insert (IdeDebuggerAddressMap            *self,
+                                                                   const IdeDebuggerAddressMapEntry *entry);
+gboolean                          ide_debugger_address_map_remove (IdeDebuggerAddressMap            *self,
+                                                                   IdeDebuggerAddress                
address);
+const IdeDebuggerAddressMapEntry *ide_debugger_address_map_lookup (const IdeDebuggerAddressMap      *self,
+                                                                   IdeDebuggerAddress                
address);
+void                              ide_debugger_address_map_free   (IdeDebuggerAddressMap            *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeDebuggerAddressMap, ide_debugger_address_map_free)
+
+G_END_DECLS
diff --git a/src/libide/debugger/ide-debugger-address-map.c b/src/libide/debugger/ide-debugger-address-map.c
index 6406f5962..3f9e2604c 100644
--- a/src/libide/debugger/ide-debugger-address-map.c
+++ b/src/libide/debugger/ide-debugger-address-map.c
@@ -1,6 +1,6 @@
 /* ide-debugger-address-map.c
  *
- * Copyright 2016-2017 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-debugger-address-map"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-address-map.h"
+#include "ide-debugger-address-map-private.h"
 
 struct _IdeDebuggerAddressMap
 {
@@ -92,7 +94,7 @@ ide_debugger_address_map_entry_free (gpointer data)
  *
  * Returns: (transfer full): A new #IdeDebuggerAddressMap
  *
- * Since: 3.26
+ * Since: 3.32
  */
 IdeDebuggerAddressMap *
 ide_debugger_address_map_new (void)
@@ -112,7 +114,7 @@ ide_debugger_address_map_new (void)
  *
  * Frees all memory associated with @self.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_address_map_free (IdeDebuggerAddressMap *self)
@@ -137,7 +139,7 @@ ide_debugger_address_map_free (IdeDebuggerAddressMap *self)
  *
  * See also: ide_debugger_address_map_remove()
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_address_map_insert (IdeDebuggerAddressMap            *self,
@@ -170,7 +172,7 @@ ide_debugger_address_map_insert (IdeDebuggerAddressMap            *self,
  *
  * Returns: (nullable): An #IdeDebuggerAddressMapEntry or %NULL
  *
- * Since: 3.26
+ * Since: 3.32
  */
 const IdeDebuggerAddressMapEntry *
 ide_debugger_address_map_lookup (const IdeDebuggerAddressMap *self,
@@ -199,7 +201,7 @@ ide_debugger_address_map_lookup (const IdeDebuggerAddressMap *self,
  *
  * Removes the entry found containing @address.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 gboolean
 ide_debugger_address_map_remove (IdeDebuggerAddressMap *self,
diff --git a/src/libide/debugger/ide-debugger-breakpoint.c b/src/libide/debugger/ide-debugger-breakpoint.c
index bff924e19..d7931f18a 100644
--- a/src/libide/debugger/ide-debugger-breakpoint.c
+++ b/src/libide/debugger/ide-debugger-breakpoint.c
@@ -1,6 +1,6 @@
 /* ide-debugger-breakpoint.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,15 +14,17 @@
  *
  * 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 "ide-debugger-breakpoint"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-breakpoint.h"
-#include "debugger/ide-debugger-private.h"
-#include "debugger/ide-debugger-types.h"
+#include "ide-debugger-breakpoint.h"
+#include "ide-debugger-private.h"
+#include "ide-debugger-types.h"
 
 typedef struct
 {
@@ -231,7 +233,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    *
    * Builder only supports up to 64-bit addresses at this time.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_ADDRESS] =
     g_param_spec_uint64 ("address",
@@ -247,7 +249,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    *
    * This is backend specific, and may not be supported by all backends.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_COUNT] =
     g_param_spec_int64 ("count",
@@ -269,7 +271,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    * This is backend specific, and not all values may be supported by all
    * backends.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_DISPOSITION] =
     g_param_spec_enum ("disposition",
@@ -284,7 +286,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    *
    * This property is %TRUE when the breakpoint is enabled.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_ENABLED] =
     g_param_spec_boolean ("enabled",
@@ -301,7 +303,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    * The value of this is backend specific and may look vastly different
    * based on the language being debugged.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_FUNCTION] =
     g_param_spec_string ("function",
@@ -317,7 +319,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    *
    * This is backend specific.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_ID] =
     g_param_spec_string ("id",
@@ -334,7 +336,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    * If the breakpoint exists at an assembly instruction that cannot be
    * represented by a file, this will be %NULL.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_FILE] =
     g_param_spec_string ("file",
@@ -349,7 +351,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    * The line number within #IdeDebuggerBreakpoint:file where the
    * breakpoint exists.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_LINE] =
     g_param_spec_uint ("line",
@@ -363,7 +365,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    *
    * The mode of the breakpoint, such as a breakpoint, countpoint, or watchpoint.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_MODE] =
     g_param_spec_enum ("mode",
@@ -379,7 +381,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    * The specification for the breakpoint, which may be used by watchpoints
    * to determine of the breakpoint should be applied while executing.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_SPEC] =
     g_param_spec_string ("spec",
@@ -393,7 +395,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    *
    * The thread the breakpoint is currently stopped in, or %NULL.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_THREAD] =
     g_param_spec_string ("thread",
@@ -413,7 +415,7 @@ ide_debugger_breakpoint_class_init (IdeDebuggerBreakpointClass *klass)
    * propagated to the next debugger instance, allowing the user to move
    * between debugger sessions without loosing state.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [RESET] =
     g_signal_new ("reset",
@@ -451,7 +453,7 @@ ide_debugger_breakpoint_new (const gchar *id)
  *
  * Returns: the id of the breakpoint
  *
- * Since: 3.26
+ * Since: 3.32
  */
 const gchar *
 ide_debugger_breakpoint_get_id (IdeDebuggerBreakpoint *self)
@@ -474,7 +476,7 @@ ide_debugger_breakpoint_get_id (IdeDebuggerBreakpoint *self)
  *
  * Returns: The address of the breakpoint, if any.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 IdeDebuggerAddress
 ide_debugger_breakpoint_get_address (IdeDebuggerBreakpoint *self)
@@ -493,7 +495,7 @@ ide_debugger_breakpoint_get_address (IdeDebuggerBreakpoint *self)
  *
  * Sets the address of the breakpoint, if any.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoint_set_address (IdeDebuggerBreakpoint *self,
@@ -520,7 +522,7 @@ ide_debugger_breakpoint_set_address (IdeDebuggerBreakpoint *self,
  *
  * Returns: (nullable): The file containing the breakpoint, or %NULL
  *
- * Since: 3.26
+ * Since: 3.32
  */
 const gchar *
 ide_debugger_breakpoint_get_file (IdeDebuggerBreakpoint *self)
@@ -539,7 +541,7 @@ ide_debugger_breakpoint_get_file (IdeDebuggerBreakpoint *self)
  *
  * Sets the file that contains the breakpoint, if any.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoint_set_file (IdeDebuggerBreakpoint *self,
@@ -567,6 +569,8 @@ ide_debugger_breakpoint_set_file (IdeDebuggerBreakpoint *self,
  * %IDE_DEBUGGER_BREAK_WATCHPOINT.
  *
  * Returns: (nullable): A string containing the spec, or %NULL
+ *
+ * Since: 3.32
  */
 const gchar *
 ide_debugger_breakpoint_get_spec (IdeDebuggerBreakpoint *self)
@@ -587,7 +591,7 @@ ide_debugger_breakpoint_get_spec (IdeDebuggerBreakpoint *self)
  * a statement which the debugger can use to determine of the breakpoint
  * should be applied when stopping the debugger.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoint_set_spec (IdeDebuggerBreakpoint *self,
@@ -614,7 +618,7 @@ ide_debugger_breakpoint_set_spec (IdeDebuggerBreakpoint *self,
  * Returns: An integer greater than or equal to zero representing the
  *   number of times the breakpoint has been reached.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 gint64
 ide_debugger_breakpoint_get_count (IdeDebuggerBreakpoint *self)
@@ -633,7 +637,7 @@ ide_debugger_breakpoint_get_count (IdeDebuggerBreakpoint *self)
  * breakpoint is a countpoint (or if the backend supports counting of
  * regular breakpoints).
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoint_set_count (IdeDebuggerBreakpoint *self,
@@ -660,7 +664,7 @@ ide_debugger_breakpoint_set_count (IdeDebuggerBreakpoint *self,
  *
  * Returns: The mode of the breakpoint
  *
- * Since: 3.26
+ * Since: 3.32
  */
 IdeDebuggerBreakMode
 ide_debugger_breakpoint_get_mode (IdeDebuggerBreakpoint *self)
@@ -684,7 +688,7 @@ ide_debugger_breakpoint_get_mode (IdeDebuggerBreakpoint *self)
  * For example, if it is a countpoint (a breakpoint which increments a
  * counter), you would use %IDE_DEBUGGER_BREAK_COUNTPOINT.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoint_set_mode (IdeDebuggerBreakpoint *self,
@@ -710,7 +714,7 @@ ide_debugger_breakpoint_set_mode (IdeDebuggerBreakpoint *self,
  *
  * Returns: An #IdeDebugerDisposition
  *
- * Since: 3.26
+ * Since: 3.32
  */
 IdeDebuggerDisposition
 ide_debugger_breakpoint_get_disposition (IdeDebuggerBreakpoint *self)
@@ -732,7 +736,7 @@ ide_debugger_breakpoint_get_disposition (IdeDebuggerBreakpoint *self)
  * The disposition property is used to to track what should happen to a
  * breakpoint when movements are made in the debugger.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoint_set_disposition (IdeDebuggerBreakpoint  *self,
@@ -757,6 +761,8 @@ ide_debugger_breakpoint_set_disposition (IdeDebuggerBreakpoint  *self,
  * Checks if the breakpoint is enabled.
  *
  * Returns: %TRUE if the breakpoint is enabled
+ *
+ * Since: 3.32
  */
 gboolean
 ide_debugger_breakpoint_get_enabled (IdeDebuggerBreakpoint *self)
@@ -778,7 +784,7 @@ ide_debugger_breakpoint_get_enabled (IdeDebuggerBreakpoint *self)
  * You must call ide_debugger_breakpoint_modify_breakpoint_async() to actually
  * modify the breakpoint in the backend.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoint_set_enabled (IdeDebuggerBreakpoint *self,
@@ -805,7 +811,7 @@ ide_debugger_breakpoint_set_enabled (IdeDebuggerBreakpoint *self,
  *
  * This is a user-readable value representing the name of the function.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 const gchar *
 ide_debugger_breakpoint_get_function (IdeDebuggerBreakpoint *self)
@@ -825,7 +831,7 @@ ide_debugger_breakpoint_get_function (IdeDebuggerBreakpoint *self)
  * Sets the "function" property, which is a user-readable value representing
  * the name of the function.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoint_set_function (IdeDebuggerBreakpoint *self,
@@ -854,7 +860,7 @@ ide_debugger_breakpoint_set_function (IdeDebuggerBreakpoint *self,
  *
  * Returns: An integer greater than 0 if set, otherwise 0.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 guint
 ide_debugger_breakpoint_get_line (IdeDebuggerBreakpoint *self)
@@ -872,7 +878,7 @@ ide_debugger_breakpoint_get_line (IdeDebuggerBreakpoint *self)
  *
  * Sets the line for the breakpoint. A value of 0 means the line is unset.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoint_set_line (IdeDebuggerBreakpoint *self,
@@ -898,7 +904,7 @@ ide_debugger_breakpoint_set_line (IdeDebuggerBreakpoint *self,
  *
  * Returns: (nullable): the thread identifier or %NULL
  *
- * Since: 3.26
+ * Since: 3.32
  */
 const gchar *
 ide_debugger_breakpoint_get_thread (IdeDebuggerBreakpoint *self)
@@ -917,7 +923,7 @@ ide_debugger_breakpoint_get_thread (IdeDebuggerBreakpoint *self)
  *
  * This should generally only be used by debugger implementations.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoint_set_thread (IdeDebuggerBreakpoint *self,
diff --git a/src/libide/debugger/ide-debugger-breakpoint.h b/src/libide/debugger/ide-debugger-breakpoint.h
index 88ddb8292..6eea3164c 100644
--- a/src/libide/debugger/ide-debugger-breakpoint.h
+++ b/src/libide/debugger/ide-debugger-breakpoint.h
@@ -1,6 +1,6 @@
 /* ide-debugger-breakpoint.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,22 +14,22 @@
  *
  * 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>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
-#include "debugger/ide-debugger-frame.h"
-#include "debugger/ide-debugger-types.h"
+#include "ide-debugger-frame.h"
+#include "ide-debugger-types.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUGGER_BREAKPOINT (ide_debugger_breakpoint_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeDebuggerBreakpoint, ide_debugger_breakpoint, IDE, DEBUGGER_BREAKPOINT, GObject)
 
 struct _IdeDebuggerBreakpointClass
@@ -49,61 +49,61 @@ struct _IdeDebuggerBreakpointClass
   gpointer _reserved8;
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gint                    ide_debugger_breakpoint_compare          (IdeDebuggerBreakpoint  *a,
                                                                   IdeDebuggerBreakpoint  *b);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerBreakpoint  *ide_debugger_breakpoint_new              (const gchar            *id);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_debugger_breakpoint_get_id           (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                ide_debugger_breakpoint_get_enabled      (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_breakpoint_set_enabled      (IdeDebuggerBreakpoint  *self,
                                                                   gboolean                enabled);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerBreakMode    ide_debugger_breakpoint_get_mode         (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_breakpoint_set_mode         (IdeDebuggerBreakpoint  *self,
                                                                   IdeDebuggerBreakMode    mode);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerDisposition  ide_debugger_breakpoint_get_disposition  (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_breakpoint_set_disposition  (IdeDebuggerBreakpoint  *self,
                                                                   IdeDebuggerDisposition  disposition);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerAddress      ide_debugger_breakpoint_get_address      (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_breakpoint_set_address      (IdeDebuggerBreakpoint  *self,
                                                                   IdeDebuggerAddress      address);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_debugger_breakpoint_get_spec         (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_breakpoint_set_spec         (IdeDebuggerBreakpoint  *self,
                                                                   const gchar            *spec);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_debugger_breakpoint_get_function     (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_breakpoint_set_function     (IdeDebuggerBreakpoint *self,
                                                                   const gchar           *function);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_debugger_breakpoint_get_file         (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_breakpoint_set_file         (IdeDebuggerBreakpoint  *self,
                                                                   const gchar            *file);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 guint                   ide_debugger_breakpoint_get_line         (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_breakpoint_set_line         (IdeDebuggerBreakpoint  *self,
                                                                   guint                   line);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gint64                  ide_debugger_breakpoint_get_count        (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_breakpoint_set_count        (IdeDebuggerBreakpoint  *self,
                                                                   gint64                  count);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_debugger_breakpoint_get_thread       (IdeDebuggerBreakpoint  *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_breakpoint_set_thread       (IdeDebuggerBreakpoint  *self,
                                                                   const gchar            *thread);
 
diff --git a/src/libide/debugger/ide-debugger-breakpoints.c b/src/libide/debugger/ide-debugger-breakpoints.c
index 185a708d7..966eb2f60 100644
--- a/src/libide/debugger/ide-debugger-breakpoints.c
+++ b/src/libide/debugger/ide-debugger-breakpoints.c
@@ -1,6 +1,6 @@
 /* ide-debugger-breakpoints.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-debugger-breakpoints"
@@ -24,8 +26,8 @@
 
 #include "ide-debug.h"
 
-#include "debugger/ide-debugger-breakpoints.h"
-#include "debugger/ide-debugger-private.h"
+#include "ide-debugger-breakpoints.h"
+#include "ide-debugger-private.h"
 
 /**
  * SECTION:ide-debugger-breakpoints
@@ -44,6 +46,8 @@
  * breakpoints as necessary by the current debugger. If no debugger is
  * active, the breakpoints are queued until the debugger has started, and
  * then synchronized to the debugger process.
+ *
+ * Since: 3.32
  */
 
 typedef struct
@@ -197,7 +201,7 @@ ide_debugger_breakpoints_init (IdeDebuggerBreakpoints *self)
  *
  * Returns: (nullable) (transfer none): An #IdeDebuggerBreakpoint or %NULL
  *
- * Since: 3.26
+ * Since: 3.32
  */
 IdeDebuggerBreakpoint *
 ide_debugger_breakpoints_get_line (IdeDebuggerBreakpoints *self,
@@ -368,6 +372,8 @@ _ide_debugger_breakpoints_remove (IdeDebuggerBreakpoints *self,
  * this container belong to.
  *
  * Returns: (transfer none): a #GFile
+ *
+ * Since: 3.32
  */
 GFile *
 ide_debugger_breakpoints_get_file (IdeDebuggerBreakpoints *self)
@@ -385,7 +391,7 @@ ide_debugger_breakpoints_get_file (IdeDebuggerBreakpoints *self)
  *
  * Call @func for every #IdeDebuggerBreakpoint in @self.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_breakpoints_foreach (IdeDebuggerBreakpoints *self,
diff --git a/src/libide/debugger/ide-debugger-breakpoints.h b/src/libide/debugger/ide-debugger-breakpoints.h
index 8ac24885d..90c38ef93 100644
--- a/src/libide/debugger/ide-debugger-breakpoints.h
+++ b/src/libide/debugger/ide-debugger-breakpoints.h
@@ -1,6 +1,6 @@
 /* ide-debugger-breakpoints.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,33 +14,33 @@
  *
  * 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>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
-#include "debugger/ide-debugger-breakpoint.h"
-#include "debugger/ide-debugger-types.h"
+#include "ide-debugger-breakpoint.h"
+#include "ide-debugger-types.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUGGER_BREAKPOINTS (ide_debugger_breakpoints_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeDebuggerBreakpoints, ide_debugger_breakpoints, IDE, DEBUGGER_BREAKPOINTS, GObject)
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GFile                 *ide_debugger_breakpoints_get_file      (IdeDebuggerBreakpoints *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerBreakMode   ide_debugger_breakpoints_get_line_mode (IdeDebuggerBreakpoints *self,
                                                                guint                   line);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerBreakpoint *ide_debugger_breakpoints_get_line      (IdeDebuggerBreakpoints *self,
                                                                guint                   line);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                   ide_debugger_breakpoints_foreach       (IdeDebuggerBreakpoints *self,
                                                                GFunc                   func,
                                                                gpointer                user_data);
diff --git a/src/libide/debugger/ide-debugger-fallbacks.c b/src/libide/debugger/ide-debugger-fallbacks.c
index 3d5444503..a12cc1d6a 100644
--- a/src/libide/debugger/ide-debugger-fallbacks.c
+++ b/src/libide/debugger/ide-debugger-fallbacks.c
@@ -1,6 +1,6 @@
 /* ide-debugger-fallbacks.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,14 +14,16 @@
  *
  * 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 "ide-debugger-fallbacks"
 
 #include "config.h"
 
-#include "debugger/ide-debugger.h"
-#include "debugger/ide-debugger-private.h"
+#include "ide-debugger.h"
+#include "ide-debugger-private.h"
 
 void
 _ide_debugger_real_list_frames_async (IdeDebugger         *self,
diff --git a/src/libide/debugger/ide-debugger-frame.c b/src/libide/debugger/ide-debugger-frame.c
index 196c1290c..82b6b90c9 100644
--- a/src/libide/debugger/ide-debugger-frame.c
+++ b/src/libide/debugger/ide-debugger-frame.c
@@ -1,6 +1,6 @@
 /* ide-debugger-frame.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-debugger-frame"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-frame.h"
+#include "ide-debugger-frame.h"
 
 typedef struct
 {
diff --git a/src/libide/debugger/ide-debugger-frame.h b/src/libide/debugger/ide-debugger-frame.h
index 728256af3..67cca9f86 100644
--- a/src/libide/debugger/ide-debugger-frame.h
+++ b/src/libide/debugger/ide-debugger-frame.h
@@ -1,6 +1,6 @@
 /* ide-debugger-frame.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,19 +14,21 @@
  *
  * 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 "ide-version-macros.h"
+#include <libide-core.h>
 
-#include "debugger/ide-debugger-types.h"
+#include "ide-debugger-types.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUGGER_FRAME (ide_debugger_frame_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeDebuggerFrame, ide_debugger_frame, IDE, DEBUGGER_FRAME, GObject)
 
 struct _IdeDebuggerFrameClass
@@ -40,41 +42,41 @@ struct _IdeDebuggerFrameClass
   gpointer _reserved4;
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerFrame    *ide_debugger_frame_new          (void);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerAddress   ide_debugger_frame_get_address  (IdeDebuggerFrame    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_frame_set_address  (IdeDebuggerFrame    *self,
                                                       IdeDebuggerAddress   address);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_debugger_frame_get_file     (IdeDebuggerFrame    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_frame_set_file     (IdeDebuggerFrame    *self,
                                                       const gchar         *file);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_debugger_frame_get_function (IdeDebuggerFrame    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_frame_set_function (IdeDebuggerFrame    *self,
                                                       const gchar         *function);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar * const *ide_debugger_frame_get_args     (IdeDebuggerFrame    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_frame_set_args     (IdeDebuggerFrame    *self,
                                                       const gchar * const *args);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_debugger_frame_get_library  (IdeDebuggerFrame    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_frame_set_library  (IdeDebuggerFrame    *self,
                                                       const gchar         *library);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 guint                ide_debugger_frame_get_depth    (IdeDebuggerFrame    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_frame_set_depth    (IdeDebuggerFrame    *self,
                                                       guint                depth);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 guint                ide_debugger_frame_get_line     (IdeDebuggerFrame    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_frame_set_line     (IdeDebuggerFrame    *self,
                                                       guint                line);
 
diff --git a/src/libide/debugger/ide-debugger-instruction.c b/src/libide/debugger/ide-debugger-instruction.c
index 4455a75c3..bb8283351 100644
--- a/src/libide/debugger/ide-debugger-instruction.c
+++ b/src/libide/debugger/ide-debugger-instruction.c
@@ -1,6 +1,6 @@
 /* ide-debugger-instruction.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-debugger-instruction"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-instruction.h"
+#include "ide-debugger-instruction.h"
 
 typedef struct
 {
diff --git a/src/libide/debugger/ide-debugger-instruction.h b/src/libide/debugger/ide-debugger-instruction.h
index fa8fd8005..50b2ecd17 100644
--- a/src/libide/debugger/ide-debugger-instruction.h
+++ b/src/libide/debugger/ide-debugger-instruction.h
@@ -1,6 +1,6 @@
 /* ide-debugger-instruction.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,19 +14,21 @@
  *
  * 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 "ide-version-macros.h"
+#include <libide-core.h>
 
-#include "debugger/ide-debugger-types.h"
+#include "ide-debugger-types.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUGGER_INSTRUCTION (ide_debugger_instruction_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeDebuggerInstruction, ide_debugger_instruction, IDE, DEBUGGER_INSTRUCTION, 
GObject)
 
 struct _IdeDebuggerInstructionClass
@@ -40,18 +42,18 @@ struct _IdeDebuggerInstructionClass
   gpointer _reserved4;
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerInstruction *ide_debugger_instruction_new          (IdeDebuggerAddress      address);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerAddress      ide_debugger_instruction_get_address  (IdeDebuggerInstruction *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_debugger_instruction_get_function (IdeDebuggerInstruction *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_instruction_set_function (IdeDebuggerInstruction *self,
                                                                const gchar            *function);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_debugger_instruction_get_display  (IdeDebuggerInstruction *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_instruction_set_display  (IdeDebuggerInstruction *self,
                                                                const gchar            *display);
 
diff --git a/src/libide/debugger/ide-debugger-library.c b/src/libide/debugger/ide-debugger-library.c
index 50479ffaf..63f6427cf 100644
--- a/src/libide/debugger/ide-debugger-library.c
+++ b/src/libide/debugger/ide-debugger-library.c
@@ -1,6 +1,6 @@
 /* ide-debugger-library.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-debugger-library"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-library.h"
+#include "ide-debugger-library.h"
 
 typedef struct
 {
@@ -230,6 +232,8 @@ ide_debugger_library_set_target_name (IdeDebuggerLibrary *self,
  *
  * Returns: (transfer none) (element-type Ide.DebuggerAddressRange): a #GPtrArray
  *   containing the list of address ranges.
+ *
+ * Since: 3.32
  */
 GPtrArray *
 ide_debugger_library_get_ranges (IdeDebuggerLibrary *self)
@@ -248,6 +252,8 @@ ide_debugger_library_get_ranges (IdeDebuggerLibrary *self)
  *
  * Adds @range to the list of ranges for which the library is mapped in
  * the inferior's address space.
+ *
+ * Since: 3.32
  */
 void
 ide_debugger_library_add_range (IdeDebuggerLibrary            *self,
diff --git a/src/libide/debugger/ide-debugger-library.h b/src/libide/debugger/ide-debugger-library.h
index dae19fb8c..0f558faaf 100644
--- a/src/libide/debugger/ide-debugger-library.h
+++ b/src/libide/debugger/ide-debugger-library.h
@@ -1,6 +1,6 @@
 /* ide-debugger-library.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,21 +14,21 @@
  *
  * 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>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
-#include "debugger/ide-debugger-types.h"
+#include "ide-debugger-types.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUGGER_LIBRARY (ide_debugger_library_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeDebuggerLibrary, ide_debugger_library, IDE, DEBUGGER_LIBRARY, GObject)
 
 struct _IdeDebuggerLibraryClass
@@ -46,26 +46,26 @@ struct _IdeDebuggerLibraryClass
   gpointer _reserved8;
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gint                ide_debugger_library_compare         (IdeDebuggerLibrary            *a,
                                                           IdeDebuggerLibrary            *b);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerLibrary *ide_debugger_library_new             (const gchar                   *id);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar        *ide_debugger_library_get_id          (IdeDebuggerLibrary            *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GPtrArray          *ide_debugger_library_get_ranges      (IdeDebuggerLibrary            *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                ide_debugger_library_add_range       (IdeDebuggerLibrary            *self,
                                                           const IdeDebuggerAddressRange *range);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar        *ide_debugger_library_get_host_name   (IdeDebuggerLibrary            *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                ide_debugger_library_set_host_name   (IdeDebuggerLibrary            *self,
                                                           const gchar                   *host_name);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar        *ide_debugger_library_get_target_name (IdeDebuggerLibrary            *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                ide_debugger_library_set_target_name (IdeDebuggerLibrary            *self,
                                                           const gchar                   *target_name);
 
diff --git a/src/libide/debugger/ide-debugger-private.h b/src/libide/debugger/ide-debugger-private.h
index b5d7c1f8f..df1c1ee08 100644
--- a/src/libide/debugger/ide-debugger-private.h
+++ b/src/libide/debugger/ide-debugger-private.h
@@ -1,6 +1,6 @@
 /* ide-debugger-private.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "debugger/ide-debug-manager.h"
-#include "debugger/ide-debugger.h"
-#include "debugger/ide-debugger-breakpoints.h"
+#include "ide-debug-manager.h"
+#include "ide-debugger.h"
+#include "ide-debugger-breakpoints.h"
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/debugger/ide-debugger-register.c b/src/libide/debugger/ide-debugger-register.c
index a8c6d2aae..1b03639ec 100644
--- a/src/libide/debugger/ide-debugger-register.c
+++ b/src/libide/debugger/ide-debugger-register.c
@@ -1,6 +1,6 @@
 /* ide-debugger-register.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-debugger-register"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-register.h"
+#include "ide-debugger-register.h"
 
 typedef struct
 {
diff --git a/src/libide/debugger/ide-debugger-register.h b/src/libide/debugger/ide-debugger-register.h
index 3da7462ed..1b74c0189 100644
--- a/src/libide/debugger/ide-debugger-register.h
+++ b/src/libide/debugger/ide-debugger-register.h
@@ -1,6 +1,6 @@
 /* ide-debugger-register.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,19 +14,19 @@
  *
  * 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>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUGGER_REGISTER (ide_debugger_register_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeDebuggerRegister, ide_debugger_register, IDE, DEBUGGER_REGISTER, GObject)
 
 struct _IdeDebuggerRegisterClass
@@ -34,31 +34,24 @@ struct _IdeDebuggerRegisterClass
   GObjectClass parent_class;
 
   /*< private >*/
-  gpointer _reserved1;
-  gpointer _reserved2;
-  gpointer _reserved3;
-  gpointer _reserved4;
-  gpointer _reserved5;
-  gpointer _reserved6;
-  gpointer _reserved7;
-  gpointer _reserved8;
+  gpointer _reserved[8];
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gint                 ide_debugger_register_compare   (IdeDebuggerRegister *a,
                                                       IdeDebuggerRegister *b);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerRegister *ide_debugger_register_new       (const gchar *id);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_debugger_register_get_id    (IdeDebuggerRegister *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_debugger_register_get_name  (IdeDebuggerRegister *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_register_set_name  (IdeDebuggerRegister *self,
                                                       const gchar         *name);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_debugger_register_get_value (IdeDebuggerRegister *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_register_set_value (IdeDebuggerRegister *self,
                                                       const gchar         *value);
 
diff --git a/src/libide/debugger/ide-debugger-thread-group.c b/src/libide/debugger/ide-debugger-thread-group.c
index c599439cd..d68acf09e 100644
--- a/src/libide/debugger/ide-debugger-thread-group.c
+++ b/src/libide/debugger/ide-debugger-thread-group.c
@@ -1,6 +1,6 @@
 /* ide-debugger-thread-group.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-debugger-thread-group"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-thread-group.h"
+#include "ide-debugger-thread-group.h"
 
 typedef struct
 {
diff --git a/src/libide/debugger/ide-debugger-thread-group.h b/src/libide/debugger/ide-debugger-thread-group.h
index a0ff884a4..73bee38ce 100644
--- a/src/libide/debugger/ide-debugger-thread-group.h
+++ b/src/libide/debugger/ide-debugger-thread-group.h
@@ -1,6 +1,6 @@
 /* ide-debugger-thread-group.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,19 +14,19 @@
  *
  * 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>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUGGER_THREAD_GROUP (ide_debugger_thread_group_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeDebuggerThreadGroup, ide_debugger_thread_group, IDE, DEBUGGER_THREAD_GROUP, 
GObject)
 
 struct _IdeDebuggerThreadGroupClass
@@ -34,27 +34,24 @@ struct _IdeDebuggerThreadGroupClass
   GObjectClass parent_class;
 
   /*< private >*/
-  gpointer _reserved1;
-  gpointer _reserved2;
-  gpointer _reserved3;
-  gpointer _reserved4;
+  gpointer _reserved[4];
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gint                    ide_debugger_thread_group_compare       (IdeDebuggerThreadGroup *a,
                                                                  IdeDebuggerThreadGroup *b);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerThreadGroup *ide_debugger_thread_group_new           (const gchar *id);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_debugger_thread_group_get_id        (IdeDebuggerThreadGroup *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_debugger_thread_group_get_pid       (IdeDebuggerThreadGroup *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_thread_group_set_pid       (IdeDebuggerThreadGroup *self,
                                                                  const gchar            *pid);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_debugger_thread_group_get_exit_code (IdeDebuggerThreadGroup *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_debugger_thread_group_set_exit_code (IdeDebuggerThreadGroup *self,
                                                                  const gchar            *exit_code);
 
diff --git a/src/libide/debugger/ide-debugger-thread.c b/src/libide/debugger/ide-debugger-thread.c
index 49bc840fc..1e3c1a725 100644
--- a/src/libide/debugger/ide-debugger-thread.c
+++ b/src/libide/debugger/ide-debugger-thread.c
@@ -1,6 +1,6 @@
 /* ide-debugger-thread.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-debugger-thread"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-thread.h"
+#include "ide-debugger-thread.h"
 
 typedef struct
 {
diff --git a/src/libide/debugger/ide-debugger-thread.h b/src/libide/debugger/ide-debugger-thread.h
index e7af9f334..dca800c65 100644
--- a/src/libide/debugger/ide-debugger-thread.h
+++ b/src/libide/debugger/ide-debugger-thread.h
@@ -1,6 +1,6 @@
 /* ide-debugger-thread.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,19 +14,19 @@
  *
  * 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>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUGGER_THREAD (ide_debugger_thread_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeDebuggerThread, ide_debugger_thread, IDE, DEBUGGER_THREAD, GObject)
 
 struct _IdeDebuggerThreadClass
@@ -40,16 +40,16 @@ struct _IdeDebuggerThreadClass
   gpointer _reserved4;
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gint               ide_debugger_thread_compare   (IdeDebuggerThread *a,
                                                   IdeDebuggerThread *b);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerThread *ide_debugger_thread_new       (const gchar       *id);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar       *ide_debugger_thread_get_id    (IdeDebuggerThread *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar       *ide_debugger_thread_get_group (IdeDebuggerThread *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_thread_set_group (IdeDebuggerThread *self,
                                                   const gchar       *thread_group);
 
diff --git a/src/libide/debugger/ide-debugger-types.c b/src/libide/debugger/ide-debugger-types.c
index 6c4c3d70c..138b08582 100644
--- a/src/libide/debugger/ide-debugger-types.c
+++ b/src/libide/debugger/ide-debugger-types.c
@@ -1,6 +1,6 @@
 /* ide-debugger-types.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-debugger-types"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-types.h"
+#include "ide-debugger-types.h"
 
 GType
 ide_debugger_stream_get_type (void)
diff --git a/src/libide/debugger/ide-debugger-types.h b/src/libide/debugger/ide-debugger-types.h
index 9ca685575..705968d96 100644
--- a/src/libide/debugger/ide-debugger-types.h
+++ b/src/libide/debugger/ide-debugger-types.h
@@ -1,6 +1,6 @@
 /* ide-debugger-types.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,13 @@
  *
  * 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>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
@@ -33,7 +33,7 @@ G_BEGIN_DECLS
  *
  * The type of stream for the log message.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 typedef enum
 {
@@ -57,7 +57,7 @@ typedef enum
  *
  * Describes the style of movement that should be performed by the debugger.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 typedef enum
 {
@@ -80,6 +80,8 @@ typedef enum
  *    received a death signal.
  *
  * Represents the reason a process has stopped executing in the debugger.
+ *
+ * Since: 3.32
  */
 typedef enum
 {
@@ -117,6 +119,8 @@ typedef enum
  *   specification matching.
  *
  * The type of breakpoint.
+ *
+ * Since: 3.32
  */
 typedef enum
 {
@@ -135,6 +139,8 @@ typedef enum
  * @IDE_DEBUGGER_BREAKPOINT_CHANGE_ENABLED: change the enabled state
  *
  * Describes the type of modification to perform on a breakpoint.
+ *
+ * Since: 3.32
  */
 typedef enum
 {
@@ -158,6 +164,8 @@ typedef enum
  *
  * The disposition determines what should happen to the breakpoint at the next
  * stop of the debugger.
+ *
+ * Since: 3.32
  */
 typedef enum
 {
@@ -175,7 +183,7 @@ typedef guint64 IdeDebuggerAddress;
 
 #define IDE_DEBUGGER_ADDRESS_INVALID (0)
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerAddress ide_debugger_address_parse (const gchar *string);
 
 typedef struct
@@ -187,25 +195,25 @@ typedef struct
 #define IDE_TYPE_DEBUGGER_ADDRESS_RANGE (ide_debugger_address_range_get_type())
 
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GType ide_debugger_stream_get_type            (void);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GType ide_debugger_movement_get_type          (void);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GType ide_debugger_stop_reason_get_type       (void);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GType ide_debugger_break_mode_get_type        (void);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GType ide_debugger_disposition_get_type       (void);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GType ide_debugger_address_range_get_type     (void);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GType ide_debugger_breakpoint_change_get_type (void);
 
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerAddressRange *ide_debugger_address_range_copy (const IdeDebuggerAddressRange *range);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                     ide_debugger_address_range_free (IdeDebuggerAddressRange       *range);
 
 
diff --git a/src/libide/debugger/ide-debugger-variable.c b/src/libide/debugger/ide-debugger-variable.c
index 717d2f9ae..54c09d2ec 100644
--- a/src/libide/debugger/ide-debugger-variable.c
+++ b/src/libide/debugger/ide-debugger-variable.c
@@ -1,6 +1,6 @@
 /* ide-debugger-variable.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-debugger-variable"
 
 #include "config.h"
 
-#include "debugger/ide-debugger-variable.h"
+#include "ide-debugger-variable.h"
 
 typedef struct
 {
diff --git a/src/libide/debugger/ide-debugger-variable.h b/src/libide/debugger/ide-debugger-variable.h
index 60d1e91c0..8344dc4e3 100644
--- a/src/libide/debugger/ide-debugger-variable.h
+++ b/src/libide/debugger/ide-debugger-variable.h
@@ -1,6 +1,6 @@
 /* ide-debugger-variable.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,19 +14,19 @@
  *
  * 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>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUGGER_VARIABLE (ide_debugger_variable_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeDebuggerVariable, ide_debugger_variable, IDE, DEBUGGER_VARIABLE, GObject)
 
 struct _IdeDebuggerVariableClass
@@ -44,23 +44,23 @@ struct _IdeDebuggerVariableClass
   gpointer _reserved8;
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerVariable *ide_debugger_variable_new              (const gchar         *name);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_debugger_variable_get_name         (IdeDebuggerVariable *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_debugger_variable_get_type_name    (IdeDebuggerVariable *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_variable_set_type_name    (IdeDebuggerVariable *self,
                                                              const gchar         *type_name);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_debugger_variable_get_value        (IdeDebuggerVariable *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_variable_set_value        (IdeDebuggerVariable *self,
                                                              const gchar         *value);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean             ide_debugger_variable_get_has_children (IdeDebuggerVariable *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_debugger_variable_set_has_children (IdeDebuggerVariable *self,
                                                              gboolean             has_children);
 
diff --git a/src/libide/debugger/ide-debugger.c b/src/libide/debugger/ide-debugger.c
index 3ad03bbd5..712e354b0 100644
--- a/src/libide/debugger/ide-debugger.c
+++ b/src/libide/debugger/ide-debugger.c
@@ -1,6 +1,6 @@
 /* ide-debugger.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,15 +14,17 @@
  *
  * 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 "ide-debugger"
 
 #include "config.h"
 
-#include "debugger/ide-debugger.h"
-#include "debugger/ide-debugger-address-map.h"
-#include "debugger/ide-debugger-private.h"
+#include "ide-debugger.h"
+#include "ide-debugger-address-map-private.h"
+#include "ide-debugger-private.h"
 
 /**
  * SECTION:ide-debugger
@@ -37,7 +39,7 @@
  * For example, when the inferior creates a new thread, the debugger
  * implementation should call ide_debugger_emit_thread_added().
  *
- * Since: 3.26
+ * Since: 3.32
  */
 
 typedef struct
@@ -496,7 +498,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    * to display the name of the debugger. You might set this to "GNU Debugger"
    * or "Python Debugger", etc.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_DISPLAY_NAME] =
     g_param_spec_string ("display-name",
@@ -510,7 +512,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    *
    * The currently selected thread.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   properties [PROP_SELECTED_THREAD] =
     g_param_spec_object ("selected-thread",
@@ -530,7 +532,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    * The "log" signal is emitted when there is new content to be
    * appended to one of the streams.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [LOG] =
     g_signal_new ("log",
@@ -550,7 +552,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    *
    * This signal is emitted when a thread-group has been added.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [THREAD_GROUP_ADDED] =
     g_signal_new ("thread-group-added",
@@ -569,7 +571,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    *
    * This signal is emitted when a thread-group has been removed.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [THREAD_GROUP_REMOVED] =
     g_signal_new ("thread-group-removed",
@@ -588,7 +590,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    *
    * This signal is emitted when a thread-group has been started.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [THREAD_GROUP_STARTED] =
     g_signal_new ("thread-group-started",
@@ -607,7 +609,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    *
    * This signal is emitted when a thread-group has exited.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [THREAD_GROUP_EXITED] =
     g_signal_new ("thread-group-exited",
@@ -624,7 +626,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    *
    * The signal is emitted when a thread is added to the inferior.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [THREAD_ADDED] =
     g_signal_new ("thread-added",
@@ -641,7 +643,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    *
    * The signal is emitted when a thread is removed from the inferior.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [THREAD_REMOVED] =
     g_signal_new ("thread-removed",
@@ -658,7 +660,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    *
    * The signal is emitted when a thread is selected in the debugger.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [THREAD_SELECTED] =
     g_signal_new ("thread-selected",
@@ -676,7 +678,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    * The "breakpoint-added" signal is emitted when a breakpoint has been
    * added to the debugger.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [BREAKPOINT_ADDED] =
     g_signal_new ("breakpoint-added",
@@ -694,7 +696,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    * The "breakpoint-removed" signal is emitted when a breakpoint has been
    * removed from the debugger.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [BREAKPOINT_REMOVED] =
     g_signal_new ("breakpoint-removed",
@@ -712,7 +714,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    * The "breakpoint-modified" signal is emitted when a breakpoint has been
    * modified by the debugger.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [BREAKPOINT_MODIFIED] =
     g_signal_new ("breakpoint-modified",
@@ -729,7 +731,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    * This signal is emitted when the debugger starts or resumes executing
    * the inferior.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [RUNNING] =
     g_signal_new ("running",
@@ -752,7 +754,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    * representable by source in the project (such as memory address based
    * breakpoints).
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [STOPPED] =
     g_signal_new ("stopped",
@@ -772,7 +774,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    *
    * This signal is emitted when a library has been loaded by the debugger.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [LIBRARY_LOADED] =
     g_signal_new ("library-loaded",
@@ -791,7 +793,7 @@ ide_debugger_class_init (IdeDebuggerClass *klass)
    * Generally, this means that the library was a module and loaded in such a
    * way that allowed unloading.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [LIBRARY_UNLOADED] =
     g_signal_new ("library-unloaded",
@@ -822,7 +824,7 @@ ide_debugger_init (IdeDebugger *self)
  *
  * Returns: The display name for the debugger
  *
- * Since: 3.26
+ * Since: 3.32
  */
 const gchar *
 ide_debugger_get_display_name (IdeDebugger *self)
@@ -839,6 +841,8 @@ ide_debugger_get_display_name (IdeDebugger *self)
  * @self: a #IdeDebugger
  *
  * Sets the #IdeDebugger:display-name property.
+ *
+ * Since: 3.32
  */
 void
 ide_debugger_set_display_name (IdeDebugger *self,
@@ -865,7 +869,7 @@ ide_debugger_set_display_name (IdeDebugger *self,
  *
  * Returns: %TRUE if @movement can be performed.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 gboolean
 ide_debugger_get_can_move (IdeDebugger         *self,
@@ -891,7 +895,7 @@ ide_debugger_get_can_move (IdeDebugger         *self,
  * Advances the debugger to the next breakpoint or until the debugger stops.
  * @movement should describe the type of movement to perform.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_move_async (IdeDebugger         *self,
@@ -920,7 +924,7 @@ ide_debugger_move_async (IdeDebugger         *self,
  *
  * Returns: %TRUE if successful, otherwise %FALSE
  *
- * Since: 3.26
+ * Since: 3.32
  */
 gboolean
 ide_debugger_move_finish (IdeDebugger   *self,
@@ -944,7 +948,7 @@ ide_debugger_move_finish (IdeDebugger   *self,
  *
  * Use the #IdeDebuggerStream to denote the particular stream.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_log (IdeDebugger       *self,
@@ -966,7 +970,7 @@ ide_debugger_emit_log (IdeDebugger       *self,
  * Debugger implementations should call this to notify that a thread group has
  * been added to the inferior.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_thread_group_added (IdeDebugger            *self,
@@ -986,7 +990,7 @@ ide_debugger_emit_thread_group_added (IdeDebugger            *self,
  * Debugger implementations should call this to notify that a thread group has
  * been removed from the inferior.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_thread_group_removed (IdeDebugger            *self,
@@ -1006,7 +1010,7 @@ ide_debugger_emit_thread_group_removed (IdeDebugger            *self,
  * Debugger implementations should call this to notify that a thread group has
  * started executing.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_thread_group_started (IdeDebugger            *self,
@@ -1026,7 +1030,7 @@ ide_debugger_emit_thread_group_started (IdeDebugger            *self,
  * Debugger implementations should call this to notify that a thread group has
  * exited.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_thread_group_exited (IdeDebugger            *self,
@@ -1046,7 +1050,7 @@ ide_debugger_emit_thread_group_exited (IdeDebugger            *self,
  * Emits the #IdeDebugger::thread-added signal notifying that a new thread
  * has been added to the inferior.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_thread_added (IdeDebugger       *self,
@@ -1066,7 +1070,7 @@ ide_debugger_emit_thread_added (IdeDebugger       *self,
  * Emits the #IdeDebugger::thread-removed signal notifying that a thread has
  * been removed to the inferior.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_thread_removed (IdeDebugger       *self,
@@ -1086,7 +1090,7 @@ ide_debugger_emit_thread_removed (IdeDebugger       *self,
  * Emits the #IdeDebugger::thread-selected signal notifying that a thread
  * has been set as the current debugging thread.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_thread_selected (IdeDebugger       *self,
@@ -1111,7 +1115,7 @@ ide_debugger_emit_thread_selected (IdeDebugger       *self,
  * If a breakpoint has changed, you should use
  * ide_debugger_emit_breakpoint_modified() to notify of the modification.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_breakpoint_added (IdeDebugger           *self,
@@ -1136,7 +1140,7 @@ ide_debugger_emit_breakpoint_added (IdeDebugger           *self,
  * If a breakpoint has changed, you should use
  * ide_debugger_emit_breakpoint_modified() to notify of the modification.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_breakpoint_removed (IdeDebugger           *self,
@@ -1158,7 +1162,7 @@ ide_debugger_emit_breakpoint_removed (IdeDebugger           *self,
  * Debugger implementations should call this when a breakpoint has changed
  * in the underlying debugger.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_breakpoint_modified (IdeDebugger           *self,
@@ -1179,7 +1183,7 @@ ide_debugger_emit_breakpoint_modified (IdeDebugger           *self,
  * Debugger implementations should call this when the debugger has started
  * or restarted executing the inferior.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_running (IdeDebugger *self)
@@ -1200,7 +1204,7 @@ ide_debugger_emit_running (IdeDebugger *self)
  * Debugger implementations should call this when the debugger has stopped
  * and include the reason and location of the stop.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_stopped (IdeDebugger           *self,
@@ -1224,7 +1228,7 @@ ide_debugger_emit_stopped (IdeDebugger           *self,
  * Debugger implementations should call this when the debugger has loaded
  * a new library.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_library_loaded (IdeDebugger        *self,
@@ -1246,7 +1250,7 @@ ide_debugger_emit_library_loaded (IdeDebugger        *self,
  * Debugger implementations should call this when the debugger has unloaded a
  * library.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_emit_library_unloaded (IdeDebugger        *self,
@@ -1270,7 +1274,7 @@ ide_debugger_emit_library_unloaded (IdeDebugger        *self,
  * #IdeDebugger implementations must implement the virtual function
  * for this method.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_list_breakpoints_async (IdeDebugger         *self,
@@ -1295,7 +1299,7 @@ ide_debugger_list_breakpoints_async (IdeDebugger         *self,
  * Returns: (transfer full) (element-type Ide.DebuggerBreakpoint): a #GPtrArray
  *   of breakpoints that are registered with the debugger.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 GPtrArray *
 ide_debugger_list_breakpoints_finish (IdeDebugger   *self,
@@ -1322,7 +1326,7 @@ ide_debugger_list_breakpoints_finish (IdeDebugger   *self,
  * registered in the debugger. Debugger implementations will emit
  * #IdeDebugger::breakpoint-added when a breakpoint has been registered.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_insert_breakpoint_async (IdeDebugger             *self,
@@ -1355,7 +1359,7 @@ ide_debugger_insert_breakpoint_async (IdeDebugger             *self,
  * Returns: %TRUE if the command was submitted successfully; otherwise %FALSE
  *   and @error is set.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 gboolean
 ide_debugger_insert_breakpoint_finish (IdeDebugger   *self,
@@ -1382,7 +1386,7 @@ ide_debugger_insert_breakpoint_finish (IdeDebugger   *self,
  * removed by the debugger. Debugger implementations will emit
  * #IdeDebugger::breakpoint-removed when a breakpoint has been removed.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_remove_breakpoint_async (IdeDebugger             *self,
@@ -1414,7 +1418,7 @@ ide_debugger_remove_breakpoint_async (IdeDebugger             *self,
  *
  * Returns: %TRUE if the command was submitted successfully; otherwise %FALSE and @error is set.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 gboolean
 ide_debugger_remove_breakpoint_finish (IdeDebugger   *self,
@@ -1444,7 +1448,7 @@ ide_debugger_remove_breakpoint_finish (IdeDebugger   *self,
  * modified by the debugger. Debugger implementations will emit
  * #IdeDebugger::breakpoint-modified when a breakpoint has been removed.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_modify_breakpoint_async (IdeDebugger                 *self,
@@ -1481,7 +1485,7 @@ ide_debugger_modify_breakpoint_async (IdeDebugger                 *self,
  *
  * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 gboolean
 ide_debugger_modify_breakpoint_finish (IdeDebugger   *self,
@@ -1509,6 +1513,8 @@ ide_debugger_modify_breakpoint_finish (IdeDebugger   *self,
  * display information on breakpoints.
  *
  * Returns: (transfer none) (not nullable): a #GListModel of #IdeDebuggerBreakpoint
+ *
+ * Since: 3.32
  */
 GListModel *
 ide_debugger_get_breakpoints (IdeDebugger *self)
@@ -1530,6 +1536,8 @@ ide_debugger_get_breakpoints (IdeDebugger *self)
  * implementation emitting varous thread-group modification signals correctly.
  *
  * Returns: (transfer none) (not nullable): a #GListModel of #IdeDebuggerThreadGroup
+ *
+ * Since: 3.32
  */
 GListModel *
 ide_debugger_get_thread_groups (IdeDebugger *self)
@@ -1551,6 +1559,8 @@ ide_debugger_get_thread_groups (IdeDebugger *self)
  * implementation emitting varous thread modification signals correctly.
  *
  * Returns: (transfer none) (not nullable): a #GListModel of #IdeDebuggerThread
+ *
+ * Since: 3.32
  */
 GListModel *
 ide_debugger_get_threads (IdeDebugger *self)
@@ -1583,6 +1593,8 @@ ide_debugger_list_frames_async (IdeDebugger         *self,
  *
  * Returns: (transfer full) (element-type Ide.DebuggerFrame) (nullable): An
  *   array of debugger frames or %NULL and @error is set.
+ *
+ * Since: 3.32
  */
 GPtrArray *
 ide_debugger_list_frames_finish (IdeDebugger   *self,
@@ -1603,7 +1615,7 @@ ide_debugger_list_frames_finish (IdeDebugger   *self,
  *
  * Returns: (transfer none) (nullable): An #IdeDebuggerThread or %NULL
  *
- * Since: 3.26
+ * Since: 3.32
  */
 IdeDebuggerThread *
 ide_debugger_get_selected_thread (IdeDebugger *self)
@@ -1628,7 +1640,7 @@ ide_debugger_get_selected_thread (IdeDebugger *self)
  * stopped together and on gdb on Linux, this is the default for all threads in
  * the process.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_interrupt_async (IdeDebugger            *self,
@@ -1716,7 +1728,7 @@ ide_debugger_send_signal_finish (IdeDebugger   *self,
  *
  * Returns: the filename of the binary or %NULL
  *
- * Since: 3.26
+ * Since: 3.32
  */
 const gchar *
 ide_debugger_locate_binary_at_address (IdeDebugger        *self,
@@ -1747,7 +1759,7 @@ ide_debugger_locate_binary_at_address (IdeDebugger        *self,
  * Requests the debugger backend to list the locals that are available to the
  * given @frame of @thread.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_list_locals_async (IdeDebugger         *self,
@@ -1781,7 +1793,7 @@ ide_debugger_list_locals_async (IdeDebugger         *self,
  * Returns: (transfer full) (element-type Ide.DebuggerVariable): a #GPtrArray of
  *   #IdeDebuggerVariable if successful; otherwise %NULL and error is set.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 GPtrArray *
 ide_debugger_list_locals_finish (IdeDebugger   *self,
@@ -1806,7 +1818,7 @@ ide_debugger_list_locals_finish (IdeDebugger   *self,
  * Requests the debugger backend to list the parameters to the given stack
  * frame.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_list_params_async (IdeDebugger         *self,
@@ -1840,7 +1852,7 @@ ide_debugger_list_params_async (IdeDebugger         *self,
  * Returns: (transfer full) (element-type Ide.DebuggerVariable): a #GPtrArray of
  *   #IdeDebuggerVariable if successful; otherwise %NULL and error is set.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 GPtrArray *
 ide_debugger_list_params_finish (IdeDebugger   *self,
@@ -1862,7 +1874,7 @@ ide_debugger_list_params_finish (IdeDebugger   *self,
  *
  * Requests the list of registers and their values.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_list_registers_async (IdeDebugger         *self,
@@ -1887,7 +1899,7 @@ ide_debugger_list_registers_async (IdeDebugger         *self,
  * Returns: (transfer full) (element-type Ide.DebuggerRegister): a #GPtrArray of
  *   #IdeDebuggerRegister if successful; otherwise %NULL and error is set.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 GPtrArray *
 ide_debugger_list_registers_finish (IdeDebugger   *self,
@@ -1910,7 +1922,7 @@ ide_debugger_list_registers_finish (IdeDebugger   *self,
  *
  * Disassembles the address range requested.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_disassemble_async (IdeDebugger                   *self,
@@ -1937,7 +1949,7 @@ ide_debugger_disassemble_async (IdeDebugger                   *self,
  * Returns: (transfer full) (element-type Ide.DebuggerInstruction): a #GPtrArray
  *   of #IdeDebuggerInstruction if successful; otherwise %NULL and error is set.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 GPtrArray *
 ide_debugger_disassemble_finish (IdeDebugger   *self,
@@ -1960,6 +1972,8 @@ ide_debugger_disassemble_finish (IdeDebugger   *self,
  * to check if the binary type matches it's expectation.
  *
  * Returns: %TRUE if the #IdeDebugger supports the runner.
+ *
+ * Since: 3.32
  */
 gboolean
 ide_debugger_supports_runner (IdeDebugger *self,
@@ -1986,7 +2000,7 @@ ide_debugger_supports_runner (IdeDebugger *self,
  *
  * Prepares the runner to launch a debugger and target process.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_debugger_prepare (IdeDebugger *self,
diff --git a/src/libide/debugger/ide-debugger.h b/src/libide/debugger/ide-debugger.h
index a20f09c9a..ba301c1e6 100644
--- a/src/libide/debugger/ide-debugger.h
+++ b/src/libide/debugger/ide-debugger.h
@@ -1,6 +1,6 @@
 /* ide-debugger.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,32 +14,31 @@
  *
  * 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>
-
-#include "ide-version-macros.h"
-
-#include "ide-object.h"
+#include <libide-core.h>
+#include <libide-code.h>
+#include <libide-foundry.h>
 
-#include "debugger/ide-debugger-breakpoint.h"
-#include "debugger/ide-debugger-frame.h"
-#include "debugger/ide-debugger-instruction.h"
-#include "debugger/ide-debugger-library.h"
-#include "debugger/ide-debugger-register.h"
-#include "debugger/ide-debugger-thread-group.h"
-#include "debugger/ide-debugger-thread.h"
-#include "debugger/ide-debugger-types.h"
-#include "debugger/ide-debugger-variable.h"
-#include "runner/ide-runner.h"
+#include "ide-debugger-breakpoint.h"
+#include "ide-debugger-frame.h"
+#include "ide-debugger-instruction.h"
+#include "ide-debugger-library.h"
+#include "ide-debugger-register.h"
+#include "ide-debugger-thread-group.h"
+#include "ide-debugger-thread.h"
+#include "ide-debugger-types.h"
+#include "ide-debugger-variable.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_DEBUGGER (ide_debugger_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeDebugger, ide_debugger, IDE, DEBUGGER, IdeObject)
 
 struct _IdeDebuggerClass
@@ -191,199 +190,199 @@ struct _IdeDebuggerClass
   gpointer _reserved[32];
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean           ide_debugger_supports_runner           (IdeDebugger                    *self,
                                                            IdeRunner                      *runner,
                                                            gint                           *priority);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_prepare                   (IdeDebugger                    *self,
                                                            IdeRunner                      *runner);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GListModel        *ide_debugger_get_breakpoints           (IdeDebugger                    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar       *ide_debugger_get_display_name          (IdeDebugger                    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_set_display_name          (IdeDebugger                    *self,
                                                            const gchar                    *display_name);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean           ide_debugger_get_is_running            (IdeDebugger                    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean           ide_debugger_get_can_move              (IdeDebugger                    *self,
                                                            IdeDebuggerMovement             movement);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GListModel        *ide_debugger_get_threads               (IdeDebugger                    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GListModel        *ide_debugger_get_thread_groups         (IdeDebugger                    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeDebuggerThread *ide_debugger_get_selected_thread       (IdeDebugger                    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_disassemble_async         (IdeDebugger                    *self,
                                                            const IdeDebuggerAddressRange  *range,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GPtrArray         *ide_debugger_disassemble_finish        (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_insert_breakpoint_async   (IdeDebugger                    *self,
                                                            IdeDebuggerBreakpoint          *breakpoint,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean           ide_debugger_insert_breakpoint_finish  (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_interrupt_async           (IdeDebugger                    *self,
                                                            IdeDebuggerThreadGroup         *thread_group,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean           ide_debugger_interrupt_finish          (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_modify_breakpoint_async   (IdeDebugger                    *self,
                                                            IdeDebuggerBreakpointChange     change,
                                                            IdeDebuggerBreakpoint          *breakpoint,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean           ide_debugger_modify_breakpoint_finish  (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_remove_breakpoint_async   (IdeDebugger                    *self,
                                                            IdeDebuggerBreakpoint          *breakpoint,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean           ide_debugger_remove_breakpoint_finish  (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_list_breakpoints_async    (IdeDebugger                    *self,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GPtrArray         *ide_debugger_list_breakpoints_finish   (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_list_frames_async         (IdeDebugger                    *self,
                                                            IdeDebuggerThread              *thread,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GPtrArray         *ide_debugger_list_frames_finish        (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_list_locals_async         (IdeDebugger                    *self,
                                                            IdeDebuggerThread              *thread,
                                                            IdeDebuggerFrame               *frame,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GPtrArray         *ide_debugger_list_locals_finish        (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_list_params_async         (IdeDebugger                    *self,
                                                            IdeDebuggerThread              *thread,
                                                            IdeDebuggerFrame               *frame,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GPtrArray         *ide_debugger_list_params_finish        (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_list_registers_async      (IdeDebugger                    *self,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GPtrArray         *ide_debugger_list_registers_finish     (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_move_async                (IdeDebugger                    *self,
                                                            IdeDebuggerMovement             movement,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean           ide_debugger_move_finish               (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_send_signal_async         (IdeDebugger                    *self,
                                                            gint                            signum,
                                                            GCancellable                   *cancellable,
                                                            GAsyncReadyCallback             callback,
                                                            gpointer                        user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean           ide_debugger_send_signal_finish        (IdeDebugger                    *self,
                                                            GAsyncResult                   *result,
                                                            GError                        **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar       *ide_debugger_locate_binary_at_address  (IdeDebugger                    *self,
                                                            IdeDebuggerAddress              address);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_log                  (IdeDebugger                    *self,
                                                            IdeDebuggerStream               stream,
                                                            GBytes                         *content);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_thread_group_added   (IdeDebugger                    *self,
                                                            IdeDebuggerThreadGroup         *thread_group);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_thread_group_removed (IdeDebugger                    *self,
                                                            IdeDebuggerThreadGroup         *thread_group);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_thread_group_started (IdeDebugger                    *self,
                                                            IdeDebuggerThreadGroup         *thread_group);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_thread_group_exited  (IdeDebugger                    *self,
                                                            IdeDebuggerThreadGroup         *thread_group);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_thread_added         (IdeDebugger                    *self,
                                                            IdeDebuggerThread              *thread);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_thread_removed       (IdeDebugger                    *self,
                                                            IdeDebuggerThread              *thread);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_thread_selected      (IdeDebugger                    *self,
                                                            IdeDebuggerThread              *thread);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_breakpoint_added     (IdeDebugger                    *self,
                                                            IdeDebuggerBreakpoint          *breakpoint);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_breakpoint_modified  (IdeDebugger                    *self,
                                                            IdeDebuggerBreakpoint          *breakpoint);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_breakpoint_removed   (IdeDebugger                    *self,
                                                            IdeDebuggerBreakpoint          *breakpoint);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_running              (IdeDebugger                    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_stopped              (IdeDebugger                    *self,
                                                            IdeDebuggerStopReason           stop_reason,
                                                            IdeDebuggerBreakpoint          *breakpoint);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_library_loaded       (IdeDebugger                    *self,
                                                            IdeDebuggerLibrary             *library);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_debugger_emit_library_unloaded     (IdeDebugger                    *self,
                                                            IdeDebuggerLibrary             *library);
 
diff --git a/src/libide/debugger/libide-debugger.h b/src/libide/debugger/libide-debugger.h
new file mode 100644
index 000000000..df7f9ed86
--- /dev/null
+++ b/src/libide/debugger/libide-debugger.h
@@ -0,0 +1,44 @@
+/* libide-debugger.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_DEBUGGER_INSIDE
+
+#include "ide-debugger-breakpoint.h"
+#include "ide-debugger-breakpoints.h"
+#include "ide-debugger-frame.h"
+#include "ide-debugger-instruction.h"
+#include "ide-debugger-library.h"
+#include "ide-debugger-register.h"
+#include "ide-debugger-thread-group.h"
+#include "ide-debugger-thread.h"
+#include "ide-debugger-types.h"
+#include "ide-debugger-variable.h"
+#include "ide-debugger.h"
+#include "ide-debug-manager.h"
+
+#undef IDE_DEBUGGER_INSIDE
+
+G_END_DECLS
diff --git a/src/libide/debugger/meson.build b/src/libide/debugger/meson.build
index 33b333109..dffca20ca 100644
--- a/src/libide/debugger/meson.build
+++ b/src/libide/debugger/meson.build
@@ -1,6 +1,14 @@
-debugger_headers = [
+libide_debugger_header_subdir = join_paths(libide_header_subdir, 'debugger')
+libide_include_directories += include_directories('.')
+
+libide_debugger_generated_headers = []
+
+#
+# Public API Headers
+#
+
+libide_debugger_public_headers = [
   'ide-debug-manager.h',
-  'ide-debugger.h',
   'ide-debugger-breakpoint.h',
   'ide-debugger-breakpoints.h',
   'ide-debugger-frame.h',
@@ -11,11 +19,24 @@ debugger_headers = [
   'ide-debugger-thread.h',
   'ide-debugger-types.h',
   'ide-debugger-variable.h',
+  'ide-debugger.h',
+  'libide-debugger.h',
 ]
 
-debugger_sources = [
+libide_debugger_private_headers = [
+  'ide-debugger-address-map-private.h',
+  'ide-debugger-private.h',
+]
+
+install_headers(libide_debugger_public_headers, subdir: libide_debugger_header_subdir)
+
+#
+# Sources
+#
+
+libide_debugger_public_sources = [
   'ide-debug-manager.c',
-  'ide-debugger.c',
+  'ide-debugger-address-map.c',
   'ide-debugger-breakpoint.c',
   'ide-debugger-breakpoints.c',
   'ide-debugger-frame.c',
@@ -26,39 +47,49 @@ debugger_sources = [
   'ide-debugger-thread.c',
   'ide-debugger-types.c',
   'ide-debugger-variable.c',
+  'ide-debugger.c',
 ]
 
-# .h files used for gtk-doc ignores
-debugger_private_sources = [
-  'ide-debugger-actions.c',
-  'ide-debugger-address-map.c',
-  'ide-debugger-address-map.h',
-  'ide-debugger-breakpoints-view.c',
-  'ide-debugger-breakpoints-view.h',
-  'ide-debugger-controls.c',
-  'ide-debugger-controls.h',
-  'ide-debugger-disassembly-view.c',
-  'ide-debugger-disassembly-view.h',
-  'ide-debugger-editor-addin.c',
-  'ide-debugger-editor-addin.h',
+libide_debugger_private_sources = [
   'ide-debugger-fallbacks.c',
-  'ide-debugger-hover-controls.c',
-  'ide-debugger-hover-controls.h',
-  'ide-debugger-hover-provider.c',
-  'ide-debugger-hover-provider.h',
-  'ide-debugger-libraries-view.c',
-  'ide-debugger-libraries-view.h',
-  'ide-debugger-locals-view.c',
-  'ide-debugger-locals-view.h',
-  'ide-debugger-plugin.c',
-  'ide-debugger-registers-view.c',
-  'ide-debugger-registers-view.h',
-  'ide-debugger-threads-view.c',
-  'ide-debugger-threads-view.h',
+  'ide-debugger-actions.c',
+]
+
+#
+# Dependencies
+#
+
+libide_debugger_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+
+  libide_core_dep,
+  libide_io_dep,
+  libide_threading_dep,
+  libide_code_dep,
+  libide_foundry_dep,
 ]
 
-libide_public_headers += files(debugger_headers)
-libide_public_sources += files(debugger_sources)
-libide_private_sources += files(debugger_private_sources)
+#
+# Library Definitions
+#
+
+libide_debugger = static_library('ide-debugger-' + libide_api_version,
+   libide_debugger_public_sources + libide_debugger_private_sources,
+   dependencies: libide_debugger_deps,
+         c_args: libide_args + release_args + ['-DIDE_DEBUGGER_COMPILATION'],
+)
+
+libide_debugger_dep = declare_dependency(
+              sources: libide_debugger_private_headers + libide_debugger_generated_headers,
+         dependencies: libide_debugger_deps,
+           link_whole: libide_debugger,
+  include_directories: include_directories('.'),
+)
 
-install_headers(debugger_headers, subdir: join_paths(libide_header_subdir, 'debugger'))
+gnome_builder_public_sources += files(libide_debugger_public_sources)
+gnome_builder_public_headers += files(libide_debugger_public_headers)
+gnome_builder_generated_headers += libide_debugger_generated_headers
+gnome_builder_include_subdirs += libide_debugger_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-debugger.h', '-DIDE_DEBUGGER_COMPILATION']
diff --git a/src/libide/editor/ide-editor-addin.c b/src/libide/editor/ide-editor-addin.c
index f3e3efeaa..b2873d0f2 100644
--- a/src/libide/editor/ide-editor-addin.c
+++ b/src/libide/editor/ide-editor-addin.c
@@ -1,6 +1,6 @@
 /* ide-editor-addin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,28 +14,32 @@
  *
  * 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 "ide-editor-addin"
 
 #include "config.h"
 
-#include "editor/ide-editor-addin.h"
-#include "editor/ide-editor-private.h"
+#include "ide-editor-addin.h"
+#include "ide-editor-private.h"
 
 /**
  * SECTION:ide-editor-addin
  * @title: IdeEditorAddin
- * @short_description: Addins for the editor perspective
+ * @short_description: Addins for the editor surface
  *
  * The #IdeEditorAddin interface provides a simplified interface for
  * plugins that want to perform operations in, or extend, the editor
- * perspective.
+ * surface.
  *
  * This differs from the #IdeWorkbenchAddin in that you are given access
- * to the editor perspective directly. This can be convenient if all you
- * need to do is add panels or perform view tracking of the current
- * focus view.
+ * to the editor surface directly. This can be convenient if all you
+ * need to do is add panels or perform page tracking of the current
+ * focus page.
+ *
+ * Since: 3.32
  */
 
 G_DEFINE_INTERFACE (IdeEditorAddin, ide_editor_addin, G_TYPE_OBJECT)
@@ -48,98 +52,103 @@ ide_editor_addin_default_init (IdeEditorAddinInterface *iface)
 /**
  * ide_editor_addin_load:
  * @self: an #IdeEditorAddin
- * @perspective: an #IdeEditorPeprsective
+ * @surface: an #IdeEditorPeprsective
  *
  * This method is called to load the addin.
  *
  * The addin should add any necessary UI components.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
-ide_editor_addin_load (IdeEditorAddin       *self,
-                       IdeEditorPerspective *perspective)
+ide_editor_addin_load (IdeEditorAddin   *self,
+                       IdeEditorSurface *surface)
 {
   g_return_if_fail (IDE_IS_EDITOR_ADDIN (self));
-  g_return_if_fail (IDE_IS_EDITOR_PERSPECTIVE (perspective));
+  g_return_if_fail (IDE_IS_EDITOR_SURFACE (surface));
 
   if (IDE_EDITOR_ADDIN_GET_IFACE (self)->load)
-    IDE_EDITOR_ADDIN_GET_IFACE (self)->load (self, perspective);
+    IDE_EDITOR_ADDIN_GET_IFACE (self)->load (self, surface);
 }
 
 /**
  * ide_editor_addin_unload:
  * @self: an #IdeEditorAddin
- * @perspective: an #IdeEditorPerspective
+ * @surface: an #IdeEditorSurface
  *
  * This method is called to unload the addin.
  *
  * The addin is responsible for undoing anything it setup in load
  * and cancel any in-flight or pending tasks immediately.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
-ide_editor_addin_unload (IdeEditorAddin       *self,
-                         IdeEditorPerspective *perspective)
+ide_editor_addin_unload (IdeEditorAddin   *self,
+                         IdeEditorSurface *surface)
 {
   g_return_if_fail (IDE_IS_EDITOR_ADDIN (self));
-  g_return_if_fail (IDE_IS_EDITOR_PERSPECTIVE (perspective));
+  g_return_if_fail (IDE_IS_EDITOR_SURFACE (surface));
 
   if (IDE_EDITOR_ADDIN_GET_IFACE (self)->unload)
-    IDE_EDITOR_ADDIN_GET_IFACE (self)->unload (self, perspective);
+    IDE_EDITOR_ADDIN_GET_IFACE (self)->unload (self, surface);
 }
 
 /**
- * ide_editor_addin_view_set:
+ * ide_editor_addin_page_set:
  * @self: an #IdeEditorAddin
- * @view: (nullable): an #IdeLayoutView or %NULL
+ * @page: (nullable): an #IdePage or %NULL
  *
- * This function is called when the current view has changed in the
- * editor perspective. This could happen when the user focus another
- * view, either with the keyboard, mouse, touch, or by opening a new
+ * This function is called when the current page has changed in the
+ * editor surface. This could happen when the user focus another
+ * page, either with the keyboard, mouse, touch, or by opening a new
  * buffer.
  *
- * Note that @view may not be an #IdeEditorView, so consumers of this
+ * Note that @page may not be an #IdeEditorView, so consumers of this
  * interface should take appropriate action based on the type.
  *
- * When the last view is removed, @view will be %NULL to indicate to the
- * addin that there is no active view.
+ * When the last page is removed, @page will be %NULL to indicate to the
+ * addin that there is no active page.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
-ide_editor_addin_view_set (IdeEditorAddin *self,
-                           IdeLayoutView  *view)
+ide_editor_addin_page_set (IdeEditorAddin *self,
+                           IdePage        *page)
 {
   g_return_if_fail (IDE_IS_EDITOR_ADDIN (self));
-  g_return_if_fail (!view || IDE_IS_LAYOUT_VIEW (view));
+  g_return_if_fail (!page || IDE_IS_PAGE (page));
 
-  if (IDE_EDITOR_ADDIN_GET_IFACE (self)->view_set)
-    IDE_EDITOR_ADDIN_GET_IFACE (self)->view_set (self, view);
+  if (IDE_EDITOR_ADDIN_GET_IFACE (self)->page_set)
+    IDE_EDITOR_ADDIN_GET_IFACE (self)->page_set (self, page);
 }
 
 /**
  * ide_editor_addin_find_by_module_name:
- * @editor: an #IdeEditorPerspective
+ * @editor: an #IdeEditorSurface
  * @module_name: the module name of the addin
  *
  * This function allows locating an #IdeEditorAddin that is attached
- * to the #IdeEditorPerspective by the addin module name. The module name
+ * to the #IdeEditorSurface by the addin module name. The module name
  * should match the value specified in the ".plugin" module definition.
  *
  * Returns: (transfer none) (nullable): An #IdeEditorAddin or %NULL
+ *
+ * Since: 3.32
  */
 IdeEditorAddin *
-ide_editor_addin_find_by_module_name (IdeEditorPerspective *editor,
-                                      const gchar          *module_name)
+ide_editor_addin_find_by_module_name (IdeEditorSurface *editor,
+                                      const gchar      *module_name)
 {
   PeasExtension *ret = NULL;
   PeasPluginInfo *plugin_info;
 
-  g_return_val_if_fail (IDE_IS_EDITOR_PERSPECTIVE (editor), NULL);
+  g_return_val_if_fail (IDE_IS_EDITOR_SURFACE (editor), NULL);
   g_return_val_if_fail (module_name != NULL, NULL);
 
+  if (editor->addins == NULL)
+    return NULL;
+
   plugin_info = peas_engine_get_plugin_info (peas_engine_get_default (), module_name);
 
   if (plugin_info != NULL)
diff --git a/src/libide/editor/ide-editor-addin.h b/src/libide/editor/ide-editor-addin.h
index 2859705c8..eaaaa9f5d 100644
--- a/src/libide/editor/ide-editor-addin.h
+++ b/src/libide/editor/ide-editor-addin.h
@@ -1,6 +1,6 @@
 /* ide-editor-addin.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,46 +14,52 @@
  *
  * 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 "ide-version-macros.h"
+#if !defined (IDE_EDITOR_INSIDE) && !defined (IDE_EDITOR_COMPILATION)
+# error "Only <libide-editor.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <libide-gui.h>
 
-#include "editor/ide-editor-perspective.h"
-#include "layout/ide-layout-view.h"
+#include "ide-editor-surface.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_EDITOR_ADDIN (ide_editor_addin_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_INTERFACE (IdeEditorAddin, ide_editor_addin, IDE, EDITOR_ADDIN, GObject)
 
 struct _IdeEditorAddinInterface
 {
   GTypeInterface parent_iface;
 
-  void (*load)     (IdeEditorAddin       *self,
-                    IdeEditorPerspective *perspective);
-  void (*unload)   (IdeEditorAddin       *self,
-                    IdeEditorPerspective *perspective);
-  void (*view_set) (IdeEditorAddin       *self,
-                    IdeLayoutView        *view);
+  void (*load)     (IdeEditorAddin   *self,
+                    IdeEditorSurface *surface);
+  void (*unload)   (IdeEditorAddin   *self,
+                    IdeEditorSurface *surface);
+  void (*page_set) (IdeEditorAddin   *self,
+                    IdePage          *page);
 };
 
-IDE_AVAILABLE_IN_ALL
-void ide_editor_addin_load     (IdeEditorAddin       *self,
-                                IdeEditorPerspective *perspective);
-IDE_AVAILABLE_IN_ALL
-void ide_editor_addin_unload   (IdeEditorAddin       *self,
-                                IdeEditorPerspective *perspective);
-IDE_AVAILABLE_IN_ALL
-void ide_editor_addin_view_set (IdeEditorAddin       *self,
-                                IdeLayoutView        *view);
-
-IDE_AVAILABLE_IN_ALL
-IdeEditorAddin *ide_editor_addin_find_by_module_name (IdeEditorPerspective *editor,
-                                                      const gchar          *module_name);
+IDE_AVAILABLE_IN_3_32
+void ide_editor_addin_load     (IdeEditorAddin   *self,
+                                IdeEditorSurface *surface);
+IDE_AVAILABLE_IN_3_32
+void ide_editor_addin_unload   (IdeEditorAddin   *self,
+                                IdeEditorSurface *surface);
+IDE_AVAILABLE_IN_3_32
+void ide_editor_addin_page_set (IdeEditorAddin   *self,
+                                IdePage          *page);
+
+IDE_AVAILABLE_IN_3_32
+IdeEditorAddin *ide_editor_addin_find_by_module_name (IdeEditorSurface *editor,
+                                                      const gchar      *module_name);
 
 G_END_DECLS
diff --git a/src/libide/editor/ide-editor-page-actions.c b/src/libide/editor/ide-editor-page-actions.c
new file mode 100644
index 000000000..cff2d6e89
--- /dev/null
+++ b/src/libide/editor/ide-editor-page-actions.c
@@ -0,0 +1,599 @@
+/* ide-editor-page-actions.c
+ *
+ * Copyright 2017-2019 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 "ide-editor-page-actions"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-code.h>
+#include <libide-gui.h>
+
+#include "ide-editor-surface.h"
+#include "ide-editor-private.h"
+#include "ide-editor-print-operation.h"
+#include "ide-editor-settings-dialog.h"
+
+static void
+ide_editor_page_actions_reload_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeBufferManager *bufmgr = (IdeBufferManager *)object;
+  g_autoptr(IdeEditorPage) self = user_data;
+  g_autoptr(IdeBuffer) buffer = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  if (self->progress_bar != NULL)
+    dzl_gtk_widget_hide_with_fade (GTK_WIDGET (self->progress_bar));
+
+  if (!(buffer = ide_buffer_manager_load_file_finish (bufmgr, result, &error)))
+    {
+      g_warning ("%s", error->message);
+      ide_page_report_error (IDE_PAGE (self),
+                             /* translators: %s is the error message */
+                             _("Failed to load file: %s"), error->message);
+      ide_page_set_failed (IDE_PAGE (self), TRUE);
+    }
+  else
+    {
+      ide_editor_page_scroll_to_line (self, 0);
+    }
+}
+
+static void
+ide_editor_page_actions_reload (GSimpleAction *action,
+                                GVariant      *param,
+                                gpointer       user_data)
+{
+  IdeEditorPage *self = user_data;
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  IdeBufferManager *bufmgr;
+  IdeBuffer *buffer;
+  GFile *file;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  buffer = ide_editor_page_get_buffer (self);
+  context = ide_buffer_ref_context (buffer);
+  bufmgr = ide_buffer_manager_from_context (context);
+  file = ide_buffer_get_file (buffer);
+
+  gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (self->progress_bar), 0.0);
+  gtk_widget_show (GTK_WIDGET (self->progress_bar));
+
+  ide_buffer_manager_load_file_async (bufmgr,
+                                      file,
+                                      IDE_BUFFER_OPEN_FLAGS_FORCE_RELOAD,
+                                      NULL,
+                                      &notif,
+                                      ide_editor_page_actions_reload_cb,
+                                      g_object_ref (self));
+
+  g_object_bind_property (notif, "progress", self->progress_bar, "fraction",
+                          G_BINDING_SYNC_CREATE);
+}
+
+static void
+handle_print_result (IdeEditorPage           *self,
+                     GtkPrintOperation       *operation,
+                     GtkPrintOperationResult  result)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (GTK_IS_PRINT_OPERATION (operation));
+
+  if (result == GTK_PRINT_OPERATION_RESULT_ERROR)
+    {
+      g_autoptr(GError) error = NULL;
+
+      gtk_print_operation_get_error (operation, &error);
+
+      g_warning ("%s", error->message);
+      ide_page_report_error (IDE_PAGE (self),
+                             /* translators: %s is the error message */
+                             _("Print failed: %s"), error->message);
+    }
+}
+
+static void
+print_done (GtkPrintOperation       *operation,
+            GtkPrintOperationResult  result,
+            gpointer                 user_data)
+{
+  IdeEditorPage *self = user_data;
+
+  g_assert (GTK_IS_PRINT_OPERATION (operation));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  handle_print_result (self, operation, result);
+
+  g_object_unref (operation);
+  g_object_unref (self);
+}
+
+static void
+ide_editor_page_actions_print (GSimpleAction *action,
+                               GVariant      *param,
+                               gpointer       user_data)
+{
+  g_autoptr(IdeEditorPrintOperation) operation = NULL;
+  IdeEditorPage *self = user_data;
+  IdeSourceView *source_view;
+  GtkWidget *toplevel;
+  GtkPrintOperationResult result;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);
+
+  source_view = ide_editor_page_get_view (self);
+  operation = ide_editor_print_operation_new (source_view);
+
+  /* keep a ref until "done" is emitted */
+  g_object_ref (operation);
+  g_signal_connect_after (g_object_ref (operation),
+                          "done",
+                          G_CALLBACK (print_done),
+                          g_object_ref (self));
+
+  result = gtk_print_operation_run (GTK_PRINT_OPERATION (operation),
+                                    GTK_PRINT_OPERATION_ACTION_PRINT_DIALOG,
+                                    GTK_WINDOW (toplevel),
+                                    NULL);
+
+  handle_print_result (self, GTK_PRINT_OPERATION (operation), result);
+}
+
+static void
+ide_editor_page_actions_save_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  IdeBuffer *buffer = (IdeBuffer *)object;
+  g_autoptr(IdeEditorPage) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  if (!ide_buffer_save_file_finish (buffer, result, &error))
+    {
+      g_warning ("%s", error->message);
+      ide_page_report_error (IDE_PAGE (self),
+                             /* translators: %s is the error message */
+                             _("Failed to save file: %s"), error->message);
+      ide_page_set_failed (IDE_PAGE (self), TRUE);
+    }
+
+  if (self->progress_bar != NULL)
+    dzl_gtk_widget_hide_with_fade (GTK_WIDGET (self->progress_bar));
+}
+
+static void
+ide_editor_page_actions_save (GSimpleAction *action,
+                              GVariant      *variant,
+                              gpointer       user_data)
+{
+  IdeEditorPage *self = user_data;
+  IdeBufferManager *bufmgr;
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GFile) local_file = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  IdeBuffer *buffer;
+  GFile *file;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  buffer = ide_editor_page_get_buffer (self);
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  context = ide_buffer_ref_context (buffer);
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  file = ide_buffer_get_file (buffer);
+  g_return_if_fail (G_IS_FILE (file));
+
+  bufmgr = ide_buffer_manager_from_context (context);
+  workdir = ide_context_ref_workdir (context);
+
+  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+  g_assert (G_IS_FILE (workdir));
+
+  if (ide_buffer_get_is_temporary (buffer))
+    {
+      GtkFileChooserNative *dialog;
+      GtkWidget *toplevel;
+      gint ret;
+
+      toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);
+
+      dialog = gtk_file_chooser_native_new (_("Save File"),
+                                            GTK_WINDOW (toplevel),
+                                            GTK_FILE_CHOOSER_ACTION_SAVE,
+                                            _("Save"), _("Cancel"));
+
+      g_object_set (dialog,
+                    "do-overwrite-confirmation", TRUE,
+                    "local-only", FALSE,
+                    "modal", TRUE,
+                    "select-multiple", FALSE,
+                    "show-hidden", FALSE,
+                    NULL);
+
+      gtk_file_chooser_set_current_folder_file (GTK_FILE_CHOOSER (dialog), workdir, NULL);
+
+      ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (dialog));
+
+      if (ret == GTK_RESPONSE_ACCEPT)
+        file = local_file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
+
+      gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (dialog));
+
+      if (local_file == NULL)
+        return;
+    }
+
+  ide_buffer_save_file_async (buffer,
+                              file,
+                              NULL,
+                              &notif,
+                              ide_editor_page_actions_save_cb,
+                              g_object_ref (self));
+
+  g_object_bind_property (notif, "progress", self->progress_bar, "fraction",
+                          G_BINDING_SYNC_CREATE);
+
+  gtk_widget_show (GTK_WIDGET (self->progress_bar));
+}
+
+
+static void
+ide_editor_page_actions_save_as_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  IdeBuffer *buffer = (IdeBuffer *)object;
+  g_autoptr(IdeEditorPage) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  if (!ide_buffer_save_file_finish (buffer, result, &error))
+    {
+      /* In this case, the editor page hasn't failed since this is for an
+       * alternate file (which maybe we just don't have access to on the
+       * network or something).
+       *
+       * But we do still need to notify the user of the error.
+       */
+      g_warning ("%s", error->message);
+      ide_page_report_error (IDE_PAGE (self),
+                             /* translators: %s is the underlying error message */
+                             _("Failed to save file: %s"),
+                             error->message);
+    }
+
+  dzl_gtk_widget_hide_with_fade (GTK_WIDGET (self->progress_bar));
+}
+
+static void
+ide_editor_page_actions_save_as (GSimpleAction *action,
+                                 GVariant      *param,
+                                 gpointer       user_data)
+{
+  IdeEditorPage *self = user_data;
+  GtkFileChooserNative *dialog;
+  IdeBuffer *buffer;
+  GtkWidget *toplevel;
+  GFile *file;
+  gint ret;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  buffer = ide_editor_page_get_buffer (self);
+  file = ide_buffer_get_file (buffer);
+
+  /* Just redirect to the save flow if we have a temporary
+   * file currently. That way we can avoid splitting the
+   * flow to handle both cases here.
+   */
+  if (ide_buffer_get_is_temporary (buffer))
+    {
+      ide_editor_page_actions_save (action, NULL, user_data);
+      return;
+    }
+
+  toplevel = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);
+  dialog = gtk_file_chooser_native_new (_("Save File As"),
+                                        GTK_WINDOW (toplevel),
+                                        GTK_FILE_CHOOSER_ACTION_SAVE,
+                                        _("Save As"),
+                                        _("Cancel"));
+
+  gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE);
+  gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (dialog), FALSE);
+  gtk_file_chooser_set_show_hidden (GTK_FILE_CHOOSER (dialog), FALSE);
+
+  if (file != NULL)
+    gtk_file_chooser_set_file (GTK_FILE_CHOOSER (dialog), file, NULL);
+
+  ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (dialog));
+
+  if (ret == GTK_RESPONSE_ACCEPT)
+    {
+      g_autoptr(GFile) save_as = NULL;
+      g_autoptr(IdeNotification) notif = NULL;
+
+      save_as = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
+
+      ide_buffer_save_file_async (buffer,
+                                  save_as,
+                                  NULL,
+                                  &notif,
+                                  ide_editor_page_actions_save_as_cb,
+                                  g_object_ref (self));
+
+      g_object_bind_property (notif, "progress", self->progress_bar, "fraction",
+                              G_BINDING_SYNC_CREATE);
+
+      gtk_widget_show (GTK_WIDGET (self->progress_bar));
+    }
+
+  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (dialog));
+}
+
+static void
+ide_editor_page_actions_find (GSimpleAction *action,
+                              GVariant      *variant,
+                              gpointer       user_data)
+{
+  IdeEditorPage *self = user_data;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end))
+    {
+      g_autofree gchar *word = gtk_text_iter_get_slice (&begin, &end);
+      ide_editor_search_set_search_text (self->search, word);
+    }
+
+  ide_editor_search_bar_set_replace_mode (self->search_bar, FALSE);
+  gtk_revealer_set_reveal_child (self->search_revealer, TRUE);
+  gtk_widget_grab_focus (GTK_WIDGET (self->search_bar));
+}
+
+static void
+ide_editor_page_actions_find_replace (GSimpleAction *action,
+                                      GVariant      *variant,
+                                      gpointer       user_data)
+{
+  IdeEditorPage *self = user_data;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end))
+    {
+      g_autofree gchar *word = gtk_text_iter_get_slice (&begin, &end);
+      ide_editor_search_set_search_text (self->search, word);
+    }
+
+  ide_editor_search_bar_set_replace_mode (self->search_bar, TRUE);
+  gtk_revealer_set_reveal_child (self->search_revealer, TRUE);
+  gtk_widget_grab_focus (GTK_WIDGET (self->search_bar));
+}
+
+static void
+ide_editor_page_actions_hide_search (GSimpleAction *action,
+                                     GVariant      *variant,
+                                     gpointer       user_data)
+{
+  IdeEditorPage *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  gtk_revealer_set_reveal_child (self->search_revealer, FALSE);
+  gtk_widget_grab_focus (GTK_WIDGET (self->source_view));
+}
+
+static void
+ide_editor_page_actions_notify_file_settings (IdeEditorPage *self,
+                                              GParamSpec    *pspec,
+                                              IdeSourceView *source_view)
+{
+  IdeFileSettings *file_settings;
+  GActionGroup *group;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_SOURCE_VIEW (source_view));
+
+  group = gtk_widget_get_action_group (GTK_WIDGET (self), "file-settings");
+  g_assert (DZL_IS_PROPERTIES_GROUP (group));
+
+  file_settings = ide_source_view_get_file_settings (source_view);
+  g_assert (!file_settings || IDE_IS_FILE_SETTINGS (file_settings));
+
+  g_object_set (group, "object", file_settings, NULL);
+}
+
+static void
+ide_editor_page_actions_move_next_error (GSimpleAction *action,
+                                         GVariant      *variant,
+                                         gpointer       user_data)
+{
+  ide_editor_page_move_next_error (user_data);
+}
+
+static void
+ide_editor_page_actions_move_previous_error (GSimpleAction *action,
+                                             GVariant      *variant,
+                                             gpointer       user_data)
+{
+  ide_editor_page_move_previous_error (user_data);
+}
+
+static void
+ide_editor_page_actions_activate_next_search_result (GSimpleAction *action,
+                                                     GVariant      *variant,
+                                                     gpointer       user_data)
+{
+  IdeEditorPage *self = user_data;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  ide_editor_page_move_next_search_result (self);
+
+  gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end);
+  gtk_widget_grab_focus (GTK_WIDGET (self->source_view));
+  gtk_text_buffer_select_range (GTK_TEXT_BUFFER (self->buffer), &begin, &end);
+  ide_source_view_scroll_to_insert (self->source_view);
+}
+
+static void
+ide_editor_page_actions_move_next_search_result (GSimpleAction *action,
+                                                 GVariant      *variant,
+                                                 gpointer       user_data)
+{
+  ide_editor_page_move_next_search_result (user_data);
+}
+
+static void
+ide_editor_page_actions_move_previous_search_result (GSimpleAction *action,
+                                                     GVariant      *variant,
+                                                     gpointer       user_data)
+{
+  ide_editor_page_move_previous_search_result (user_data);
+}
+
+static void
+ide_editor_page_actions_properties (GSimpleAction *action,
+                                    GVariant      *variant,
+                                    gpointer       user_data)
+{
+  IdeEditorPage *self = user_data;
+  IdeEditorSettingsDialog *dialog;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  dialog = ide_editor_settings_dialog_new (self);
+  g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+ide_editor_page_actions_toggle_map (GSimpleAction *action,
+                                    GVariant      *variant,
+                                    gpointer       user_data)
+{
+  IdeEditorPage *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  ide_editor_page_set_show_map (self, !ide_editor_page_get_show_map (self));
+}
+
+static const GActionEntry editor_view_entries[] = {
+  { "activate-next-search-result", ide_editor_page_actions_activate_next_search_result },
+  { "find", ide_editor_page_actions_find },
+  { "find-replace", ide_editor_page_actions_find_replace },
+  { "hide-search", ide_editor_page_actions_hide_search },
+  { "move-next-error", ide_editor_page_actions_move_next_error },
+  { "move-next-search-result", ide_editor_page_actions_move_next_search_result },
+  { "move-previous-error", ide_editor_page_actions_move_previous_error },
+  { "move-previous-search-result", ide_editor_page_actions_move_previous_search_result },
+  { "properties", ide_editor_page_actions_properties },
+  { "print", ide_editor_page_actions_print },
+  { "reload", ide_editor_page_actions_reload },
+  { "save", ide_editor_page_actions_save },
+  { "save-as", ide_editor_page_actions_save_as },
+  { "toggle-map", ide_editor_page_actions_toggle_map },
+};
+
+void
+_ide_editor_page_init_actions (IdeEditorPage *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+  g_autoptr(DzlPropertiesGroup) sv_props = NULL;
+  g_autoptr(DzlPropertiesGroup) file_props = NULL;
+  IdeSourceView *source_view;
+
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+
+  source_view = ide_editor_page_get_view (self);
+
+  /* Setup our user-facing actions */
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   editor_view_entries,
+                                   G_N_ELEMENTS (editor_view_entries),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "editor-page", G_ACTION_GROUP (group));
+
+  /* We want to access some settings properties as stateful GAction so they
+   * manipulated using regular Gtk widgets from the properties panel.
+   */
+  sv_props = dzl_properties_group_new (G_OBJECT (source_view));
+  dzl_properties_group_add_all_properties (sv_props);
+  dzl_properties_group_add_property_full (sv_props,
+                                          "use-spaces",
+                                          "insert-spaces-instead-of-tabs",
+                                          DZL_PROPERTIES_FLAGS_STATEFUL_BOOLEANS);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "source-view", G_ACTION_GROUP (sv_props));
+
+  /*
+   * We want to bind our file-settings, used to tweak values in the
+   * source-view, to a GActionGroup that can be manipulated by the properties
+   * editor. Make sure we get notified of changes and sink the current state.
+   */
+  file_props = dzl_properties_group_new_for_type (IDE_TYPE_FILE_SETTINGS);
+  dzl_properties_group_add_all_properties (file_props);
+  g_signal_connect_swapped (source_view,
+                            "notify::file-settings",
+                            G_CALLBACK (ide_editor_page_actions_notify_file_settings),
+                            self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "file-settings", G_ACTION_GROUP (file_props));
+  ide_editor_page_actions_notify_file_settings (self, NULL, source_view);
+}
+
+void
+_ide_editor_page_update_actions (IdeEditorPage *self)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+
+}
diff --git a/src/libide/editor/ide-editor-page-addin.c b/src/libide/editor/ide-editor-page-addin.c
new file mode 100644
index 000000000..5bb1d8476
--- /dev/null
+++ b/src/libide/editor/ide-editor-page-addin.c
@@ -0,0 +1,113 @@
+/* ide-editor-page-addin.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-editor-page-addin"
+
+#include "config.h"
+
+#include "ide-editor-private.h"
+#include "ide-editor-page-addin.h"
+
+G_DEFINE_INTERFACE (IdeEditorPageAddin, ide_editor_page_addin, G_TYPE_OBJECT)
+
+static void
+ide_editor_page_addin_default_init (IdeEditorPageAddinInterface *iface)
+{
+}
+
+void
+ide_editor_page_addin_load (IdeEditorPageAddin *self,
+                            IdeEditorPage      *page)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE_ADDIN (self));
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (page));
+
+  if (IDE_EDITOR_PAGE_ADDIN_GET_IFACE (self)->load)
+    IDE_EDITOR_PAGE_ADDIN_GET_IFACE (self)->load (self, page);
+}
+
+void
+ide_editor_page_addin_unload (IdeEditorPageAddin *self,
+                              IdeEditorPage      *page)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE_ADDIN (self));
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (page));
+
+  if (IDE_EDITOR_PAGE_ADDIN_GET_IFACE (self)->unload)
+    IDE_EDITOR_PAGE_ADDIN_GET_IFACE (self)->unload (self, page);
+}
+
+void
+ide_editor_page_addin_language_changed (IdeEditorPageAddin *self,
+                                        const gchar        *language_id)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE_ADDIN (self));
+
+  if (IDE_EDITOR_PAGE_ADDIN_GET_IFACE (self)->language_changed)
+    IDE_EDITOR_PAGE_ADDIN_GET_IFACE (self)->language_changed (self, language_id);
+}
+
+void
+ide_editor_page_addin_frame_set (IdeEditorPageAddin *self,
+                                 IdeFrame           *frame)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE_ADDIN (self));
+  g_return_if_fail (IDE_IS_FRAME (frame));
+
+  if (IDE_EDITOR_PAGE_ADDIN_GET_IFACE (self)->frame_set)
+    IDE_EDITOR_PAGE_ADDIN_GET_IFACE (self)->frame_set (self, frame);
+}
+
+/**
+ * ide_editor_page_addin_find_by_module_name:
+ * @page: an #IdeEditorPage
+ * @module_name: the module name which provides the addin
+ *
+ * This function will locate the #IdeEditorPageAddin that was registered
+ * by the addin named @module_name (which should match the module_name
+ * provided in the .plugin file).
+ *
+ * If no module was found or that module does not implement the
+ * #IdeEditorPageAddinInterface, then %NULL is returned.
+ *
+ * Returns: (transfer none) (nullable): An #IdeEditorPageAddin or %NULL
+ *
+ * Since: 3.32
+ */
+IdeEditorPageAddin *
+ide_editor_page_addin_find_by_module_name (IdeEditorPage *page,
+                                           const gchar   *module_name)
+{
+  PeasExtension *ret = NULL;
+  PeasPluginInfo *plugin_info;
+
+  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (page), NULL);
+  g_return_val_if_fail (page->addins != NULL, NULL);
+  g_return_val_if_fail (module_name != NULL, NULL);
+
+  plugin_info = peas_engine_get_plugin_info (peas_engine_get_default (), module_name);
+
+  if (plugin_info != NULL)
+    ret = ide_extension_set_adapter_get_extension (page->addins, plugin_info);
+  else
+    g_warning ("No addin could be found matching module \"%s\"", module_name);
+
+  return ret ? IDE_EDITOR_PAGE_ADDIN (ret) : NULL;
+}
diff --git a/src/libide/editor/ide-editor-page-addin.h b/src/libide/editor/ide-editor-page-addin.h
new file mode 100644
index 000000000..4b929b7aa
--- /dev/null
+++ b/src/libide/editor/ide-editor-page-addin.h
@@ -0,0 +1,70 @@
+/* ide-editor-page-addin.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_EDITOR_INSIDE) && !defined (IDE_EDITOR_COMPILATION)
+# error "Only <libide-editor.h> can be included directly."
+#endif
+
+
+#include <libide-core.h>
+#include <libide-gui.h>
+
+#include "ide-editor-page.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_EDITOR_PAGE_ADDIN (ide_editor_page_addin_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeEditorPageAddin, ide_editor_page_addin, IDE, EDITOR_PAGE_ADDIN, GObject)
+
+struct _IdeEditorPageAddinInterface
+{
+  GTypeInterface parent;
+
+  void (*load)               (IdeEditorPageAddin *self,
+                              IdeEditorPage      *page);
+  void (*unload)             (IdeEditorPageAddin *self,
+                              IdeEditorPage      *page);
+  void (*language_changed)   (IdeEditorPageAddin *self,
+                              const gchar        *language_id);
+  void (*frame_set)          (IdeEditorPageAddin *self,
+                              IdeFrame           *frame);
+};
+
+IDE_AVAILABLE_IN_3_32
+void                ide_editor_page_addin_load                (IdeEditorPageAddin *self,
+                                                               IdeEditorPage      *page);
+IDE_AVAILABLE_IN_3_32
+void                ide_editor_page_addin_unload              (IdeEditorPageAddin *self,
+                                                               IdeEditorPage      *page);
+IDE_AVAILABLE_IN_3_32
+void                ide_editor_page_addin_frame_set           (IdeEditorPageAddin *self,
+                                                               IdeFrame           *frame);
+IDE_AVAILABLE_IN_3_32
+void                ide_editor_page_addin_language_changed    (IdeEditorPageAddin *self,
+                                                               const gchar        *language_id);
+IDE_AVAILABLE_IN_3_32
+IdeEditorPageAddin *ide_editor_page_addin_find_by_module_name (IdeEditorPage      *page,
+                                                               const gchar        *module_name);
+
+G_END_DECLS
diff --git a/src/libide/editor/ide-editor-page-settings.c b/src/libide/editor/ide-editor-page-settings.c
new file mode 100644
index 000000000..c542e4d58
--- /dev/null
+++ b/src/libide/editor/ide-editor-page-settings.c
@@ -0,0 +1,233 @@
+/* ide-editor-page-settings.c
+ *
+ * Copyright 2017-2019 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 "ide-editor-page-settings"
+
+#include "config.h"
+
+#include "ide-editor-private.h"
+
+#include <gtksourceview/gtksource.h>
+
+static gboolean
+get_smart_home_end (GValue   *value,
+                    GVariant *variant,
+                    gpointer  user_data)
+{
+  if (g_variant_get_boolean (variant))
+    g_value_set_enum (value, GTK_SOURCE_SMART_HOME_END_BEFORE);
+  else
+    g_value_set_enum (value, GTK_SOURCE_SMART_HOME_END_DISABLED);
+  return TRUE;
+}
+
+static gboolean
+get_wrap_mode (GValue   *value,
+               GVariant *variant,
+               gpointer  user_data)
+{
+  if (g_variant_get_boolean (variant))
+    g_value_set_enum (value, GTK_WRAP_WORD);
+  else
+    g_value_set_enum (value, GTK_WRAP_NONE);
+  return TRUE;
+}
+
+static void
+on_keybindings_changed (IdeEditorPage *self,
+                        const gchar   *key,
+                        GSettings     *settings)
+{
+  IdeSourceView *source_view;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (g_strcmp0 (key, "keybindings") == 0);
+  g_assert (G_IS_SETTINGS (settings));
+
+  source_view = ide_editor_page_get_view (self);
+
+  g_signal_emit_by_name (source_view,
+                         "set-mode",
+                         NULL,
+                         IDE_SOURCE_VIEW_MODE_TYPE_PERMANENT);
+}
+
+static void
+on_draw_spaces_changed (IdeEditorPage *self,
+                        const gchar   *key,
+                        GSettings     *settings)
+{
+  GtkSourceView *source_view;
+  GtkSourceSpaceDrawer *drawer;
+  guint flags;
+  GtkSourceSpaceLocationFlags location_flags = GTK_SOURCE_SPACE_LOCATION_NONE;
+  GtkSourceSpaceTypeFlags type_flags = GTK_SOURCE_SPACE_TYPE_NONE;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (g_strcmp0 (key, "draw-spaces") == 0);
+  g_assert (G_IS_SETTINGS (settings));
+
+  source_view = GTK_SOURCE_VIEW (ide_editor_page_get_view (self));
+  drawer = gtk_source_view_get_space_drawer (source_view);
+  flags = g_settings_get_flags (settings, "draw-spaces");
+
+  if (flags == 0)
+    {
+      gtk_source_space_drawer_set_enable_matrix (drawer, FALSE);
+      return;
+    }
+
+  /* Reset the matrix before setting it */
+  gtk_source_space_drawer_set_types_for_locations (drawer, GTK_SOURCE_SPACE_LOCATION_ALL, 
GTK_SOURCE_SPACE_TYPE_NONE);
+
+  if (flags & 1)
+    type_flags |= GTK_SOURCE_SPACE_TYPE_SPACE;
+
+  if (flags & 2)
+    type_flags |= GTK_SOURCE_SPACE_TYPE_TAB;
+
+  if (flags & 4)
+    {
+      gtk_source_space_drawer_set_types_for_locations (drawer, GTK_SOURCE_SPACE_LOCATION_ALL, 
GTK_SOURCE_SPACE_TYPE_NEWLINE);
+      type_flags |= GTK_SOURCE_SPACE_TYPE_NEWLINE;
+    }
+
+  if (flags & 8)
+    type_flags |= GTK_SOURCE_SPACE_TYPE_NBSP;
+
+  if (flags & 16)
+    location_flags |= GTK_SOURCE_SPACE_LOCATION_LEADING;
+
+  if (flags & 32)
+    location_flags |= GTK_SOURCE_SPACE_LOCATION_INSIDE_TEXT;
+
+  if (flags & 64)
+    location_flags |= GTK_SOURCE_SPACE_LOCATION_TRAILING;
+
+  if (type_flags > 0 && location_flags == 0)
+    location_flags |= GTK_SOURCE_SPACE_LOCATION_ALL;
+
+  gtk_source_space_drawer_set_enable_matrix (drawer, TRUE);
+  gtk_source_space_drawer_set_types_for_locations (drawer, location_flags, type_flags);
+}
+
+void
+_ide_editor_page_init_settings (IdeEditorPage *self)
+{
+  IdeSourceView *source_view;
+  IdeBuffer *buffer;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (self->editor_settings == NULL);
+  g_assert (self->insight_settings == NULL);
+
+  source_view = ide_editor_page_get_view (self);
+  buffer = ide_editor_page_get_buffer (self);
+
+  self->editor_settings = g_settings_new ("org.gnome.builder.editor");
+
+  g_settings_bind (self->editor_settings, "highlight-current-line",
+                   source_view, "highlight-current-line",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "highlight-matching-brackets",
+                   buffer, "highlight-matching-brackets",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "show-line-changes",
+                   source_view, "show-line-changes",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "show-line-diagnostics",
+                   source_view, "show-line-diagnostics",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "show-line-numbers",
+                   source_view, "show-line-numbers",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "smart-backspace",
+                   source_view, "smart-backspace",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind_with_mapping (self->editor_settings, "smart-home-end",
+                                source_view, "smart-home-end",
+                                G_SETTINGS_BIND_GET,
+                                get_smart_home_end, NULL, NULL, NULL);
+
+  g_settings_bind (self->editor_settings, "style-scheme-name",
+                   buffer, "style-scheme-name",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "font-name",
+                   source_view, "font-name",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "overscroll",
+                   source_view, "overscroll",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "scroll-offset",
+                   source_view, "scroll-offset",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "show-grid-lines",
+                   source_view, "show-grid-lines",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind_with_mapping (self->editor_settings, "wrap-text",
+                                source_view, "wrap-mode",
+                                G_SETTINGS_BIND_GET,
+                                get_wrap_mode, NULL, NULL, NULL);
+
+  g_settings_bind (self->editor_settings, "completion-n-rows",
+                   source_view, "completion-n-rows",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "interactive-completion",
+                   source_view, "interactive-completion",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "show-map",
+                   self, "show-map",
+                   G_SETTINGS_BIND_GET);
+
+  g_settings_bind (self->editor_settings, "auto-hide-map",
+                   self, "auto-hide-map",
+                   G_SETTINGS_BIND_GET);
+
+  g_signal_connect_object (self->editor_settings,
+                           "changed::keybindings",
+                           G_CALLBACK (on_keybindings_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  on_keybindings_changed (self, "keybindings", self->editor_settings);
+
+  g_signal_connect_object (self->editor_settings,
+                           "changed::draw-spaces",
+                           G_CALLBACK (on_draw_spaces_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  on_draw_spaces_changed (self, "draw-spaces", self->editor_settings);
+
+  self->insight_settings = g_settings_new ("org.gnome.builder.code-insight");
+}
diff --git a/src/libide/editor/ide-editor-page-shortcuts.c b/src/libide/editor/ide-editor-page-shortcuts.c
new file mode 100644
index 000000000..a73e81f6f
--- /dev/null
+++ b/src/libide/editor/ide-editor-page-shortcuts.c
@@ -0,0 +1,141 @@
+/* ide-editor-page-shortcuts.c
+ *
+ * Copyright 2017-2019 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
+ */
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "ide-editor-private.h"
+
+#define I_(s) (g_intern_static_string(s))
+
+static DzlShortcutEntry editor_view_shortcuts[] = {
+  { "org.gnome.builder.editor-page.save",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Files"),
+    NC_("shortcut window", "Save the document") },
+
+  { "org.gnome.builder.editor-page.save-as",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Files"),
+    NC_("shortcut window", "Save the document with a new name") },
+
+  { "org.gnome.builder.editor-page.find",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Find and replace"),
+    NC_("shortcut window", "Find") },
+
+  { "org.gnome.builder.editor-page.find-replace",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Find and replace"),
+    NC_("shortcut window", "Find and replace") },
+
+  { "org.gnome.builder.editor-page.next-match",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Find and replace"),
+    NC_("shortcut window", "Move to the next match") },
+
+  { "org.gnome.builder.editor-page.prev-match",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Find and replace"),
+    NC_("shortcut window", "Move to the previous match") },
+
+  { "org.gnome.builder.editor-page.next-error",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Find and replace"),
+    NC_("shortcut window", "Move to the next error") },
+
+  { "org.gnome.builder.editor-page.prev-error",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Find and replace"),
+    NC_("shortcut window", "Move to the previous error") },
+};
+
+void
+_ide_editor_page_init_shortcuts (IdeEditorPage *self)
+{
+  DzlShortcutController *controller;
+
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor-page.find"),
+                                              "<Primary>f",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("editor-page.find"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor-page.find-replace"),
+                                              "<Primary>h",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("editor-page.find-replace"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor-page.next-match"),
+                                              "<Primary>g",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("editor-page.move-next-search-result"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor-page.prev-match"),
+                                              "<Primary><Shift>g",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("editor-page.move-previous-search-result"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor-page.next-error"),
+                                              "<alt>n",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("editor-page.move-next-error"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor-page.prev-error"),
+                                              "<alt>p",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("editor-page.move-previous-error"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor-page.save"),
+                                              "<Primary>s",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("editor-page.save"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor-page.save-as"),
+                                              "<Primary><Shift>s",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("editor-page.save-as"));
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             editor_view_shortcuts,
+                                             G_N_ELEMENTS (editor_view_shortcuts),
+                                             GETTEXT_PACKAGE);
+}
diff --git a/src/libide/editor/ide-editor-page.c b/src/libide/editor/ide-editor-page.c
new file mode 100644
index 000000000..e31a8b22e
--- /dev/null
+++ b/src/libide/editor/ide-editor-page.c
@@ -0,0 +1,1407 @@
+/* ide-editor-page.c
+ *
+ * Copyright 2017-2019 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 "ide-editor-page"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libpeas/peas.h>
+#include <gtksourceview/gtksource.h>
+#include <pango/pangofc-fontmap.h>
+
+#include "ide-editor-page.h"
+#include "ide-editor-page-addin.h"
+#include "ide-editor-private.h"
+#include "ide-line-change-gutter-renderer.h"
+
+#define AUTO_HIDE_TIMEOUT_SECONDS 5
+
+enum {
+  PROP_0,
+  PROP_AUTO_HIDE_MAP,
+  PROP_BUFFER,
+  PROP_SEARCH,
+  PROP_SHOW_MAP,
+  PROP_VIEW,
+  N_PROPS
+};
+
+static void ide_editor_page_update_reveal_timer (IdeEditorPage *self);
+
+G_DEFINE_TYPE (IdeEditorPage, ide_editor_page, IDE_TYPE_PAGE)
+
+DZL_DEFINE_COUNTER (instances, "Editor", "N Views", "Number of editor views");
+
+static GParamSpec *properties [N_PROPS];
+static FcConfig *localFontConfig;
+
+static void
+ide_editor_page_load_fonts (IdeEditorPage *self)
+{
+  PangoFontMap *font_map;
+  PangoFontDescription *font_desc;
+
+  if (g_once_init_enter (&localFontConfig))
+    {
+      const gchar *font_path = PACKAGE_DATADIR "/gnome-builder/fonts/BuilderBlocks.ttf";
+      FcConfig *config = FcInitLoadConfigAndFonts ();
+
+      if (g_getenv ("GB_IN_TREE_FONTS") != NULL)
+        font_path = "data/fonts/BuilderBlocks.ttf";
+
+      if (!g_file_test (font_path, G_FILE_TEST_IS_REGULAR))
+        g_warning ("Failed to locate \"%s\"", font_path);
+
+      FcConfigAppFontAddFile (config, (const FcChar8 *)font_path);
+
+      g_once_init_leave (&localFontConfig, config);
+    }
+
+  font_map = pango_cairo_font_map_new_for_font_type (CAIRO_FONT_TYPE_FT);
+  pango_fc_font_map_set_config (PANGO_FC_FONT_MAP (font_map), localFontConfig);
+  gtk_widget_set_font_map (GTK_WIDGET (self->map), font_map);
+  font_desc = pango_font_description_from_string ("Builder Blocks 1");
+
+  g_assert (localFontConfig != NULL);
+  g_assert (font_map != NULL);
+  g_assert (font_desc != NULL);
+
+  g_object_set (self->map, "font-desc", font_desc, NULL);
+
+  pango_font_description_free (font_desc);
+  g_object_unref (font_map);
+}
+
+static void
+ide_editor_page_update_icon (IdeEditorPage *self)
+{
+  g_autofree gchar *name = NULL;
+  g_autofree gchar *content_type = NULL;
+  g_autofree gchar *sniff = NULL;
+  g_autoptr(GIcon) icon = NULL;
+  GtkTextIter begin, end;
+  GFile *file;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_BUFFER (self->buffer));
+
+  /* Get first 1024 bytes to help determine content type */
+  gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end);
+  if (gtk_text_iter_get_offset (&end) > 1024)
+    gtk_text_iter_set_offset (&end, 1024);
+  sniff = gtk_text_iter_get_slice (&begin, &end);
+
+  /* Now get basename for content type */
+  file = ide_buffer_get_file (self->buffer);
+  name = g_file_get_basename (file);
+
+  /* Guess content type */
+  content_type = g_content_type_guess (name, (const guchar *)sniff, strlen (sniff), NULL);
+
+  /* Update icon to match guess */
+  icon = ide_g_content_type_get_symbolic_icon (content_type);
+  ide_page_set_icon (IDE_PAGE (self), icon);
+}
+
+static void
+ide_editor_page_buffer_notify_failed (IdeEditorPage *self,
+                                      GParamSpec    *pspec,
+                                      IdeBuffer     *buffer)
+{
+  gboolean failed;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  failed = ide_buffer_get_failed (buffer);
+
+  ide_page_set_failed (IDE_PAGE (self), failed);
+}
+
+static void
+ide_editor_page_stop_search (IdeEditorPage      *self,
+                             IdeEditorSearchBar *search_bar)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_EDITOR_SEARCH_BAR (search_bar));
+
+  gtk_revealer_set_reveal_child (self->search_revealer, FALSE);
+  gtk_widget_grab_focus (GTK_WIDGET (self->source_view));
+}
+
+static void
+ide_editor_page_notify_child_revealed (IdeEditorPage *self,
+                                       GParamSpec    *pspec,
+                                       GtkRevealer   *revealer)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (GTK_IS_REVEALER (revealer));
+
+  if (gtk_revealer_get_child_revealed (revealer))
+    {
+      GtkWidget *toplevel = gtk_widget_get_ancestor (GTK_WIDGET (revealer), GTK_TYPE_WINDOW);
+      GtkWidget *focus = gtk_window_get_focus (GTK_WINDOW (toplevel));
+
+      /* Only focus the search bar if it doesn't already have focus,
+       * as it can reselect the search text.
+       */
+      if (focus == NULL || !gtk_widget_is_ancestor (focus, GTK_WIDGET (revealer)))
+        gtk_widget_grab_focus (GTK_WIDGET (self->search_bar));
+    }
+}
+
+static gboolean
+ide_editor_page_focus_in_event (IdeEditorPage *self,
+                                GdkEventFocus *focus,
+                                IdeSourceView *source_view)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_SOURCE_VIEW (source_view));
+
+  gtk_revealer_set_reveal_child (self->search_revealer, FALSE);
+
+  ide_page_mark_used (IDE_PAGE (self));
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_editor_page_buffer_loaded (IdeEditorPage *self,
+                               IdeBuffer     *buffer)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  ide_editor_page_update_icon (self);
+
+  /* Scroll to the insertion location once the buffer
+   * has loaded. This is useful if it is not onscreen.
+   */
+  ide_source_view_scroll_to_insert (self->source_view);
+}
+
+static void
+ide_editor_page_buffer_modified_changed (IdeEditorPage *self,
+                                         IdeBuffer     *buffer)
+{
+  gboolean modified = FALSE;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (!ide_buffer_get_loading (buffer))
+    modified = gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (buffer));
+
+  ide_page_set_modified (IDE_PAGE (self), modified);
+}
+
+static void
+ide_editor_page_buffer_notify_language_cb (IdeExtensionSetAdapter *set,
+                                           PeasPluginInfo         *plugin_info,
+                                           PeasExtension          *exten,
+                                           gpointer                user_data)
+{
+  const gchar *language_id = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (exten));
+
+  ide_editor_page_addin_language_changed (IDE_EDITOR_PAGE_ADDIN (exten), language_id);
+}
+
+static void
+ide_editor_page_buffer_notify_language (IdeEditorPage *self,
+                                        GParamSpec    *pspec,
+                                        IdeBuffer     *buffer)
+{
+  const gchar *lang_id = NULL;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (self->addins == NULL)
+    return;
+
+  lang_id = ide_buffer_get_language_id (buffer);
+
+  /* Update extensions that change based on language */
+  ide_extension_set_adapter_set_value (self->addins, lang_id);
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_editor_page_buffer_notify_language_cb,
+                                     (gpointer)lang_id);
+
+  ide_editor_page_update_icon (self);
+}
+
+static void
+ide_editor_page_buffer_notify_style_scheme (IdeEditorPage *self,
+                                            GParamSpec    *pspec,
+                                            IdeBuffer     *buffer)
+{
+  g_autofree gchar *background = NULL;
+  g_autofree gchar *foreground = NULL;
+  GtkSourceStyleScheme *scheme;
+  GtkSourceStyle *style;
+  gboolean background_set = FALSE;
+  gboolean foreground_set = FALSE;
+  GdkRGBA rgba;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (NULL == (scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer))) ||
+      NULL == (style = gtk_source_style_scheme_get_style (scheme, "text")))
+    goto unset_primary_color;
+
+  g_object_get (style,
+                "background-set", &background_set,
+                "background", &background,
+                "foreground-set", &foreground_set,
+                "foreground", &foreground,
+                NULL);
+
+  if (!background_set || background == NULL || !gdk_rgba_parse (&rgba, background))
+    goto unset_primary_color;
+
+  if (background_set && background != NULL && gdk_rgba_parse (&rgba, background))
+    ide_page_set_primary_color_bg (IDE_PAGE (self), &rgba);
+  else
+    goto unset_primary_color;
+
+  if (foreground_set && foreground != NULL && gdk_rgba_parse (&rgba, foreground))
+    ide_page_set_primary_color_fg (IDE_PAGE (self), &rgba);
+  else
+    ide_page_set_primary_color_fg (IDE_PAGE (self), NULL);
+
+  return;
+
+unset_primary_color:
+  ide_page_set_primary_color_bg (IDE_PAGE (self), NULL);
+  ide_page_set_primary_color_fg (IDE_PAGE (self), NULL);
+}
+
+static void
+ide_editor_page__buffer_notify_changed_on_volume (IdeEditorPage *self,
+                                                  GParamSpec    *pspec,
+                                                  IdeBuffer     *buffer)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  gtk_revealer_set_reveal_child (self->modified_revealer,
+                                 ide_buffer_get_changed_on_volume (buffer));
+}
+
+static void
+ide_editor_page_hide_reload_bar (IdeEditorPage *self,
+                                 GtkWidget     *button)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  gtk_revealer_set_reveal_child (self->modified_revealer, FALSE);
+}
+
+static gboolean
+ide_editor_page_source_view_event (IdeEditorPage *self,
+                                   GdkEvent      *event,
+                                   IdeSourceView *source_view)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (event != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (source_view) || GTK_SOURCE_IS_MAP (source_view));
+
+  if (self->auto_hide_map)
+    {
+      ide_editor_page_update_reveal_timer (self);
+      gtk_revealer_set_reveal_child (self->map_revealer, TRUE);
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_editor_page_bind_signals (IdeEditorPage  *self,
+                              IdeBuffer      *buffer,
+                              DzlSignalGroup *buffer_signals)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (DZL_IS_SIGNAL_GROUP (buffer_signals));
+
+  ide_editor_page_buffer_modified_changed (self, buffer);
+  ide_editor_page_buffer_notify_language (self, NULL, buffer);
+  ide_editor_page_buffer_notify_style_scheme (self, NULL, buffer);
+  ide_editor_page_buffer_notify_failed (self, NULL, buffer);
+}
+
+static void
+ide_editor_page_set_buffer (IdeEditorPage *self,
+                            IdeBuffer     *buffer)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (!buffer || IDE_IS_BUFFER (buffer));
+
+  if (g_set_object (&self->buffer, buffer))
+    {
+      dzl_signal_group_set_target (self->buffer_signals, buffer);
+      dzl_binding_group_set_source (self->buffer_bindings, buffer);
+      gtk_text_view_set_buffer (GTK_TEXT_VIEW (self->source_view),
+                                GTK_TEXT_BUFFER (buffer));
+      gtk_drag_dest_unset (GTK_WIDGET (self->source_view));
+      ide_editor_page_update_icon (self);
+    }
+}
+
+static IdePage *
+ide_editor_page_create_split (IdePage *view)
+{
+  IdeEditorPage *self = (IdeEditorPage *)view;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  return g_object_new (IDE_TYPE_EDITOR_PAGE,
+                       "buffer", self->buffer,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static void
+ide_editor_page_notify_frame_set (IdeExtensionSetAdapter *set,
+                                  PeasPluginInfo         *plugin_info,
+                                  PeasExtension          *exten,
+                                  gpointer                user_data)
+{
+  IdeFrame *frame = user_data;
+  IdeEditorPageAddin *addin = (IdeEditorPageAddin *)exten;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_FRAME (frame));
+
+  ide_editor_page_addin_frame_set (addin, frame);
+}
+
+static void
+ide_editor_page_addin_added (IdeExtensionSetAdapter *set,
+                             PeasPluginInfo         *plugin_info,
+                             PeasExtension          *exten,
+                             gpointer                user_data)
+{
+  IdeEditorPage *self = user_data;
+  IdeEditorPageAddin *addin = (IdeEditorPageAddin *)exten;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  ide_editor_page_addin_load (addin, self);
+
+  /*
+   * Notify of the current frame, but refetch the frame pointer just
+   * to be sure we aren't re-using an old pointer in case we're racing
+   * with a finalizer.
+   */
+  if (self->last_frame_ptr != NULL)
+    {
+      GtkWidget *frame = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
+      if (frame != NULL)
+        ide_editor_page_addin_frame_set (addin, IDE_FRAME (frame));
+    }
+}
+
+static void
+ide_editor_page_addin_removed (IdeExtensionSetAdapter *set,
+                               PeasPluginInfo         *plugin_info,
+                               PeasExtension          *exten,
+                               gpointer                user_data)
+{
+  IdeEditorPage *self = user_data;
+  IdeEditorPageAddin *addin = (IdeEditorPageAddin *)exten;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  ide_editor_page_addin_unload (addin, self);
+}
+
+static void
+ide_editor_page_hierarchy_changed (GtkWidget *widget,
+                                   GtkWidget *old_toplevel)
+{
+  IdeEditorPage *self = (IdeEditorPage *)widget;
+  IdeFrame *frame;
+  IdeContext *context;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));
+
+  /*
+   * We don't need to chain up today, but if IdePage starts
+   * using the hierarchy_changed signal to handle anything, we want
+   * to make sure we aren't surprised.
+   */
+  if (GTK_WIDGET_CLASS (ide_editor_page_parent_class)->hierarchy_changed)
+    GTK_WIDGET_CLASS (ide_editor_page_parent_class)->hierarchy_changed (widget, old_toplevel);
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  frame = (IdeFrame *)gtk_widget_get_ancestor (widget, IDE_TYPE_FRAME);
+
+  /*
+   * We don't want to create addins until the widget has been placed into
+   * the widget tree. That way the addins can get access to the context
+   * or other useful details.
+   */
+  if (context != NULL && self->addins == NULL)
+    {
+      self->addins = ide_extension_set_adapter_new (IDE_OBJECT (context),
+                                                    peas_engine_get_default (),
+                                                    IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                                    "Editor-Page-Languages",
+                                                    ide_editor_page_get_language_id (self));
+
+      g_signal_connect (self->addins,
+                        "extension-added",
+                        G_CALLBACK (ide_editor_page_addin_added),
+                        self);
+
+      g_signal_connect (self->addins,
+                        "extension-removed",
+                        G_CALLBACK (ide_editor_page_addin_removed),
+                        self);
+
+      ide_extension_set_adapter_foreach (self->addins,
+                                         ide_editor_page_addin_added,
+                                         self);
+    }
+
+  /*
+   * If we have been moved into a new frame, notify the addins of the
+   * hierarchy change.
+   */
+  if (frame != NULL && frame != self->last_frame_ptr && self->addins != NULL)
+    {
+      self->last_frame_ptr = frame;
+      ide_extension_set_adapter_foreach (self->addins,
+                                         ide_editor_page_notify_frame_set,
+                                         frame);
+    }
+}
+
+static void
+ide_editor_page_update_map (IdeEditorPage *self)
+{
+  GtkWidget *parent;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  parent = gtk_widget_get_parent (GTK_WIDGET (self->map));
+
+  g_object_ref (self->map);
+
+  gtk_container_remove (GTK_CONTAINER (parent), GTK_WIDGET (self->map));
+
+  if (self->auto_hide_map)
+    gtk_container_add (GTK_CONTAINER (self->map_revealer), GTK_WIDGET (self->map));
+  else
+    gtk_container_add (GTK_CONTAINER (self->scroller_box), GTK_WIDGET (self->map));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->map_revealer), self->show_map && self->auto_hide_map);
+  gtk_widget_set_visible (GTK_WIDGET (self->map), self->show_map);
+  gtk_revealer_set_reveal_child (self->map_revealer, self->show_map);
+
+  ide_editor_page_update_reveal_timer (self);
+
+  g_object_unref (self->map);
+}
+
+static void
+search_revealer_notify_reveal_child (IdeEditorPage *self,
+                                     GParamSpec    *pspec,
+                                     GtkRevealer   *revealer)
+{
+  IdeCompletion *completion;
+
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+  g_return_if_fail (pspec != NULL);
+  g_return_if_fail (GTK_IS_REVEALER (revealer));
+
+  completion = ide_source_view_get_completion (IDE_SOURCE_VIEW (self->source_view));
+
+  if (!gtk_revealer_get_reveal_child (revealer))
+    {
+      ide_editor_search_end_interactive (self->search);
+
+      /* Restore completion that we blocked below. */
+      ide_completion_unblock_interactive (completion);
+    }
+  else
+    {
+      ide_editor_search_begin_interactive (self->search);
+
+      /*
+       * Block the completion while the search bar is set. It only
+       * slows things down like replace functionality. We'll
+       * restore it above when we clear state.
+       */
+      ide_completion_block_interactive (completion);
+    }
+}
+
+static void
+ide_editor_page_focus_location (IdeEditorPage *self,
+                                IdeLocation   *location,
+                                IdeSourceView *source_view)
+{
+  GtkWidget *editor;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (location != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (source_view));
+
+  editor = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_EDITOR_SURFACE);
+  ide_editor_surface_focus_location (IDE_EDITOR_SURFACE (editor), location);
+}
+
+static void
+ide_editor_page_clear_search (IdeEditorPage *self,
+                              IdeSourceView *view)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_EDITOR_SEARCH (self->search));
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  ide_editor_search_set_search_text (self->search, NULL);
+  ide_editor_search_set_visible (self->search, FALSE);
+}
+
+static void
+ide_editor_page_move_search (IdeEditorPage    *self,
+                             GtkDirectionType  dir,
+                             gboolean          extend_selection,
+                             gboolean          select_match,
+                             gboolean          exclusive,
+                             gboolean          apply_count,
+                             gboolean          at_word_boundaries,
+                             IdeSourceView    *view)
+{
+  IdeEditorSearchSelect sel = 0;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_EDITOR_SEARCH (self->search));
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  if (extend_selection && select_match)
+    sel = IDE_EDITOR_SEARCH_SELECT_WITH_RESULT;
+  else if (extend_selection)
+    sel = IDE_EDITOR_SEARCH_SELECT_TO_RESULT;
+
+  ide_editor_search_set_extend_selection (self->search, sel);
+  ide_editor_search_set_visible (self->search, TRUE);
+
+  if (apply_count)
+    {
+      ide_editor_search_set_repeat (self->search, ide_source_view_get_count (view));
+      g_signal_emit_by_name (view, "clear-count");
+    }
+
+  ide_editor_search_set_at_word_boundaries (self->search, at_word_boundaries);
+
+  switch (dir)
+    {
+    case GTK_DIR_DOWN:
+    case GTK_DIR_RIGHT:
+      ide_editor_search_set_reverse (self->search, FALSE);
+      ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT);
+      break;
+
+    case GTK_DIR_TAB_FORWARD:
+      if (extend_selection)
+        ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_FORWARD);
+      else
+        ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT);
+      break;
+
+    case GTK_DIR_UP:
+    case GTK_DIR_LEFT:
+      ide_editor_search_set_reverse (self->search, TRUE);
+      ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT);
+      break;
+
+    case GTK_DIR_TAB_BACKWARD:
+      if (extend_selection)
+        ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_BACKWARD);
+      else
+        ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_PREVIOUS);
+      break;
+
+    default:
+      break;
+    }
+}
+
+static void
+ide_editor_page_set_search_text (IdeEditorPage *self,
+                                 const gchar   *search_text,
+                                 gboolean       from_selection,
+                                 IdeSourceView *view)
+{
+  g_autofree gchar *freeme = NULL;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+  g_assert (IDE_IS_EDITOR_SEARCH (self->search));
+  g_assert (search_text != NULL || from_selection);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  /* Use interactive mode if we're copying from the clipboard, because that
+   * is usually going to be followed by focusing the search box and we want
+   * to make sure the occurrance count is updated.
+   */
+
+  if (from_selection)
+    ide_editor_search_begin_interactive (self->search);
+
+  if (from_selection)
+    {
+      if (gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (self->buffer), &begin, &end))
+        search_text = freeme = gtk_text_iter_get_slice (&begin, &end);
+    }
+
+  ide_editor_search_set_search_text (self->search, search_text);
+  ide_editor_search_set_regex_enabled (self->search, FALSE);
+
+  if (from_selection)
+    ide_editor_search_end_interactive (self->search);
+}
+
+static void
+ide_editor_page_constructed (GObject *object)
+{
+  IdeEditorPage *self = (IdeEditorPage *)object;
+  GtkSourceGutterRenderer *renderer;
+  GtkSourceGutter *gutter;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  G_OBJECT_CLASS (ide_editor_page_parent_class)->constructed (object);
+
+  gutter = gtk_source_view_get_gutter (GTK_SOURCE_VIEW (self->map), GTK_TEXT_WINDOW_LEFT);
+  renderer = g_object_new (IDE_TYPE_LINE_CHANGE_GUTTER_RENDERER,
+                           "size", 1,
+                           "visible", TRUE,
+                           NULL);
+  gtk_source_gutter_insert (gutter, renderer, 0);
+
+  _ide_editor_page_init_actions (self);
+  _ide_editor_page_init_shortcuts (self);
+  _ide_editor_page_init_settings (self);
+
+  g_signal_connect_swapped (self->source_view,
+                            "focus-in-event",
+                            G_CALLBACK (ide_editor_page_focus_in_event),
+                            self);
+
+  g_signal_connect_swapped (self->source_view,
+                            "motion-notify-event",
+                            G_CALLBACK (ide_editor_page_source_view_event),
+                            self);
+
+  g_signal_connect_swapped (self->source_view,
+                            "scroll-event",
+                            G_CALLBACK (ide_editor_page_source_view_event),
+                            self);
+
+  g_signal_connect_swapped (self->source_view,
+                            "focus-location",
+                            G_CALLBACK (ide_editor_page_focus_location),
+                            self);
+
+  g_signal_connect_swapped (self->source_view,
+                            "set-search-text",
+                            G_CALLBACK (ide_editor_page_set_search_text),
+                            self);
+
+  g_signal_connect_swapped (self->source_view,
+                            "clear-search",
+                            G_CALLBACK (ide_editor_page_clear_search),
+                            self);
+
+  g_signal_connect_swapped (self->source_view,
+                            "move-search",
+                            G_CALLBACK (ide_editor_page_move_search),
+                            self);
+
+  g_signal_connect_swapped (self->map,
+                            "motion-notify-event",
+                            G_CALLBACK (ide_editor_page_source_view_event),
+                            self);
+
+
+
+  /*
+   * We want to track when the search revealer is visible. We will discard
+   * the search context when the revealer is not visible so that we don't
+   * continue performing expensive buffer operations.
+   */
+  g_signal_connect_swapped (self->search_revealer,
+                            "notify::reveal-child",
+                            G_CALLBACK (search_revealer_notify_reveal_child),
+                            self);
+
+  self->search = ide_editor_search_new (GTK_SOURCE_VIEW (self->source_view));
+  ide_editor_search_bar_set_search (self->search_bar, self->search);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "editor-search",
+                                  G_ACTION_GROUP (self->search));
+
+  ide_editor_page_load_fonts (self);
+  ide_editor_page_update_map (self);
+}
+
+static void
+ide_editor_page_destroy (GtkWidget *widget)
+{
+  IdeEditorPage *self = (IdeEditorPage *)widget;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  /*
+   * WORKAROUND: We need to reset the drag dest to avoid warnings by Gtk
+   * reseting the target list for the source view.
+   */
+  if (self->source_view != NULL)
+    gtk_drag_dest_set (GTK_WIDGET (self->source_view),
+                       GTK_DEST_DEFAULT_ALL,
+                       NULL, 0, GDK_ACTION_COPY);
+
+  dzl_clear_source (&self->toggle_map_source);
+
+  ide_clear_and_destroy_object (&self->addins);
+
+  gtk_widget_insert_action_group (widget, "editor-search", NULL);
+  gtk_widget_insert_action_group (widget, "editor-page", NULL);
+
+  g_cancellable_cancel (self->destroy_cancellable);
+  g_clear_object (&self->destroy_cancellable);
+
+  g_clear_object (&self->search);
+  g_clear_object (&self->editor_settings);
+  g_clear_object (&self->insight_settings);
+
+  g_clear_object (&self->buffer);
+
+  if (self->buffer_bindings != NULL)
+    {
+      dzl_binding_group_set_source (self->buffer_bindings, NULL);
+      g_clear_object (&self->buffer_bindings);
+    }
+
+  if (self->buffer_signals != NULL)
+    {
+      dzl_signal_group_set_target (self->buffer_signals, NULL);
+      g_clear_object (&self->buffer_signals);
+    }
+
+  GTK_WIDGET_CLASS (ide_editor_page_parent_class)->destroy (widget);
+}
+
+static void
+ide_editor_page_finalize (GObject *object)
+{
+  G_OBJECT_CLASS (ide_editor_page_parent_class)->finalize (object);
+
+  DZL_COUNTER_DEC (instances);
+}
+
+static void
+ide_editor_page_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeEditorPage *self = IDE_EDITOR_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_AUTO_HIDE_MAP:
+      g_value_set_boolean (value, ide_editor_page_get_auto_hide_map (self));
+      break;
+
+    case PROP_BUFFER:
+      g_value_set_object (value, ide_editor_page_get_buffer (self));
+      break;
+
+    case PROP_VIEW:
+      g_value_set_object (value, ide_editor_page_get_view (self));
+      break;
+
+    case PROP_SEARCH:
+      g_value_set_object (value, ide_editor_page_get_search (self));
+      break;
+
+    case PROP_SHOW_MAP:
+      g_value_set_boolean (value, ide_editor_page_get_show_map (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_editor_page_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeEditorPage *self = IDE_EDITOR_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_AUTO_HIDE_MAP:
+      ide_editor_page_set_auto_hide_map (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_BUFFER:
+      ide_editor_page_set_buffer (self, g_value_get_object (value));
+      break;
+
+    case PROP_SHOW_MAP:
+      ide_editor_page_set_show_map (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_editor_page_class_init (IdeEditorPageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdePageClass *page_class = IDE_PAGE_CLASS (klass);
+
+  object_class->finalize = ide_editor_page_finalize;
+  object_class->constructed = ide_editor_page_constructed;
+  object_class->get_property = ide_editor_page_get_property;
+  object_class->set_property = ide_editor_page_set_property;
+
+  widget_class->destroy = ide_editor_page_destroy;
+  widget_class->hierarchy_changed = ide_editor_page_hierarchy_changed;
+
+  page_class->create_split = ide_editor_page_create_split;
+
+  properties [PROP_BUFFER] =
+    g_param_spec_object ("buffer",
+                         "Buffer",
+                         "The buffer for the view",
+                         IDE_TYPE_BUFFER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SEARCH] =
+    g_param_spec_object ("search",
+                         "Search",
+                         "An search helper for the document",
+                         IDE_TYPE_EDITOR_SEARCH,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SHOW_MAP] =
+    g_param_spec_boolean ("show-map",
+                          "Show Map",
+                          "If the overview map should be shown",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_AUTO_HIDE_MAP] =
+    g_param_spec_boolean ("auto-hide-map",
+                          "Auto Hide Map",
+                          "If the overview map should be auto-hidden",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_VIEW] =
+    g_param_spec_object ("view",
+                         "View",
+                         "The view for editing the buffer",
+                         IDE_TYPE_SOURCE_VIEW,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-editor/ui/ide-editor-page.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, map);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, map_revealer);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, overlay);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, progress_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, scroller);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, scroller_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, search_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, search_revealer);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, modified_revealer);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, modified_cancel_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorPage, source_view);
+  gtk_widget_class_bind_template_callback (widget_class, ide_editor_page_notify_child_revealed);
+  gtk_widget_class_bind_template_callback (widget_class, ide_editor_page_stop_search);
+
+  g_type_ensure (IDE_TYPE_SOURCE_VIEW);
+  g_type_ensure (IDE_TYPE_EDITOR_SEARCH_BAR);
+}
+
+static void
+ide_editor_page_init (IdeEditorPage *self)
+{
+  DZL_COUNTER_INC (instances);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  ide_page_set_can_split (IDE_PAGE (self), TRUE);
+  ide_page_set_menu_id (IDE_PAGE (self), "ide-editor-page-document-menu");
+
+  self->destroy_cancellable = g_cancellable_new ();
+
+  /* Setup signals to monitor on the buffer. */
+  self->buffer_signals = dzl_signal_group_new (IDE_TYPE_BUFFER);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "loaded",
+                                    G_CALLBACK (ide_editor_page_buffer_loaded),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "modified-changed",
+                                    G_CALLBACK (ide_editor_page_buffer_modified_changed),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::failed",
+                                    G_CALLBACK (ide_editor_page_buffer_notify_failed),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::language",
+                                    G_CALLBACK (ide_editor_page_buffer_notify_language),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::style-scheme",
+                                    G_CALLBACK (ide_editor_page_buffer_notify_style_scheme),
+                                    self);
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::changed-on-volume",
+                                    G_CALLBACK (ide_editor_page__buffer_notify_changed_on_volume),
+                                    self);
+
+  g_signal_connect_swapped (self->buffer_signals,
+                            "bind",
+                            G_CALLBACK (ide_editor_page_bind_signals),
+                            self);
+
+  g_signal_connect_object (self->modified_cancel_button,
+                           "clicked",
+                           G_CALLBACK (ide_editor_page_hide_reload_bar),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* Setup bindings for the buffer. */
+  self->buffer_bindings = dzl_binding_group_new ();
+  dzl_binding_group_bind (self->buffer_bindings, "title", self, "title", 0);
+
+  /* Load our custom font for the overview map. */
+  gtk_source_map_set_view (self->map, GTK_SOURCE_VIEW (self->source_view));
+}
+
+/**
+ * ide_editor_page_get_buffer:
+ * @self: a #IdeEditorPage
+ *
+ * Gets the underlying buffer for the view.
+ *
+ * Returns: (transfer none): An #IdeBuffer
+ *
+ * Since: 3.32
+ */
+IdeBuffer *
+ide_editor_page_get_buffer (IdeEditorPage *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);
+
+  return self->buffer;
+}
+
+/**
+ * ide_editor_page_get_view:
+ * @self: a #IdeEditorPage
+ *
+ * Gets the #IdeSourceView that is part of the #IdeEditorPage.
+ *
+ * Returns: (transfer none): An #IdeSourceView
+ *
+ * Since: 3.32
+ */
+IdeSourceView *
+ide_editor_page_get_view (IdeEditorPage *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);
+
+  return self->source_view;
+}
+
+/**
+ * ide_editor_page_get_language_id:
+ * @self: a #IdeEditorPage
+ *
+ * This is a helper to get the language-id of the underlying buffer.
+ *
+ * Returns: (nullable): the language-id as a string, or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_editor_page_get_language_id (IdeEditorPage *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);
+
+  if (self->buffer != NULL)
+    {
+      GtkSourceLanguage *language;
+
+      language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (self->buffer));
+
+      if (language != NULL)
+        return gtk_source_language_get_id (language);
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_editor_page_scroll_to_line:
+ * @self: a #IdeEditorPage
+ * @line: the line to scroll to
+ *
+ * This is a helper to quickly jump to a given line without all the frills. It
+ * will also ensure focus on the editor view, so that refocusing the view
+ * afterwards does not cause the view to restore the cursor to the previous
+ * location.
+ *
+ * This will move the insert cursor.
+ *
+ * Lines start from 0.
+ *
+ * Since: 3.32
+ */
+void
+ide_editor_page_scroll_to_line (IdeEditorPage *self,
+                                guint          line)
+{
+  ide_editor_page_scroll_to_line_offset (self, line, 0);
+}
+
+/**
+ * ide_editor_page_scroll_to_line_offset:
+ * @self: a #IdeEditorPage
+ * @line: the line to scroll to
+ * @line_offset: the line offset
+ *
+ * Like ide_editor_page_scroll_to_line() but allows specifying the
+ * line offset (column) to place the cursor on.
+ *
+ * This will move the insert cursor.
+ *
+ * Lines and offsets start from 0.
+ *
+ * If @line_offset is zero, the first non-space character of @line will be
+ * used instead.
+ *
+ * Since: 3.32
+ */
+void
+ide_editor_page_scroll_to_line_offset (IdeEditorPage *self,
+                                       guint          line,
+                                       guint          line_offset)
+{
+  GtkTextIter iter;
+
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+  g_return_if_fail (self->buffer != NULL);
+  g_return_if_fail (line <= G_MAXINT);
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->source_view));
+
+  gtk_text_buffer_get_iter_at_line_offset (GTK_TEXT_BUFFER (self->buffer), &iter,
+                                           line, line_offset);
+
+  if (line_offset == 0)
+    {
+      while (!gtk_text_iter_ends_line (&iter) &&
+             g_unichar_isspace (gtk_text_iter_get_char (&iter)))
+        {
+          if (!gtk_text_iter_forward_char (&iter))
+            break;
+        }
+    }
+
+  gtk_text_buffer_select_range (GTK_TEXT_BUFFER (self->buffer), &iter, &iter);
+  ide_source_view_scroll_to_insert (self->source_view);
+}
+
+gboolean
+ide_editor_page_get_auto_hide_map (IdeEditorPage *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), FALSE);
+
+  return self->auto_hide_map;
+}
+
+static gboolean
+ide_editor_page_auto_hide_cb (gpointer user_data)
+{
+  IdeEditorPage *self = user_data;
+
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  self->toggle_map_source = 0;
+  gtk_revealer_set_reveal_child (self->map_revealer, FALSE);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_editor_page_update_reveal_timer (IdeEditorPage *self)
+{
+  g_assert (IDE_IS_EDITOR_PAGE (self));
+
+  dzl_clear_source (&self->toggle_map_source);
+
+  if (self->auto_hide_map && gtk_revealer_get_reveal_child (self->map_revealer))
+    {
+      self->toggle_map_source =
+        gdk_threads_add_timeout_seconds_full (G_PRIORITY_LOW,
+                                              AUTO_HIDE_TIMEOUT_SECONDS,
+                                              ide_editor_page_auto_hide_cb,
+                                              g_object_ref (self),
+                                              g_object_unref);
+    }
+}
+
+void
+ide_editor_page_set_auto_hide_map (IdeEditorPage *self,
+                                   gboolean       auto_hide_map)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+
+  auto_hide_map = !!auto_hide_map;
+
+  if (auto_hide_map != self->auto_hide_map)
+    {
+      self->auto_hide_map = auto_hide_map;
+      ide_editor_page_update_map (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_AUTO_HIDE_MAP]);
+    }
+}
+
+gboolean
+ide_editor_page_get_show_map (IdeEditorPage *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), FALSE);
+
+  return self->show_map;
+}
+
+void
+ide_editor_page_set_show_map (IdeEditorPage *self,
+                              gboolean       show_map)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+
+  show_map = !!show_map;
+
+  if (show_map != self->show_map)
+    {
+      self->show_map = show_map;
+      g_object_set (self->scroller,
+                    "vscrollbar-policy", show_map ? GTK_POLICY_EXTERNAL : GTK_POLICY_AUTOMATIC,
+                    NULL);
+      ide_editor_page_update_map (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_AUTO_HIDE_MAP]);
+    }
+}
+
+/**
+ * ide_editor_page_set_language:
+ * @self: a #IdeEditorPage
+ *
+ * This is a convenience function to set the language on the underlying
+ * #IdeBuffer text buffer.
+ *
+ * Since: 3.32
+ */
+void
+ide_editor_page_set_language (IdeEditorPage     *self,
+                              GtkSourceLanguage *language)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+  g_return_if_fail (!language || GTK_SOURCE_IS_LANGUAGE (language));
+
+  gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (self->buffer), language);
+}
+
+/**
+ * ide_editor_page_get_language:
+ * @self: a #IdeEditorPage
+ *
+ * Gets the #GtkSourceLanguage that is used by the underlying buffer.
+ *
+ * Returns: (transfer none) (nullable): a #GtkSourceLanguage or %NULL.
+ *
+ * Since: 3.32
+ */
+GtkSourceLanguage *
+ide_editor_page_get_language (IdeEditorPage *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);
+
+  return gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (self->buffer));
+}
+
+/**
+ * ide_editor_page_move_next_error:
+ * @self: a #IdeEditorPage
+ *
+ * Moves to the next error, if any.
+ *
+ * If there is no error, the insertion cursor is not moved.
+ *
+ * Since: 3.32
+ */
+void
+ide_editor_page_move_next_error (IdeEditorPage *self)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+
+  g_signal_emit_by_name (self->source_view, "move-error", GTK_DIR_DOWN);
+}
+
+/**
+ * ide_editor_page_move_previous_error:
+ * @self: a #IdeEditorPage
+ *
+ * Moves the insertion cursor to the previous error.
+ *
+ * If there is no error, the insertion cursor is not moved.
+ *
+ * Since: 3.32
+ */
+void
+ide_editor_page_move_previous_error (IdeEditorPage *self)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+
+  g_signal_emit_by_name (self->source_view, "move-error", GTK_DIR_UP);
+}
+
+/**
+ * ide_editor_page_move_next_search_result:
+ * @self: a #IdeEditorPage
+ *
+ * Moves the insertion cursor to the next search result.
+ *
+ * If there is no search result, the insertion cursor is not moved.
+ *
+ * Since: 3.32
+ */
+void
+ide_editor_page_move_next_search_result (IdeEditorPage *self)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+  g_return_if_fail (self->destroy_cancellable != NULL);
+  g_return_if_fail (self->buffer != NULL);
+
+  ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_NEXT);
+}
+
+/**
+ * ide_editor_page_move_previous_search_result:
+ * @self: a #IdeEditorPage
+ *
+ * Moves the insertion cursor to the previous search result.
+ *
+ * If there is no search result, the insertion cursor is not moved.
+ *
+ * Since: 3.32
+ */
+void
+ide_editor_page_move_previous_search_result (IdeEditorPage *self)
+{
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (self));
+  g_return_if_fail (self->destroy_cancellable != NULL);
+  g_return_if_fail (self->buffer != NULL);
+
+  ide_editor_search_move (self->search, IDE_EDITOR_SEARCH_PREVIOUS);
+}
+
+/**
+ * ide_editor_page_get_search:
+ * @self: a #IdeEditorPage
+ *
+ * Gets the #IdeEditorSearch used to search within the document.
+ *
+ * Returns: (transfer none): An #IdeEditorSearch
+ *
+ * Since: 3.32
+ */
+IdeEditorSearch *
+ide_editor_page_get_search (IdeEditorPage *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);
+
+  return self->search;
+}
+
+/**
+ * ide_editor_page_get_file:
+ * @self: a #IdeEditorPage
+ *
+ * Gets the #GFile that represents the current file. This may be a temporary
+ * file, but a #GFile will still be used for the temporary file.
+ *
+ * Returns: (transfer none): a #GFile for the current buffer
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_editor_page_get_file (IdeEditorPage *self)
+{
+  IdeBuffer *buffer;
+
+  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (self), NULL);
+
+  if ((buffer = ide_editor_page_get_buffer (self)))
+    return ide_buffer_get_file (buffer);
+
+  return NULL;
+}
diff --git a/src/libide/editor/ide-editor-page.h b/src/libide/editor/ide-editor-page.h
new file mode 100644
index 000000000..e47c9bfd9
--- /dev/null
+++ b/src/libide/editor/ide-editor-page.h
@@ -0,0 +1,82 @@
+/* ide-editor-page.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_EDITOR_INSIDE) && !defined (IDE_EDITOR_COMPILATION)
+# error "Only <libide-editor.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+#include <libide-core.h>
+#include <libide-gui.h>
+#include <libide-sourceview.h>
+
+#include "ide-editor-search.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_EDITOR_PAGE (ide_editor_page_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeEditorPage, ide_editor_page, IDE, EDITOR_PAGE, IdePage)
+
+IDE_AVAILABLE_IN_3_32
+GFile             *ide_editor_page_get_file                    (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+IdeBuffer         *ide_editor_page_get_buffer                  (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+IdeSourceView     *ide_editor_page_get_view                    (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+IdeEditorSearch   *ide_editor_page_get_search                  (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+const gchar       *ide_editor_page_get_language_id             (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_editor_page_scroll_to_line              (IdeEditorPage     *self,
+                                                                guint              line);
+IDE_AVAILABLE_IN_3_32
+void               ide_editor_page_scroll_to_line_offset       (IdeEditorPage     *self,
+                                                                guint              line,
+                                                                guint              line_offset);
+IDE_AVAILABLE_IN_3_32
+gboolean           ide_editor_page_get_auto_hide_map           (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_editor_page_set_auto_hide_map           (IdeEditorPage     *self,
+                                                                gboolean           auto_hide_map);
+IDE_AVAILABLE_IN_3_32
+gboolean           ide_editor_page_get_show_map                (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_editor_page_set_show_map                (IdeEditorPage     *self,
+                                                                gboolean           show_map);
+IDE_AVAILABLE_IN_3_32
+GtkSourceLanguage *ide_editor_page_get_language                (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_editor_page_set_language                (IdeEditorPage     *self,
+                                                                GtkSourceLanguage *language);
+IDE_AVAILABLE_IN_3_32
+void               ide_editor_page_move_next_error             (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_editor_page_move_previous_error         (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_editor_page_move_next_search_result     (IdeEditorPage     *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_editor_page_move_previous_search_result (IdeEditorPage     *self);
+
+G_END_DECLS
diff --git a/src/libide/editor/ide-editor-page.ui b/src/libide/editor/ide-editor-page.ui
new file mode 100644
index 000000000..f62c387c3
--- /dev/null
+++ b/src/libide/editor/ide-editor-page.ui
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeEditorPage" parent="IdePage">
+    <child>
+      <object class="GtkOverlay" id="overlay">
+        <property name="visible">true</property>
+        <child type="overlay">
+          <object class="GtkRevealer" id="search_revealer">
+            <property name="width-request">525</property>
+            <property name="halign">end</property>
+            <property name="valign">start</property>
+            <property name="margin-right">12</property>
+            <property name="reveal-child">false</property>
+            <property name="visible">true</property>
+            <signal name="notify::child-revealed" handler="ide_editor_page_notify_child_revealed" 
swapped="true" object="IdeEditorPage"/>
+            <child>
+              <object class="IdeEditorSearchBar" id="search_bar">
+                <property name="visible">true</property>
+                <signal name="stop-search" handler="ide_editor_page_stop_search" swapped="true" 
object="IdeEditorPage"/>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="index">1</property>
+          </packing>
+        </child>
+        <child type="overlay">
+          <object class="GtkRevealer" id="modified_revealer">
+            <property name="halign">fill</property>
+            <property name="valign">start</property>
+            <property name="visible">true</property>
+            <property name="reveal-child">false</property>
+            <child>
+              <object class="GtkInfoBar">
+                <property name="visible">true</property>
+                <child internal-child="action_area">
+                  <object class="GtkButtonBox">
+                    <property name="spacing">6</property>
+                    <property name="layout_style">end</property>
+                    <child>
+                      <object class="GtkButton">
+                        <property name="action-name">editor-view.reload</property>
+                        <property name="label" translatable="yes">_Reload</property>
+                        <property name="visible">true</property>
+                        <property name="receives_default">true</property>
+                        <property name="use_underline">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="modified_cancel_button">
+                        <property name="label" translatable="yes">_Cancel</property>
+                        <property name="visible">true</property>
+                        <property name="use_underline">true</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child internal-child="content_area">
+                  <object class="GtkBox">
+                    <property name="spacing">16</property>
+                    <child>
+                      <object class="GtkLabel" id="modified_label">
+                        <property name="hexpand">true</property>
+                        <property name="label" translatable="yes">Builder has discovered that this file has 
been modified externally. Would you like to reload the file?</property>
+                        <property name="visible">true</property>
+                        <property name="wrap">true</property>
+                        <property name="xalign">0</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="overlay">
+          <object class="GtkProgressBar" id="progress_bar">
+            <property name="hexpand">true</property>
+            <property name="valign">start</property>
+            <style>
+              <class name="osd"/>
+            </style>
+          </object>
+        </child>
+        <child type="overlay">
+          <object class="GtkRevealer" id="map_revealer">
+            <property name="halign">end</property>
+            <property name="transition-type">slide-left</property>
+            <property name="vexpand">true</property>
+          </object>
+          <packing>
+            <property name="index">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="scroller_box">
+            <property name="orientation">horizontal</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkScrolledWindow" id="scroller">
+                <property name="resize-mode">queue</property>
+                <property name="expand">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="IdeSourceView" id="source_view">
+                    <property name="auto-indent">true</property>
+                    <property name="show-line-changes">true</property>
+                    <property name="show-line-numbers">true</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkSourceMap" id="map">
+                <property name="visible">false</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/editor/ide-editor-plugin-private.h b/src/libide/editor/ide-editor-plugin-private.h
new file mode 100644
index 000000000..c86344740
--- /dev/null
+++ b/src/libide/editor/ide-editor-plugin-private.h
@@ -0,0 +1,27 @@
+/* ide-editor-plugin-private.h
+ *
+ * Copyright 2018-2019 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-core.h>
+#include <libpeas/peas.h>
+
+IDE_AVAILABLE_IN_3_32
+void _ide_editor_register_types (PeasObjectModule *module);
diff --git a/src/libide/editor/ide-editor-print-operation.c b/src/libide/editor/ide-editor-print-operation.c
index 751db2a27..dbe895b3b 100644
--- a/src/libide/editor/ide-editor-print-operation.c
+++ b/src/libide/editor/ide-editor-print-operation.c
@@ -14,6 +14,8 @@
  *
  * 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 "ide-editor-print-operation"
@@ -23,8 +25,8 @@
 #include <glib/gi18n.h>
 #include <gtksourceview/gtksource.h>
 
-#include "editor/ide-editor-print-operation.h"
-#include "editor/ide-editor-view.h"
+#include "ide-editor-print-operation.h"
+#include "ide-editor-page.h"
 
 struct _IdeEditorPrintOperation
 {
diff --git a/src/libide/editor/ide-editor-print-operation.h b/src/libide/editor/ide-editor-print-operation.h
index d59d09c9c..3e95e8388 100644
--- a/src/libide/editor/ide-editor-print-operation.h
+++ b/src/libide/editor/ide-editor-print-operation.h
@@ -14,11 +14,13 @@
  *
  * 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 "editor/ide-editor-view.h"
+#include "ide-editor-page.h"
 
 G_BEGIN_DECLS
 
@@ -26,6 +28,6 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeEditorPrintOperation, ide_editor_print_operation, IDE, EDITOR_PRINT_OPERATION, 
GtkPrintOperation)
 
-IdeEditorPrintOperation  *ide_editor_print_operation_new    (IdeSourceView *view);
+IdeEditorPrintOperation *ide_editor_print_operation_new (IdeSourceView *view);
 
 G_END_DECLS
diff --git a/src/libide/editor/ide-editor-private.h b/src/libide/editor/ide-editor-private.h
index 4551f48cd..1a3a3996e 100644
--- a/src/libide/editor/ide-editor-private.h
+++ b/src/libide/editor/ide-editor-private.h
@@ -1,6 +1,6 @@
 /* ide-editor-private.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,36 +14,35 @@
  *
  * 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 <dazzle.h>
+#include <libide-gui.h>
+#include <libide-plugins.h>
+#include <libide-sourceview.h>
 #include <libpeas/peas.h>
 
-#include "editor/ide-editor-perspective.h"
-#include "editor/ide-editor-properties.h"
-#include "editor/ide-editor-search.h"
-#include "editor/ide-editor-search-bar.h"
-#include "editor/ide-editor-sidebar.h"
-#include "editor/ide-editor-view-addin.h"
-#include "editor/ide-editor-view.h"
-#include "layout/ide-layout-grid.h"
-#include "layout/ide-layout-view.h"
-#include "plugins/ide-extension-set-adapter.h"
+#include "ide-editor-addin.h"
+#include "ide-editor-page.h"
+#include "ide-editor-search-bar.h"
+#include "ide-editor-search.h"
+#include "ide-editor-sidebar.h"
+#include "ide-editor-surface.h"
 
 G_BEGIN_DECLS
 
-struct _IdeEditorPerspective
+struct _IdeEditorSurface
 {
-  IdeLayout            parent_instance;
+  IdeSurface           parent_instance;
 
   PeasExtensionSet    *addins;
 
   /* Template widgets */
-  IdeLayoutGrid       *grid;
+  IdeGrid             *grid;
   GtkOverlay          *overlay;
-  IdeEditorProperties *properties;
   GtkStack            *loading_stack;
 
   /* State before entering focus mode */
@@ -51,9 +50,9 @@ struct _IdeEditorPerspective
   guint                prefocus_had_bottom : 1;
 };
 
-struct _IdeEditorView
+struct _IdeEditorPage
 {
-  IdeLayoutView            parent_instance;
+  IdePage                  parent_instance;
 
   IdeExtensionSetAdapter  *addins;
 
@@ -80,8 +79,8 @@ struct _IdeEditorView
   GtkRevealer             *modified_revealer;
   GtkButton               *modified_cancel_button;
 
-  /* Raw pointer used to determine when stack changes */
-  IdeLayoutStack          *last_stack_ptr;
+  /* Raw pointer used to determine when frame changes */
+  IdeFrame                *last_frame_ptr;
 
   guint                    toggle_map_source;
 
@@ -89,18 +88,16 @@ struct _IdeEditorView
   guint                    show_map : 1;
 };
 
-void _ide_editor_view_init_actions           (IdeEditorView        *self);
-void _ide_editor_view_init_settings          (IdeEditorView        *self);
-void _ide_editor_view_init_shortcuts         (IdeEditorView        *self);
-void _ide_editor_view_update_actions         (IdeEditorView        *self);
-void _ide_editor_search_bar_init_shortcuts   (IdeEditorSearchBar   *self);
-void _ide_editor_sidebar_set_open_pages      (IdeEditorSidebar     *self,
-                                              GListModel           *open_pages);
-void _ide_editor_perspective_show_properties (IdeEditorPerspective *self,
-                                              IdeEditorView        *view);
-void _ide_editor_perspective_set_loading     (IdeEditorPerspective *self,
-                                              gboolean              loading);
-void _ide_editor_perspective_init_actions    (IdeEditorPerspective *self);
-void _ide_editor_perspective_init_shortcuts  (IdeEditorPerspective *self);
+void _ide_editor_page_init_actions         (IdeEditorPage      *self);
+void _ide_editor_page_init_settings        (IdeEditorPage      *self);
+void _ide_editor_page_init_shortcuts       (IdeEditorPage      *self);
+void _ide_editor_page_update_actions       (IdeEditorPage      *self);
+void _ide_editor_search_bar_init_shortcuts (IdeEditorSearchBar *self);
+void _ide_editor_sidebar_set_open_pages    (IdeEditorSidebar   *self,
+                                            GListModel         *open_pages);
+void _ide_editor_surface_set_loading       (IdeEditorSurface   *self,
+                                            gboolean            loading);
+void _ide_editor_surface_init_actions      (IdeEditorSurface   *self);
+void _ide_editor_surface_init_shortcuts    (IdeEditorSurface   *self);
 
 G_END_DECLS
diff --git a/src/libide/editor/ide-editor-search-bar-shortcuts.c 
b/src/libide/editor/ide-editor-search-bar-shortcuts.c
index a3c9a30b0..af5e5a197 100644
--- a/src/libide/editor/ide-editor-search-bar-shortcuts.c
+++ b/src/libide/editor/ide-editor-search-bar-shortcuts.c
@@ -1,6 +1,6 @@
 /* ide-editor-search-bar-shortcuts.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,14 +14,16 @@
  *
  * 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 "ide-editor-search-bar-shortcuts"
 
 #include "config.h"
 
-#include "editor/ide-editor-private.h"
-#include "editor/ide-editor-search-bar.h"
+#include "ide-editor-private.h"
+#include "ide-editor-search-bar.h"
 
 static void
 ide_editor_search_bar_shortcuts_activate_previous (GtkWidget *widget,
diff --git a/src/libide/editor/ide-editor-search-bar.c b/src/libide/editor/ide-editor-search-bar.c
index 5bdf40caf..dbabceba3 100644
--- a/src/libide/editor/ide-editor-search-bar.c
+++ b/src/libide/editor/ide-editor-search-bar.c
@@ -1,6 +1,6 @@
 /* ide-editor-search-bar.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-editor-search-bar"
@@ -23,10 +25,9 @@
 #include <dazzle.h>
 #include <glib/gi18n.h>
 
-#include "editor/ide-editor-private.h"
-#include "editor/ide-editor-search.h"
-#include "editor/ide-editor-search-bar.h"
-#include "search/ide-tagged-entry.h"
+#include "ide-editor-private.h"
+#include "ide-editor-search.h"
+#include "ide-editor-search-bar.h"
 
 struct _IdeEditorSearchBar
 {
@@ -445,7 +446,7 @@ ide_editor_search_bar_class_init (IdeEditorSearchBarClass *klass)
                                 g_cclosure_marshal_VOID__VOID,
                                 G_TYPE_NONE, 0);
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-editor-search-bar.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-editor/ui/ide-editor-search-bar.ui");
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, case_sensitive);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, replace_all_button);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSearchBar, replace_button);
@@ -565,6 +566,8 @@ ide_editor_search_bar_set_show_options (IdeEditorSearchBar *self,
  * Gets the #IdeEditorSearch used by the search bar.
  *
  * Returns: (transfer none) (nullable): An #IdeEditorSearch or %NULL.
+ *
+ * Since: 3.32
  */
 IdeEditorSearch *
 ide_editor_search_bar_get_search (IdeEditorSearchBar *self)
diff --git a/src/libide/editor/ide-editor-search-bar.h b/src/libide/editor/ide-editor-search-bar.h
index fe3e6da72..034b36381 100644
--- a/src/libide/editor/ide-editor-search-bar.h
+++ b/src/libide/editor/ide-editor-search-bar.h
@@ -1,6 +1,6 @@
 /* ide-editor-search-bar.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
@@ -21,7 +23,7 @@
 #include <dazzle.h>
 #include <gtksourceview/gtksource.h>
 
-#include "editor/ide-editor-search.h"
+#include "ide-editor-search.h"
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/editor/ide-editor-search.c b/src/libide/editor/ide-editor-search.c
index 3276dc827..d621fa981 100644
--- a/src/libide/editor/ide-editor-search.c
+++ b/src/libide/editor/ide-editor-search.c
@@ -1,6 +1,6 @@
 /* ide-editor-search.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-editor-search"
@@ -21,10 +23,10 @@
 #include "config.h"
 
 #include <dazzle.h>
+#include <libide-sourceview.h>
 #include <string.h>
 
-#include "editor/ide-editor-search.h"
-#include "sourceview/ide-source-view.h"
+#include "ide-editor-search.h"
 
 /**
  * SECTION:ide-editor-search
@@ -42,7 +44,7 @@
  * Additionally, it provides an addin layer to highlight similar words
  * when then buffer selection changes.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 
 struct _IdeEditorSearch
@@ -94,20 +96,23 @@ enum {
   N_PROPS
 };
 
-static void ide_editor_search_actions_move_next     (IdeEditorSearch *self,
-                                                     GVariant        *param);
-static void ide_editor_search_actions_move_previous (IdeEditorSearch *self,
-                                                     GVariant        *param);
-static void ide_editor_search_actions_replace       (IdeEditorSearch *self,
-                                                     GVariant        *param);
-static void ide_editor_search_actions_replace_all   (IdeEditorSearch *self,
-                                                     GVariant        *param);
+static void ide_editor_search_actions_move_next        (IdeEditorSearch *self,
+                                                        GVariant        *param);
+static void ide_editor_search_actions_move_previous    (IdeEditorSearch *self,
+                                                        GVariant        *param);
+static void ide_editor_search_actions_replace          (IdeEditorSearch *self,
+                                                        GVariant        *param);
+static void ide_editor_search_actions_replace_all      (IdeEditorSearch *self,
+                                                        GVariant        *param);
+static void ide_editor_search_actions_at_word_boundary (IdeEditorSearch *self,
+                                                        GVariant        *param);
 
 DZL_DEFINE_ACTION_GROUP (IdeEditorSearch, ide_editor_search, {
   { "move-next", ide_editor_search_actions_move_next },
   { "move-previous", ide_editor_search_actions_move_previous },
   { "replace", ide_editor_search_actions_replace },
   { "replace-all", ide_editor_search_actions_replace_all },
+  { "at-word-boundaries", ide_editor_search_actions_at_word_boundary, "b" },
 })
 
 G_DEFINE_TYPE_WITH_CODE (IdeEditorSearch, ide_editor_search, G_TYPE_OBJECT,
@@ -536,7 +541,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * The "active" property is %TRUE when their is an active search
    * in progress.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_ACTIVE] =
     g_param_spec_boolean ("active", NULL, NULL,
@@ -550,7 +555,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * is being searched. This must be set when creating the
    * #IdeEditorSearch and may not be changed after construction.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_VIEW] =
     g_param_spec_object ("view", NULL,  NULL,
@@ -563,7 +568,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * The "at-word-boundaries" property specifies if the search-text must
    * only be matched starting from the beginning of a word.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_AT_WORD_BOUNDARIES] =
     g_param_spec_boolean ("at-word-boundaries", NULL, NULL,
@@ -576,7 +581,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * The "case-sensitive" property specifies if the search text should
    * be case sensitive.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_CASE_SENSITIVE] =
     g_param_spec_boolean ("case-sensitive", NULL, NULL,
@@ -590,7 +595,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * the editor should be extended as the user navigates between search
    * results.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_EXTEND_SELECTION] =
     g_param_spec_enum ("extend-selection",
@@ -611,7 +616,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * ide_editor_search_begin_interactive() and before calling
    * ide_editor_search_end_interactive().
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_MATCH_COUNT] =
     g_param_spec_uint ("match-count", NULL, NULL,
@@ -627,7 +632,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * This value starts from 1, and 0 indicates that the insertion cursor
    * is not placed within the a search result.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_MATCH_POSITION] =
     g_param_spec_uint ("match-position", NULL, NULL,
@@ -643,7 +648,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    *
    * This property will be cleared after an attempt to move.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_REPEAT] =
     g_param_spec_uint ("repeat", NULL, NULL,
@@ -658,7 +663,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * user to search using common regex values such as "foo.*bar". It
    * also allows for capture groups to be used in replacement text.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_REGEX_ENABLED] =
     g_param_spec_boolean ("regex-enabled", NULL, NULL,
@@ -675,7 +680,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * If #IdeEditorSearch:regex-enabled is %TRUE, then the user may use
    * references to capture groups specified in #IdeEditorSearch:search-text.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_REPLACEMENT_TEXT] =
     g_param_spec_string ("replacement-text", NULL, NULL, NULL,
@@ -687,7 +692,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * The "reverse" property determines if relative directions should be
    * switched, so next is backward, and previous is forward.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_REVERSE] =
     g_param_spec_boolean ("reverse", NULL, NULL, FALSE,
@@ -703,7 +708,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * buffer. They may also specify capture groups to use in search and
    * replace.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_SEARCH_TEXT] =
     g_param_spec_string ("search-text", NULL, NULL, NULL,
@@ -720,7 +725,7 @@ ide_editor_search_class_init (IdeEditorSearchClass *klass)
    * However, some cases, such as Vim search movements, may want to show
    * the search highlights, but are not within an interactive search.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   properties [PROP_VISIBLE] =
     g_param_spec_string ("visible", NULL, NULL, FALSE,
@@ -776,7 +781,7 @@ ide_editor_search_init (IdeEditorSearch *self)
  *
  * Returns: (transfer full): A new #IdeEditorSearch instance
  *
- * Since: 3.28
+ * Since: 3.32
  */
 IdeEditorSearch *
 ide_editor_search_new (GtkSourceView *view)
@@ -887,7 +892,7 @@ ide_editor_search_release_context (IdeEditorSearch *self)
  * @case_sensitive: %TRUE if the search should be case-sensitive
  *
  * See also: #GtkSourceSearchSettings:case-sensitive
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_set_case_sensitive (IdeEditorSearch *self,
@@ -907,7 +912,7 @@ ide_editor_search_set_case_sensitive (IdeEditorSearch *self,
  * Returns: %TRUE if the search is case-sensitive.
  *
  * See also: #GtkSourceSearchSettings:case-sensitive
- * Since: 3.28
+ * Since: 3.32
  */
 gboolean
 ide_editor_search_get_case_sensitive (IdeEditorSearch *self)
@@ -961,7 +966,7 @@ ide_editor_search_scan_forward_cb (GObject      *object,
  *
  * See also: #GtkSourceSearchSettings:search-text
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_set_search_text (IdeEditorSearch *self,
@@ -1017,7 +1022,7 @@ ide_editor_search_set_search_text (IdeEditorSearch *self,
  *
  * Returns: (nullable): The search text or %NULL
  *
- * Since: 3.28
+ * Since: 3.32
  */
 const gchar *
 ide_editor_search_get_search_text (IdeEditorSearch *self)
@@ -1039,7 +1044,7 @@ ide_editor_search_get_search_text (IdeEditorSearch *self)
  * Returns: %TRUE if the search text contains invalid content. If %TRUE,
  *   then @invalid_begin and @invalid_end is set.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 gboolean
 ide_editor_search_get_search_text_invalid (IdeEditorSearch  *self,
@@ -1120,7 +1125,7 @@ ide_editor_search_get_search_text_invalid (IdeEditorSearch  *self,
  * This will allow the user to still make search movements based on the
  * previous search request, and re-enable visibility upon doing so.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_set_visible (IdeEditorSearch *self,
@@ -1148,7 +1153,7 @@ ide_editor_search_set_visible (IdeEditorSearch *self,
  *
  * Returns: %TRUE if the current search should be highlighted.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 gboolean
 ide_editor_search_get_visible (IdeEditorSearch *self)
@@ -1165,7 +1170,7 @@ ide_editor_search_get_visible (IdeEditorSearch *self)
  *
  * See also: #GtkSourceSearchSettings:regex-enabled
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_set_regex_enabled (IdeEditorSearch *self,
@@ -1188,7 +1193,7 @@ ide_editor_search_set_regex_enabled (IdeEditorSearch *self,
  *
  * Returns: %TRUE if search text can use regex
  *
- * Since: 3.28
+ * Since: 3.32
  */
 gboolean
 ide_editor_search_get_regex_enabled (IdeEditorSearch *self)
@@ -1210,7 +1215,7 @@ ide_editor_search_get_regex_enabled (IdeEditorSearch *self)
  * If #IdeEditorSearch:regex-enabled is set, then you may reference
  * regex groups from the regex in #IdeEditorSearch:search-text.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_set_replacement_text (IdeEditorSearch *self,
@@ -1236,7 +1241,7 @@ ide_editor_search_set_replacement_text (IdeEditorSearch *self,
  *
  * Returns: (nullable): the replacement text, or %NULL
  *
- * Since: 3.28
+ * Since: 3.32
  */
 const gchar *
 ide_editor_search_get_replacement_text (IdeEditorSearch *self)
@@ -1253,7 +1258,7 @@ ide_editor_search_get_replacement_text (IdeEditorSearch *self)
  *
  * See also: gtk_source_search_settings_set_word_boundaries()
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_set_at_word_boundaries (IdeEditorSearch *self,
@@ -1275,7 +1280,7 @@ ide_editor_search_set_at_word_boundaries (IdeEditorSearch *self,
  * Returns: %TRUE if the search should only match word boundaries.
  *
  * See also: #GtkSourceSearchSettings:at-word-boundaries
- * Since: 3.28
+ * Since: 3.32
  */
 gboolean
 ide_editor_search_get_at_word_boundaries (IdeEditorSearch *self)
@@ -1292,7 +1297,7 @@ ide_editor_search_get_at_word_boundaries (IdeEditorSearch *self)
  * Gets the number of matches currently found in the editor. This
  * will update as new matches are found while scanning the buffer.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 guint
 ide_editor_search_get_match_count (IdeEditorSearch *self)
@@ -1310,7 +1315,7 @@ ide_editor_search_get_match_count (IdeEditorSearch *self)
  * cursor is within a match, this will be a 1-based index
  * will update as new matches are found while scanning the buffer.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 guint
 ide_editor_search_get_match_position (IdeEditorSearch *self)
@@ -1408,7 +1413,7 @@ ide_editor_search_backward_cb (GObject      *object,
   g_assert (GTK_SOURCE_IS_SEARCH_CONTEXT (context));
   g_assert (IDE_IS_EDITOR_SEARCH (self));
 
-  if (gtk_source_search_context_backward_finish (context, result, &begin, &end, NULL, NULL))
+  if (gtk_source_search_context_forward_finish (context, result, &begin, &end, NULL, NULL))
     {
       if (self->view != NULL)
         {
@@ -1521,7 +1526,7 @@ maybe_flip_selection_bounds (IdeEditorSearch *self,
  * automatically wrap around to the end of the buffer once the beginning
  * of the buffer has been reached.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_move (IdeEditorSearch          *self,
@@ -1614,7 +1619,7 @@ ide_editor_search_move (IdeEditorSearch          *self,
  * Replaces the next occurrance of a search result with the
  * value of #IdeEditorSearch:replacement-text.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_replace (IdeEditorSearch *self)
@@ -1653,7 +1658,7 @@ ide_editor_search_replace (IdeEditorSearch *self)
  * Replaces all the occurrances of #IdeEditorSearch:search-text with the
  * value of #IdeEditorSearch:replacement-text.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_replace_all (IdeEditorSearch *self)
@@ -1684,7 +1689,7 @@ ide_editor_search_replace_all (IdeEditorSearch *self)
  * result automatically, and then snap back to the previous location if
  * the search is aborted.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_begin_interactive (IdeEditorSearch *self)
@@ -1721,7 +1726,7 @@ ide_editor_search_begin_interactive (IdeEditorSearch *self)
  * as it might allow the editor to restore positioning back to the
  * previous editor location from before the interactive search began.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_end_interactive (IdeEditorSearch *self)
@@ -1752,7 +1757,7 @@ ide_editor_search_end_interactive (IdeEditorSearch *self)
  *
  * Returns: %TRUE if relative movements are reversed directions.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 gboolean
 ide_editor_search_get_reverse (IdeEditorSearch *self)
@@ -1774,7 +1779,7 @@ ide_editor_search_get_reverse (IdeEditorSearch *self)
  * directions so that %IDE_EDITOR_SEARCH_PREVIOUS will search forwards
  * in the buffer and %IDE_EDITOR_SEARCH_NEXT wills earch backwards.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_set_reverse (IdeEditorSearch *self,
@@ -1802,7 +1807,7 @@ ide_editor_search_set_reverse (IdeEditorSearch *self,
  *
  * Returns: An %IdeEditorSearchSelect
  *
- * Since: 3.28
+ * Since: 3.32
  */
 IdeEditorSearchSelect
 ide_editor_search_get_extend_selection (IdeEditorSearch *self)
@@ -1838,6 +1843,8 @@ ide_editor_search_set_extend_selection (IdeEditorSearch       *self,
  * will be performed.
  *
  * Returns: A number containing the number of moves.
+ *
+ * Since: 3.32
  */
 guint
 ide_editor_search_get_repeat (IdeEditorSearch *self)
@@ -1857,7 +1864,7 @@ ide_editor_search_get_repeat (IdeEditorSearch *self)
  *
  * See also: ide_editor_search_get_repeat()
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_editor_search_set_repeat (IdeEditorSearch *self,
@@ -1882,6 +1889,8 @@ ide_editor_search_set_repeat (IdeEditorSearch *self,
  * context loaded and the search text is not empty.
  *
  * Returns: %TRUE if a search is active
+ *
+ * Since: 3.32
  */
 gboolean
 ide_editor_search_get_active (IdeEditorSearch *self)
@@ -1908,6 +1917,13 @@ ide_editor_search_actions_move_previous (IdeEditorSearch *self,
   ide_editor_search_move (self, IDE_EDITOR_SEARCH_PREVIOUS);
 }
 
+static void
+ide_editor_search_actions_at_word_boundary (IdeEditorSearch *self,
+                                            GVariant        *param)
+{
+  ide_editor_search_set_at_word_boundaries (self, g_variant_get_boolean (param));
+}
+
 static void
 ide_editor_search_actions_replace_all (IdeEditorSearch *self,
                                        GVariant        *param)
diff --git a/src/libide/editor/ide-editor-search.h b/src/libide/editor/ide-editor-search.h
index 31b198f0e..12b9122d4 100644
--- a/src/libide/editor/ide-editor-search.h
+++ b/src/libide/editor/ide-editor-search.h
@@ -1,6 +1,6 @@
 /* ide-editor-search.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,18 @@
  *
  * 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 <gtksourceview/gtksource.h>
+#if !defined (IDE_EDITOR_INSIDE) && !defined (IDE_EDITOR_COMPILATION)
+# error "Only <libide-editor.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <gtksourceview/gtksource.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
@@ -42,6 +47,8 @@ typedef enum
  *
  * This enum can be used to determine how the selection should be extending
  * when moving between the search results.
+ *
+ * Since: 3.32
  */
 typedef enum
 {
@@ -54,85 +61,85 @@ typedef enum
 #define IDE_TYPE_EDITOR_SEARCH_DIRECTION (ide_editor_search_direction_get_type())
 #define IDE_TYPE_EDITOR_SEARCH_SELECT    (ide_editor_search_select_get_type())
 
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeEditorSearch, ide_editor_search, IDE, EDITOR_SEARCH, GObject)
 
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 GType                  ide_editor_search_direction_get_type           (void);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 GType                  ide_editor_search_select_get_type              (void);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 IdeEditorSearch       *ide_editor_search_new                          (GtkSourceView             *view);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean               ide_editor_search_get_active                   (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_set_case_sensitive           (IdeEditorSearch           *self,
                                                                        gboolean                   
case_sensitive);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean               ide_editor_search_get_case_sensitive           (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 IdeEditorSearchSelect  ide_editor_search_get_extend_selection         (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_set_extend_selection         (IdeEditorSearch           *self,
                                                                        IdeEditorSearchSelect      
extend_selection);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean               ide_editor_search_get_reverse                  (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_set_reverse                  (IdeEditorSearch           *self,
                                                                        gboolean                   reverse);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_set_search_text              (IdeEditorSearch           *self,
                                                                        const gchar               
*search_text);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 const gchar           *ide_editor_search_get_search_text              (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean               ide_editor_search_get_search_text_invalid      (IdeEditorSearch           *self,
                                                                        guint                     
*invalid_begin,
                                                                        guint                     
*invalid_end,
                                                                        GError                   **error);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_set_visible                  (IdeEditorSearch           *self,
                                                                        gboolean                   visible);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean               ide_editor_search_get_visible                  (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_set_regex_enabled            (IdeEditorSearch           *self,
                                                                        gboolean                   
regex_enabled);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean               ide_editor_search_get_regex_enabled            (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_set_replacement_text         (IdeEditorSearch           *self,
                                                                        const gchar               
*replacement_text);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 const gchar           *ide_editor_search_get_replacement_text         (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean               ide_editor_search_get_replacement_text_invalid (IdeEditorSearch           *self,
                                                                        guint                     
*invalid_begin,
                                                                        guint                     
*invalid_end);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_set_at_word_boundaries       (IdeEditorSearch           *self,
                                                                        gboolean                   
at_word_boundaries);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean               ide_editor_search_get_at_word_boundaries       (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 guint                  ide_editor_search_get_repeat                   (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_set_repeat                   (IdeEditorSearch           *self,
                                                                        guint                      repeat);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 guint                  ide_editor_search_get_match_count              (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 guint                  ide_editor_search_get_match_position           (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_move                         (IdeEditorSearch           *self,
                                                                        IdeEditorSearchDirection   direction);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_replace                      (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_replace_all                  (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_begin_interactive            (IdeEditorSearch           *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                   ide_editor_search_end_interactive              (IdeEditorSearch           *self);
 
 G_END_DECLS
diff --git a/src/libide/editor/ide-editor-settings-dialog.c b/src/libide/editor/ide-editor-settings-dialog.c
new file mode 100644
index 000000000..b1b1d67ca
--- /dev/null
+++ b/src/libide/editor/ide-editor-settings-dialog.c
@@ -0,0 +1,331 @@
+/* ide-editor-settings-dialog.c
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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-editor-settings"
+
+#include "config.h"
+
+#include "ide-editor-settings-dialog.h"
+
+struct _IdeEditorSettingsDialog
+{
+  GtkDialog       parent_instance;
+
+  IdeEditorPage  *page;
+
+  GtkTreeView    *tree_view;
+  GtkListStore   *store;
+  GtkSearchEntry *entry;
+};
+
+G_DEFINE_TYPE (IdeEditorSettingsDialog, ide_editor_settings_dialog, GTK_TYPE_DIALOG)
+
+enum {
+  PROP_0,
+  PROP_PAGE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_editor_settings_dialog_row_activated (IdeEditorSettingsDialog *self,
+                                          GtkTreePath             *path,
+                                          GtkTreeViewColumn       *column,
+                                          GtkTreeView             *tree_view)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EDITOR_SETTINGS_DIALOG (self));
+  g_assert (path != NULL);
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  model = gtk_tree_view_get_model (tree_view);
+
+  if (gtk_tree_model_get_iter (model, &iter, path))
+    {
+      g_autofree gchar *id = NULL;
+      IdeBuffer *buffer;
+
+      gtk_tree_model_get (model, &iter, 0, &id, -1);
+
+      if ((buffer = ide_editor_page_get_buffer (self->page)))
+        ide_buffer_set_language_id (buffer, id);
+    }
+}
+
+static void
+ide_editor_settings_dialog_notify_file_settings (IdeEditorSettingsDialog *self,
+                                                 GParamSpec              *pspec,
+                                                 IdeBuffer               *buffer)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_EDITOR_SETTINGS_DIALOG (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  /* Update muxed action groups for new file-settings */
+  dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self),
+                                    GTK_WIDGET (self->page),
+                                    "IDE_EDITOR_PAGE_ACTIONS");
+}
+
+static void
+ide_editor_settings_dialog_notify_language (IdeEditorSettingsDialog *self,
+                                            GParamSpec              *pspec,
+                                            IdeBuffer               *buffer)
+{
+  const gchar *lang_id;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_EDITOR_SETTINGS_DIALOG (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if ((lang_id = ide_buffer_get_language_id (buffer)))
+    {
+      GtkTreeSelection *selection = gtk_tree_view_get_selection (self->tree_view);
+      GtkTreeModel *model = gtk_tree_view_get_model (self->tree_view);
+      GtkTreeIter iter;
+
+      if (gtk_tree_model_get_iter_first (model, &iter))
+        {
+          do
+            {
+              GValue idval = {0};
+
+              gtk_tree_model_get_value (model, &iter, 0, &idval);
+
+              if (ide_str_equal0 (lang_id, g_value_get_string (&idval)))
+                {
+                  g_autoptr(GtkTreePath) path = gtk_tree_model_get_path (model, &iter);
+
+                  gtk_tree_selection_select_iter (selection, &iter);
+                  gtk_tree_view_scroll_to_cell (self->tree_view, path, NULL, FALSE, 0, 0);
+
+                  return;
+                }
+            }
+          while (gtk_tree_model_iter_next (model, &iter));
+
+          gtk_tree_selection_unselect_all (selection);
+        }
+    }
+}
+
+static gboolean
+filter_func (GtkTreeModel *model,
+             GtkTreeIter  *iter,
+             gpointer      data)
+{
+  DzlPatternSpec *spec = data;
+  GValue idval = {0};
+  GValue nameval = {0};
+  gboolean ret;
+
+  gtk_tree_model_get_value (model, iter, 0, &idval);
+  gtk_tree_model_get_value (model, iter, 1, &nameval);
+
+  ret = dzl_pattern_spec_match (spec, g_value_get_string (&idval)) ||
+        dzl_pattern_spec_match (spec, g_value_get_string (&nameval));
+
+  g_value_unset (&idval);
+  g_value_unset (&nameval);
+
+  return ret;
+}
+
+static void
+ide_editor_settings_dialog_entry_changed (IdeEditorSettingsDialog *self,
+                                          GtkSearchEntry          *entry)
+{
+  g_autoptr(GtkTreeModel) filter = NULL;
+  const gchar *text;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EDITOR_SETTINGS_DIALOG (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (entry));
+
+  text = gtk_entry_get_text (GTK_ENTRY (entry));
+  filter = gtk_tree_model_filter_new (GTK_TREE_MODEL (self->store), NULL);
+  gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (filter),
+                                          filter_func,
+                                          dzl_pattern_spec_new (text),
+                                          (GDestroyNotify)dzl_pattern_spec_unref);
+
+  gtk_tree_view_set_model (self->tree_view, GTK_TREE_MODEL (filter));
+}
+
+static void
+ide_editor_settings_dialog_set_page (IdeEditorSettingsDialog *self,
+                                     IdeEditorPage           *page)
+{
+  IdeBuffer *buffer;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_EDITOR_SETTINGS_DIALOG (self));
+
+  g_set_object (&self->page, page);
+
+  g_signal_connect_object (self->entry,
+                           "changed",
+                           G_CALLBACK (ide_editor_settings_dialog_entry_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self),
+                                    GTK_WIDGET (page),
+                                    "IDE_EDITOR_PAGE_ACTIONS");
+
+  buffer = ide_editor_page_get_buffer (page);
+
+  g_signal_connect_object (buffer,
+                           "notify::language",
+                           G_CALLBACK (ide_editor_settings_dialog_notify_language),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (buffer,
+                           "notify::file-settings",
+                           G_CALLBACK (ide_editor_settings_dialog_notify_file_settings),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_editor_settings_dialog_notify_language (self, NULL, buffer);
+}
+
+IdeEditorSettingsDialog *
+ide_editor_settings_dialog_new (IdeEditorPage *page)
+{
+  GtkWidget *toplevel;
+
+  g_return_val_if_fail (IDE_IS_EDITOR_PAGE (page), NULL);
+
+  if ((toplevel = gtk_widget_get_toplevel (GTK_WIDGET (page))) && !IDE_IS_WORKSPACE (toplevel))
+    toplevel = NULL;
+
+  return g_object_new (IDE_TYPE_EDITOR_SETTINGS_DIALOG,
+                       "transient-for", toplevel,
+                       "modal", FALSE,
+                       "page", page,
+                       NULL);
+}
+
+static void
+ide_editor_settings_dialog_destroy (GtkWidget *widget)
+{
+  IdeEditorSettingsDialog *self = (IdeEditorSettingsDialog *)widget;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EDITOR_SETTINGS_DIALOG (self));
+
+  g_clear_object (&self->page);
+
+  GTK_WIDGET_CLASS (ide_editor_settings_dialog_parent_class)->destroy (widget);
+}
+
+static void
+ide_editor_settings_dialog_set_property (GObject      *object,
+                                         guint         prop_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec)
+{
+  IdeEditorSettingsDialog *self = IDE_EDITOR_SETTINGS_DIALOG (object);
+
+  switch (prop_id)
+    {
+    case PROP_PAGE:
+      ide_editor_settings_dialog_set_page (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_editor_settings_dialog_class_init (IdeEditorSettingsDialogClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->set_property = ide_editor_settings_dialog_set_property;
+
+  widget_class->destroy = ide_editor_settings_dialog_destroy;
+
+  properties [PROP_PAGE] =
+    g_param_spec_object ("page",
+                         "Page",
+                         "The editor page to be observed",
+                         IDE_TYPE_EDITOR_PAGE,
+                         (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-editor/ui/ide-editor-settings-dialog.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSettingsDialog, entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSettingsDialog, tree_view);
+}
+
+static void
+ide_editor_settings_dialog_init (IdeEditorSettingsDialog *self)
+{
+  g_autoptr(GtkListStore) store = NULL;
+  GtkSourceLanguageManager *manager;
+  const gchar * const *lang_ids;
+  GValue idval = {0};
+  GValue nameval = {0};
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_window_set_resizable (GTK_WINDOW (self), FALSE);
+
+  g_signal_connect_object (self->tree_view,
+                           "row-activated",
+                           G_CALLBACK (ide_editor_settings_dialog_row_activated),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  manager = gtk_source_language_manager_get_default ();
+  lang_ids = gtk_source_language_manager_get_language_ids (manager);
+  self->store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_STRING);
+
+  g_value_init (&idval, G_TYPE_STRING);
+  g_value_init (&nameval, G_TYPE_STRING);
+
+  for (guint i = 0; lang_ids[i]; i++)
+    {
+      GtkSourceLanguage *lang = gtk_source_language_manager_get_language (manager, lang_ids[i]);
+      GtkTreeIter iter;
+
+      g_value_set_static_string (&idval, g_intern_string (gtk_source_language_get_id (lang)));
+      g_value_set_static_string (&nameval, g_intern_string (gtk_source_language_get_name (lang)));
+
+      gtk_list_store_append (self->store, &iter);
+      gtk_list_store_set_value (self->store, &iter, 0, &idval);
+      gtk_list_store_set_value (self->store, &iter, 1, &nameval);
+
+      g_value_reset (&idval);
+      g_value_reset (&nameval);
+    }
+
+  gtk_tree_view_set_model (self->tree_view, GTK_TREE_MODEL (self->store));
+}
diff --git a/src/libide/editor/ide-editor-settings-dialog.h b/src/libide/editor/ide-editor-settings-dialog.h
new file mode 100644
index 000000000..433f30d2c
--- /dev/null
+++ b/src/libide/editor/ide-editor-settings-dialog.h
@@ -0,0 +1,34 @@
+/* ide-editor-settings-dialog.h
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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 <gtk/gtk.h>
+#include <libide-editor.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_EDITOR_SETTINGS_DIALOG (ide_editor_settings_dialog_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeEditorSettingsDialog, ide_editor_settings_dialog, IDE, EDITOR_SETTINGS_DIALOG, 
GtkDialog)
+
+IdeEditorSettingsDialog *ide_editor_settings_dialog_new (IdeEditorPage *page);
+
+G_END_DECLS
diff --git a/src/libide/editor/ide-editor-settings-dialog.ui b/src/libide/editor/ide-editor-settings-dialog.ui
new file mode 100644
index 000000000..cae1e6362
--- /dev/null
+++ b/src/libide/editor/ide-editor-settings-dialog.ui
@@ -0,0 +1,288 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <template class="IdeEditorSettingsDialog" parent="GtkDialog">
+    <property name="title" translatable="yes">Document Properties</property>
+    <child internal-child="vbox">
+      <object class="GtkBox">
+        <child>
+          <object class="GtkBox">
+            <property name="margin">24</property>
+            <property name="spacing">24</property>
+            <property name="visible">true</property>
+            <property name="orientation">horizontal</property>
+            <child>
+              <object class="GtkBox">
+                <property name="spacing">12</property>
+                <property name="orientation">vertical</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">Highlight Mode</property>
+                    <property name="xalign">0.0</property>
+                    <property name="visible">true</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkSearchEntry" id="entry">
+                    <property name="width-chars">25</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkScrolledWindow">
+                    <property name="hscrollbar-policy">never</property>
+                    <property name="shadow-type">in</property>
+                    <property name="vexpand">true</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkTreeView" id="tree_view">
+                        <property name="activate-on-single-click">true</property>
+                        <property name="headers-visible">false</property>
+                        <property name="visible">true</property>
+                        <child internal-child="selection">
+                          <object class="GtkTreeSelection">
+                            <property name="mode">browse</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn">
+                             <property name="visible">true</property>
+                             <child>
+                               <object class="GtkCellRendererText">
+                                 <property name="xalign">0.0</property>
+                                 <property name="ypad">3</property>
+                                 <property name="xpad">6</property>
+                               </object>
+                               <attributes>
+                                 <attribute name="text">1</attribute>
+                               </attributes>
+                             </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <property name="spacing">12</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">General</property>
+                    <property name="xalign">0.0</property>
+                    <property name="visible">true</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">6</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkCheckButton">
+                        <property name="label" translatable="yes">Display line numbers</property>
+                        <property name="action-name">source-view.show-line-numbers</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton">
+                        <property name="label" translatable="yes">Display right margin</property>
+                        <property name="action-name">file-settings.show-right-margin</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton">
+                        <property name="label" translatable="yes">Highlight current line</property>
+                        <property name="action-name">source-view.highlight-current-line</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton">
+                        <property name="label" translatable="yes">Automatic indentation</property>
+                        <property name="action-name">file-settings.auto-indent</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton">
+                        <property name="label" translatable="yes">Smart backspace</property>
+                        <property name="action-name">source-view.smart-backspace</property>
+                        <property name="tooltip-text" translatable="yes">Enabling smart backspace will treat 
multiple spaces as a tabs</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton">
+                        <property name="visible">true</property>
+                        <property name="action-name">file-settings.insert-trailing-newline</property>
+                        <property name="label" translatable="yes">Insert trailing newline</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkCheckButton">
+                        <property name="visible">true</property>
+                        <property name="action-name">file-settings.overwrite-braces</property>
+                        <property name="label" translatable="yes">Overwrite trailing braces and 
quotations</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkGrid">
+                    <property name="column-spacing">12</property>
+                    <property name="row-spacing">12</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Indentation</property>
+                        <property name="visible">true</property>
+                        <property name="valign">baseline</property>
+                        <property name="xalign">0.0</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="top-attach">0</property>
+                        <property name="left-attach">0</property>
+                        <property name="width">1</property>
+                        <property name="height">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="margin-top">6</property>
+                        <property name="visible">true</property>
+                        <property name="orientation">horizontal</property>
+                        <property name="hexpand">true</property>
+                        <style>
+                          <class name="linked"/>
+                        </style>
+                        <child>
+                          <object class="GtkToggleButton">
+                            <property name="visible">true</property>
+                            <property name="label" translatable="yes">2</property>
+                            <property name="focus-on-click">false</property>
+                            <property name="hexpand">true</property>
+                            <property name="action-name">file-settings.tab-width</property>
+                            <property name="action-target">uint32 2</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkToggleButton">
+                            <property name="visible">true</property>
+                            <property name="label" translatable="yes">3</property>
+                            <property name="focus-on-click">false</property>
+                            <property name="hexpand">true</property>
+                            <property name="action-name">file-settings.tab-width</property>
+                            <property name="action-target">uint32 3</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkToggleButton">
+                            <property name="visible">true</property>
+                            <property name="label" translatable="yes">4</property>
+                            <property name="focus-on-click">false</property>
+                            <property name="hexpand">true</property>
+                            <property name="action-name">file-settings.tab-width</property>
+                            <property name="action-target">uint32 4</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkToggleButton">
+                            <property name="visible">true</property>
+                            <property name="label" translatable="yes">8</property>
+                            <property name="focus-on-click">false</property>
+                            <property name="hexpand">true</property>
+                            <property name="action-name">file-settings.tab-width</property>
+                            <property name="action-target">uint32 8</property>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="top-attach">1</property>
+                        <property name="left-attach">1</property>
+                        <property name="width">1</property>
+                        <property name="height">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="visible">true</property>
+                        <property name="hexpand">true</property>
+                        <property name="orientation">horizontal</property>
+                        <style>
+                          <class name="linked"/>
+                        </style>
+                        <child>
+                          <object class="GtkToggleButton">
+                            <property name="draw-indicator">false</property>
+                            <property name="visible">true</property>
+                            <property name="label" translatable="yes">Spaces</property>
+                            <property name="focus-on-click">false</property>
+                            <property name="hexpand">true</property>
+                            <property name="action-name">file-settings.indent-style</property>
+                            <property name="action-target">'spaces'</property>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkToggleButton" id="tabs_button">
+                            <property name="draw-indicator">false</property>
+                            <property name="visible">true</property>
+                            <property name="label" translatable="yes">Tabs</property>
+                            <property name="focus-on-click">false</property>
+                            <property name="hexpand">true</property>
+                            <property name="action-name">file-settings.indent-style</property>
+                            <property name="action-target">'tabs'</property>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="top-attach">0</property>
+                        <property name="left-attach">1</property>
+                        <property name="width">1</property>
+                        <property name="height">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Spaces per tab</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">0.0</property>
+                        <property name="valign">baseline</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="top-attach">1</property>
+                        <property name="left-attach">0</property>
+                        <property name="width">1</property>
+                        <property name="height">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/editor/ide-editor-sidebar.c b/src/libide/editor/ide-editor-sidebar.c
index df87f7a01..13c746f2a 100644
--- a/src/libide/editor/ide-editor-sidebar.c
+++ b/src/libide/editor/ide-editor-sidebar.c
@@ -1,6 +1,6 @@
 /* ide-editor-sidebar.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-editor-sidebar"
@@ -21,12 +23,12 @@
 #include "config.h"
 
 #include <dazzle.h>
+#include <libide-gui.h>
+
+#include "ide-gui-private.h"
 
-#include "editor/ide-editor-private.h"
-#include "editor/ide-editor-sidebar.h"
-#include "layout/ide-layout-private.h"
-#include "layout/ide-layout-stack.h"
-#include "layout/ide-layout-view.h"
+#include "ide-editor-private.h"
+#include "ide-editor-sidebar.h"
 
 /**
  * SECTION:ide-editor-sidebar
@@ -34,15 +36,17 @@
  * @short_description: The left sidebar for the editor
  *
  * The #IdeEditorSidebar is the widget displayed on the left of the
- * #IdeEditorPerspective.  It contains an open document list, and then the
+ * #IdeEditorSurface.  It contains an open document list, and then the
  * various sections that have been added to the sidebar.
  *
  * Use ide_editor_sidebar_add_section() to add a section to the sidebar.
+ *
+ * Since: 3.32
  */
 
 struct _IdeEditorSidebar
 {
-  IdeLayoutPane      parent_instance;
+  IdePanel           parent_instance;
 
   GSettings         *settings;
   GListModel        *open_pages;
@@ -57,7 +61,7 @@ struct _IdeEditorSidebar
   GtkStack          *stack;
 };
 
-G_DEFINE_TYPE (IdeEditorSidebar, ide_editor_sidebar, IDE_TYPE_LAYOUT_PANE)
+G_DEFINE_TYPE (IdeEditorSidebar, ide_editor_sidebar, IDE_TYPE_PANEL)
 
 static void
 ide_editor_sidebar_update_title (IdeEditorSidebar *self)
@@ -114,20 +118,20 @@ ide_editor_sidebar_open_pages_row_activated (IdeEditorSidebar *self,
                                              GtkListBoxRow    *row,
                                              GtkListBox       *list_box)
 {
-  IdeLayoutView *view;
+  IdePage *view;
   GtkWidget *stack;
 
   g_assert (IDE_IS_EDITOR_SIDEBAR (self));
   g_assert (GTK_IS_LIST_BOX_ROW (row));
   g_assert (GTK_IS_LIST_BOX (list_box));
 
-  view = g_object_get_data (G_OBJECT (row), "IDE_LAYOUT_VIEW");
-  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  view = g_object_get_data (G_OBJECT (row), "IDE_PAGE");
+  g_assert (IDE_IS_PAGE (view));
 
-  stack = gtk_widget_get_ancestor (GTK_WIDGET (view), IDE_TYPE_LAYOUT_STACK);
-  g_assert (IDE_IS_LAYOUT_STACK (stack));
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (view), IDE_TYPE_FRAME);
+  g_assert (IDE_IS_FRAME (stack));
 
-  ide_layout_stack_set_visible_child (IDE_LAYOUT_STACK (stack), view);
+  ide_frame_set_visible_child (IDE_FRAME (stack), view);
 
   gtk_widget_grab_focus (GTK_WIDGET (view));
 }
@@ -190,7 +194,7 @@ ide_editor_sidebar_class_init (IdeEditorSidebarClass *klass)
 
   widget_class->destroy = ide_editor_sidebar_destroy;
 
-  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-editor-sidebar.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-editor/ui/ide-editor-sidebar.ui");
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSidebar, box);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSidebar, open_pages_list_box);
   gtk_widget_class_bind_template_child (widget_class, IdeEditorSidebar, open_pages_section);
@@ -234,7 +238,7 @@ ide_editor_sidebar_init (IdeEditorSidebar *self)
  *
  * Returns: (transfer full): A new #IdeEditorSidebar
  *
- * Since: 3.26
+ * Since: 3.32
  */
 GtkWidget *
 ide_editor_sidebar_new (void)
@@ -300,7 +304,7 @@ find_position (IdeEditorSidebar *self,
  *
  * To remove your section, call gtk_widget_destroy() on @section.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_editor_sidebar_add_section (IdeEditorSidebar *self,
@@ -362,7 +366,7 @@ ide_editor_sidebar_add_section (IdeEditorSidebar *self,
  *
  * Returns: (nullable): The id of the current section if it registered one.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 const gchar *
 ide_editor_sidebar_get_section_id (IdeEditorSidebar *self)
@@ -379,7 +383,7 @@ ide_editor_sidebar_get_section_id (IdeEditorSidebar *self)
  *
  * Changes the current section to @section_id.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 ide_editor_sidebar_set_section_id (IdeEditorSidebar *self,
@@ -393,37 +397,37 @@ ide_editor_sidebar_set_section_id (IdeEditorSidebar *self,
 
 static void
 ide_editor_sidebar_close_view (GtkButton     *button,
-                               IdeLayoutView *view)
+                               IdePage *view)
 {
   GtkWidget *stack;
 
   g_assert (GTK_IS_BUTTON (button));
-  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_PAGE (view));
 
-  stack = gtk_widget_get_ancestor (GTK_WIDGET (view), IDE_TYPE_LAYOUT_STACK);
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (view), IDE_TYPE_FRAME);
 
   if (stack != NULL)
-    _ide_layout_stack_request_close (IDE_LAYOUT_STACK (stack), view);
+    _ide_frame_request_close (IDE_FRAME (stack), view);
 }
 
 static GtkWidget *
 create_open_page_row (gpointer item,
                       gpointer user_data)
 {
-  IdeLayoutView *view = item;
+  IdePage *view = item;
   GtkListBoxRow *row;
   GtkButton *button;
   GtkImage *image;
   GtkLabel *label;
   GtkBox *box;
 
-  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_PAGE (view));
   g_assert (IDE_IS_EDITOR_SIDEBAR (user_data));
 
   row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
                       "visible", TRUE,
                       NULL);
-  g_object_set_data (G_OBJECT (row), "IDE_LAYOUT_VIEW", view);
+  g_object_set_data (G_OBJECT (row), "IDE_PAGE", view);
 
   box = g_object_new (GTK_TYPE_BOX,
                       "orientation", GTK_ORIENTATION_HORIZONTAL,
@@ -476,8 +480,10 @@ create_open_page_row (gpointer item,
  * @open_pages: a #GListModel describing the open pages
  *
  * This private function is used to set the GListModel to use for the list
- * of open pages in the sidebar. It should contain a list of IdeLayoutView
+ * of open pages in the sidebar. It should contain a list of IdePage
  * which we will use to keep the rows up to date.
+ *
+ * Since: 3.32
  */
 void
 _ide_editor_sidebar_set_open_pages (IdeEditorSidebar *self,
@@ -486,7 +492,7 @@ _ide_editor_sidebar_set_open_pages (IdeEditorSidebar *self,
   g_return_if_fail (IDE_IS_EDITOR_SIDEBAR (self));
   g_return_if_fail (!open_pages || G_IS_LIST_MODEL (open_pages));
   g_return_if_fail (!open_pages ||
-                    g_list_model_get_item_type (open_pages) == IDE_TYPE_LAYOUT_VIEW);
+                    g_list_model_get_item_type (open_pages) == IDE_TYPE_PAGE);
 
   g_set_object (&self->open_pages, open_pages);
 
diff --git a/src/libide/editor/ide-editor-sidebar.h b/src/libide/editor/ide-editor-sidebar.h
index d3a54da2f..746c5491c 100644
--- a/src/libide/editor/ide-editor-sidebar.h
+++ b/src/libide/editor/ide-editor-sidebar.h
@@ -1,6 +1,6 @@
 /* ide-editor-sidebar.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,29 +14,34 @@
  *
  * 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 "ide-version-macros.h"
+#if !defined (IDE_EDITOR_INSIDE) && !defined (IDE_EDITOR_COMPILATION)
+# error "Only <libide-editor.h> can be included directly."
+#endif
 
-#include "layout/ide-layout-pane.h"
+#include <libide-core.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_EDITOR_SIDEBAR (ide_editor_sidebar_get_type())
 
-IDE_AVAILABLE_IN_ALL
-G_DECLARE_FINAL_TYPE (IdeEditorSidebar, ide_editor_sidebar, IDE, EDITOR_SIDEBAR, IdeLayoutPane)
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeEditorSidebar, ide_editor_sidebar, IDE, EDITOR_SIDEBAR, IdePanel)
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GtkWidget   *ide_editor_sidebar_new            (void);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar *ide_editor_sidebar_get_section_id (IdeEditorSidebar *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void         ide_editor_sidebar_set_section_id (IdeEditorSidebar *self,
                                                 const gchar      *section_id);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void         ide_editor_sidebar_add_section    (IdeEditorSidebar *self,
                                                 const gchar      *id,
                                                 const gchar      *title,
diff --git a/src/libide/editor/ide-editor-sidebar.ui b/src/libide/editor/ide-editor-sidebar.ui
index 149354164..341bd98f4 100644
--- a/src/libide/editor/ide-editor-sidebar.ui
+++ b/src/libide/editor/ide-editor-sidebar.ui
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <template class="IdeEditorSidebar" parent="IdeLayoutPane">
+  <template class="IdeEditorSidebar" parent="IdePanel">
     <child>
       <object class="GtkBox" id="box">
         <property name="vexpand">true</property>
diff --git a/src/libide/editor/ide-editor-surface-actions.c b/src/libide/editor/ide-editor-surface-actions.c
new file mode 100644
index 000000000..7e88e64f7
--- /dev/null
+++ b/src/libide/editor/ide-editor-surface-actions.c
@@ -0,0 +1,164 @@
+/* ide-editor-surface-actions.c
+ *
+ * Copyright 2017-2019 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 "ide-editor-surface-actions"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-editor-private.h"
+
+static void
+ide_editor_surface_actions_new_file (GSimpleAction *action,
+                                     GVariant      *variant,
+                                     gpointer       user_data)
+{
+  IdeEditorSurface *self = user_data;
+  IdeBufferManager *bufmgr;
+  IdeContext *context;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  bufmgr = ide_buffer_manager_from_context (context);
+
+  ide_buffer_manager_load_file_async (bufmgr,
+                                      NULL,
+                                      IDE_BUFFER_OPEN_FLAGS_NONE,
+                                      NULL,
+                                      NULL,
+                                      NULL,
+                                      NULL);
+}
+
+static void
+ide_editor_surface_actions_open_file (GSimpleAction *action,
+                                      GVariant      *variant,
+                                      gpointer       user_data)
+{
+  IdeEditorSurface *self = user_data;
+  GtkFileChooserNative *chooser;
+  IdeWorkbench *workbench;
+  GtkWidget *workspace;
+  gint ret;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  if (!(workbench = ide_widget_get_workbench (GTK_WIDGET (self))))
+    return;
+
+  workspace = gtk_widget_get_toplevel (GTK_WIDGET (self));
+
+  chooser = gtk_file_chooser_native_new (_("Open File"),
+                                         GTK_WINDOW (workspace),
+                                         GTK_FILE_CHOOSER_ACTION_OPEN,
+                                         _("Open"),
+                                         _("Cancel"));
+  gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (chooser), FALSE);
+  gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (chooser), TRUE);
+
+  ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (chooser));
+
+  if (ret == GTK_RESPONSE_ACCEPT)
+    {
+      g_autoptr(GPtrArray) ar = NULL;
+      GSList *files;
+
+      ar = g_ptr_array_new_with_free_func (g_object_unref);
+      files = gtk_file_chooser_get_files (GTK_FILE_CHOOSER (chooser));
+      for (const GSList *iter = files; iter; iter = iter->next)
+        g_ptr_array_add (ar, iter->data);
+      g_slist_free (files);
+
+      if (ar->len > 0)
+        ide_workbench_open_all_async (workbench,
+                                      (GFile **)ar->pdata,
+                                      ar->len,
+                                      "editor",
+                                      NULL, NULL, NULL);
+    }
+
+  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (chooser));
+}
+
+static void
+collect_pages (GtkWidget *widget,
+               gpointer   user_data)
+{
+  IdePage *view = (IdePage *)widget;
+  GPtrArray *views = user_data;
+
+  g_assert (views != NULL);
+  g_assert (IDE_IS_PAGE (view));
+
+  g_ptr_array_add (views, g_object_ref (view));
+}
+
+static void
+ide_editor_surface_actions_close_all (GSimpleAction *action,
+                                      GVariant      *param,
+                                      gpointer       user_data)
+{
+  IdeEditorSurface *self = user_data;
+  g_autoptr(GPtrArray) views = NULL;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  /* First collect all the views and hold a reference to them
+   * so that we do not need to worry about contains being destroyed
+   * as we work through the list.
+   */
+  views = g_ptr_array_new_full (0, g_object_unref);
+  ide_grid_foreach_page (self->grid, collect_pages, views);
+
+  for (guint i = 0; i < views->len; i++)
+    {
+      IdePage *view = g_ptr_array_index (views, i);
+
+      /* TODO: Should we allow suspending the close with
+       *       agree_to_close_async()?
+       */
+
+      gtk_widget_destroy (GTK_WIDGET (view));
+    }
+}
+
+static const GActionEntry editor_actions[] = {
+  { "new-file", ide_editor_surface_actions_new_file },
+  { "open-file", ide_editor_surface_actions_open_file },
+  { "close-all", ide_editor_surface_actions_close_all },
+};
+
+void
+_ide_editor_surface_init_actions (IdeEditorSurface *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   editor_actions,
+                                   G_N_ELEMENTS (editor_actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "editor", G_ACTION_GROUP (group));
+}
diff --git a/src/libide/editor/ide-editor-surface-shortcuts.c 
b/src/libide/editor/ide-editor-surface-shortcuts.c
new file mode 100644
index 000000000..1e037c3e6
--- /dev/null
+++ b/src/libide/editor/ide-editor-surface-shortcuts.c
@@ -0,0 +1,107 @@
+/* ide-editor-surface-shortcuts.c
+ *
+ * Copyright 2017-2019 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 "ide-editor-surface-shortcuts"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <dazzle.h>
+
+#include "ide-editor-private.h"
+
+#define I_(s) g_intern_static_string(s)
+
+static const DzlShortcutEntry editor_surface_entries[] = {
+  { "org.gnome.builder.editor.new-file",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Files"),
+    NC_("shortcut window", "Create a new document") },
+
+  { "org.gnome.builder.editor.open-file",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Files"),
+    NC_("shortcut window", "Open a document") },
+
+  { "org.gnome.builder.editor.navigation-panel",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Panels"),
+    NC_("shortcut window", "Toggle navigation panel") },
+
+  { "org.gnome.builder.editor.utilities-panel",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Panels"),
+    NC_("shortcut window", "Toggle utilities panel") },
+
+  { "org.gnome.builder.editor.close-all",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Files"),
+    NC_("shortcut window", "Close all files") },
+};
+
+void
+_ide_editor_surface_init_shortcuts (IdeEditorSurface *self)
+{
+  DzlShortcutController *controller;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor.new-file"),
+                                              I_("<Primary>n"),
+                                              DZL_SHORTCUT_PHASE_GLOBAL,
+                                              I_("editor.new-file"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor.open-file"),
+                                              I_("<Primary>o"),
+                                              DZL_SHORTCUT_PHASE_GLOBAL,
+                                              I_("editor.open-file"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor.navigation-panel"),
+                                              I_("F9"),
+                                              DZL_SHORTCUT_PHASE_CAPTURE | DZL_SHORTCUT_PHASE_GLOBAL,
+                                              I_("dockbin.left-visible"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor.utilities-panel"),
+                                              I_("<Control>F9"),
+                                              DZL_SHORTCUT_PHASE_CAPTURE | DZL_SHORTCUT_PHASE_GLOBAL,
+                                              I_("dockbin.bottom-visible"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.editor.close-all"),
+                                              I_("<Primary><Shift>w"),
+                                              DZL_SHORTCUT_PHASE_GLOBAL,
+                                              I_("editor.close-all"));
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             editor_surface_entries,
+                                             G_N_ELEMENTS (editor_surface_entries),
+                                             GETTEXT_PACKAGE);
+}
diff --git a/src/libide/editor/ide-editor-surface.c b/src/libide/editor/ide-editor-surface.c
new file mode 100644
index 000000000..78f3ffde4
--- /dev/null
+++ b/src/libide/editor/ide-editor-surface.c
@@ -0,0 +1,919 @@
+/* ide-editor-surface.c
+ *
+ * Copyright 2017-2019 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 "ide-editor-surface"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-code.h>
+
+#include "ide-editor-addin.h"
+#include "ide-editor-surface.h"
+#include "ide-editor-private.h"
+#include "ide-editor-sidebar.h"
+#include "ide-editor-utilities.h"
+#include "ide-editor-page.h"
+
+typedef struct
+{
+  IdeEditorSurface *self;
+  IdeLocation      *location;
+} FocusLocation;
+
+static void ide_editor_surface_focus_location_full (IdeEditorSurface *self,
+                                                    IdeLocation      *location,
+                                                    gboolean          open_if_not_found);
+
+G_DEFINE_TYPE (IdeEditorSurface, ide_editor_surface, IDE_TYPE_SURFACE)
+
+static void
+ide_editor_surface_foreach_page (IdeSurface  *surface,
+                                 GtkCallback  callback,
+                                 gpointer     user_data)
+{
+  IdeEditorSurface *self = (IdeEditorSurface *)surface;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+  g_assert (callback != NULL);
+
+  ide_grid_foreach_page (self->grid, callback, user_data);
+}
+
+static void
+set_reveal_child_without_transition (DzlDockRevealer *revealer,
+                                     gboolean         reveal)
+{
+  DzlDockRevealerTransitionType type;
+
+  g_assert (DZL_IS_DOCK_REVEALER (revealer));
+
+  type = dzl_dock_revealer_get_transition_type (revealer);
+  dzl_dock_revealer_set_transition_type (revealer, DZL_DOCK_REVEALER_TRANSITION_TYPE_NONE);
+  dzl_dock_revealer_set_reveal_child (revealer, reveal);
+  dzl_dock_revealer_set_transition_type (revealer, type);
+}
+
+static void
+ide_editor_surface_restore_panel_state (IdeEditorSurface *self)
+{
+  g_autoptr(GSettings) settings = NULL;
+  GtkWidget *pane;
+  gboolean reveal;
+  guint position;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  /* TODO: This belongs in editor settings probably */
+
+  settings = g_settings_new ("org.gnome.builder.workbench");
+
+  pane = dzl_dock_bin_get_left_edge (DZL_DOCK_BIN (self));
+  reveal = g_settings_get_boolean (settings, "left-visible");
+  position = g_settings_get_int (settings, "left-position");
+  dzl_dock_revealer_set_position (DZL_DOCK_REVEALER (pane), position);
+  set_reveal_child_without_transition (DZL_DOCK_REVEALER (pane), reveal);
+
+  pane = dzl_dock_bin_get_right_edge (DZL_DOCK_BIN (self));
+  position = g_settings_get_int (settings, "right-position");
+  dzl_dock_revealer_set_position (DZL_DOCK_REVEALER (pane), position);
+  set_reveal_child_without_transition (DZL_DOCK_REVEALER (pane), FALSE);
+
+  pane = dzl_dock_bin_get_bottom_edge (DZL_DOCK_BIN (self));
+  reveal = g_settings_get_boolean (settings, "bottom-visible");
+  position = g_settings_get_int (settings, "bottom-position");
+  dzl_dock_revealer_set_position (DZL_DOCK_REVEALER (pane), position);
+  set_reveal_child_without_transition (DZL_DOCK_REVEALER (pane), reveal);
+}
+
+static void
+ide_editor_surface_save_panel_state (IdeEditorSurface *self)
+{
+  g_autoptr(GSettings) settings = NULL;
+  GtkWidget *pane;
+  gboolean reveal;
+  guint position;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  /* TODO: possibly belongs in editor settings */
+  settings = g_settings_new ("org.gnome.builder.workbench");
+
+  pane = dzl_dock_bin_get_left_edge (DZL_DOCK_BIN (self));
+  position = dzl_dock_revealer_get_position (DZL_DOCK_REVEALER (pane));
+  reveal = dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (pane));
+  g_settings_set_boolean (settings, "left-visible", reveal);
+  g_settings_set_int (settings, "left-position", position);
+
+  pane = dzl_dock_bin_get_right_edge (DZL_DOCK_BIN (self));
+  position = dzl_dock_revealer_get_position (DZL_DOCK_REVEALER (pane));
+  reveal = dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (pane));
+  g_settings_set_boolean (settings, "right-visible", reveal);
+  g_settings_set_int (settings, "right-position", position);
+
+  pane = dzl_dock_bin_get_bottom_edge (DZL_DOCK_BIN (self));
+  position = dzl_dock_revealer_get_position (DZL_DOCK_REVEALER (pane));
+  reveal = dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (pane));
+  g_settings_set_boolean (settings, "bottom-visible", reveal);
+  g_settings_set_int (settings, "bottom-position", position);
+}
+
+static gboolean
+ide_editor_surface_agree_to_shutdown (IdeSurface *surface)
+{
+  IdeEditorSurface *self = (IdeEditorSurface *)surface;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  ide_editor_surface_save_panel_state (self);
+
+  return TRUE;
+}
+
+static void
+ide_editor_surface_set_fullscreen (IdeSurface *surface,
+                                   gboolean    fullscreen)
+{
+  IdeEditorSurface *self = (IdeEditorSurface *)surface;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  if (fullscreen)
+    {
+      gboolean left_visible;
+      gboolean bottom_visible;
+
+      g_object_get (self,
+                    "left-visible", &left_visible,
+                    "bottom-visible", &bottom_visible,
+                    NULL);
+
+      self->prefocus_had_left = left_visible;
+      self->prefocus_had_bottom = bottom_visible;
+
+      g_object_set (self,
+                    "left-visible", FALSE,
+                    "bottom-visible", FALSE,
+                    NULL);
+    }
+  else
+    {
+      g_object_set (self,
+                    "left-visible", self->prefocus_had_left,
+                    "bottom-visible", self->prefocus_had_bottom,
+                    NULL);
+    }
+}
+
+
+static void
+ide_editor_surface_addin_added (PeasExtensionSet *set,
+                                PeasPluginInfo   *plugin_info,
+                                PeasExtension    *exten,
+                                gpointer          user_data)
+{
+  IdeEditorSurface *self = user_data;
+  IdeEditorAddin *addin = (IdeEditorAddin *)exten;
+  IdePage *page;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+  g_assert (IDE_IS_EDITOR_ADDIN (addin));
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+
+  ide_editor_addin_load (addin, self);
+
+  page = ide_grid_get_current_page (self->grid);
+  if (page != NULL)
+    ide_editor_addin_page_set (addin, page);
+}
+
+static void
+ide_editor_surface_addin_removed (PeasExtensionSet *set,
+                                      PeasPluginInfo   *plugin_info,
+                                      PeasExtension    *exten,
+                                      gpointer          user_data)
+{
+  IdeEditorSurface *self = user_data;
+  IdeEditorAddin *addin = (IdeEditorAddin *)exten;
+  IdePage *page;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+  g_assert (IDE_IS_EDITOR_ADDIN (addin));
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+
+  page = ide_grid_get_current_page (self->grid);
+  if (page != NULL)
+    ide_editor_addin_page_set (addin, NULL);
+
+  ide_editor_addin_unload (addin, self);
+}
+
+static void
+ide_editor_surface_hierarchy_changed (GtkWidget *widget,
+                                      GtkWidget *old_toplevel)
+{
+  IdeEditorSurface *self = (IdeEditorSurface *)widget;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));
+
+  if (self->addins == NULL)
+    {
+      GtkWidget *toplevel;
+
+      /*
+       * If we just got a new toplevel and it is a workbench,
+       * and we have not yet created our addins, do so now.
+       */
+
+      toplevel = gtk_widget_get_ancestor (widget, IDE_TYPE_WORKSPACE);
+
+      if (toplevel != NULL)
+        {
+          self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                                 IDE_TYPE_EDITOR_ADDIN,
+                                                 NULL);
+          g_signal_connect (self->addins,
+                            "extension-added",
+                            G_CALLBACK (ide_editor_surface_addin_added),
+                            self);
+          g_signal_connect (self->addins,
+                            "extension-removed",
+                            G_CALLBACK (ide_editor_surface_addin_removed),
+                            self);
+          peas_extension_set_foreach (self->addins,
+                                      ide_editor_surface_addin_added,
+                                      self);
+        }
+    }
+}
+
+static void
+ide_editor_surface_addins_page_set (PeasExtensionSet *set,
+                                    PeasPluginInfo   *plugin_info,
+                                    PeasExtension    *exten,
+                                    gpointer          user_data)
+{
+  IdeEditorAddin *addin = (IdeEditorAddin *)exten;
+  IdePage *page = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_EDITOR_ADDIN (addin));
+  g_assert (!page || IDE_IS_PAGE (page));
+
+  ide_editor_addin_page_set (addin, page);
+}
+
+static void
+ide_editor_surface_notify_current_page (IdeEditorSurface *self,
+                                        GParamSpec       *pspec,
+                                        IdeGrid          *grid)
+{
+  IdePage *page;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_GRID (grid));
+
+  page = ide_grid_get_current_page (grid);
+
+  if (self->addins != NULL)
+    peas_extension_set_foreach (self->addins,
+                                ide_editor_surface_addins_page_set,
+                                page);
+}
+
+static void
+ide_editor_surface_add (GtkContainer *container,
+                            GtkWidget    *widget)
+{
+  IdeEditorSurface *self = (IdeEditorSurface *)container;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (IDE_IS_PAGE (widget))
+    gtk_container_add (GTK_CONTAINER (self->grid), widget);
+  else
+    GTK_CONTAINER_CLASS (ide_editor_surface_parent_class)->add (container, widget);
+}
+
+static GtkWidget *
+ide_editor_surface_create_edge (DzlDockBin      *dock_bin,
+                                GtkPositionType  edge)
+{
+  g_assert (DZL_IS_DOCK_BIN (dock_bin));
+  g_assert (edge >= GTK_POS_LEFT);
+  g_assert (edge <= GTK_POS_BOTTOM);
+
+  if (edge == GTK_POS_LEFT)
+    return g_object_new (IDE_TYPE_EDITOR_SIDEBAR,
+                         "edge", edge,
+                         "transition-duration", 333,
+                         "reveal-child", FALSE,
+                         "visible", TRUE,
+                         NULL);
+
+  if (edge == GTK_POS_RIGHT)
+    return g_object_new (IDE_TYPE_TRANSIENT_SIDEBAR,
+                         "edge", edge,
+                         "reveal-child", FALSE,
+                         "transition-duration", 333,
+                         "visible", FALSE,
+                         NULL);
+
+  if (edge == GTK_POS_BOTTOM)
+    return g_object_new (IDE_TYPE_EDITOR_UTILITIES,
+                         "edge", edge,
+                         "reveal-child", FALSE,
+                         "transition-duration", 333,
+                         "visible", TRUE,
+                         NULL);
+
+  return DZL_DOCK_BIN_CLASS (ide_editor_surface_parent_class)->create_edge (dock_bin, edge);
+}
+
+static void
+ide_editor_surface_load_file_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  IdeBufferManager *bufmgr = (IdeBufferManager *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeBuffer) buffer = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  buffer = ide_buffer_manager_load_file_finish (bufmgr, result, &error);
+
+  if (error != NULL)
+    g_warning ("%s", error->message);
+
+  /* TODO: Ensure that the page is marked as failed */
+
+  IDE_EXIT;
+}
+
+static IdePage *
+ide_editor_surface_create_page (IdeEditorSurface *self,
+                                const gchar      *uri,
+                                IdeGrid          *grid)
+{
+  g_autoptr(GFile) file = NULL;
+  IdeBufferManager *bufmgr;
+  IdeContext *context;
+  IdeBuffer *buffer;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+  g_assert (uri != NULL);
+  g_assert (IDE_IS_GRID (grid));
+
+  g_debug ("Creating page for %s", uri);
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+
+  file = g_file_new_for_uri (uri);
+  bufmgr = ide_buffer_manager_from_context (context);
+  buffer = ide_buffer_manager_find_buffer (bufmgr, file);
+
+  /*
+   * If we failed to locate an already loaded buffer, we need to start
+   * loading the buffer. But that could take some time. Either way, after
+   * we start the loading process, we can access the buffer and we'll
+   * display it while it loads.
+   */
+
+  if (buffer == NULL)
+    {
+      ide_buffer_manager_load_file_async (bufmgr,
+                                          file,
+                                          IDE_BUFFER_OPEN_FLAGS_NO_VIEW,
+                                          NULL,
+                                          NULL,
+                                          ide_editor_surface_load_file_cb,
+                                          g_object_ref (self));
+      buffer = ide_buffer_manager_find_buffer (bufmgr, file);
+    }
+
+  return g_object_new (IDE_TYPE_EDITOR_PAGE,
+                       "buffer", buffer,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static void
+ide_editor_surface_grab_focus (GtkWidget *widget)
+{
+  IdeEditorSurface *self = (IdeEditorSurface *)widget;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->grid));
+}
+
+static void
+ide_editor_surface_destroy (GtkWidget *widget)
+{
+  IdeEditorSurface *self = (IdeEditorSurface *)widget;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  g_clear_object (&self->addins);
+
+  GTK_WIDGET_CLASS (ide_editor_surface_parent_class)->destroy (widget);
+}
+
+static void
+ide_editor_surface_realize (GtkWidget *widget)
+{
+  IdeEditorSurface *self = (IdeEditorSurface *)widget;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+
+  ide_editor_surface_restore_panel_state (self);
+
+  GTK_WIDGET_CLASS (ide_editor_surface_parent_class)->realize (widget);
+}
+
+static void
+ide_editor_surface_class_init (IdeEditorSurfaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+  DzlDockBinClass *dock_bin_class = DZL_DOCK_BIN_CLASS (klass);
+  IdeSurfaceClass *surface_class = IDE_SURFACE_CLASS (klass);
+
+  widget_class->destroy = ide_editor_surface_destroy;
+  widget_class->hierarchy_changed = ide_editor_surface_hierarchy_changed;
+  widget_class->grab_focus = ide_editor_surface_grab_focus;
+  widget_class->realize = ide_editor_surface_realize;
+
+  container_class->add = ide_editor_surface_add;
+
+  dock_bin_class->create_edge = ide_editor_surface_create_edge;
+
+  surface_class->agree_to_shutdown = ide_editor_surface_agree_to_shutdown;
+  surface_class->foreach_page = ide_editor_surface_foreach_page;
+  surface_class->set_fullscreen = ide_editor_surface_set_fullscreen;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-editor/ui/ide-editor-surface.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSurface, grid);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSurface, overlay);
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorSurface, loading_stack);
+
+  g_type_ensure (IDE_TYPE_EDITOR_SIDEBAR);
+  g_type_ensure (IDE_TYPE_GRID);
+}
+
+static void
+ide_editor_surface_init (IdeEditorSurface *self)
+{
+  IdeEditorSidebar *sidebar;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  ide_surface_set_icon_name (IDE_SURFACE (self), "builder-editor-symbolic");
+  ide_surface_set_title (IDE_SURFACE (self), _("Editor"));
+
+  _ide_editor_surface_init_actions (self);
+  _ide_editor_surface_init_shortcuts (self);
+
+  /* ensure we default to the grid visible */
+  _ide_editor_surface_set_loading (self, FALSE);
+
+  g_signal_connect_swapped (self->grid,
+                            "notify::current-page",
+                            G_CALLBACK (ide_editor_surface_notify_current_page),
+                            self);
+
+  g_signal_connect_swapped (self->grid,
+                            "create-page",
+                            G_CALLBACK (ide_editor_surface_create_page),
+                            self);
+
+  sidebar = ide_editor_surface_get_sidebar (self);
+  _ide_editor_sidebar_set_open_pages (sidebar, G_LIST_MODEL (self->grid));
+}
+
+/**
+ * ide_editor_surface_get_grid:
+ * @self: a #IdeEditorSurface
+ *
+ * Gets the grid for the surface. This is the area containing
+ * grid columns, stacks, and pages.
+ *
+ * Returns: (transfer none): An #IdeGrid.
+ *
+ * Since: 3.32
+ */
+IdeGrid *
+ide_editor_surface_get_grid (IdeEditorSurface *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SURFACE (self), NULL);
+
+  return self->grid;
+}
+
+static void
+ide_editor_surface_find_source_location (GtkWidget *widget,
+                                         gpointer   user_data)
+{
+  struct {
+    GFile *file;
+    IdeEditorPage *page;
+  } *lookup = user_data;
+  IdeBuffer *buffer;
+  GFile *file;
+
+  g_return_if_fail (IDE_IS_PAGE (widget));
+
+  if (lookup->page != NULL)
+    return;
+
+  if (!IDE_IS_EDITOR_PAGE (widget))
+    return;
+
+  buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (widget));
+  file = ide_buffer_get_file (buffer);
+
+  if (g_file_equal (file, lookup->file))
+    lookup->page = IDE_EDITOR_PAGE (widget);
+}
+
+static void
+ide_editor_surface_focus_location_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  IdeBufferManager *bufmgr = (IdeBufferManager *)object;
+  g_autoptr(IdeBuffer) buffer = NULL;
+  FocusLocation *state = user_data;
+  GError *error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+  g_assert (state != NULL);
+  g_assert (IDE_IS_EDITOR_SURFACE (state->self));
+  g_assert (state->location != NULL);
+
+  if (!(buffer = ide_buffer_manager_load_file_finish (bufmgr, result, &error)))
+    {
+      /* TODO: display warning breifly to the user in the frame? */
+      g_warning ("%s", error->message);
+      g_clear_error (&error);
+      IDE_GOTO (cleanup);
+    }
+
+  /* try again now that we have loaded */
+  ide_editor_surface_focus_location_full (state->self, state->location, FALSE);
+
+cleanup:
+  g_clear_object (&state->self);
+  g_clear_object (&state->location);
+  g_slice_free (FocusLocation, state);
+
+  IDE_EXIT;
+}
+
+static void
+ide_editor_surface_focus_location_full (IdeEditorSurface  *self,
+                                        IdeLocation *location,
+                                        gboolean           open_if_not_found)
+{
+  struct {
+    GFile *file;
+    IdeEditorPage *page;
+  } lookup = { 0 };
+  GtkWidget *stack;
+  gint line;
+  gint line_offset;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EDITOR_SURFACE (self));
+  g_assert (location != NULL);
+
+  lookup.file = ide_location_get_file (location);
+  lookup.page = NULL;
+
+  if (lookup.file == NULL)
+    {
+      g_warning ("IdeLocation does not contain a file");
+      IDE_EXIT;
+    }
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    const gchar *path = g_file_peek_path (lookup.file);
+    IDE_TRACE_MSG ("Locating %s, open_if_not_found=%d",
+                   path, open_if_not_found);
+  }
+#endif
+
+  ide_surface_foreach_page (IDE_SURFACE (self),
+                            ide_editor_surface_find_source_location,
+                            &lookup);
+
+  if (!open_if_not_found && lookup.page == NULL)
+    IDE_EXIT;
+
+  if (lookup.page == NULL)
+    {
+      FocusLocation *state;
+      IdeBufferManager *bufmgr;
+      IdeWorkbench *workbench;
+      IdeContext *context;
+
+      workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+      context = ide_workbench_get_context (workbench);
+      bufmgr = ide_buffer_manager_from_context (context);
+
+      state = g_slice_new0 (FocusLocation);
+      state->self = g_object_ref (self);
+      state->location = g_object_ref (location);
+
+      ide_buffer_manager_load_file_async (bufmgr,
+                                          lookup.file,
+                                          IDE_BUFFER_OPEN_FLAGS_NONE,
+                                          NULL,
+                                          NULL,
+                                          ide_editor_surface_focus_location_cb,
+                                          state);
+      IDE_EXIT;
+    }
+
+  line = ide_location_get_line (location);
+  line_offset = ide_location_get_line_offset (location);
+
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (lookup.page), IDE_TYPE_FRAME);
+  ide_frame_set_visible_child (IDE_FRAME (stack), IDE_PAGE (lookup.page));
+
+  /*
+   * Ignore 0:0 so that we don't jump from the previous cursor position,
+   * if any. It's somewhat problematic if we know we need to go to 0:0,
+   * but that is less likely.
+   */
+  if (line > 0 || line_offset > 0)
+    ide_editor_page_scroll_to_line_offset (lookup.page,
+                                           MAX (line, 0),
+                                           MAX (line_offset, 0));
+  else
+    gtk_widget_grab_focus (GTK_WIDGET (lookup.page));
+
+  IDE_EXIT;
+}
+
+void
+ide_editor_surface_focus_location (IdeEditorSurface  *self,
+                                   IdeLocation *location)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_EDITOR_SURFACE (self));
+  g_return_if_fail (location != NULL);
+
+  ide_editor_surface_focus_location_full (self, location, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+locate_page_for_buffer (GtkWidget *widget,
+                        gpointer   user_data)
+{
+  struct {
+    IdeBuffer *buffer;
+    IdePage   *page;
+  } *lookup = user_data;
+
+  if (lookup->page != NULL)
+    return;
+
+  if (IDE_IS_EDITOR_PAGE (widget))
+    {
+      if (ide_editor_page_get_buffer (IDE_EDITOR_PAGE (widget)) == lookup->buffer)
+        lookup->page = IDE_PAGE (widget);
+    }
+}
+
+static gboolean
+ide_editor_surface_focus_if_found (IdeEditorSurface *self,
+                                   IdeBuffer        *buffer,
+                                   gboolean          any_stack)
+{
+  IdeFrame *stack;
+  struct {
+    IdeBuffer *buffer;
+    IdePage   *page;
+  } lookup = { buffer };
+
+  g_return_val_if_fail (IDE_IS_EDITOR_SURFACE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_BUFFER (buffer), FALSE);
+
+  stack = ide_grid_get_current_stack (self->grid);
+
+  if (any_stack)
+    ide_grid_foreach_page (self->grid, locate_page_for_buffer, &lookup);
+  else
+    ide_frame_foreach_page (stack, locate_page_for_buffer, &lookup);
+
+  if (lookup.page != NULL)
+    {
+      stack = IDE_FRAME (gtk_widget_get_ancestor (GTK_WIDGET (lookup.page), IDE_TYPE_FRAME));
+      ide_frame_set_visible_child (stack, lookup.page);
+      gtk_widget_grab_focus (GTK_WIDGET (lookup.page));
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+void
+ide_editor_surface_focus_buffer (IdeEditorSurface *self,
+                                 IdeBuffer        *buffer)
+{
+  IdeEditorPage *page;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_EDITOR_SURFACE (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (ide_editor_surface_focus_if_found (self, buffer, TRUE))
+    IDE_EXIT;
+
+  page = g_object_new (IDE_TYPE_EDITOR_PAGE,
+                       "buffer", buffer,
+                       "visible", TRUE,
+                       NULL);
+  gtk_container_add (GTK_CONTAINER (self->grid), GTK_WIDGET (page));
+
+  IDE_EXIT;
+}
+
+void
+ide_editor_surface_focus_buffer_in_current_stack (IdeEditorSurface *self,
+                                                  IdeBuffer        *buffer)
+{
+  IdeFrame *stack;
+  IdeEditorPage *page;
+
+  g_return_if_fail (IDE_IS_EDITOR_SURFACE (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  if (ide_editor_surface_focus_if_found (self, buffer, FALSE))
+    return;
+
+  stack = ide_grid_get_current_stack (self->grid);
+
+  page = g_object_new (IDE_TYPE_EDITOR_PAGE,
+                       "buffer", buffer,
+                       "visible", TRUE,
+                       NULL);
+
+  gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page));
+}
+
+/**
+ * ide_editor_surface_get_active_page:
+ * @self: a #IdeEditorSurface
+ *
+ * Gets the active page for the surface, or %NULL if there is not one.
+ *
+ * Returns: (nullable) (transfer none): An #IdePage or %NULL.
+ *
+ * Since: 3.32
+ */
+IdePage *
+ide_editor_surface_get_active_page (IdeEditorSurface *self)
+{
+  IdeFrame *stack;
+
+  g_return_val_if_fail (IDE_IS_EDITOR_SURFACE (self), NULL);
+
+  stack = ide_grid_get_current_stack (self->grid);
+
+  return ide_frame_get_visible_child (stack);
+}
+
+/**
+ * ide_editor_surface_get_sidebar:
+ * @self: a #IdeEditorSurface
+ *
+ * Gets the #IdeEditorSidebar for the editor surface.
+ *
+ * Returns: (transfer none): an #IdeEditorSidebar
+ *
+ * Since: 3.32
+ */
+IdeEditorSidebar *
+ide_editor_surface_get_sidebar (IdeEditorSurface *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SURFACE (self), NULL);
+
+  return IDE_EDITOR_SIDEBAR (dzl_dock_bin_get_left_edge (DZL_DOCK_BIN (self)));
+}
+
+/**
+ * ide_editor_surface_get_transient_sidebar:
+ * @self: a #IdeEditorSurface
+ *
+ * Gets the transient sidebar for the editor surface.
+ *
+ * The transient sidebar is a sidebar on the right side of the surface. It
+ * is displayed only when necessary. It animates in and out of page based on
+ * focus tracking and other heuristics.
+ *
+ * Returns: (transfer none): An #IdeTransientSidebar
+ *
+ * Since: 3.32
+ */
+IdeTransientSidebar *
+ide_editor_surface_get_transient_sidebar (IdeEditorSurface *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SURFACE (self), NULL);
+
+  return IDE_TRANSIENT_SIDEBAR (dzl_dock_bin_get_right_edge (DZL_DOCK_BIN (self)));
+}
+
+/**
+ * ide_editor_surface_get_utilities:
+ *
+ * Returns: (transfer none): An #IdeEditorUtilities
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_editor_surface_get_utilities (IdeEditorSurface *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SURFACE (self), NULL);
+
+  return dzl_dock_bin_get_bottom_edge (DZL_DOCK_BIN (self));
+}
+
+/**
+ * ide_editor_surface_get_overlay:
+ * @self: a #IdeEditorSurface
+ *
+ * Gets the overlay widget which can be used to layer things above all
+ * items in the layout grid.
+ *
+ * Returns: (transfer none) (type Gtk.Overlay): a #GtkWidget
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_editor_surface_get_overlay (IdeEditorSurface *self)
+{
+  g_return_val_if_fail (IDE_IS_EDITOR_SURFACE (self), NULL);
+
+  return GTK_WIDGET (self->overlay);
+}
+
+void
+_ide_editor_surface_set_loading (IdeEditorSurface *self,
+                                 gboolean          loading)
+{
+  g_return_if_fail (IDE_IS_EDITOR_SURFACE (self));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->grid), !loading);
+  gtk_stack_set_visible_child_name (self->loading_stack,
+                                    loading ? "empty_state" : "grid");
+}
+
+/**
+ * ide_editor_surface_new:
+ *
+ * Returns: (transfer full): Creates a new #IdeEditorSurface
+ *
+ * Since: 3.32
+ */
+IdeSurface *
+ide_editor_surface_new (void)
+{
+  return g_object_new (IDE_TYPE_EDITOR_SURFACE, NULL);
+}
diff --git a/src/libide/editor/ide-editor-surface.h b/src/libide/editor/ide-editor-surface.h
new file mode 100644
index 000000000..ea2a4eef6
--- /dev/null
+++ b/src/libide/editor/ide-editor-surface.h
@@ -0,0 +1,65 @@
+/* ide-editor-surface.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_EDITOR_INSIDE) && !defined (IDE_EDITOR_COMPILATION)
+# error "Only <idide-editor.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-code.h>
+#include <libide-gui.h>
+
+#include "ide-editor-sidebar.h"
+#include "ide-editor-utilities.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_EDITOR_SURFACE (ide_editor_surface_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeEditorSurface, ide_editor_surface, IDE, EDITOR_SURFACE, IdeSurface)
+
+IDE_AVAILABLE_IN_3_32
+IdeSurface          *ide_editor_surface_new                           (void);
+IDE_AVAILABLE_IN_3_32
+void                 ide_editor_surface_focus_buffer                  (IdeEditorSurface *self,
+                                                                       IdeBuffer        *buffer);
+IDE_AVAILABLE_IN_3_32
+void                 ide_editor_surface_focus_buffer_in_current_stack (IdeEditorSurface *self,
+                                                                       IdeBuffer        *buffer);
+IDE_AVAILABLE_IN_3_32
+void                 ide_editor_surface_focus_location                (IdeEditorSurface *self,
+                                                                       IdeLocation      *location);
+IDE_AVAILABLE_IN_3_32
+IdePage             *ide_editor_surface_get_active_page               (IdeEditorSurface *self);
+IDE_AVAILABLE_IN_3_32
+IdeGrid             *ide_editor_surface_get_grid                      (IdeEditorSurface *self);
+IDE_AVAILABLE_IN_3_32
+IdeEditorSidebar    *ide_editor_surface_get_sidebar                   (IdeEditorSurface *self);
+IDE_AVAILABLE_IN_3_32
+IdeTransientSidebar *ide_editor_surface_get_transient_sidebar         (IdeEditorSurface *self);
+IDE_AVAILABLE_IN_3_32
+GtkWidget           *ide_editor_surface_get_utilities                 (IdeEditorSurface *self);
+IDE_AVAILABLE_IN_3_32
+GtkWidget           *ide_editor_surface_get_overlay                   (IdeEditorSurface *self);
+
+G_END_DECLS
diff --git a/src/libide/editor/ide-editor-surface.ui b/src/libide/editor/ide-editor-surface.ui
new file mode 100644
index 000000000..a65af292a
--- /dev/null
+++ b/src/libide/editor/ide-editor-surface.ui
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeEditorSurface" parent="IdeSurface">
+    <child>
+      <object class="GtkOverlay" id="overlay">
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkStack" id="loading_stack">
+            <property name="visible">true</property>
+            <property name="transition-type">crossfade</property>
+            <child>
+              <object class="DzlEmptyState" id="empty_state">
+                <property name="icon-name">document-open-recent-symbolic</property>
+                <property name="title" translatable="yes">Restoring previous session</property>
+                <property name="subtitle" translatable="yes">Your previous session will be ready in a 
moment.</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="name">empty_state</property>
+              </packing>
+            </child>
+            <child>
+              <object class="IdeGrid" id="grid">
+                <property name="orientation">horizontal</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="name">grid</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/editor/ide-editor-utilities.c b/src/libide/editor/ide-editor-utilities.c
index 373b9f098..b8fccaf26 100644
--- a/src/libide/editor/ide-editor-utilities.c
+++ b/src/libide/editor/ide-editor-utilities.c
@@ -1,6 +1,6 @@
 /* ide-editor-utilities.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-editor-utilities"
 
 #include "config.h"
 
-#include "editor/ide-editor-utilities.h"
+#include "ide-editor-utilities.h"
 
 /**
  * SECTION:ide-editor-utilities
@@ -34,16 +36,16 @@
  *
  * You can get this widget via ide_editor_perspective_get_utilities().
  *
- * Since: 3.26
+ * Since: 3.32
  */
 
 struct _IdeEditorUtilities
 {
-  IdeLayoutPane  parent_instance;
+  IdePanel  parent_instance;
   DzlDockStack  *stack;
 };
 
-G_DEFINE_TYPE (IdeEditorUtilities, ide_editor_utilities, IDE_TYPE_LAYOUT_PANE)
+G_DEFINE_TYPE (IdeEditorUtilities, ide_editor_utilities, IDE_TYPE_PANEL)
 
 static void
 ide_editor_utilities_add (GtkContainer *container,
diff --git a/src/libide/editor/ide-editor-utilities.h b/src/libide/editor/ide-editor-utilities.h
index 8723273d5..8893f35ec 100644
--- a/src/libide/editor/ide-editor-utilities.h
+++ b/src/libide/editor/ide-editor-utilities.h
@@ -1,6 +1,6 @@
 /* ide-editor-utilities.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,20 +14,25 @@
  *
  * 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 "ide-version-macros.h"
+#if !defined (IDE_EDITOR_INSIDE) && !defined (IDE_EDITOR_COMPILATION)
+# error "Only <libide-editor.h> can be included directly."
+#endif
 
-#include "layout/ide-layout-pane.h"
+#include <libide-core.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_EDITOR_UTILITIES (ide_editor_utilities_get_type())
 
-IDE_AVAILABLE_IN_ALL
-G_DECLARE_FINAL_TYPE (IdeEditorUtilities, ide_editor_utilities, IDE, EDITOR_UTILITIES, IdeLayoutPane)
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeEditorUtilities, ide_editor_utilities, IDE, EDITOR_UTILITIES, IdePanel)
 
 /* Use GtkContainer api to add your DzlDockWidget */
 
diff --git a/src/libide/editor/ide-editor-workspace.c b/src/libide/editor/ide-editor-workspace.c
new file mode 100644
index 000000000..611dd4fa5
--- /dev/null
+++ b/src/libide/editor/ide-editor-workspace.c
@@ -0,0 +1,110 @@
+/* ide-editor-workspace.c
+ *
+ * Copyright 2018-2019 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 "ide-editor-workspace"
+
+#include "config.h"
+
+#include "ide-editor-surface.h"
+#include "ide-editor-workspace.h"
+
+/**
+ * SECTION:ide-editor-workspace
+ * @title: IdeEditorWorkspace
+ * @short_description: A simplified workspace for dedicated editing
+ *
+ * The #IdeEditorWorkspace is a secondary workspace that can be used to
+ * add additional #IdePage to. It does not contain the full contents of
+ * the #IdePrimaryWorkspace. It is suitable for using on an additional
+ * monitor as well as a dedicated editor in simplified Builder mode when
+ * running directly from the command line.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeEditorWorkspace
+{
+  IdeWorkspace   parent_instance;
+  DzlMenuButton *surface_menu_button;
+};
+
+G_DEFINE_TYPE (IdeEditorWorkspace, ide_editor_workspace, IDE_TYPE_WORKSPACE)
+
+static void
+ide_editor_workspace_surface_set (IdeWorkspace *workspace,
+                                   IdeSurface   *surface)
+{
+  IdeEditorWorkspace *self = (IdeEditorWorkspace *)workspace;
+
+  g_assert (IDE_IS_EDITOR_WORKSPACE (self));
+  g_assert (!surface || IDE_IS_SURFACE (surface));
+
+  if (DZL_IS_DOCK_ITEM (surface))
+    {
+      g_autofree gchar *icon_name = NULL;
+
+      icon_name = dzl_dock_item_get_icon_name (DZL_DOCK_ITEM (surface));
+      g_object_set (self->surface_menu_button,
+                    "icon-name", icon_name,
+                    NULL);
+    }
+
+  IDE_WORKSPACE_CLASS (ide_editor_workspace_parent_class)->surface_set (workspace, surface);
+}
+
+static void
+ide_editor_workspace_class_init (IdeEditorWorkspaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdeWorkspaceClass *workspace_class = IDE_WORKSPACE_CLASS (klass);
+
+  ide_workspace_class_set_kind (workspace_class, "editor");
+
+  workspace_class->surface_set = ide_editor_workspace_surface_set;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-editor/ui/ide-editor-workspace.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeEditorWorkspace, surface_menu_button);
+}
+
+static void
+ide_editor_workspace_init (IdeEditorWorkspace *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+/**
+ * ide_editor_workspace_new:
+ * @app: an #IdeApplication
+ *
+ * Creates a new #IdeEditorWorkspace.
+ *
+ * You'll need to add this to a workbench to be functional.
+ *
+ * Returns: (transfer full): an #IdeEditorWorkspace
+ *
+ * Since: 3.32
+ */
+IdeEditorWorkspace *
+ide_editor_workspace_new (IdeApplication *app)
+{
+  return g_object_new (IDE_TYPE_EDITOR_WORKSPACE,
+                       "application", app,
+                       NULL);
+}
diff --git a/src/libide/editor/ide-editor-workspace.h b/src/libide/editor/ide-editor-workspace.h
new file mode 100644
index 000000000..48a4472c1
--- /dev/null
+++ b/src/libide/editor/ide-editor-workspace.h
@@ -0,0 +1,39 @@
+/* ide-editor-workspace.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_EDITOR_INSIDE) && !defined (IDE_EDITOR_COMPILATION)
+# error "Only <libide-editor.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_EDITOR_WORKSPACE (ide_editor_workspace_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeEditorWorkspace, ide_editor_workspace, IDE, EDITOR_WORKSPACE, IdeWorkspace)
+
+IDE_AVAILABLE_IN_3_32
+IdeEditorWorkspace *ide_editor_workspace_new (IdeApplication *app);
+
+G_END_DECLS
diff --git a/src/libide/editor/ide-editor-workspace.ui b/src/libide/editor/ide-editor-workspace.ui
new file mode 100644
index 000000000..160d93c29
--- /dev/null
+++ b/src/libide/editor/ide-editor-workspace.ui
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeEditorWorkspace" parent="IdeWorkspace">
+    <child type="titlebar">
+      <object class="IdeHeaderBar">
+        <property name="show-close-button">true</property>
+        <property name="show-fullscreen-button">true</property>
+        <property name="menu-id">ide-editor-workspace-menu</property>
+        <property name="visible">true</property>
+        <child type="left">
+          <object class="IdeSurfacesButton" id="surface_menu_button">
+            <property name="focus-on-click">false</property>
+            <property name="menu-id">ide-editor-workspace-surfaces-menu</property>
+            <property name="show-accels">true</property>
+            <property name="show-arrow">true</property>
+            <property name="show-icons">true</property>
+            <!-- disable transitions since they'll cause jitter with the
+                 whole surface changing below it. -->
+            <property name="transitions-enabled">false</property>
+            <property name="has-tooltip">true</property>
+            <property name="tooltip-text" translatable="yes">Change workspace surface</property>
+          </object>
+        </child>
+        <child type="right">
+          <object class="DzlPriorityBox">
+            <property name="visible">true</property>
+            <child>
+              <object class="IdeSearchEntry" id="search_entry">
+                <property name="primary-icon-name">edit-find-symbolic</property>
+                <property name="placeholder-text" translatable="yes">Press Ctrl+. to search</property>
+                <property name="width-chars">5</property>
+                <property name="max-width-chars">24</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="pack-type">end</property>
+                <property name="padding">6</property>
+                <property name="priority">-100</property>
+              </packing>
+            </child>
+            <child>
+              <object class="IdeNotificationsButton" id="notifications_button">
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="pack-type">end</property>
+                <property name="priority">-200</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/editor/libide-editor.gresource.xml b/src/libide/editor/libide-editor.gresource.xml
new file mode 100644
index 000000000..2c390f7e5
--- /dev/null
+++ b/src/libide/editor/libide-editor.gresource.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/libide-editor/ui/">
+    <file preprocess="xml-stripblanks">ide-editor-page.ui</file>
+    <file preprocess="xml-stripblanks">ide-editor-search-bar.ui</file>
+    <file preprocess="xml-stripblanks">ide-editor-settings-dialog.ui</file>
+    <file preprocess="xml-stripblanks">ide-editor-sidebar.ui</file>
+    <file preprocess="xml-stripblanks">ide-editor-surface.ui</file>
+    <file preprocess="xml-stripblanks">ide-editor-workspace.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/editor/libide-editor.h b/src/libide/editor/libide-editor.h
new file mode 100644
index 000000000..b541f5833
--- /dev/null
+++ b/src/libide/editor/libide-editor.h
@@ -0,0 +1,41 @@
+/* libide-editor.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+#include <libide-sourceview.h>
+
+G_BEGIN_DECLS
+
+#define IDE_EDITOR_INSIDE
+
+#include "ide-editor-addin.h"
+#include "ide-editor-page.h"
+#include "ide-editor-page-addin.h"
+#include "ide-editor-search.h"
+#include "ide-editor-sidebar.h"
+#include "ide-editor-surface.h"
+#include "ide-editor-utilities.h"
+#include "ide-editor-workspace.h"
+
+#undef IDE_EDITOR_INSIDE
+
+G_END_DECLS
diff --git a/src/libide/editor/meson.build b/src/libide/editor/meson.build
index 4092e8d1a..ceaf83d3a 100644
--- a/src/libide/editor/meson.build
+++ b/src/libide/editor/meson.build
@@ -1,52 +1,121 @@
-editor_headers = [
+libide_editor_header_dir = join_paths(libide_header_dir, 'editor')
+libide_editor_header_subdir = join_paths(libide_header_subdir, 'editor')
+libide_include_directories += include_directories('.')
+
+libide_editor_sources = []
+libide_editor_public_headers = []
+libide_editor_generated_headers = []
+
+#
+# Public API Headers
+#
+
+libide_editor_public_headers = [
   'ide-editor-addin.h',
-  'ide-editor-perspective.h',
+  'ide-editor-page.h',
+  'ide-editor-page-addin.h',
   'ide-editor-search.h',
   'ide-editor-sidebar.h',
+  'ide-editor-surface.h',
   'ide-editor-utilities.h',
-  'ide-editor-view-addin.h',
-  'ide-editor-view.h',
+  'ide-editor-workspace.h',
+  'libide-editor.h',
+]
+
+libide_editor_private_headers = [
+  'ide-editor-print-operation.h',
+  'ide-editor-search-bar.h',
+  'ide-editor-settings-dialog.h',
 ]
 
-editor_sources = [
+install_headers(libide_editor_public_headers, subdir: libide_editor_header_subdir)
+
+#
+# Sources
+#
+
+libide_editor_public_sources = [
   'ide-editor-addin.c',
-  'ide-editor-perspective.c',
+  'ide-editor-page.c',
+  'ide-editor-page-addin.c',
   'ide-editor-search.c',
   'ide-editor-sidebar.c',
+  'ide-editor-surface.c',
   'ide-editor-utilities.c',
-  'ide-editor-view-addin.c',
-  'ide-editor-view.c',
+  'ide-editor-workspace.c',
 ]
 
-# .h files used for gtk-doc ignores
-editor_private_sources = [
-  'ide-editor-hover-provider.c',
-  'ide-editor-hover-provider.h',
-  'ide-editor-layout-stack-addin.c',
-  'ide-editor-layout-stack-addin.h',
-  'ide-editor-layout-stack-controls.c',
-  'ide-editor-layout-stack-controls.h',
-  'ide-editor-perspective-actions.c',
-  'ide-editor-perspective-shortcuts.c',
-  'ide-editor-plugin.c',
+
+libide_editor_private_sources = [
+  'ide-editor-page-actions.c',
+  'ide-editor-page-settings.c',
+  'ide-editor-page-shortcuts.c',
   'ide-editor-print-operation.c',
-  'ide-editor-print-operation.h',
-  'ide-editor-properties.c',
-  'ide-editor-properties.h',
   'ide-editor-search-bar.c',
-  'ide-editor-search-bar.h',
   'ide-editor-search-bar-shortcuts.c',
-  'ide-editor-session-addin.c',
-  'ide-editor-session-addin.h',
-  'ide-editor-view-actions.c',
-  'ide-editor-view-settings.c',
-  'ide-editor-view-shortcuts.c',
-  'ide-editor-workbench-addin.c',
-  'ide-editor-workbench-addin.h',
+  'ide-editor-settings-dialog.c',
+  'ide-editor-surface-actions.c',
+  'ide-editor-surface-shortcuts.c',
 ]
 
-libide_public_headers += files(editor_headers)
-libide_public_sources += files(editor_sources)
-libide_private_sources += files(editor_private_sources)
+libide_editor_sources += libide_editor_public_sources
+libide_editor_sources += libide_editor_private_sources
+
+#
+# Generated Resource Files
+#
+
+libide_editor_resources = gnome.compile_resources(
+  'ide-editor-resources',
+  'libide-editor.gresource.xml',
+  c_name: 'ide_editor',
+)
+libide_editor_generated_headers += [libide_editor_resources[1]]
+libide_editor_sources += libide_editor_resources[0]
+
+#
+# Dependencies
+#
+
+libide_editor_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+  libpeas_dep,
+
+  libide_core_dep,
+  libide_io_dep,
+  libide_projects_dep,
+  libide_search_dep,
+  libide_sourceview_dep,
+  libide_threading_dep,
+  libide_gui_dep,
+]
+
+libide_editor_internal_deps = [
+  libpangoft2_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_editor = static_library('ide-editor-' + libide_api_version, libide_editor_sources,
+   dependencies: libide_editor_deps + libide_editor_internal_deps,
+         c_args: libide_args + release_args + ['-DIDE_EDITOR_COMPILATION'],
+)
+
+libide_editor_dep = declare_dependency(
+         dependencies: libide_editor_deps,
+           link_whole: libide_editor,
+  include_directories: include_directories('.'),
+              sources: libide_editor_generated_headers,
+)
 
-install_headers(editor_headers, subdir: join_paths(libide_header_subdir, 'editor'))
+gnome_builder_public_sources += files(libide_editor_public_sources)
+gnome_builder_public_headers += files(libide_editor_public_headers)
+gnome_builder_private_sources += files(libide_editor_private_sources)
+gnome_builder_private_headers += files(libide_editor_private_headers)
+gnome_builder_generated_headers += libide_editor_generated_headers
+gnome_builder_include_subdirs += libide_editor_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-editor.h', '-DIDE_EDITOR_COMPILATION']
diff --git a/src/libide/foundry/ide-build-log-private.h b/src/libide/foundry/ide-build-log-private.h
new file mode 100644
index 000000000..c0ecca3ed
--- /dev/null
+++ b/src/libide/foundry/ide-build-log-private.h
@@ -0,0 +1,46 @@
+/* ide-build-log-private.h
+ *
+ * Copyright 2016-2019 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>
+
+#include "ide-build-log.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_LOG (ide_build_log_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeBuildLog, ide_build_log, IDE, BUILD_LOG, GObject)
+
+IdeBuildLog *ide_build_log_new              (void);
+void         ide_build_log_observer         (IdeBuildLogStream    stream,
+                                             const gchar         *message,
+                                             gssize               message_len,
+                                             gpointer             user_data);
+guint        ide_build_log_add_observer     (IdeBuildLog         *self,
+                                             IdeBuildLogObserver  observer,
+                                             gpointer             observer_data,
+                                             GDestroyNotify       observer_data_destroy);
+gboolean     ide_build_log_remove_observer  (IdeBuildLog         *self,
+                                             guint                observer_id);
+
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-log.c b/src/libide/foundry/ide-build-log.c
new file mode 100644
index 000000000..ba0cdb180
--- /dev/null
+++ b/src/libide/foundry/ide-build-log.c
@@ -0,0 +1,247 @@
+/* ide-build-log.c
+ *
+ * Copyright 2016-2019 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 "ide-build-log"
+
+#include "config.h"
+
+#include <libide-core.h>
+#include <string.h>
+
+#include "ide-build-log.h"
+#include "ide-build-log-private.h"
+
+#define POINTER_MARK(p)   GSIZE_TO_POINTER(GPOINTER_TO_SIZE(p)|1)
+#define POINTER_UNMARK(p) GSIZE_TO_POINTER(GPOINTER_TO_SIZE(p)&~(gsize)1)
+#define POINTER_MARKED(p) (GPOINTER_TO_SIZE(p)&1)
+#define DISPATCH_MAX      20
+
+struct _IdeBuildLog
+{
+  GObject      parent_instance;
+
+  GArray      *observers;
+  GAsyncQueue *log_queue;
+  GSource     *log_source;
+
+  guint        sequence;
+};
+
+typedef struct
+{
+  IdeBuildLogObserver callback;
+  gpointer            data;
+  GDestroyNotify      destroy;
+  guint               id;
+} Observer;
+
+G_DEFINE_TYPE (IdeBuildLog, ide_build_log, G_TYPE_OBJECT)
+
+static gboolean
+emit_log_from_main (gpointer user_data)
+{
+  IdeBuildLog *self = user_data;
+  g_autoptr(GPtrArray) ar = g_ptr_array_new ();
+  gpointer item;
+
+  g_assert (IDE_IS_BUILD_LOG (self));
+
+  /*
+   * Pull up to DISPATCH_MAX items from the log queue. We have an upper
+   * bound here so that we don't stall the main loop. Additionally, we
+   * update the ready-time when we run out of items while holding the
+   * async queue lock to synchronize with the caller for further wakeups.
+   */
+  g_async_queue_lock (self->log_queue);
+  for (guint i = 0; i < DISPATCH_MAX; i++)
+    {
+      if (NULL == (item = g_async_queue_try_pop_unlocked (self->log_queue)))
+        {
+          g_source_set_ready_time (self->log_source, -1);
+          break;
+        }
+      g_ptr_array_add (ar, item);
+    }
+  g_async_queue_unlock (self->log_queue);
+
+  for (guint i = 0; i < ar->len; i++)
+    {
+      IdeBuildLogStream stream = IDE_BUILD_LOG_STDOUT;
+      gchar *message;
+      gsize message_len;
+
+      item = g_ptr_array_index (ar, i);
+      message = POINTER_UNMARK (item);
+      message_len = strlen (message);
+
+      if (POINTER_MARKED (item))
+        stream = IDE_BUILD_LOG_STDERR;
+
+      for (guint j = 0; j < self->observers->len; j++)
+        {
+          const Observer *observer = &g_array_index (self->observers, Observer, j);
+
+          observer->callback (stream, message, message_len, observer->data);
+        }
+
+      g_free (message);
+    }
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+ide_build_log_finalize (GObject *object)
+{
+  IdeBuildLog *self = (IdeBuildLog *)object;
+
+  g_clear_pointer (&self->log_queue, g_async_queue_unref);
+  g_clear_pointer (&self->log_source, g_source_destroy);
+  g_clear_pointer (&self->observers, g_array_unref);
+
+  G_OBJECT_CLASS (ide_build_log_parent_class)->finalize (object);
+}
+
+static void
+ide_build_log_class_init (IdeBuildLogClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_build_log_finalize;
+}
+
+static void
+ide_build_log_init (IdeBuildLog *self)
+{
+  self->observers = g_array_new (FALSE, FALSE, sizeof (Observer));
+
+  self->log_queue = g_async_queue_new ();
+
+  self->log_source = g_timeout_source_new (G_MAXINT);
+  g_source_set_priority (self->log_source, G_PRIORITY_LOW);
+  g_source_set_ready_time (self->log_source, -1);
+  g_source_set_name (self->log_source, "[ide] IdeBuildLog");
+  g_source_set_callback (self->log_source, emit_log_from_main, self, NULL);
+  g_source_attach (self->log_source, g_main_context_default ());
+}
+
+static void
+ide_build_log_via_main (IdeBuildLog       *self,
+                        IdeBuildLogStream  stream,
+                        const gchar       *message,
+                        gsize              message_len)
+{
+  gchar *copied = g_strndup (message, message_len);
+
+  if G_UNLIKELY (stream == IDE_BUILD_LOG_STDERR)
+    copied = POINTER_MARK (copied);
+
+  /*
+   * Add the log entry to our queue to be dispatched in the main thread.
+   * However, we hold the async queue lock while updating the source ready
+   * time so we are synchronized with the main thread for setting the
+   * ready time. This is needed because the main thread may not dispatch
+   * all available items in a single dispatch (to avoid stalling the
+   * main loop).
+   */
+
+  g_async_queue_lock (self->log_queue);
+  g_async_queue_push_unlocked (self->log_queue, copied);
+  g_source_set_ready_time (self->log_source, 0);
+  g_async_queue_unlock (self->log_queue);
+}
+
+void
+ide_build_log_observer (IdeBuildLogStream  stream,
+                        const gchar       *message,
+                        gssize             message_len,
+                        gpointer           user_data)
+{
+  IdeBuildLog *self = user_data;
+
+  g_assert (message != NULL);
+
+  if (message_len < 0)
+    message_len = strlen (message);
+
+  g_assert (message[message_len] == '\0');
+
+  if G_LIKELY (IDE_IS_MAIN_THREAD ())
+    {
+      for (guint i = 0; i < self->observers->len; i++)
+        {
+          const Observer *observer = &g_array_index (self->observers, Observer, i);
+
+          observer->callback (stream, message, message_len, observer->data);
+        }
+    }
+  else
+    {
+      ide_build_log_via_main (self, stream, message, message_len);
+    }
+}
+
+guint
+ide_build_log_add_observer (IdeBuildLog         *self,
+                            IdeBuildLogObserver  observer,
+                            gpointer             observer_data,
+                            GDestroyNotify       observer_data_destroy)
+{
+  Observer ele;
+
+  g_return_val_if_fail (IDE_IS_BUILD_LOG (self), 0);
+  g_return_val_if_fail (observer != NULL, 0);
+
+  ele.id = ++self->sequence;
+  ele.callback = observer;
+  ele.data = observer_data;
+  ele.destroy = observer_data_destroy;
+
+  g_array_append_val (self->observers, ele);
+
+  return ele.id;
+}
+
+gboolean
+ide_build_log_remove_observer (IdeBuildLog *self,
+                               guint        observer_id)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_LOG (self), FALSE);
+  g_return_val_if_fail (observer_id > 0, FALSE);
+
+  for (guint i = 0; i < self->observers->len; i++)
+    {
+      const Observer *observer = &g_array_index (self->observers, Observer, i);
+
+      if (observer->id == observer_id)
+        {
+          g_array_remove_index (self->observers, i);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+IdeBuildLog *
+ide_build_log_new (void)
+{
+  return g_object_new (IDE_TYPE_BUILD_LOG, NULL);
+}
diff --git a/src/libide/foundry/ide-build-log.h b/src/libide/foundry/ide-build-log.h
new file mode 100644
index 000000000..e7f42b32f
--- /dev/null
+++ b/src/libide/foundry/ide-build-log.h
@@ -0,0 +1,42 @@
+/* ide-build-log.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_BUILD_LOG_STDOUT,
+  IDE_BUILD_LOG_STDERR,
+} IdeBuildLogStream;
+
+typedef void (*IdeBuildLogObserver) (IdeBuildLogStream  log_stream,
+                                     const gchar       *message,
+                                     gssize             message_len,
+                                     gpointer           user_data);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-manager.c b/src/libide/foundry/ide-build-manager.c
new file mode 100644
index 000000000..9ab244b6b
--- /dev/null
+++ b/src/libide/foundry/ide-build-manager.c
@@ -0,0 +1,1848 @@
+/* ide-build-manager.c
+ *
+ * Copyright 2016-2019 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 "ide-build-manager"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-code.h>
+#include <libide-threading.h>
+#include <libide-vcs.h>
+
+#include "ide-build-manager.h"
+#include "ide-build-pipeline.h"
+#include "ide-build-private.h"
+#include "ide-configuration-manager.h"
+#include "ide-configuration.h"
+#include "ide-device-info.h"
+#include "ide-device-manager.h"
+#include "ide-device.h"
+#include "ide-foundry-compat.h"
+#include "ide-runtime-manager.h"
+#include "ide-runtime-private.h"
+#include "ide-runtime.h"
+#include "ide-toolchain-manager.h"
+#include "ide-toolchain-private.h"
+
+/**
+ * SECTION:ide-build-manager
+ * @title: IdeBuildManager
+ * @short_description: Manages the active build configuration and pipeline
+ *
+ * The #IdeBuildManager is responsible for managing the active build pipeline
+ * as well as providing common high-level actions to plugins.
+ *
+ * You can use various async operations such as
+ * ide_build_manager_execute_async(), ide_build_manager_clean_async(), or
+ * ide_build_manager_rebuild_async() to build, clean, and rebuild respectively
+ * without needing to track the build pipeline.
+ *
+ * The #IdeBuildPipeline is used to specify how and when build operations
+ * should occur. Plugins attach build stages to the pipeline to perform
+ * build actions.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeBuildManager
+{
+  IdeObject         parent_instance;
+
+  GCancellable     *cancellable;
+
+  IdeBuildPipeline *pipeline;
+  GDateTime        *last_build_time;
+  DzlSignalGroup   *pipeline_signals;
+
+  gchar            *branch_name;
+
+  GTimer           *running_time;
+
+  guint             diagnostic_count;
+  guint             error_count;
+  guint             warning_count;
+
+  guint             timer_source;
+
+  guint             can_build : 1;
+  guint             can_export : 1;
+  guint             building : 1;
+  guint             needs_rediagnose : 1;
+  guint             has_configured : 1;
+};
+
+static void initable_iface_init              (GInitableIface  *iface);
+static void ide_build_manager_set_can_build  (IdeBuildManager *self,
+                                              gboolean         can_build);
+static void ide_build_manager_action_build   (IdeBuildManager *self,
+                                              GVariant        *param);
+static void ide_build_manager_action_rebuild (IdeBuildManager *self,
+                                              GVariant        *param);
+static void ide_build_manager_action_cancel  (IdeBuildManager *self,
+                                              GVariant        *param);
+static void ide_build_manager_action_clean   (IdeBuildManager *self,
+                                              GVariant        *param);
+static void ide_build_manager_action_export  (IdeBuildManager *self,
+                                              GVariant        *param);
+static void ide_build_manager_action_install (IdeBuildManager *self,
+                                              GVariant        *param);
+
+DZL_DEFINE_ACTION_GROUP (IdeBuildManager, ide_build_manager, {
+  { "build", ide_build_manager_action_build },
+  { "cancel", ide_build_manager_action_cancel },
+  { "clean", ide_build_manager_action_clean },
+  { "export", ide_build_manager_action_export },
+  { "install", ide_build_manager_action_install },
+  { "rebuild", ide_build_manager_action_rebuild },
+})
+
+G_DEFINE_TYPE_EXTENDED (IdeBuildManager, ide_build_manager, IDE_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
+                        G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
+                                               ide_build_manager_init_action_group))
+
+enum {
+  PROP_0,
+  PROP_BUSY,
+  PROP_CAN_BUILD,
+  PROP_ERROR_COUNT,
+  PROP_HAS_DIAGNOSTICS,
+  PROP_LAST_BUILD_TIME,
+  PROP_MESSAGE,
+  PROP_PIPELINE,
+  PROP_RUNNING_TIME,
+  PROP_WARNING_COUNT,
+  N_PROPS
+};
+
+enum {
+  BUILD_STARTED,
+  BUILD_FINISHED,
+  BUILD_FAILED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static gboolean
+timer_callback (gpointer data)
+{
+  IdeBuildManager *self = data;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+ide_build_manager_start_timer (IdeBuildManager *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (self->timer_source == 0);
+
+  if (self->running_time != NULL)
+    g_timer_start (self->running_time);
+  else
+    self->running_time = g_timer_new ();
+
+  /*
+   * We use the DzlFrameSource for our timer callback because we only want to
+   * update at a rate somewhat close to a typical monitor refresh rate.
+   * Additionally, we want to handle drift (which that source does) so that we
+   * don't constantly fall behind.
+   */
+  self->timer_source = g_timeout_add_seconds (1, timer_callback, self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_stop_timer (IdeBuildManager *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  dzl_clear_source (&self->timer_source);
+
+  if (self->running_time != NULL)
+    {
+      g_timer_stop (self->running_time);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_handle_diagnostic (IdeBuildManager  *self,
+                                     IdeDiagnostic    *diagnostic,
+                                     IdeBuildPipeline *pipeline)
+{
+  IdeDiagnosticSeverity severity;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (diagnostic != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  self->diagnostic_count++;
+  if (self->diagnostic_count == 1)
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
+
+  severity = ide_diagnostic_get_severity (diagnostic);
+
+  if (severity == IDE_DIAGNOSTIC_WARNING)
+    {
+      self->warning_count++;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WARNING_COUNT]);
+    }
+  else if (severity == IDE_DIAGNOSTIC_ERROR || severity == IDE_DIAGNOSTIC_FATAL)
+    {
+      self->error_count++;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ERROR_COUNT]);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_update_action_enabled (IdeBuildManager *self)
+{
+  gboolean busy;
+  gboolean can_build;
+  gboolean can_export;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  busy = ide_build_manager_get_busy (self);
+  can_build = ide_build_manager_get_can_build (self);
+  can_export = self->pipeline ? ide_build_pipeline_get_can_export (self->pipeline) : FALSE;
+
+  ide_build_manager_set_action_enabled (self, "build", !busy && can_build);
+  ide_build_manager_set_action_enabled (self, "cancel", busy);
+  ide_build_manager_set_action_enabled (self, "clean", !busy && can_build);
+  ide_build_manager_set_action_enabled (self, "export", !busy && can_build && can_export);
+  ide_build_manager_set_action_enabled (self, "install", !busy && can_build);
+  ide_build_manager_set_action_enabled (self, "rebuild", !busy && can_build);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+}
+
+static void
+ide_build_manager_notify_busy (IdeBuildManager  *self,
+                               GParamSpec       *pspec,
+                               IdeBuildPipeline *pipeline)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (G_IS_PARAM_SPEC (pspec));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  ide_build_manager_update_action_enabled (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_notify_message (IdeBuildManager  *self,
+                                  GParamSpec       *pspec,
+                                  IdeBuildPipeline *pipeline)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (G_IS_PARAM_SPEC (pspec));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (pipeline == self->pipeline)
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_pipeline_started (IdeBuildManager  *self,
+                                    IdeBuildPhase     phase,
+                                    IdeBuildPipeline *pipeline)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  self->building = TRUE;
+
+  g_signal_emit (self, signals [BUILD_STARTED], 0, pipeline);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_pipeline_finished (IdeBuildManager  *self,
+                                     gboolean          failed,
+                                     IdeBuildPipeline *pipeline)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  self->building = FALSE;
+
+  if (failed)
+    g_signal_emit (self, signals [BUILD_FAILED], 0, pipeline);
+  else
+    g_signal_emit (self, signals [BUILD_FINISHED], 0, pipeline);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_ensure_toolchain_cb (GObject      *object,
+                                       GAsyncResult *result,
+                                       gpointer      user_data)
+{
+  IdeToolchainManager *toolchain_manager = (IdeToolchainManager *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  IdeBuildPipeline *pipeline;
+  IdeBuildManager *self;
+  GCancellable *cancellable;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (toolchain_manager));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  pipeline = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (!_ide_toolchain_manager_prepare_finish (toolchain_manager, result, &error))
+    {
+      g_message ("Failed to prepare toolchain: %s", error->message);
+      IDE_GOTO (failure);
+    }
+
+  if (pipeline != self->pipeline)
+    {
+      IDE_TRACE_MSG ("pipeline is no longer active, ignoring");
+      IDE_GOTO (failure);
+    }
+
+  if (ide_task_return_error_if_cancelled (task))
+    IDE_GOTO (failure);
+
+  cancellable = ide_task_get_cancellable (task);
+
+  /* This will cause plugins to load on the pipeline. */
+  if (!g_initable_init (G_INITABLE (pipeline), cancellable, &error))
+    {
+      /* translators: %s is replaced with the error message */
+      ide_object_warning (self,
+                          _("Failed to initialize build pipeline: %s"),
+                          error->message);
+      IDE_GOTO (failure);
+    }
+
+  ide_build_manager_set_can_build (self, TRUE);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PIPELINE]);
+
+  ide_task_return_boolean (task, TRUE);
+  IDE_EXIT;
+
+failure:
+
+  if (error != NULL)
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "Failed to setup build pipeline");
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_ensure_runtime_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  IdeRuntimeManager *runtime_manager = (IdeRuntimeManager *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  IdeBuildPipeline *pipeline;
+  IdeBuildManager *self;
+  IdeToolchainManager *toolchain_manager;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUNTIME_MANAGER (runtime_manager));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  pipeline = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (!_ide_runtime_manager_prepare_finish (runtime_manager, result, &error))
+    {
+      g_message ("Failed to prepare runtime: %s", error->message);
+      IDE_GOTO (failure);
+    }
+
+  if (pipeline != self->pipeline)
+    {
+      IDE_TRACE_MSG ("pipeline is no longer active, ignoring");
+      IDE_GOTO (failure);
+    }
+
+  if (ide_task_return_error_if_cancelled (task))
+    IDE_GOTO (failure);
+
+  context = ide_object_get_context (IDE_OBJECT (pipeline));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  toolchain_manager = ide_toolchain_manager_from_context (context);
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (toolchain_manager));
+
+  _ide_toolchain_manager_prepare_async (toolchain_manager,
+                                        pipeline,
+                                        ide_task_get_cancellable (task),
+                                        ide_build_manager_ensure_toolchain_cb,
+                                        g_object_ref (task));
+
+  IDE_EXIT;
+
+failure:
+
+  if (error != NULL)
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "Failed to setup build pipeline");
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_device_get_info_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  IdeDevice *device = (IdeDevice *)object;
+  g_autoptr(IdeDeviceInfo) info = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  IdeRuntimeManager *runtime_manager;
+  IdeBuildPipeline *pipeline;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEVICE (device));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  pipeline = ide_task_get_task_data (task);
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  context = ide_object_get_context (IDE_OBJECT (pipeline));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  runtime_manager = ide_runtime_manager_from_context (context);
+  g_assert (IDE_IS_RUNTIME_MANAGER (runtime_manager));
+
+  if (!(info = ide_device_get_info_finish (device, result, &error)))
+    {
+      ide_context_warning (context,
+                           _("Failed to get device information: %s"),
+                           error->message);
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  _ide_build_pipeline_check_toolchain (pipeline, info);
+
+  _ide_runtime_manager_prepare_async (runtime_manager,
+                                      pipeline,
+                                      ide_task_get_cancellable (task),
+                                      ide_build_manager_ensure_runtime_cb,
+                                      g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_invalidate_pipeline (IdeBuildManager *self)
+{
+  g_autoptr(IdeTask) task = NULL;
+  IdeConfigurationManager *config_manager;
+  IdeDeviceManager *device_manager;
+  IdeConfiguration *config;
+  IdeContext *context;
+  IdeDevice *device;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  IDE_TRACE_MSG ("Reloading pipeline due to configuration change");
+
+  /*
+   * If we are currently building, we need to synthesize the failure
+   * of that build and re-setup the pipeline.
+   */
+  if (self->building)
+    {
+      g_assert (self->pipeline != NULL);
+
+      self->building = FALSE;
+      dzl_clear_source (&self->timer_source);
+      g_signal_emit (self, signals [BUILD_FAILED], 0, self->pipeline);
+    }
+
+  /*
+   * Cancel and clear our previous pipeline and associated components
+   * as they are not invalide.
+   */
+  ide_build_manager_cancel (self);
+
+  ide_clear_and_destroy_object (&self->pipeline);
+
+  g_clear_pointer (&self->running_time, g_timer_destroy);
+
+  self->diagnostic_count = 0;
+  self->error_count = 0;
+  self->warning_count = 0;
+
+  /* Don't setup anything new if we're in shutdown */
+  if (ide_object_in_destruction (IDE_OBJECT (context)))
+    IDE_EXIT;
+
+  config_manager = ide_configuration_manager_from_context (context);
+  device_manager = ide_device_manager_from_context (context);
+
+  config = ide_configuration_manager_get_current (config_manager);
+  device = ide_device_manager_get_device (device_manager);
+
+  /*
+   * We want to set the pipeline before connecting things using the GInitable
+   * interface so that we can access the builddir from
+   * IdeRuntime.create_launcher() during pipeline addin initialization.
+   *
+   * We will delay the initialization until after the we have ensured the
+   * runtime is available (possibly installing it).
+   */
+  ide_build_manager_set_can_build (self, FALSE);
+  self->pipeline = g_object_new (IDE_TYPE_BUILD_PIPELINE,
+                                 "configuration", config,
+                                 "device", device,
+                                 NULL);
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (self->pipeline));
+  dzl_signal_group_set_target (self->pipeline_signals, self->pipeline);
+
+  /*
+   * Create a task to manage our async pipeline initialization state.
+   */
+  task = ide_task_new (self, self->cancellable, NULL, NULL);
+  ide_task_set_task_data (task, g_object_ref (self->pipeline), g_object_unref);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  /*
+   * Next, we need to get information on the build device, which may require
+   * connecting to it. So we will query that information (so we can get
+   * arch/kernel/system too). We might need that when bootstrapping the
+   * runtime (if it's missing) among other things.
+   */
+  ide_device_get_info_async (device,
+                             self->cancellable,
+                             ide_build_manager_device_get_info_cb,
+                             g_steal_pointer (&task));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ERROR_COUNT]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LAST_BUILD_TIME]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WARNING_COUNT]);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_vcs_changed (IdeBuildManager *self,
+                               IdeVcs          *vcs)
+{
+  g_autofree gchar *branch_name = NULL;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (IDE_IS_VCS (vcs));
+
+  /* Only invalidate the pipeline if they switched branches. Ignore things
+   * like opening up `git gui` or other things that could touch the index
+   * without really changing things out from underneath us.
+   */
+
+  branch_name = ide_vcs_get_branch_name (vcs);
+
+  if (!ide_str_equal0 (branch_name, self->branch_name))
+    {
+      g_free (self->branch_name);
+      self->branch_name = g_strdup (branch_name);
+      ide_build_manager_invalidate_pipeline (self);
+    }
+}
+
+static gboolean
+initable_init (GInitable     *initable,
+               GCancellable  *cancellable,
+               GError       **error)
+{
+  IdeBuildManager *self = (IdeBuildManager *)initable;
+  IdeConfigurationManager *config_manager;
+  IdeDeviceManager *device_manager;
+  IdeContext *context;
+  IdeVcs *vcs;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  config_manager = ide_configuration_manager_from_context (context);
+  device_manager = ide_device_manager_from_context (context);
+  vcs = ide_vcs_from_context (context);
+
+  self->branch_name = ide_vcs_get_branch_name (vcs);
+
+  g_signal_connect_object (config_manager,
+                           "invalidate",
+                           G_CALLBACK (ide_build_manager_invalidate_pipeline),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (device_manager,
+                           "notify::device",
+                           G_CALLBACK (ide_build_manager_invalidate_pipeline),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (vcs,
+                           "changed",
+                           G_CALLBACK (ide_build_manager_vcs_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_build_manager_invalidate_pipeline (self);
+
+  IDE_RETURN (TRUE);
+}
+
+static void
+ide_build_manager_real_build_started (IdeBuildManager  *self,
+                                      IdeBuildPipeline *pipeline)
+{
+  IdeBuildPhase phase;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  ide_build_manager_start_timer (self);
+
+  /*
+   * When the build completes, we may want to update diagnostics for
+   * files that are open. But we only want to do this if we are reaching
+   * configure for the first time, or performing a real build.
+   */
+
+  phase = ide_build_pipeline_get_requested_phase (pipeline);
+  g_assert ((phase & IDE_BUILD_PHASE_MASK) == phase);
+
+  if (phase == IDE_BUILD_PHASE_BUILD ||
+      (phase == IDE_BUILD_PHASE_CONFIGURE && !self->has_configured))
+    {
+      self->needs_rediagnose = TRUE;
+      self->has_configured = TRUE;
+    }
+}
+
+static void
+ide_build_manager_real_build_failed (IdeBuildManager  *self,
+                                     IdeBuildPipeline *pipeline)
+{
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  ide_build_manager_stop_timer (self);
+}
+
+static void
+ide_build_manager_real_build_finished (IdeBuildManager  *self,
+                                       IdeBuildPipeline *pipeline)
+{
+  IdeDiagnosticsManager *diagnostics;
+  IdeBufferManager *bufmgr;
+  IdeContext *context;
+  guint n_items;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  ide_build_manager_stop_timer (self);
+
+  /*
+   * If this was not a full build (such as advancing to just the configure
+   * phase or so), then there is nothing more to do.
+   */
+  if (!self->needs_rediagnose)
+    return;
+
+  /*
+   * We had a successful build, so lets notify the build manager to reload
+   * dianostics on loaded buffers so the user doesn't have to make a change
+   * to force the update.
+   */
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  diagnostics = ide_diagnostics_manager_from_context (context);
+  bufmgr = ide_buffer_manager_from_context (context);
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (bufmgr));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeBuffer) buffer = g_list_model_get_item (G_LIST_MODEL (bufmgr), i);
+
+      ide_diagnostics_manager_rediagnose (diagnostics, buffer);
+    }
+
+  self->needs_rediagnose = FALSE;
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+  iface->init = initable_init;
+}
+
+static void
+ide_build_manager_finalize (GObject *object)
+{
+  IdeBuildManager *self = (IdeBuildManager *)object;
+
+  ide_clear_and_destroy_object (&self->pipeline);
+  g_clear_object (&self->pipeline_signals);
+  g_clear_object (&self->cancellable);
+  g_clear_pointer (&self->last_build_time, g_date_time_unref);
+  g_clear_pointer (&self->running_time, g_timer_destroy);
+  g_clear_pointer (&self->branch_name, g_free);
+
+  dzl_clear_source (&self->timer_source);
+
+  G_OBJECT_CLASS (ide_build_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_build_manager_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeBuildManager *self = IDE_BUILD_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUSY:
+      g_value_set_boolean (value, ide_build_manager_get_busy (self));
+      break;
+
+    case PROP_CAN_BUILD:
+      g_value_set_boolean (value, ide_build_manager_get_can_build (self));
+      break;
+
+    case PROP_MESSAGE:
+      g_value_take_string (value, ide_build_manager_get_message (self));
+      break;
+
+    case PROP_LAST_BUILD_TIME:
+      g_value_set_boxed (value, ide_build_manager_get_last_build_time (self));
+      break;
+
+    case PROP_RUNNING_TIME:
+      g_value_set_int64 (value, ide_build_manager_get_running_time (self));
+      break;
+
+    case PROP_HAS_DIAGNOSTICS:
+      g_value_set_boolean (value, self->diagnostic_count > 0);
+      break;
+
+    case PROP_ERROR_COUNT:
+      g_value_set_uint (value, self->error_count);
+      break;
+
+    case PROP_WARNING_COUNT:
+      g_value_set_uint (value, self->warning_count);
+      break;
+
+    case PROP_PIPELINE:
+      g_value_set_object (value, self->pipeline);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_manager_class_init (IdeBuildManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_build_manager_finalize;
+  object_class->get_property = ide_build_manager_get_property;
+
+  /**
+   * IdeBuildManager:can-build:
+   *
+   * Gets if the build manager can queue a build request.
+   *
+   * This might be false if the required runtime is not available or other
+   * errors in setting up the build pipeline.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CAN_BUILD] =
+    g_param_spec_boolean ("can-build",
+                          "Can Build",
+                          "If the manager can queue a build",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildManager:busy:
+   *
+   * The "busy" property indicates if there is currently a build
+   * executing. This can be bound to UI elements to display to the
+   * user that a build is active (and therefore other builds cannot
+   * be activated at the moment).
+   *
+   * Since: 3.32
+   */
+  properties [PROP_BUSY] =
+    g_param_spec_boolean ("busy",
+                          "Busy",
+                          "If a build is actively executing",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeBuildManager:error-count:
+   *
+   * The number of errors discovered during the build process.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ERROR_COUNT] =
+    g_param_spec_uint ("error-count",
+                       "Error Count",
+                       "The number of errors that have been seen in the current build",
+                       0, G_MAXUINT, 0,
+                       (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildManager:has-diagnostics:
+   *
+   * The "has-diagnostics" property indicates that there have been
+   * diagnostics found during the last execution of the build pipeline.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HAS_DIAGNOSTICS] =
+    g_param_spec_boolean ("has-diagnostics",
+                          "Has Diagnostics",
+                          "Has Diagnostics",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeBuildManager:last-build-time:
+   *
+   * The "last-build-time" property contains a #GDateTime of the time
+   * the last build request was submitted.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_LAST_BUILD_TIME] =
+    g_param_spec_boxed ("last-build-time",
+                        "Last Build Time",
+                        "The time of the last build request",
+                        G_TYPE_DATE_TIME,
+                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeBuildManager:message:
+   *
+   * The "message" property contains a string message describing
+   * the current state of the build process. This may be bound to
+   * UI elements to notify the user of the buid progress.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_MESSAGE] =
+    g_param_spec_string ("message",
+                         "Message",
+                         "The current build message",
+                         NULL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeBuildManager:pipeline:
+   *
+   * The "pipeline" property is the build pipeline that the build manager
+   * is currently managing.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PIPELINE] =
+    g_param_spec_object ("pipeline",
+                         "Pipeline",
+                         "The build pipeline",
+                         IDE_TYPE_BUILD_PIPELINE,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildManager:running-time:
+   *
+   * The "running-time" property can be bound by UI elements that
+   * want to track how long the current build has taken. g_object_notify()
+   * is called on a regular interval during the build so that the UI
+   * elements may automatically update.
+   *
+   * The value of this property is a #GTimeSpan, which are 64-bit signed
+   * integers with microsecond precision. See %G_USEC_PER_SEC for a constant
+   * to tranform this to seconds.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_RUNNING_TIME] =
+    g_param_spec_int64 ("running-time",
+                        "Running Time",
+                        "The amount of elapsed time performing the current build",
+                        0,
+                        G_MAXINT64,
+                        0,
+                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeBuildManager:warning-count:
+   *
+   * The "warning-count" property contains the number of warnings that have
+   * been discovered in the current build request.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_WARNING_COUNT] =
+    g_param_spec_uint ("warning-count",
+                       "Warning Count",
+                       "The number of warnings that have been seen in the current build",
+                       0, G_MAXUINT, 0,
+                       (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeBuildManager::build-started:
+   * @self: An #IdeBuildManager
+   * @pipeline: An #IdeBuildPipeline
+   *
+   * The "build-started" signal is emitted when a new build has started.
+   * The build may be an incremental build. The @pipeline instance is
+   * the build pipeline which is being executed.
+   *
+   * Since: 3.32
+   */
+  signals [BUILD_STARTED] =
+    g_signal_new_class_handler ("build-started",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_build_manager_real_build_started),
+                                NULL, NULL,
+                                NULL,
+                                G_TYPE_NONE, 1, IDE_TYPE_BUILD_PIPELINE);
+
+  /**
+   * IdeBuildManager::build-failed:
+   * @self: An #IdeBuildManager
+   * @pipeline: An #IdeBuildPipeline
+   *
+   * The "build-failed" signal is emitted when a build that was previously
+   * notified via #IdeBuildManager::build-started has failed to complete
+   * successfully.
+   *
+   * Contrast this with #IdeBuildManager::build-finished for a successful
+   * build.
+   *
+   * Since: 3.32
+   */
+  signals [BUILD_FAILED] =
+    g_signal_new_class_handler ("build-failed",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_build_manager_real_build_failed),
+                                NULL, NULL,
+                                NULL,
+                                G_TYPE_NONE, 1, IDE_TYPE_BUILD_PIPELINE);
+
+  /**
+   * IdeBuildManager::build-finished:
+   * @self: An #IdeBuildManager
+   * @pipeline: An #IdeBuildPipeline
+   *
+   * The "build-finished" signal is emitted when a build completed
+   * successfully.
+   *
+   * Since: 3.32
+   */
+  signals [BUILD_FINISHED] =
+    g_signal_new_class_handler ("build-finished",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_build_manager_real_build_finished),
+                                NULL, NULL,
+                                NULL,
+                                G_TYPE_NONE, 1, IDE_TYPE_BUILD_PIPELINE);
+}
+
+static void
+ide_build_manager_action_cancel (IdeBuildManager *self,
+                                 GVariant        *param)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  ide_build_manager_cancel (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_action_build (IdeBuildManager *self,
+                                GVariant        *param)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  ide_build_manager_execute_async (self, IDE_BUILD_PHASE_BUILD, NULL, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_action_rebuild (IdeBuildManager *self,
+                                  GVariant        *param)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  ide_build_manager_rebuild_async (self, IDE_BUILD_PHASE_BUILD, NULL, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_action_clean (IdeBuildManager *self,
+                                GVariant        *param)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  ide_build_manager_clean_async (self, IDE_BUILD_PHASE_BUILD, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_action_install (IdeBuildManager *self,
+                                  GVariant        *param)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  ide_build_manager_execute_async (self, IDE_BUILD_PHASE_INSTALL, NULL, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_action_export (IdeBuildManager *self,
+                                 GVariant        *param)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+
+  ide_build_manager_execute_async (self, IDE_BUILD_PHASE_EXPORT, NULL, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_init (IdeBuildManager *self)
+{
+  IDE_ENTRY;
+
+  ide_build_manager_update_action_enabled (self);
+
+  self->cancellable = g_cancellable_new ();
+  self->needs_rediagnose = TRUE;
+
+  self->pipeline_signals = dzl_signal_group_new (IDE_TYPE_BUILD_PIPELINE);
+
+  dzl_signal_group_connect_object (self->pipeline_signals,
+                                   "diagnostic",
+                                   G_CALLBACK (ide_build_manager_handle_diagnostic),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->pipeline_signals,
+                                   "notify::busy",
+                                   G_CALLBACK (ide_build_manager_notify_busy),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->pipeline_signals,
+                                   "notify::message",
+                                   G_CALLBACK (ide_build_manager_notify_message),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->pipeline_signals,
+                                   "started",
+                                   G_CALLBACK (ide_build_manager_pipeline_started),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->pipeline_signals,
+                                   "finished",
+                                   G_CALLBACK (ide_build_manager_pipeline_finished),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_manager_get_busy:
+ * @self: An #IdeBuildManager
+ *
+ * Gets if the #IdeBuildManager is currently busy building the project.
+ *
+ * See #IdeBuildManager:busy for more information.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_manager_get_busy (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
+
+  if G_LIKELY (self->pipeline != NULL)
+    return ide_build_pipeline_get_busy (self->pipeline);
+
+  return FALSE;
+}
+
+/**
+ * ide_build_manager_get_message:
+ * @self: An #IdeBuildManager
+ *
+ * This function returns the current build message as a string.
+ *
+ * See #IdeBuildManager:message for more information.
+ *
+ * Returns: (transfer full): A string containing the build message or %NULL
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_build_manager_get_message (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), NULL);
+
+  if G_LIKELY (self->pipeline != NULL)
+    return ide_build_pipeline_get_message (self->pipeline);
+
+  return NULL;
+}
+
+/**
+ * ide_build_manager_get_last_build_time:
+ * @self: An #IdeBuildManager
+ *
+ * This function returns a #GDateTime of the last build request. If
+ * there has not yet been a build request, this will return %NULL.
+ *
+ * See #IdeBuildManager:last-build-time for more information.
+ *
+ * Returns: (nullable) (transfer none): a #GDateTime or %NULL.
+ *
+ * Since: 3.32
+ */
+GDateTime *
+ide_build_manager_get_last_build_time (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), NULL);
+
+  return self->last_build_time;
+}
+
+/**
+ * ide_build_manager_get_running_time:
+ *
+ * Gets the amount of elapsed time of the current build as a
+ * #GTimeSpan.
+ *
+ * Returns: a #GTimeSpan containing the elapsed time of the build.
+ *
+ * Since: 3.32
+ */
+GTimeSpan
+ide_build_manager_get_running_time (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), 0);
+
+  if (self->running_time != NULL)
+    return g_timer_elapsed (self->running_time, NULL) * G_TIME_SPAN_SECOND;
+
+  return 0;
+}
+
+/**
+ * ide_build_manager_cancel:
+ * @self: An #IdeBuildManager
+ *
+ * This function will cancel any in-flight builds.
+ *
+ * You may also activate this using the "cancel" #GAction provided
+ * by the #GActionGroup interface.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_manager_cancel (IdeBuildManager *self)
+{
+  g_autoptr(GCancellable) cancellable = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_MANAGER (self));
+
+  cancellable = g_steal_pointer (&self->cancellable);
+  self->cancellable = g_cancellable_new ();
+
+  g_debug ("Cancelling [%p] build due to user request", cancellable);
+
+  if (!g_cancellable_is_cancelled (cancellable))
+    g_cancellable_cancel (cancellable);
+
+  if (self->pipeline != NULL)
+    _ide_build_pipeline_cancel (self->pipeline);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_manager_get_pipeline:
+ * @self: An #IdeBuildManager
+ *
+ * This function gets the current build pipeline. The pipeline will be
+ * reloaded as build configurations change.
+ *
+ * Returns: (transfer none) (nullable): An #IdeBuildPipeline.
+ *
+ * Since: 3.32
+ */
+IdeBuildPipeline *
+ide_build_manager_get_pipeline (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), NULL);
+
+  return self->pipeline;
+}
+
+/**
+ * ide_build_manager_ref_pipeline:
+ * @self: a #IdeBuildManager
+ *
+ * A thread-safe variant of ide_build_manager_get_pipeline().
+ *
+ * Returns: (transfer full) (nullable): an #IdeBuildPipeline or %NULL
+ *
+ * Since: 3.32
+ */
+IdeBuildPipeline *
+ide_build_manager_ref_pipeline (IdeBuildManager *self)
+{
+  IdeBuildPipeline *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  g_set_object (&ret, self->pipeline);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+static void
+ide_build_manager_execute_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  IdeBuildPipeline *pipeline = (IdeBuildPipeline *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_build_pipeline_execute_finish (pipeline, result, &error))
+    {
+      ide_object_warning (pipeline, "%s", error->message);
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_GOTO (failure);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+
+failure:
+  IDE_EXIT;
+}
+
+static void
+ide_build_manager_save_all_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  IdeBufferManager *buffer_manager = (IdeBufferManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeBuildManager *self;
+  GCancellable *cancellable;
+  GPtrArray *targets;
+  IdeBuildPhase phase;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  cancellable = ide_task_get_cancellable (task);
+  targets = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUILD_MANAGER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!ide_buffer_manager_save_all_finish (buffer_manager, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  phase = ide_build_pipeline_get_requested_phase (self->pipeline);
+
+  ide_build_pipeline_build_targets_async (self->pipeline,
+                                          phase,
+                                          targets,
+                                          cancellable,
+                                          ide_build_manager_execute_cb,
+                                          g_steal_pointer (&task));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LAST_BUILD_TIME]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_manager_execute_async:
+ * @self: An #IdeBuildManager
+ * @phase: An #IdeBuildPhase or 0
+ * @targets: (nullable) (element-type IdeBuildTarget): an array of
+ *   #IdeBuildTarget to build
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: A callback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * This function will request that @phase is completed in the underlying
+ * build pipeline and execute a build. Upon completion, @callback will be
+ * executed and it can determine the success or failure of the operation
+ * using ide_build_manager_execute_finish().
+ *
+ * Since: 3.32
+ */
+void
+ide_build_manager_execute_async (IdeBuildManager     *self,
+                                 IdeBuildPhase        phase,
+                                 GPtrArray           *targets,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  IdeBufferManager *buffer_manager;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_MANAGER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (!g_cancellable_is_cancelled (self->cancellable));
+
+  cancellable = dzl_cancellable_chain (cancellable, self->cancellable);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_build_manager_execute_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_return_on_cancel (task, TRUE);
+
+  if (targets != NULL)
+    ide_task_set_task_data (task, _g_ptr_array_copy_objects (targets), g_ptr_array_unref);
+
+  if (self->pipeline == NULL ||
+      self->can_build == FALSE ||
+      !ide_build_pipeline_is_ready (self->pipeline))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_PENDING,
+                                 "Cannot execute pipeline, it has not yet been prepared");
+      IDE_EXIT;
+    }
+
+  if (!ide_build_pipeline_request_phase (self->pipeline, phase))
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  /*
+   * Only update our "build time" if we are advancing to IDE_BUILD_PHASE_BUILD,
+   * we don't really care about "builds" for configure stages and less.
+   */
+  if ((phase & IDE_BUILD_PHASE_MASK) >= IDE_BUILD_PHASE_BUILD)
+    {
+      g_clear_pointer (&self->last_build_time, g_date_time_unref);
+      self->last_build_time = g_date_time_new_now_local ();
+      self->diagnostic_count = 0;
+      self->warning_count = 0;
+      self->error_count = 0;
+    }
+
+  /*
+   * If we are performing a real build (not just something like configure),
+   * then we want to ensure we save all the buffers. We don't want to do this
+   * on every keypress (and execute_async() could be called on every keypress)
+   * for ensuring build flags are up to date.
+   */
+  if ((phase & IDE_BUILD_PHASE_MASK) >= IDE_BUILD_PHASE_BUILD)
+    {
+      context = ide_object_get_context (IDE_OBJECT (self));
+      buffer_manager = ide_buffer_manager_from_context (context);
+      ide_buffer_manager_save_all_async (buffer_manager,
+                                         cancellable,
+                                         ide_build_manager_save_all_cb,
+                                         g_steal_pointer (&task));
+      IDE_EXIT;
+    }
+
+  ide_build_pipeline_build_targets_async (self->pipeline,
+                                          phase,
+                                          targets,
+                                          cancellable,
+                                          ide_build_manager_execute_cb,
+                                          g_steal_pointer (&task));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ERROR_COUNT]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LAST_BUILD_TIME]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNNING_TIME]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WARNING_COUNT]);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_manager_execute_finish:
+ * @self: An #IdeBuildManager
+ * @result: a #GAsyncResult
+ * @error: A location for a #GError or %NULL
+ *
+ * Completes a request to ide_build_manager_execute_async().
+ *
+ * Returns: %TRUE if successful, otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_manager_execute_finish (IdeBuildManager  *self,
+                                  GAsyncResult     *result,
+                                  GError          **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_build_manager_clean_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeBuildPipeline *pipeline = (IdeBuildPipeline *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_build_pipeline_clean_finish (pipeline, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+/**
+ * ide_build_manager_clean_async:
+ * @self: a #IdeBuildManager
+ * @phase: the build phase to clean
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: (nullable): a callback to execute upon completion, or %NULL
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously requests that the build pipeline clean up to @phase.
+ *
+ * See ide_build_pipeline_clean_async() for more information.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_manager_clean_async (IdeBuildManager     *self,
+                               IdeBuildPhase        phase,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_MANAGER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (!g_cancellable_is_cancelled (self->cancellable));
+
+  cancellable = dzl_cancellable_chain (cancellable, self->cancellable);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_build_manager_clean_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_return_on_cancel (task, TRUE);
+
+  if (self->pipeline == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_PENDING,
+                                 "Cannot execute pipeline, it has not yet been prepared");
+      IDE_EXIT;
+    }
+
+  self->diagnostic_count = 0;
+  self->error_count = 0;
+  self->warning_count = 0;
+
+  ide_build_pipeline_clean_async (self->pipeline,
+                                  phase,
+                                  cancellable,
+                                  ide_build_manager_clean_cb,
+                                  g_steal_pointer (&task));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ERROR_COUNT]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_DIAGNOSTICS]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_WARNING_COUNT]);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_manager_clean_finish:
+ * @self: a #IdeBuildManager
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_build_manager_clean_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_manager_clean_finish (IdeBuildManager  *self,
+                                GAsyncResult     *result,
+                                GError          **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_build_manager_rebuild_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  IdeBuildPipeline *pipeline = (IdeBuildPipeline *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_build_pipeline_rebuild_finish (pipeline, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_manager_rebuild_async:
+ * @self: a #IdeBuildManager
+ * @phase: the build phase to rebuild to
+ * @targets: (element-type IdeBuildTarget) (nullable): an array of #GPtrArray
+ *   of #IdeBuildTarget or %NULL.
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: (nullable): a callback to execute upon completion, or %NULL
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously requests that the build pipeline clean and rebuild up
+ * to the given phase. This may involve discarding previous build artifacts
+ * to allow for the rebuild process.
+ *
+ * See ide_build_pipeline_rebuild_async() for more information.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_manager_rebuild_async (IdeBuildManager     *self,
+                                 IdeBuildPhase        phase,
+                                 GPtrArray           *targets,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_MANAGER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (!g_cancellable_is_cancelled (self->cancellable));
+
+  cancellable = dzl_cancellable_chain (cancellable, self->cancellable);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_build_manager_rebuild_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_return_on_cancel (task, TRUE);
+
+  if (self->pipeline == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_PENDING,
+                                 "Cannot execute pipeline, it has not yet been prepared");
+      IDE_EXIT;
+    }
+
+  ide_build_pipeline_rebuild_async (self->pipeline,
+                                    phase,
+                                    targets,
+                                    cancellable,
+                                    ide_build_manager_rebuild_cb,
+                                    g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_manager_rebuild_finish:
+ * @self: a #IdeBuildManager
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_build_manager_rebuild_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_manager_rebuild_finish (IdeBuildManager  *self,
+                                  GAsyncResult     *result,
+                                  GError          **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_build_manager_get_can_build:
+ * @self: a #IdeBuildManager
+ *
+ * Checks if the current pipeline is ready to build.
+ *
+ * Returns: %TRUE if a build operation can advance the pipeline.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_manager_get_can_build (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), FALSE);
+
+  return self->can_build;
+}
+
+static void
+ide_build_manager_set_can_build (IdeBuildManager *self,
+                                 gboolean         can_build)
+{
+  g_return_if_fail (IDE_IS_BUILD_MANAGER (self));
+
+  can_build = !!can_build;
+
+  if (self->can_build != can_build)
+    {
+      self->can_build = can_build;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_BUILD]);
+      ide_build_manager_update_action_enabled (self);
+    }
+}
+
+/**
+ * ide_build_manager_invalidate:
+ * @self: a #IdeBuildManager
+ *
+ * Requests that the #IdeBuildManager invalidate the current pipeline and
+ * setup a new pipeline.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_manager_invalidate (IdeBuildManager *self)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_BUILD_MANAGER (self));
+
+  ide_build_manager_invalidate_pipeline (self);
+}
+
+guint
+ide_build_manager_get_error_count (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), 0);
+
+  return self->error_count;
+}
+
+guint
+ide_build_manager_get_warning_count (IdeBuildManager *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_MANAGER (self), 0);
+
+  return self->warning_count;
+}
diff --git a/src/libide/foundry/ide-build-manager.h b/src/libide/foundry/ide-build-manager.h
new file mode 100644
index 000000000..3a761d47d
--- /dev/null
+++ b/src/libide/foundry/ide-build-manager.h
@@ -0,0 +1,97 @@
+/* ide-build-manager.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-build-pipeline.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_MANAGER (ide_build_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeBuildManager, ide_build_manager, IDE, BUILD_MANAGER, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeBuildManager  *ide_build_manager_from_context        (IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+IdeBuildManager  *ide_build_manager_ref_from_context    (IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_build_manager_get_busy            (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_build_manager_get_can_build       (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+guint             ide_build_manager_get_error_count     (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+guint             ide_build_manager_get_warning_count   (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+gchar            *ide_build_manager_get_message         (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+GDateTime        *ide_build_manager_get_last_build_time (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+GTimeSpan         ide_build_manager_get_running_time    (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+void              ide_build_manager_invalidate          (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+void              ide_build_manager_cancel              (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+IdeBuildPipeline *ide_build_manager_get_pipeline        (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+IdeBuildPipeline *ide_build_manager_ref_pipeline        (IdeBuildManager       *self);
+IDE_AVAILABLE_IN_3_32
+void              ide_build_manager_rebuild_async       (IdeBuildManager       *self,
+                                                         IdeBuildPhase          phase,
+                                                         GPtrArray             *targets,
+                                                         GCancellable          *cancellable,
+                                                         GAsyncReadyCallback    callback,
+                                                         gpointer               user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_build_manager_rebuild_finish      (IdeBuildManager       *self,
+                                                         GAsyncResult          *result,
+                                                         GError               **error);
+IDE_AVAILABLE_IN_3_32
+void              ide_build_manager_execute_async       (IdeBuildManager       *self,
+                                                         IdeBuildPhase          phase,
+                                                         GPtrArray             *targets,
+                                                         GCancellable          *cancellable,
+                                                         GAsyncReadyCallback    callback,
+                                                         gpointer               user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_build_manager_execute_finish      (IdeBuildManager       *self,
+                                                         GAsyncResult          *result,
+                                                         GError               **error);
+IDE_AVAILABLE_IN_3_32
+void              ide_build_manager_clean_async         (IdeBuildManager       *self,
+                                                         IdeBuildPhase          phase,
+                                                         GCancellable          *cancellable,
+                                                         GAsyncReadyCallback    callback,
+                                                         gpointer               user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_build_manager_clean_finish        (IdeBuildManager       *self,
+                                                         GAsyncResult          *result,
+                                                         GError               **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-pipeline-addin.c b/src/libide/foundry/ide-build-pipeline-addin.c
new file mode 100644
index 000000000..7d3f5db1e
--- /dev/null
+++ b/src/libide/foundry/ide-build-pipeline-addin.c
@@ -0,0 +1,108 @@
+/* ide-build-pipeline-addin.c
+ *
+ * Copyright 2016-2019 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 "ide-build-pipeline-addin"
+
+#include "config.h"
+
+#include "ide-build-pipeline.h"
+#include "ide-build-pipeline-addin.h"
+
+G_DEFINE_INTERFACE (IdeBuildPipelineAddin, ide_build_pipeline_addin, IDE_TYPE_OBJECT)
+
+static void
+ide_build_pipeline_addin_default_init (IdeBuildPipelineAddinInterface *iface)
+{
+}
+
+void
+ide_build_pipeline_addin_load (IdeBuildPipelineAddin *self,
+                               IdeBuildPipeline      *pipeline)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (IDE_BUILD_PIPELINE_ADDIN_GET_IFACE (self)->load)
+    IDE_BUILD_PIPELINE_ADDIN_GET_IFACE (self)->load (self, pipeline);
+}
+
+void
+ide_build_pipeline_addin_unload (IdeBuildPipelineAddin *self,
+                                 IdeBuildPipeline      *pipeline)
+{
+  GArray *ar;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE_ADDIN (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (IDE_BUILD_PIPELINE_ADDIN_GET_IFACE (self)->unload)
+    IDE_BUILD_PIPELINE_ADDIN_GET_IFACE (self)->unload (self, pipeline);
+
+  /* Unload any stages that are tracked by the addin */
+  ar = g_object_get_data (G_OBJECT (self), "IDE_BUILD_PIPELINE_ADDIN_STAGES");
+
+  if G_LIKELY (ar != NULL)
+    {
+      for (guint i = 0; i < ar->len; i++)
+        {
+          guint stage_id = g_array_index (ar, guint, i);
+
+          ide_build_pipeline_detach (pipeline, stage_id);
+        }
+    }
+}
+
+/**
+ * ide_build_pipeline_addin_track:
+ * @self: An #IdeBuildPipelineAddin
+ * @stage_id: a stage id returned from ide_build_pipeline_attach()
+ *
+ * This function will track the stage_id that was returned from
+ * ide_build_pipeline_attach() or similar functions. Doing so results in
+ * the stage being automatically disconnected when the addin is unloaded.
+ *
+ * This means that many #IdeBuildPipelineAddin implementations do not need
+ * an unload vfunc if they track all registered stages.
+ *
+ * You should not mix this function with manual pipeline disconnections.
+ * While it should work, that is not yet guaranteed.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_pipeline_addin_track (IdeBuildPipelineAddin *self,
+                                guint                  stage_id)
+{
+  GArray *ar;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE_ADDIN (self));
+  g_return_if_fail (stage_id > 0);
+
+  ar = g_object_get_data (G_OBJECT (self), "IDE_BUILD_PIPELINE_ADDIN_STAGES");
+
+  if (ar == NULL)
+    {
+      ar = g_array_new (FALSE, FALSE, sizeof (guint));
+      g_object_set_data_full (G_OBJECT (self), "IDE_BUILD_PIPELINE_ADDIN_STAGES",
+                              ar, (GDestroyNotify)g_array_unref);
+    }
+
+  g_array_append_val (ar, stage_id);
+}
diff --git a/src/libide/foundry/ide-build-pipeline-addin.h b/src/libide/foundry/ide-build-pipeline-addin.h
new file mode 100644
index 000000000..6341e8cf9
--- /dev/null
+++ b/src/libide/foundry/ide-build-pipeline-addin.h
@@ -0,0 +1,58 @@
+/* ide-build-pipeline-addin.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-build-pipeline.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_PIPELINE_ADDIN (ide_build_pipeline_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeBuildPipelineAddin, ide_build_pipeline_addin, IDE, BUILD_PIPELINE_ADDIN, IdeObject)
+
+struct _IdeBuildPipelineAddinInterface
+{
+  GTypeInterface type_interface;
+
+  void (*load)   (IdeBuildPipelineAddin *self,
+                  IdeBuildPipeline      *pipeline);
+  void (*unload) (IdeBuildPipelineAddin *self,
+                  IdeBuildPipeline      *pipeline);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_build_pipeline_addin_load   (IdeBuildPipelineAddin *self,
+                                      IdeBuildPipeline      *pipeline);
+IDE_AVAILABLE_IN_3_32
+void ide_build_pipeline_addin_unload (IdeBuildPipelineAddin *self,
+                                      IdeBuildPipeline      *pipeline);
+IDE_AVAILABLE_IN_3_32
+void ide_build_pipeline_addin_track  (IdeBuildPipelineAddin *self,
+                                      guint                  stage_id);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-pipeline.c b/src/libide/foundry/ide-build-pipeline.c
new file mode 100644
index 000000000..e3c8ff91b
--- /dev/null
+++ b/src/libide/foundry/ide-build-pipeline.c
@@ -0,0 +1,4119 @@
+/* ide-build-pipeline.c
+ *
+ * Copyright 2016-2019 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 "ide-build-pipeline"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <dazzle.h>
+#include <libide-plugins.h>
+#include <libpeas/peas.h>
+#include <string.h>
+#include <vte/vte.h>
+
+#include <libide-core.h>
+#include <libide-code.h>
+#include <libide-io.h>
+#include <libide-plugins.h>
+#include <libide-projects.h>
+#include <libide-threading.h>
+
+#include "ide-build-log-private.h"
+#include "ide-build-log.h"
+#include "ide-build-pipeline-addin.h"
+#include "ide-build-pipeline.h"
+#include "ide-build-private.h"
+#include "ide-build-stage-launcher.h"
+#include "ide-build-stage-private.h"
+#include "ide-build-stage.h"
+#include "ide-build-system.h"
+#include "ide-device-info.h"
+#include "ide-device.h"
+#include "ide-foundry-compat.h"
+#include "ide-foundry-enums.h"
+#include "ide-runtime.h"
+#include "ide-toolchain-manager.h"
+#include "ide-toolchain.h"
+
+DZL_DEFINE_COUNTER (Instances, "Pipeline", "N Pipelines", "Number of Pipeline instances")
+G_DEFINE_QUARK (ide_build_error, ide_build_error)
+
+/**
+ * SECTION:idebuildpipeline
+ * @title: IdeBuildPipeline
+ * @short_description: Pluggable build pipeline
+ * @include: ide.h
+ *
+ * The #IdeBuildPipeline is responsible for managing the build process
+ * for Builder. It consists of multiple build "phases" (see #IdeBuildPhase
+ * for the individual phases). An #IdeBuildStage can be attached with
+ * a priority to each phase and is the primary mechanism that plugins
+ * use to perform their operations in the proper ordering.
+ *
+ * For example, the flatpak plugin provides its download stage as part of the
+ * %IDE_BUILD_PHASE_DOWNLOAD phase. The autotools plugin provides stages to
+ * phases such as %IDE_BUILD_PHASE_AUTOGEN, %IDE_BUILD_PHASE_CONFIGURE,
+ * %IDE_BUILD_PHASE_BUILD, and %IDE_BUILD_PHASE_INSTALL.
+ *
+ * If you want ensure a particular phase is performed as part of a build,
+ * then fall ide_build_pipeline_request_phase() with the phase you are
+ * interested in seeing complete successfully.
+ *
+ * If your plugin has discovered that something has changed that invalidates a
+ * given phase, use ide_build_pipeline_invalidate_phase() to ensure that the
+ * phase is re-executed the next time a requested phase of higher precidence
+ * is requested.
+ *
+ * It can be useful to perform operations before or after a given stage (but
+ * still be executed as part of that stage) so the %IDE_BUILD_PHASE_BEFORE and
+ * %IDE_BUILD_PHASE_AFTER flags may be xor'd with the requested phase.  If more
+ * precise ordering is required, you may use the priority parameter to order
+ * the operation with regards to other stages in that phase.
+ *
+ * Transient stages may be added to the pipeline and they will be removed after
+ * the ide_build_pipeline_execute_async() operation has completed successfully
+ * or has failed. You can mark a stage as trandient with
+ * ide_build_stage_set_transient(). This may be useful to perform operations
+ * such as an "export tarball" stage which should only run once as determined
+ * by the user requesting a "make dist" style operation.
+ *
+ * Since: 3.32
+ */
+
+typedef struct
+{
+  guint          id;
+  IdeBuildPhase  phase;
+  gint           priority;
+  IdeBuildStage *stage;
+} PipelineEntry;
+
+typedef struct
+{
+  IdeBuildPipeline *self;
+  GPtrArray        *addins;
+} IdleLoadState;
+
+typedef struct
+{
+  guint   id;
+  GRegex *regex;
+} ErrorFormat;
+
+struct _IdeBuildPipeline
+{
+  IdeObject parent_instance;
+
+  /*
+   * A cancellable we can use to chain to all incoming requests so that
+   * all tasks may be cancelled at once when _ide_build_pipeline_cancel()
+   * is called.
+   */
+  GCancellable *cancellable;
+
+  /*
+   * These are our extensions to the BuildPipeline. Plugins insert
+   * them and they might go about adding stages to the pipeline,
+   * add error formats, or just monitor logs.
+   */
+  IdeExtensionSetAdapter *addins;
+
+  /*
+   * This is the configuration for the build. It is a snapshot of
+   * the real configuration so that we do not need to synchronize
+   * with the UI process for accesses.
+   */
+  IdeConfiguration *configuration;
+
+  /*
+   * The device we are building for. This allows components to setup
+   * cross-compiling if necessary based on the architecture and system of
+   * the device in question. It also allows for determining a deployment
+   * strategy to get the compiled bits onto the device.
+   */
+  IdeDevice *device;
+
+  /*
+   * The cached triplet for the device we're compiling for. This allows
+   * plugins to avoid some classes of work when building for the same
+   * system that Builder is running upon.
+   */
+  IdeTriplet *host_triplet;
+
+  /*
+   * The runtime we're using to build. This may be different than what
+   * is specified in the IdeConfiguration, as the @device could alter
+   * what architecture we're building for (and/or cross-compiling).
+   */
+  IdeRuntime *runtime;
+
+  /*
+   * The toolchain we're using to build. This may be different than what
+   * is specified in the IdeConfiguration, as the @device could alter
+   * what architecture we're building for (and/or cross-compiling).
+   */
+  IdeToolchain *toolchain;
+
+  /*
+   * The IdeBuildLog is a private implementation that we use to
+   * log things from addins via observer callbacks.
+   */
+  IdeBuildLog *log;
+
+  /*
+   * These are our builddir/srcdir paths. Useful for building paths
+   * by addins. We try to create a new builddir that will be unique
+   * based on hashing of the configuration.
+   */
+  gchar *builddir;
+  gchar *srcdir;
+
+  /*
+   * This is an array of PipelineEntry, which contain information we
+   * need about the stage and an identifier that addins can use to
+   * remove their inserted stages.
+   */
+  GArray *pipeline;
+
+  /*
+   * This contains the GBinding objects used to keep the "completed"
+   * property of chained stages updated.
+   */
+  GPtrArray *chained_bindings;
+
+  /*
+   * This are used for ErrorFormat registration so that we have a
+   * single place to extract "GCC-style" warnings and errors. Other
+   * languages can also register these so they show up in the build
+   * errors panel.
+   */
+  GArray *errfmts;
+  gchar  *errfmt_current_dir;
+  gchar  *errfmt_top_dir;
+  guint   errfmt_seqnum;
+
+  /*
+   * The VtePty is used to connect to a VteTerminal. It's basically
+   * just a wrapper around a PTY master. We then add a IdePtyIntercept
+   * to proxy PTY data while allowing us to tap into the content being
+   * transmitted. We can use that to run regexes against and perform
+   * additional error extraction. Finally, pty_slave is the PTY device
+   * we created that will get attached to stdin/stdout/stderr in our
+   * spawned subprocesses. It is a slave to the PTY master owned by
+   * the IdePtyIntercept.
+   */
+  VtePty          *pty;
+  IdePtyIntercept  intercept;
+  IdePtyFd         pty_slave;
+
+  /*
+   * If the terminal interpreting our Pty has received a terminal
+   * title update, it might set this message which we can use for
+   * better build messages.
+   */
+  gchar *message;
+
+  /*
+   * No reference to the current stage. It is only available during
+   * the asynchronous execution of the stage.
+   */
+  IdeBuildStage *current_stage;
+
+  /*
+   * The index of our current PipelineEntry. This should start at -1
+   * to indicate that no stage is currently active.
+   */
+  gint position;
+
+  /*
+   * This is the requested mask to be built. It should be reset after
+   * performing a build so that a followup execute_async() would be
+   * innocuous.
+   */
+  IdeBuildPhase requested_mask;
+
+  /*
+   * We queue incoming tasks in case we need for a finish task to
+   * complete before our task can continue. The items in the queue
+   * are DelayedTask structs with a IdeTask and the type id so we
+   * can progress the task upon completion of the previous task.
+   */
+  GQueue task_queue;
+
+  /*
+   * We use this sequence number to give PipelineEntry instances a
+   * unique identifier. The addins can use this to remove their
+   * inserted build stages.
+   */
+  guint seqnum;
+
+  /* We use a GSource to load addins in an idle callback so that
+   * we don't block the main loop for too long. When disposing the
+   * pipeline, we need to kill that operation too (since it may
+   * lose access to IdeContext in the process).
+   */
+  guint idle_addins_load_source;
+
+  /*
+   * If we failed to build, this should be set.
+   */
+  guint failed : 1;
+
+  /*
+   * If we are within a build, this should be set.
+   */
+  guint busy : 1;
+
+  /*
+   * If we are in the middle of a clean operation.
+   */
+  guint in_clean : 1;
+
+  /*
+   * Precalculation if we need to look for errors on stdout. We can't rely
+   * on @current_stage for this, becase log entries might come in
+   * asynchronously and after the processes/stage has completed.
+   */
+  guint errors_on_stdout : 1;
+
+  /*
+   * This is set to TRUE if the pipeline has failed initialization. That means
+   * that all future operations will fail (but we can keep the object alive to
+   * ensure that the manager has a valid object instance for the pipeline).
+   */
+  guint broken : 1;
+
+  /*
+   * This is set to TRUE when we attempt to load plugins (after the config
+   * has been marked as ready).
+   */
+  guint loaded : 1;
+};
+
+typedef enum
+{
+  TASK_BUILD   = 1,
+  TASK_CLEAN   = 2,
+  TASK_REBUILD = 3,
+} TaskType;
+
+typedef struct
+{
+  /*
+   * Our operation type. This will indicate one of the TaskType enum
+   * which corellate to the various async functions of the pipeline.
+   */
+  TaskType type;
+
+  /*
+   * This is an unowned pointer to the task. Since the Operation structure is
+   * the task data, we cannot reference as that would create a cycle. Instead,
+   * we just rely on this becoming invalid during the task cleanup.
+   */
+  IdeTask *task;
+
+  /*
+   * The phase that should be met for the given pipeline operation.
+   */
+  IdeBuildPhase phase;
+
+  union {
+    struct {
+      GPtrArray *stages;
+    } clean;
+    struct {
+      GPtrArray *targets;
+    } build;
+    struct {
+      GPtrArray *targets;
+    } rebuild;
+  };
+} TaskData;
+
+static void ide_build_pipeline_queue_flush  (IdeBuildPipeline    *self);
+static void ide_build_pipeline_tick_execute (IdeBuildPipeline    *self,
+                                             IdeTask             *task);
+static void ide_build_pipeline_tick_clean   (IdeBuildPipeline    *self,
+                                             IdeTask             *task);
+static void ide_build_pipeline_tick_rebuild (IdeBuildPipeline    *self,
+                                             IdeTask             *task);
+static void initable_iface_init             (GInitableIface      *iface);
+static void list_model_iface_init           (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeBuildPipeline, ide_build_pipeline, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))
+
+enum {
+  PROP_0,
+  PROP_BUSY,
+  PROP_CONFIGURATION,
+  PROP_DEVICE,
+  PROP_MESSAGE,
+  PROP_PHASE,
+  PROP_PTY,
+  N_PROPS
+};
+
+enum {
+  DIAGNOSTIC,
+  STARTED,
+  FINISHED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+static const gchar *task_type_names[] = {
+  NULL,
+  "build",
+  "clean",
+  "rebuild",
+};
+
+static void
+chained_binding_clear (gpointer data)
+{
+  GBinding *binding = data;
+
+  g_binding_unbind (binding);
+  g_object_unref (binding);
+}
+
+static void
+idle_load_state_free (gpointer data)
+{
+  IdleLoadState *state = data;
+
+  g_clear_pointer (&state->addins, g_ptr_array_unref);
+  g_clear_object (&state->self);
+  g_slice_free (IdleLoadState, state);
+}
+
+static void
+task_data_free (gpointer data)
+{
+  TaskData *td = data;
+
+  if (td != NULL)
+    {
+      if (td->type == TASK_CLEAN)
+        g_clear_pointer (&td->clean.stages, g_ptr_array_unref);
+      if (td->type == TASK_BUILD)
+        g_clear_pointer (&td->build.targets, g_ptr_array_unref);
+      if (td->type == TASK_REBUILD)
+        g_clear_pointer (&td->rebuild.targets, g_ptr_array_unref);
+      td->type = 0;
+      td->task = NULL;
+      g_slice_free (TaskData, td);
+    }
+}
+
+static TaskData *
+task_data_new (IdeTask  *task,
+               TaskType  type)
+{
+  TaskData *td;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (type > 0);
+  g_assert (type <= TASK_REBUILD);
+
+  td = g_slice_new0 (TaskData);
+  td->type = type;
+  td->task = task;
+
+  return td;
+}
+
+static void
+clear_error_format (gpointer data)
+{
+  ErrorFormat *errfmt = data;
+
+  errfmt->id = 0;
+  g_clear_pointer (&errfmt->regex, g_regex_unref);
+}
+
+static inline const gchar *
+build_phase_nick (IdeBuildPhase phase)
+{
+  GFlagsClass *klass = g_type_class_peek (IDE_TYPE_BUILD_PHASE);
+  GFlagsValue *value;
+
+  g_assert (klass != NULL);
+
+  phase &= IDE_BUILD_PHASE_MASK;
+  value = g_flags_get_first_value (klass, phase);
+
+  if (value != NULL)
+    return value->value_nick;
+
+  return "unknown";
+}
+
+static IdeDiagnosticSeverity
+parse_severity (const gchar *str)
+{
+  g_autofree gchar *lower = NULL;
+
+  if (str == NULL)
+    return IDE_DIAGNOSTIC_WARNING;
+
+  lower = g_utf8_strdown (str, -1);
+
+  if (strstr (lower, "fatal") != NULL)
+    return IDE_DIAGNOSTIC_FATAL;
+
+  if (strstr (lower, "error") != NULL)
+    return IDE_DIAGNOSTIC_ERROR;
+
+  if (strstr (lower, "warning") != NULL)
+    return IDE_DIAGNOSTIC_WARNING;
+
+  if (strstr (lower, "ignored") != NULL)
+    return IDE_DIAGNOSTIC_IGNORED;
+
+  if (strstr (lower, "deprecated") != NULL)
+    return IDE_DIAGNOSTIC_DEPRECATED;
+
+  if (strstr (lower, "note") != NULL)
+    return IDE_DIAGNOSTIC_NOTE;
+
+  return IDE_DIAGNOSTIC_WARNING;
+}
+
+static IdeDiagnostic *
+create_diagnostic (IdeBuildPipeline *self,
+                   GMatchInfo       *match_info)
+{
+  g_autofree gchar *filename = NULL;
+  g_autofree gchar *line = NULL;
+  g_autofree gchar *column = NULL;
+  g_autofree gchar *message = NULL;
+  g_autofree gchar *level = NULL;
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(IdeLocation) location = NULL;
+  IdeContext *context;
+  struct {
+    gint64 line;
+    gint64 column;
+    IdeDiagnosticSeverity severity;
+  } parsed = { 0 };
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (match_info != NULL);
+
+  message = g_match_info_fetch_named (match_info, "message");
+
+  /* XXX: This is a hack to ignore a common but unuseful error message.
+   *      This really belongs somewhere else, but it's easier to do the
+   *      check here for now. We need proper callback for ErrorRegex in
+   *      the future so they can ignore it.
+   */
+  if (message == NULL || strncmp (message, "#warning _FORTIFY_SOURCE requires compiling with optimization", 
61) == 0)
+    return NULL;
+
+  filename = g_match_info_fetch_named (match_info, "filename");
+  line = g_match_info_fetch_named (match_info, "line");
+  column = g_match_info_fetch_named (match_info, "column");
+  level = g_match_info_fetch_named (match_info, "level");
+
+  if (line != NULL)
+    {
+      parsed.line = g_ascii_strtoll (line, NULL, 10);
+      if (parsed.line < 1 || parsed.line > G_MAXINT32)
+        return NULL;
+      parsed.line--;
+    }
+
+  if (column != NULL)
+    {
+      parsed.column = g_ascii_strtoll (column, NULL, 10);
+      if (parsed.column < 1 || parsed.column > G_MAXINT32)
+        return NULL;
+      parsed.column--;
+    }
+
+  parsed.severity = parse_severity (level);
+
+  if (!g_path_is_absolute (filename))
+    {
+      gchar *path;
+
+      if (self->errfmt_current_dir != NULL)
+        {
+          const gchar *basedir = self->errfmt_current_dir;
+
+          if (g_str_has_prefix (basedir, self->errfmt_top_dir))
+            {
+              basedir += strlen (self->errfmt_top_dir);
+              if (*basedir == G_DIR_SEPARATOR)
+                basedir++;
+            }
+
+          path = g_build_filename (basedir, filename, NULL);
+          g_free (filename);
+          filename = path;
+        }
+      else
+        {
+          path = g_build_filename (self->builddir, filename, NULL);
+          g_free (filename);
+          filename = path;
+        }
+    }
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  if (!g_path_is_absolute (filename))
+    {
+      g_autoptr(GFile) child = NULL;
+      g_autoptr(GFile) workdir = NULL;
+      gchar *path;
+
+      workdir = ide_context_ref_workdir (context);
+      child = g_file_get_child (workdir, filename);
+      path = g_file_get_path (child);
+
+      g_free (filename);
+      filename = path;
+    }
+
+  file = ide_context_build_file (context, filename);
+  location = ide_location_new (file, parsed.line, parsed.column);
+
+  return ide_diagnostic_new (parsed.severity, message, location);
+}
+
+static gboolean
+extract_directory_change (IdeBuildPipeline *self,
+                          const guint8     *data,
+                          gsize             len)
+{
+  g_autofree gchar *dir = NULL;
+  const guint8 *begin;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  if (len == 0)
+    return FALSE;
+
+#define ENTERING_DIRECTORY_BEGIN "Entering directory '"
+#define ENTERING_DIRECTORY_END   "'"
+
+  begin = memmem (data, len, ENTERING_DIRECTORY_BEGIN, strlen (ENTERING_DIRECTORY_BEGIN));
+  if (begin == NULL)
+    return FALSE;
+
+  begin += strlen (ENTERING_DIRECTORY_BEGIN);
+
+  if (data[len - 1] != '\'')
+    return FALSE;
+
+  len = &data[len - 1] - begin;
+  dir = g_memdup (begin, len);
+
+  if (g_utf8_validate (dir, len, NULL))
+    {
+      g_free (self->errfmt_current_dir);
+
+      if (len == 0)
+        self->errfmt_current_dir = g_strdup (self->errfmt_top_dir);
+      else
+        self->errfmt_current_dir = g_strndup (dir, len);
+
+      if (self->errfmt_top_dir == NULL)
+        self->errfmt_top_dir = g_strdup (self->errfmt_current_dir);
+
+      return TRUE;
+    }
+
+#undef ENTERING_DIRECTORY_BEGIN
+#undef ENTERING_DIRECTORY_END
+
+  return FALSE;
+}
+
+static void
+extract_diagnostics (IdeBuildPipeline *self,
+                     const guint8     *data,
+                     gsize             len)
+{
+  g_autofree guint8 *unescaped = NULL;
+  IdeLineReader reader;
+  gchar *line;
+  gsize line_len;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (data != NULL);
+
+  if (len == 0 || self->errfmts->len == 0)
+    return;
+
+  /* If we have any color escape sequences, remove them */
+  if G_UNLIKELY (memchr (data, '\033', len) || memmem (data, len, "\\e", 2))
+    {
+      gsize out_len = 0;
+
+      unescaped = _ide_build_utils_filter_color_codes (data, len, &out_len);
+      if (out_len == 0)
+        return;
+
+      data = unescaped;
+      len = out_len;
+    }
+
+  ide_line_reader_init (&reader, (gchar *)data, len);
+
+  while (NULL != (line = ide_line_reader_next (&reader, &line_len)))
+    {
+      if (extract_directory_change (self, (const guint8 *)line, line_len))
+        continue;
+
+      for (guint i = 0; i < self->errfmts->len; i++)
+        {
+          const ErrorFormat *errfmt = &g_array_index (self->errfmts, ErrorFormat, i);
+          g_autoptr(GMatchInfo) match_info = NULL;
+
+          if (g_regex_match_full (errfmt->regex, line, line_len, 0, 0, &match_info, NULL))
+            {
+              g_autoptr(IdeDiagnostic) diagnostic = create_diagnostic (self, match_info);
+
+              if (diagnostic != NULL)
+                {
+                  ide_build_pipeline_emit_diagnostic (self, diagnostic);
+                  break;
+                }
+            }
+        }
+    }
+}
+
+static void
+ide_build_pipeline_log_observer (IdeBuildLogStream  stream,
+                                 const gchar       *message,
+                                 gssize             message_len,
+                                 gpointer           user_data)
+{
+  IdeBuildPipeline *self = user_data;
+
+  g_assert (stream == IDE_BUILD_LOG_STDOUT || stream == IDE_BUILD_LOG_STDERR);
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (message != NULL);
+
+  if (message_len < 0)
+    message_len = strlen (message);
+
+  if (self->log != NULL)
+    ide_build_log_observer (stream, message, message_len, self->log);
+
+  extract_diagnostics (self, (const guint8 *)message, message_len);
+}
+
+static void
+ide_build_pipeline_intercept_pty_master_cb (const IdePtyIntercept     *intercept,
+                                            const IdePtyInterceptSide *side,
+                                            const guint8              *data,
+                                            gsize                      len,
+                                            gpointer                   user_data)
+{
+  IdeBuildPipeline *self = user_data;
+
+  g_assert (intercept != NULL);
+  g_assert (side != NULL);
+  g_assert (data != NULL);
+  g_assert (len > 0);
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  extract_diagnostics (self, data, len);
+}
+
+static void
+ide_build_pipeline_release_transients (IdeBuildPipeline *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (self->pipeline != NULL);
+
+  for (guint i = self->pipeline->len; i > 0; i--)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i - 1);
+
+      g_assert (IDE_IS_BUILD_STAGE (entry->stage));
+
+      if (ide_build_stage_get_transient (entry->stage))
+        {
+          IDE_TRACE_MSG ("Releasing transient stage %s at index %u",
+                         G_OBJECT_TYPE_NAME (entry->stage),
+                         i - 1);
+          g_array_remove_index (self->pipeline, i);
+        }
+    }
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_build_pipeline_check_ready (IdeBuildPipeline *self,
+                                IdeTask          *task)
+{
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_TASK (task));
+
+  if (self->broken)
+    {
+      ide_task_return_new_error (task,
+                                 IDE_BUILD_ERROR,
+                                 IDE_BUILD_ERROR_BROKEN,
+                                 _("The build pipeline is in a failed state"));
+      return FALSE;
+    }
+
+  if (self->loaded == FALSE)
+    {
+      /* configuration:ready is FALSE */
+      ide_task_return_new_error (task,
+                                 IDE_BUILD_ERROR,
+                                 IDE_BUILD_ERROR_NOT_LOADED,
+                                 _("The build configuration has errors"));
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+/**
+ * ide_build_pipeline_get_phase:
+ *
+ * Gets the current phase that is executing. This is only useful during
+ * execution of the pipeline.
+ *
+ * Since: 3.32
+ */
+IdeBuildPhase
+ide_build_pipeline_get_phase (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+
+  if (self->position < 0)
+    return IDE_BUILD_PHASE_NONE;
+  else if (self->failed)
+    return IDE_BUILD_PHASE_FAILED;
+  else if ((guint)self->position < self->pipeline->len)
+    return g_array_index (self->pipeline, PipelineEntry, self->position).phase & IDE_BUILD_PHASE_MASK;
+  else
+    return IDE_BUILD_PHASE_FINISHED;
+}
+
+/**
+ * ide_build_pipeline_get_configuration:
+ *
+ * Gets the #IdeConfiguration to use for the pipeline.
+ *
+ * Returns: (transfer none): An #IdeConfiguration
+ *
+ * Since: 3.32
+ */
+IdeConfiguration *
+ide_build_pipeline_get_configuration (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->configuration;
+}
+
+static void
+clear_pipeline_entry (gpointer data)
+{
+  PipelineEntry *entry = data;
+
+  if (entry->stage != NULL)
+    {
+      ide_build_stage_set_log_observer (entry->stage, NULL, NULL, NULL);
+      g_clear_object (&entry->stage);
+    }
+}
+
+static gint
+pipeline_entry_compare (gconstpointer a,
+                        gconstpointer b)
+{
+  const PipelineEntry *entry_a = a;
+  const PipelineEntry *entry_b = b;
+  gint ret;
+
+  ret = (gint)(entry_a->phase & IDE_BUILD_PHASE_MASK)
+      - (gint)(entry_b->phase & IDE_BUILD_PHASE_MASK);
+
+  if (ret == 0)
+    {
+      gint whence_a = (entry_a->phase & IDE_BUILD_PHASE_WHENCE_MASK);
+      gint whence_b = (entry_b->phase & IDE_BUILD_PHASE_WHENCE_MASK);
+
+      if (whence_a != whence_b)
+        {
+          if (whence_a == IDE_BUILD_PHASE_BEFORE)
+            return -1;
+
+          if (whence_b == IDE_BUILD_PHASE_BEFORE)
+            return 1;
+
+          if (whence_a == 0)
+            return -1;
+
+          if (whence_b == 0)
+            return 1;
+
+          g_assert_not_reached ();
+        }
+    }
+
+  if (ret == 0)
+    ret = entry_a->priority - entry_b->priority;
+
+  return ret;
+}
+
+static void
+ide_build_pipeline_real_started (IdeBuildPipeline *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  self->errors_on_stdout = FALSE;
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if (ide_build_stage_get_check_stdout (entry->stage))
+        {
+          self->errors_on_stdout = TRUE;
+          break;
+        }
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_real_finished (IdeBuildPipeline *self,
+                                  gboolean          failed)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_extension_added (IdeExtensionSetAdapter *set,
+                                    PeasPluginInfo         *plugin_info,
+                                    PeasExtension          *exten,
+                                    gpointer                user_data)
+{
+  IdeBuildPipeline *self = user_data;
+  IdeBuildPipelineAddin *addin = (IdeBuildPipelineAddin *)exten;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE_ADDIN (addin));
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  /* Mark that we loaded this addin, so we don't unload it if it
+   * was never loaded (during async loading).
+   */
+  g_object_set_data (G_OBJECT (addin), "HAS_LOADED", GINT_TO_POINTER (1));
+
+  ide_build_pipeline_addin_load (addin, self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_extension_removed (IdeExtensionSetAdapter *set,
+                                      PeasPluginInfo         *plugin_info,
+                                      PeasExtension          *exten,
+                                      gpointer                user_data)
+{
+  IdeBuildPipeline *self = user_data;
+  IdeBuildPipelineAddin *addin = (IdeBuildPipelineAddin *)exten;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE_ADDIN (addin));
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  if (g_object_get_data (G_OBJECT (addin), "HAS_LOADED"))
+    ide_build_pipeline_addin_unload (addin, self);
+
+  IDE_EXIT;
+}
+
+static void
+build_command_query_cb (IdeBuildStage    *stage,
+                        IdeBuildPipeline *pipeline,
+                        GPtrArray        *targets,
+                        GCancellable     *cancellable,
+                        gpointer          user_data)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_LAUNCHER (stage));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (user_data == NULL);
+
+  ide_build_stage_set_completed (stage, FALSE);
+
+  IDE_EXIT;
+}
+
+static void
+register_build_commands_stage (IdeBuildPipeline *self,
+                               IdeContext       *context)
+{
+  g_autoptr(GError) error = NULL;
+  const gchar * const *build_commands;
+  g_autofree gchar *rundir_path = NULL;
+  GFile *rundir;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_CONTEXT (context));
+  g_assert (IDE_IS_CONFIGURATION (self->configuration));
+
+  if (NULL == (build_commands = ide_configuration_get_build_commands (self->configuration)))
+    return;
+
+  if ((rundir = ide_configuration_get_build_commands_dir (self->configuration)))
+    rundir_path = g_file_get_path (rundir);
+
+  for (guint i = 0; build_commands[i]; i++)
+    {
+      g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+      g_autoptr(IdeBuildStage) stage = NULL;
+
+      if (NULL == (launcher = ide_build_pipeline_create_launcher (self, &error)))
+        {
+          g_warning ("%s", error->message);
+          return;
+        }
+
+      /* Request deprecation warnings from the GLib stack by default */
+      ide_subprocess_launcher_setenv (launcher, "G_ENABLE_DIAGNOSTIC", "1", FALSE);
+
+      ide_subprocess_launcher_push_argv (launcher, "/bin/sh");
+      ide_subprocess_launcher_push_argv (launcher, "-c");
+      ide_subprocess_launcher_push_argv (launcher, build_commands[i]);
+
+      if (rundir_path != NULL)
+        ide_subprocess_launcher_set_cwd (launcher, rundir_path);
+
+      stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
+                            "launcher", launcher,
+                            NULL);
+
+      g_signal_connect (stage, "query", G_CALLBACK (build_command_query_cb), NULL);
+
+      ide_build_pipeline_attach (self,
+                                  IDE_BUILD_PHASE_BUILD | IDE_BUILD_PHASE_AFTER,
+                                  i,
+                                  stage);
+    }
+}
+
+static void
+register_post_install_commands_stage (IdeBuildPipeline *self,
+                                      IdeContext       *context)
+{
+  g_autoptr(GError) error = NULL;
+  const gchar * const *post_install_commands;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_CONTEXT (context));
+  g_assert (IDE_IS_CONFIGURATION (self->configuration));
+
+  post_install_commands = ide_configuration_get_post_install_commands (self->configuration);
+  if (post_install_commands == NULL)
+    return;
+  for (guint i = 0; post_install_commands[i]; i++)
+    {
+      g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+      g_autoptr(IdeBuildStage) stage = NULL;
+
+      if (NULL == (launcher = ide_build_pipeline_create_launcher (self, &error)))
+        {
+          ide_object_warning (self, "%s", error->message);
+          return;
+        }
+
+      ide_subprocess_launcher_push_argv (launcher, "/bin/sh");
+      ide_subprocess_launcher_push_argv (launcher, "-c");
+      ide_subprocess_launcher_push_argv (launcher, post_install_commands[i]);
+
+      stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
+                            "launcher", launcher,
+                            NULL);
+
+      ide_build_pipeline_attach (self,
+                                  IDE_BUILD_PHASE_INSTALL | IDE_BUILD_PHASE_AFTER,
+                                  i,
+                                  stage);
+    }
+}
+
+static void
+collect_pipeline_addins (IdeExtensionSetAdapter *set,
+                         PeasPluginInfo         *plugin_info,
+                         PeasExtension          *exten,
+                         gpointer                user_data)
+{
+  GPtrArray *addins = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE_ADDIN (exten));
+  g_assert (addins != NULL);
+
+  g_ptr_array_add (addins, g_object_ref (exten));
+}
+
+static gboolean
+ide_build_pipeline_load_cb (IdleLoadState *state)
+{
+  g_assert (state != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE (state->self));
+  g_assert (state->addins != NULL);
+
+  /*
+   * We only load a single addin per idle callback so that we can return to
+   * the main loop and potentially start the next frame at a higher priority
+   * than the addin loading.
+   */
+
+  if (state->addins->len > 0)
+    {
+      IdeBuildPipelineAddin *addin = g_ptr_array_index (state->addins, state->addins->len - 1);
+      gint64 begin, end;
+
+      begin = g_get_monotonic_time ();
+      ide_build_pipeline_addin_load (addin, state->self);
+      end = g_get_monotonic_time ();
+
+      g_debug ("%s loaded in %lf seconds",
+               G_OBJECT_TYPE_NAME (addin),
+               (end - begin) / (gdouble)G_USEC_PER_SEC);
+
+      g_ptr_array_remove_index (state->addins, state->addins->len - 1);
+
+      if (state->addins->len > 0)
+        return G_SOURCE_CONTINUE;
+    }
+
+  state->self->loaded = TRUE;
+  state->self->idle_addins_load_source = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+/**
+ * ide_build_pipeline_load:
+ *
+ * This manages the loading of addins which will register their necessary build
+ * stages.  We do this separately from ::constructed so that we can
+ * enable/disable the pipeline as the IdeConfiguration:ready property changes.
+ * This could happen when the device or runtime is added/removed while the
+ * application is running.
+ *
+ * Since: 3.32
+ */
+static void
+ide_build_pipeline_load (IdeBuildPipeline *self)
+{
+  g_autoptr(GPtrArray) addins = NULL;
+  IdleLoadState *state;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (self->addins == NULL);
+
+  /* We might have already disposed if our pipeline got discarded */
+  if (!(context = ide_object_get_context (IDE_OBJECT (self))))
+    return;
+
+  register_build_commands_stage (self, context);
+  register_post_install_commands_stage (self, context);
+
+  self->addins = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                                NULL, NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_build_pipeline_extension_added),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_build_pipeline_extension_removed),
+                    self);
+
+  /* Collect our addins so we can incrementally load them in an
+   * idle callback to reduce chances of stalling the main loop.
+   */
+  addins = g_ptr_array_new_with_free_func (g_object_unref);
+  ide_extension_set_adapter_foreach (self->addins,
+                                     collect_pipeline_addins,
+                                     addins);
+
+  state = g_slice_new0 (IdleLoadState);
+  state->self = g_object_ref (self);
+  state->addins = g_steal_pointer (&addins);
+
+  self->idle_addins_load_source =
+    g_idle_add_full (G_PRIORITY_LOW,
+                     (GSourceFunc) ide_build_pipeline_load_cb,
+                     state,
+                     idle_load_state_free);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_load_get_info_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  IdeDevice *device = (IdeDevice *)object;
+  g_autoptr(IdeBuildPipeline) self = user_data;
+  g_autoptr(IdeDeviceInfo) info = NULL;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEVICE (device));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  if (!(info = ide_device_get_info_finish (device, result, &error)))
+    {
+      g_warning ("Failed to get device information: %s", error->message);
+      IDE_EXIT;
+    }
+
+  if (g_cancellable_is_cancelled (self->cancellable))
+    IDE_EXIT;
+
+  _ide_build_pipeline_check_toolchain (self, info);
+
+  ide_build_pipeline_load (self);
+}
+
+static void
+ide_build_pipeline_begin_load (IdeBuildPipeline *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_DEVICE (self->device));
+
+  /*
+   * The first thing we need to do is get some information from the
+   * configured device. We want to know the arch/kernel/system triplet
+   * for the device as some pipeline addins may need that. We can also
+   * use that to ensure that we load the proper runtime and toolchain
+   * for the device.
+   *
+   * We have to load this information asynchronously, as the device might
+   * be remote (and we need to connect to it to get the information).
+   */
+
+  ide_device_get_info_async (self->device,
+                             self->cancellable,
+                             ide_build_pipeline_load_get_info_cb,
+                             g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_pipeline_unload:
+ * @self: an #IdeBuildPipeline
+ *
+ * This clears things up that were initialized in ide_build_pipeline_load().
+ * This function is safe to run even if load has not been called. We will not
+ * clean things up if the pipeline is currently executing (we can wait until
+ * its finished or dispose/finalize to cleanup up further.
+ *
+ * Since: 3.32
+ */
+static void
+ide_build_pipeline_unload (IdeBuildPipeline *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  g_clear_object (&self->addins);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_notify_ready (IdeBuildPipeline *self,
+                                 GParamSpec       *pspec,
+                                 IdeConfiguration *configuration)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_CONFIGURATION (configuration));
+
+  /*
+   * If we're being realistic, we can only really setup the build pipeline one
+   * time, once the configuration is ready. So cancel all tracking after that
+   * so that and just rely on the build manager to create a new pipeline when
+   * the active configuration changes.
+   */
+
+  if (ide_configuration_get_ready (configuration))
+    {
+      g_signal_handlers_disconnect_by_func (configuration,
+                                            G_CALLBACK (ide_build_pipeline_notify_ready),
+                                            self);
+      ide_build_pipeline_begin_load (self);
+    }
+  else
+    g_debug ("Configuration not yet ready, delaying pipeline setup");
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_finalize (GObject *object)
+{
+  IdeBuildPipeline *self = (IdeBuildPipeline *)object;
+
+  IDE_ENTRY;
+
+  g_assert (self->task_queue.length == 0);
+  g_queue_clear (&self->task_queue);
+
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->log);
+  g_clear_object (&self->device);
+  g_clear_object (&self->runtime);
+  g_clear_object (&self->toolchain);
+  g_clear_object (&self->configuration);
+  g_clear_pointer (&self->pipeline, g_array_unref);
+  g_clear_pointer (&self->srcdir, g_free);
+  g_clear_pointer (&self->builddir, g_free);
+  g_clear_pointer (&self->errfmts, g_array_unref);
+  g_clear_pointer (&self->errfmt_top_dir, g_free);
+  g_clear_pointer (&self->errfmt_current_dir, g_free);
+  g_clear_pointer (&self->chained_bindings, g_ptr_array_unref);
+  g_clear_pointer (&self->host_triplet, ide_triplet_unref);
+
+  G_OBJECT_CLASS (ide_build_pipeline_parent_class)->finalize (object);
+
+  DZL_COUNTER_DEC (Instances);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_destroy (IdeObject *object)
+{
+  IdeBuildPipeline *self = IDE_BUILD_PIPELINE (object);
+  g_auto(IdePtyFd) fd = IDE_PTY_FD_INVALID;
+
+  IDE_ENTRY;
+
+  g_clear_handle_id (&self->idle_addins_load_source, g_source_remove);
+
+  _ide_build_pipeline_cancel (self);
+
+  ide_build_pipeline_unload (self);
+
+  g_clear_pointer (&self->message, g_free);
+
+  g_clear_object (&self->pty);
+  fd = pty_fd_steal (&self->pty_slave);
+
+  if (IDE_IS_PTY_INTERCEPT (&self->intercept))
+    ide_pty_intercept_clear (&self->intercept);
+
+  IDE_OBJECT_CLASS (ide_build_pipeline_parent_class)->destroy (object);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_build_pipeline_initable_init (GInitable     *initable,
+                                  GCancellable  *cancellable,
+                                  GError       **error)
+{
+  IdeBuildPipeline *self = (IdeBuildPipeline *)initable;
+  IdePtyFd master_fd;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_CONFIGURATION (self->configuration));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_debug ("initializing build pipeline with device %s",
+           G_OBJECT_TYPE_NAME (self->device));
+
+  if (self->runtime == NULL)
+    {
+      g_set_error_literal (error,
+                           G_IO_ERROR,
+                           G_IO_ERROR_FAILED,
+                           "No runtime assigned to build pipeline");
+      IDE_RETURN (FALSE);
+    }
+
+  /*
+   * Create a PTY for subprocess launchers. PTY initialization does not
+   * support cancellation, so do not pass @cancellable along to it.
+   */
+  self->pty = vte_pty_new_sync (VTE_PTY_DEFAULT, NULL, error);
+  if (self->pty == NULL)
+    IDE_RETURN (FALSE);
+
+  vte_pty_set_utf8 (self->pty, TRUE, NULL);
+
+  master_fd = vte_pty_get_fd (self->pty);
+
+  if (!ide_pty_intercept_init (&self->intercept, master_fd, NULL))
+    {
+      g_set_error_literal (error,
+                           G_IO_ERROR,
+                           G_IO_ERROR_FAILED,
+                           "Failed to initialize PTY intercept");
+      IDE_RETURN (FALSE);
+    }
+
+  ide_pty_intercept_set_callback (&self->intercept,
+                              &self->intercept.master,
+                              ide_build_pipeline_intercept_pty_master_cb,
+                              self);
+
+  g_signal_connect_object (self->configuration,
+                           "notify::ready",
+                           G_CALLBACK (ide_build_pipeline_notify_ready),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_build_pipeline_notify_ready (self, NULL, self->configuration);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PTY]);
+
+  IDE_RETURN (TRUE);
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+  iface->init = ide_build_pipeline_initable_init;
+}
+
+static void
+ide_build_pipeline_parent_set (IdeObject *object,
+                               IdeObject *parent)
+{
+  IdeBuildPipeline *self = IDE_BUILD_PIPELINE (object);
+  IdeToolchainManager *toolchain_manager;
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GFile) workdir = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+  g_assert (IDE_IS_CONFIGURATION (self->configuration));
+
+  if (parent == NULL)
+    return;
+
+  context = IDE_CONTEXT (ide_object_ref_root (IDE_OBJECT (self)));
+  workdir = ide_context_ref_workdir (context);
+
+  self->srcdir = g_file_get_path (workdir);
+
+  toolchain_manager = ide_toolchain_manager_from_context (context);
+  self->toolchain = ide_toolchain_manager_get_toolchain (toolchain_manager, "default");
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeBuildPipeline *self = IDE_BUILD_PIPELINE (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUSY:
+      g_value_set_boolean (value, self->busy);
+      break;
+
+    case PROP_CONFIGURATION:
+      g_value_set_object (value, ide_build_pipeline_get_configuration (self));
+      break;
+
+    case PROP_MESSAGE:
+      g_value_set_string (value, ide_build_pipeline_get_message (self));
+      break;
+
+    case PROP_PHASE:
+      g_value_set_flags (value, ide_build_pipeline_get_phase (self));
+      break;
+
+    case PROP_PTY:
+      g_value_set_object (value, ide_build_pipeline_get_pty (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_pipeline_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeBuildPipeline *self = IDE_BUILD_PIPELINE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONFIGURATION:
+      self->configuration = g_value_dup_object (value);
+      break;
+
+    case PROP_DEVICE:
+      self->device = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_pipeline_class_init (IdeBuildPipelineClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_build_pipeline_finalize;
+  object_class->get_property = ide_build_pipeline_get_property;
+  object_class->set_property = ide_build_pipeline_set_property;
+
+  i_object_class->destroy = ide_build_pipeline_destroy;
+  i_object_class->parent_set = ide_build_pipeline_parent_set;
+
+  /**
+   * IdeBuildPipeline:busy:
+   *
+   * Gets the "busy" property. If %TRUE, the pipeline is busy executing.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_BUSY] =
+    g_param_spec_boolean ("busy",
+                          "Busy",
+                          "If the pipeline is busy",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildPipeline:configuration:
+   *
+   * The configuration to use for the build pipeline.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CONFIGURATION] =
+    g_param_spec_object ("configuration",
+                         "Configuration",
+                         "Configuration",
+                         IDE_TYPE_CONFIGURATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildPipeline:device:
+   *
+   * The "device" property is the device we are compiling for.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_DEVICE] =
+    g_param_spec_object ("device",
+                         "Device",
+                         "The device we are building for",
+                         IDE_TYPE_DEVICE,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeBuildPipeline:message:
+   *
+   * The "message" property is descriptive text about what the the
+   * pipeline is doing or it's readiness status.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_MESSAGE] =
+    g_param_spec_string ("message",
+                         "Message",
+                         "The message for the build phase",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildPipeline:phase:
+   *
+   * The current build phase during execution of the pipeline.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PHASE] =
+    g_param_spec_flags ("phase",
+                        "Phase",
+                        "The phase that is being executed",
+                        IDE_TYPE_BUILD_PHASE,
+                        IDE_BUILD_PHASE_NONE,
+                        (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildPipeline:pty:
+   *
+   * The "pty" property is the #VtePty that is used by build stages that
+   * execute subprocesses with a pseudo terminal.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PTY] =
+    g_param_spec_object ("pty",
+                         "PTY",
+                         "The PTY used by the pipeline",
+                         VTE_TYPE_PTY,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeBuildPipeline::diagnostic:
+   * @self: An #IdeBuildPipeline
+   * @diagnostic: The newly created diagnostic
+   *
+   * This signal is emitted when a plugin has detected a diagnostic while
+   * building the pipeline.
+   *
+   * Since: 3.32
+   */
+  signals [DIAGNOSTIC] =
+    g_signal_new_class_handler ("diagnostic",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                NULL, NULL, NULL, NULL,
+                                G_TYPE_NONE, 1, IDE_TYPE_DIAGNOSTIC);
+
+  /**
+   * IdeBuildPipeline::started:
+   * @self: An #IdeBuildPipeline
+   * @phase: the #IdeBuildPhase for which we are advancing
+   *
+   * This signal is emitted when the pipeline has started executing in
+   * response to ide_build_pipeline_execute_async() being called.
+   *
+   * Since: 3.32
+   */
+  signals [STARTED] =
+    g_signal_new_class_handler ("started",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_build_pipeline_real_started),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE, 1, IDE_TYPE_BUILD_PHASE);
+
+  /**
+   * IdeBuildPipeline::finished:
+   * @self: An #IdeBuildPipeline
+   * @failed: If the build was a failure
+   *
+   * This signal is emitted when the build process has finished executing.
+   * If the build failed to complete all requested stages, then @failed will
+   * be set to %TRUE, otherwise %FALSE.
+   *
+   * Since: 3.32
+   */
+  signals [FINISHED] =
+    g_signal_new_class_handler ("finished",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_build_pipeline_real_finished),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+}
+
+static void
+ide_build_pipeline_init (IdeBuildPipeline *self)
+{
+  DZL_COUNTER_INC (Instances);
+
+  self->cancellable = g_cancellable_new ();
+
+  self->position = -1;
+  self->pty_slave = -1;
+
+  self->pipeline = g_array_new (FALSE, FALSE, sizeof (PipelineEntry));
+  g_array_set_clear_func (self->pipeline, clear_pipeline_entry);
+
+  self->errfmts = g_array_new (FALSE, FALSE, sizeof (ErrorFormat));
+  g_array_set_clear_func (self->errfmts, clear_error_format);
+
+  self->chained_bindings = g_ptr_array_new_with_free_func ((GDestroyNotify)chained_binding_clear);
+
+  self->log = ide_build_log_new ();
+}
+
+static void
+ide_build_pipeline_stage_execute_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  IdeBuildStage *stage = (IdeBuildStage *)object;
+  IdeBuildPipeline *self;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  if (!_ide_build_stage_execute_with_query_finish (stage, result, &error))
+    {
+      g_debug ("stage of type %s failed: %s",
+               G_OBJECT_TYPE_NAME (stage),
+               error->message);
+      self->failed = TRUE;
+      ide_task_return_error (task, g_steal_pointer (&error));
+    }
+
+  ide_build_stage_set_completed (stage, !self->failed);
+
+  g_clear_pointer (&self->chained_bindings, g_ptr_array_unref);
+  self->chained_bindings = g_ptr_array_new_with_free_func (g_object_unref);
+
+  if (self->failed == FALSE)
+    ide_build_pipeline_tick_execute (self, task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_try_chain (IdeBuildPipeline *self,
+                              IdeBuildStage    *stage,
+                              guint             position)
+{
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+
+  for (; position < self->pipeline->len; position++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, position);
+      gboolean chained;
+      GBinding *chained_binding;
+
+      /*
+       * Ignore all future stages if they were not requested by the current
+       * pipeline execution.
+       */
+      if (((entry->phase & IDE_BUILD_PHASE_MASK) & self->requested_mask) == 0)
+        return;
+
+      /* Skip past the stage if it is disabled. */
+      if (ide_build_stage_get_disabled (entry->stage))
+        continue;
+
+      chained = ide_build_stage_chain (stage, entry->stage);
+
+      IDE_TRACE_MSG ("Checking if %s chains to stage[%d] (%s) = %s",
+                     G_OBJECT_TYPE_NAME (stage),
+                     position,
+                     G_OBJECT_TYPE_NAME (entry->stage),
+                     chained ? "yes" : "no");
+
+      if (!chained)
+        return;
+
+      chained_binding = g_object_bind_property (stage, "completed", entry->stage, "completed", 0);
+      g_ptr_array_add (self->chained_bindings, g_object_ref (chained_binding));
+
+      self->position = position;
+    }
+}
+
+static void
+complete_queued_before_phase (IdeBuildPipeline *self,
+                              IdeBuildPhase     phase)
+{
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  phase = phase & IDE_BUILD_PHASE_MASK;
+
+  for (GList *iter = self->task_queue.head; iter; iter = iter->next)
+    {
+      IdeTask *task;
+      TaskData *task_data;
+
+    again:
+      task = iter->data;
+      task_data = ide_task_get_task_data (task);
+
+      g_assert (IDE_IS_TASK (task));
+      g_assert (task_data->task == task);
+
+      /*
+       * If this task has a phase that is less-than the phase given
+       * to us, we can complete the task immediately.
+       */
+      if (task_data->phase < phase)
+        {
+          GList *to_remove = iter;
+
+          iter = iter->next;
+          g_queue_delete_link (&self->task_queue, to_remove);
+          ide_task_return_boolean (task, TRUE);
+          g_object_unref (task);
+
+          if (iter == NULL)
+            break;
+
+          goto again;
+        }
+    }
+}
+
+static void
+ide_build_pipeline_tick_execute (IdeBuildPipeline *self,
+                                 IdeTask          *task)
+{
+  GCancellable *cancellable;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_TASK (task));
+
+  self->current_stage = NULL;
+
+  td = ide_task_get_task_data (task);
+  cancellable = ide_task_get_cancellable (task);
+
+  g_assert (td != NULL);
+  g_assert (td->type == TASK_BUILD || td->type == TASK_REBUILD);
+  g_assert (td->task == task);
+  g_assert (td->phase != IDE_BUILD_PHASE_NONE);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* Clear any message from the previous stage */
+  _ide_build_pipeline_set_message (self, NULL);
+
+  /* Clear cached directory enter/leave tracking */
+  g_clear_pointer (&self->errfmt_current_dir, g_free);
+  g_clear_pointer (&self->errfmt_top_dir, g_free);
+
+  /* Short circuit now if the task was cancelled */
+  if (ide_task_return_error_if_cancelled (task))
+    IDE_EXIT;
+
+  /* If we can skip walking the pipeline, go ahead and do so now. */
+  if (!ide_build_pipeline_request_phase (self, td->phase))
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  /*
+   * Walk forward to the next stage requiring execution and asynchronously
+   * execute it. The stage may also need to perform an async ::query signal
+   * delaying pipeline execution. _ide_build_stage_execute_with_query_async()
+   * will handle all of that for us, in cause they call ide_build_stage_pause()
+   * during the ::query callback.
+   */
+  for (self->position++; (guint)self->position < self->pipeline->len; self->position++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, self->position);
+
+      g_assert (entry->stage != NULL);
+      g_assert (IDE_IS_BUILD_STAGE (entry->stage));
+
+      /* Complete any tasks that are waiting for this to complete */
+      complete_queued_before_phase (self, entry->phase);
+
+      /* Ignore the stage if it is disabled */
+      if (ide_build_stage_get_disabled (entry->stage))
+        continue;
+
+      if ((entry->phase & IDE_BUILD_PHASE_MASK) & self->requested_mask)
+        {
+          GPtrArray *targets = NULL;
+
+          self->current_stage = entry->stage;
+
+          if (td->type == TASK_BUILD)
+            targets = td->build.targets;
+          else if (td->type == TASK_REBUILD)
+            targets = td->rebuild.targets;
+
+          /*
+           * We might be able to chain upcoming stages to this stage and avoid
+           * duplicate work. This will also advance self->position based on
+           * how many stages were chained.
+           */
+          ide_build_pipeline_try_chain (self, entry->stage, self->position + 1);
+
+          _ide_build_stage_execute_with_query_async (entry->stage,
+                                                     self,
+                                                     targets,
+                                                     cancellable,
+                                                     ide_build_pipeline_stage_execute_cb,
+                                                     g_object_ref (task));
+
+          g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+          g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PHASE]);
+
+          IDE_EXIT;
+        }
+    }
+
+  ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_task_notify_completed (IdeBuildPipeline *self,
+                                          GParamSpec       *pspec,
+                                          IdeTask          *task)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_TASK (task));
+
+  IDE_TRACE_MSG ("Clearing busy bit for pipeline");
+
+  self->current_stage = NULL;
+  self->busy = FALSE;
+  self->requested_mask = 0;
+  self->in_clean = FALSE;
+
+  g_clear_pointer (&self->message, g_free);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+
+  /*
+   * XXX: How do we ensure transients are executed with the part of the
+   *      pipeline we care about? We might just need to ensure that :busy is
+   *      FALSE before adding transients.
+   */
+  ide_build_pipeline_release_transients (self);
+
+  g_signal_emit (self, signals [FINISHED], 0, self->failed);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PHASE]);
+
+  /*
+   * We might have a delayed addin unloading that needs to occur after the
+   * build operation completes. If the configuration is no longer valid,
+   * go ahead and unload the pipeline.
+   */
+  if (!ide_configuration_get_ready (self->configuration))
+    ide_build_pipeline_unload (self);
+  else
+    ide_build_pipeline_queue_flush (self);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_pipeline_build_targets_async:
+ * @self: A @IdeBuildPipeline
+ * @phase: the requested build phase
+ * @targets: (nullable) (element-type IdeBuildTarget): an optional array of
+ *   #IdeBuildTarget for the pipeline to build.
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: data for @callback
+ *
+ * Asynchronously starts the build pipeline.
+ *
+ * The @phase parameter should contain the #IdeBuildPhase that is
+ * necessary to complete. If you simply want to trigger a generic
+ * build, you probably want %IDE_BUILD_PHASE_BUILD. If you only
+ * need to configure the project (and necessarily the dependencies
+ * up to that phase) you might want %IDE_BUILD_PHASE_CONFIGURE.
+ *
+ * You may not specify %IDE_BUILD_PHASE_AFTER or
+ * %IDE_BUILD_PHASE_BEFORE flags as those must always be processed
+ * with the underlying phase they are attached to.
+ *
+ * Upon completion, @callback will be executed and should call
+ * ide_build_pipeline_execute_finish() to get the status of the
+ * operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_pipeline_build_targets_async (IdeBuildPipeline    *self,
+                                        IdeBuildPhase        phase,
+                                        GPtrArray           *targets,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  TaskData *task_data;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  cancellable = dzl_cancellable_chain (cancellable, self->cancellable);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_build_pipeline_build_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  if (!ide_build_pipeline_check_ready (self, task))
+    return;
+
+  /*
+   * If the requested phase has already been met (by a previous build
+   * or by an active build who has already surpassed this build phase,
+   * we can return a result immediately.
+   *
+   * Only short circuit if we're running a build, otherwise we need to
+   * touch each entry and ::query() to see if it needs execution.
+   */
+
+  if (self->busy && !self->in_clean)
+    {
+      if (self->position >= self->pipeline->len)
+        {
+          goto short_circuit;
+        }
+      else if (self->position >= 0)
+        {
+          const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, self->position);
+
+          /* This phase is past the requested phase, we can complete the
+           * task immediately.
+           */
+          if (entry->phase > phase)
+            goto short_circuit;
+        }
+    }
+
+  task_data = task_data_new (task, TASK_BUILD);
+  task_data->phase = 1 << g_bit_nth_msf (phase, -1);
+  task_data->build.targets = _g_ptr_array_copy_objects (targets);
+  ide_task_set_task_data (task, task_data, task_data_free);
+
+  g_queue_push_tail (&self->task_queue, g_steal_pointer (&task));
+
+  ide_build_pipeline_queue_flush (self);
+
+  IDE_EXIT;
+
+short_circuit:
+  ide_task_return_boolean (task, TRUE);
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_pipeline_build_targets_finish:
+ * @self: An #IdeBuildPipeline
+ * @result: a #GAsyncResult provided to callback
+ * @error: A location for a #GError, or %NULL
+ *
+ * This function completes the asynchronous request to build
+ * up to a particular phase and targets of the build pipeline.
+ *
+ * Returns: %TRUE if the build stages were executed successfully
+ *   up to the requested build phase provided to
+ *   ide_build_pipeline_build_targets_async().
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_pipeline_build_targets_finish (IdeBuildPipeline  *self,
+                                         GAsyncResult      *result,
+                                         GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_build_pipeline_build_async:
+ * @self: A @IdeBuildPipeline
+ * @phase: the requested build phase
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: data for @callback
+ *
+ * Asynchronously starts the build pipeline.
+ *
+ * The @phase parameter should contain the #IdeBuildPhase that is
+ * necessary to complete. If you simply want to trigger a generic
+ * build, you probably want %IDE_BUILD_PHASE_BUILD. If you only
+ * need to configure the project (and necessarily the dependencies
+ * up to that phase) you might want %IDE_BUILD_PHASE_CONFIGURE.
+ *
+ * You may not specify %IDE_BUILD_PHASE_AFTER or
+ * %IDE_BUILD_PHASE_BEFORE flags as those must always be processed
+ * with the underlying phase they are attached to.
+ *
+ * Upon completion, @callback will be executed and should call
+ * ide_build_pipeline_execute_finish() to get the status of the
+ * operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_pipeline_build_async (IdeBuildPipeline    *self,
+                                IdeBuildPhase        phase,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  ide_build_pipeline_build_targets_async (self, phase, NULL, cancellable, callback, user_data);
+}
+
+/**
+ * ide_build_pipeline_build_finish:
+ * @self: An #IdeBuildPipeline
+ * @result: a #GAsyncResult provided to callback
+ * @error: A location for a #GError, or %NULL
+ *
+ * This function completes the asynchronous request to build
+ * up to a particular phase of the build pipeline.
+ *
+ * Returns: %TRUE if the build stages were executed successfully
+ *   up to the requested build phase provided to
+ *   ide_build_pipeline_build_async().
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_pipeline_build_finish (IdeBuildPipeline  *self,
+                                 GAsyncResult      *result,
+                                 GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_build_pipeline_execute_async:
+ * @self: A @IdeBuildPipeline
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: data for @callback
+ *
+ * Asynchronously starts the build pipeline.
+ *
+ * Any phase that has been invalidated up to the requested phase
+ * will be executed until a stage has failed.
+ *
+ * Upon completion, @callback will be executed and should call
+ * ide_build_pipeline_execute_finish() to get the status of the
+ * operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_pipeline_execute_async (IdeBuildPipeline    *self,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_build_pipeline_build_async (self, self->requested_mask, cancellable, callback, user_data);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_build_pipeline_do_flush (gpointer data)
+{
+  IdeBuildPipeline *self = data;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) builddir = NULL;
+  g_autoptr(GError) error = NULL;
+  TaskData *task_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  /*
+   * If the busy bit is set, there is nothing to do right now.
+   */
+  if (self->busy)
+    {
+      IDE_TRACE_MSG ("pipeline already busy, defering flush");
+      IDE_RETURN (G_SOURCE_REMOVE);
+    }
+
+  /* Ensure our builddir is created, or else we will fail all pending tasks. */
+  builddir = g_file_new_for_path (self->builddir);
+  if (!g_file_make_directory_with_parents (builddir, NULL, &error) &&
+      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+    {
+      IdeTask *failed_task;
+
+      while (NULL != (failed_task = g_queue_pop_head (&self->task_queue)))
+        {
+          ide_task_return_error (failed_task, g_error_copy (error));
+          g_object_unref (failed_task);
+        }
+
+      IDE_RETURN (G_SOURCE_REMOVE);
+    }
+
+  /*
+   * Pop the next task off the queue from the head (we push to the
+   * tail and we want FIFO semantics).
+   */
+  task = g_queue_pop_head (&self->task_queue);
+
+  if (task == NULL)
+    {
+      IDE_TRACE_MSG ("No tasks to process");
+      IDE_RETURN (G_SOURCE_REMOVE);
+    }
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (self->busy == FALSE);
+
+  /*
+   * Now prepare the task so that when it completes we can make
+   * forward progress again.
+   */
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_build_pipeline_task_notify_completed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* We need access to the task data to determine how to process the task. */
+  task_data = ide_task_get_task_data (task);
+
+  g_assert (task_data != NULL);
+  g_assert (task_data->type > 0);
+  g_assert (task_data->type <= TASK_REBUILD);
+  g_assert (IDE_IS_TASK (task_data->task));
+
+  /*
+   * If this build request could cause us to spin while we are continually
+   * failing to reach the CONFIGURE stage, protect ourselves as early as we
+   * can. We'll defer to a rebuild request to cause the full thing to build.
+   */
+  if (self->failed &&
+      task_data->type == TASK_BUILD &&
+      task_data->phase <= IDE_BUILD_PHASE_CONFIGURE)
+    {
+      ide_task_return_new_error (task,
+                                 IDE_BUILD_ERROR,
+                                 IDE_BUILD_ERROR_NEEDS_REBUILD,
+                                 "The build pipeline is in a failed state and requires a rebuild");
+      IDE_RETURN (G_SOURCE_REMOVE);
+    }
+
+  /*
+   * Now mark the pipeline as busy to protect ourself from anything recursively
+   * calling into the pipeline.
+   */
+  self->busy = TRUE;
+  self->failed = FALSE;
+  self->position = -1;
+  self->in_clean = (task_data->type == TASK_CLEAN);
+
+  /* Clear any lingering message */
+  g_clear_pointer (&self->message, g_free);
+
+  /*
+   * The following logs some helpful information about the build to our
+   * debug log. This is useful to allow users to debug some problems
+   * with our assistance (using gnome-builder -vvv).
+   */
+  {
+    g_autoptr(GString) str = g_string_new (NULL);
+    GFlagsClass *klass;
+    IdeBuildPhase phase = self->requested_mask;
+
+    klass = g_type_class_peek (IDE_TYPE_BUILD_PHASE);
+
+    for (guint i = 0; i < klass->n_values; i++)
+      {
+        const GFlagsValue *value = &klass->values[i];
+
+        if (phase & value->value)
+          {
+            if (str->len > 0)
+              g_string_append (str, ", ");
+            g_string_append (str, value->value_nick);
+          }
+      }
+
+    g_debug ("Executing pipeline %s stages %s with %u pipeline entries",
+             task_type_names[task_data->type],
+             str->str,
+             self->pipeline->len);
+
+    for (guint i = 0; i < self->pipeline->len; i++)
+      {
+        const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+        g_debug (" pipeline[%u]: %12s: %s [%s]",
+                 i,
+                 build_phase_nick (entry->phase),
+                 G_OBJECT_TYPE_NAME (entry->stage),
+                 ide_build_stage_get_completed (entry->stage) ? "completed" : "pending");
+      }
+  }
+
+  /* Notify any observers that a build (of some sort) is about to start. */
+  g_signal_emit (self, signals [STARTED], 0, task_data->phase);
+
+  switch (task_data->type)
+    {
+    case TASK_BUILD:
+      ide_build_pipeline_tick_execute (self, task);
+      break;
+
+    case TASK_CLEAN:
+      ide_build_pipeline_tick_clean (self, task);
+      break;
+
+    case TASK_REBUILD:
+      ide_build_pipeline_tick_rebuild (self, task);
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+ide_build_pipeline_queue_flush (IdeBuildPipeline *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  gdk_threads_add_idle_full (G_PRIORITY_LOW,
+                             ide_build_pipeline_do_flush,
+                             g_object_ref (self),
+                             g_object_unref);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_pipeline_execute_finish:
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_pipeline_execute_finish (IdeBuildPipeline  *self,
+                                   GAsyncResult      *result,
+                                   GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_build_pipeline_attach:
+ * @self: an #IdeBuildPipeline
+ * @phase: An #IdeBuildPhase
+ * @priority: an optional priority for sorting within the phase
+ * @stage: An #IdeBuildStage
+ *
+ * Insert @stage into the pipeline as part of the phase denoted by @phase.
+ *
+ * If priority is non-zero, it will be used to sort the stage among other
+ * stages that are part of the same phase.
+ *
+ * Returns: A stage_id that may be passed to ide_build_pipeline_detach().
+ *
+ * Since: 3.32
+ */
+guint
+ide_build_pipeline_attach (IdeBuildPipeline *self,
+                            IdeBuildPhase     phase,
+                            gint              priority,
+                            IdeBuildStage    *stage)
+{
+  GFlagsClass *klass, *unref_class = NULL;
+  guint ret = 0;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (stage), 0);
+  g_return_val_if_fail ((phase & IDE_BUILD_PHASE_MASK) != IDE_BUILD_PHASE_NONE, 0);
+  g_return_val_if_fail ((phase & IDE_BUILD_PHASE_WHENCE_MASK) == 0 ||
+                        (phase & IDE_BUILD_PHASE_WHENCE_MASK) == IDE_BUILD_PHASE_BEFORE ||
+                        (phase & IDE_BUILD_PHASE_WHENCE_MASK) == IDE_BUILD_PHASE_AFTER, 0);
+
+  if (!(klass = g_type_class_peek (IDE_TYPE_BUILD_PHASE)))
+    klass = unref_class = g_type_class_ref (IDE_TYPE_BUILD_PHASE);
+
+  for (guint i = 0; i < klass->n_values; i++)
+    {
+      const GFlagsValue *value = &klass->values[i];
+
+      if ((phase & IDE_BUILD_PHASE_MASK) == value->value)
+        {
+          PipelineEntry entry = { 0 };
+
+          _ide_build_stage_set_phase (stage, phase);
+
+          IDE_TRACE_MSG ("Adding stage to pipeline with phase %s and priority %d",
+                         value->value_nick, priority);
+
+          entry.id = ++self->seqnum;
+          entry.phase = phase;
+          entry.priority = priority;
+          entry.stage = g_object_ref (stage);
+
+          g_array_append_val (self->pipeline, entry);
+          g_array_sort (self->pipeline, pipeline_entry_compare);
+
+          ret = entry.id;
+
+          ide_build_stage_set_log_observer (stage,
+                                            ide_build_pipeline_log_observer,
+                                            self,
+                                            NULL);
+
+          /*
+           * We need to emit items-changed for the newly added entry, but we relied
+           * on insertion sort above to get our final position. So now we need to
+           * scan the pipeline for where we ended up, and then emit items-changed for
+           * the new stage.
+           */
+          for (guint j = 0; j < self->pipeline->len; j++)
+            {
+              const PipelineEntry *ele = &g_array_index (self->pipeline, PipelineEntry, j);
+
+              if (ele->id == entry.id)
+                {
+                  g_list_model_items_changed (G_LIST_MODEL (self), j, 0, 1);
+                  break;
+                }
+            }
+
+          ide_object_append (IDE_OBJECT (self), IDE_OBJECT (stage));
+
+          IDE_GOTO (cleanup);
+        }
+    }
+
+  g_warning ("No such pipeline phase %02x", phase);
+
+cleanup:
+  if (unref_class != NULL)
+    g_type_class_unref (unref_class);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_build_pipeline_attach_launcher:
+ * @self: an #IdeBuildPipeline
+ * @phase: An #IdeBuildPhase
+ * @priority: an optional priority for sorting within the phase
+ * @launcher: An #IdeSubprocessLauncher
+ *
+ * This creates a new stage that will spawn a process using @launcher and log
+ * the output of stdin/stdout.
+ *
+ * It is a programmer error to modify @launcher after passing it to this
+ * function.
+ *
+ * Returns: A stage_id that may be passed to ide_build_pipeline_remove().
+ *
+ * Since: 3.32
+ */
+guint
+ide_build_pipeline_attach_launcher (IdeBuildPipeline      *self,
+                                     IdeBuildPhase          phase,
+                                     gint                   priority,
+                                     IdeSubprocessLauncher *launcher)
+{
+  g_autoptr(IdeBuildStage) stage = NULL;
+  IdeContext *context;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+  g_return_val_if_fail ((phase & IDE_BUILD_PHASE_MASK) != IDE_BUILD_PHASE_NONE, 0);
+  g_return_val_if_fail ((phase & IDE_BUILD_PHASE_WHENCE_MASK) == 0 ||
+                        (phase & IDE_BUILD_PHASE_WHENCE_MASK) == IDE_BUILD_PHASE_BEFORE ||
+                        (phase & IDE_BUILD_PHASE_WHENCE_MASK) == IDE_BUILD_PHASE_AFTER, 0);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  stage = ide_build_stage_launcher_new (context, launcher);
+
+  return ide_build_pipeline_attach (self, phase, priority, stage);
+}
+
+/**
+ * ide_build_pipeline_request_phase:
+ * @self: An #IdeBuildPipeline
+ * @phase: An #IdeBuildPhase
+ *
+ * Requests that the next execution of the pipeline will build up to @phase
+ * including all stages that were previously invalidated.
+ *
+ * Returns: %TRUE if a stage is known to require execution.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_pipeline_request_phase (IdeBuildPipeline *self,
+                                  IdeBuildPhase     phase)
+{
+  GFlagsClass *klass, *unref_class = NULL;
+  gboolean ret = FALSE;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+  g_return_val_if_fail ((phase & IDE_BUILD_PHASE_MASK) != IDE_BUILD_PHASE_NONE, FALSE);
+
+  /*
+   * You can only request basic phases. That does not include modifiers
+   * like BEFORE, AFTER, FAILED, FINISHED.
+   */
+  phase &= IDE_BUILD_PHASE_MASK;
+
+  if (!(klass = g_type_class_peek (IDE_TYPE_BUILD_PHASE)))
+    klass = unref_class = g_type_class_ref (IDE_TYPE_BUILD_PHASE);
+
+  for (guint i = 0; i < klass->n_values; i++)
+    {
+      const GFlagsValue *value = &klass->values[i];
+
+      if ((guint)phase == value->value)
+        {
+          IDE_TRACE_MSG ("requesting pipeline phase %s", value->value_nick);
+          /*
+           * Each flag is a power of two, so we can simply subtract one
+           * to get a mask of all the previous phases.
+           */
+          self->requested_mask |= phase | (phase - 1);
+          IDE_GOTO (cleanup);
+        }
+    }
+
+  g_warning ("No such phase %02x", (guint)phase);
+
+cleanup:
+
+  /*
+   * If we have a stage in one of the requested phases, then we can let the
+   * caller know that they need to run execute_async() to be up to date. This
+   * is useful for situations where you might want to avoid calling
+   * execute_async() altogether. Additionally, we want to know if there are
+   * any connections to the "query" which could cause the completed state
+   * to be invalidated.
+   */
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if (!(entry->phase & self->requested_mask))
+        continue;
+
+      if (!ide_build_stage_get_completed (entry->stage) ||
+          _ide_build_stage_has_query (entry->stage))
+        {
+          ret = TRUE;
+          break;
+        }
+    }
+
+  if (unref_class != NULL)
+    g_type_class_unref (unref_class);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_build_pipeline_get_builddir:
+ * @self: An #IdeBuildPipeline
+ *
+ * Gets the "builddir" to be used for the build process. This is generally
+ * the location that build systems will use for out-of-tree builds.
+ *
+ * Returns: the path of the build directory
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_build_pipeline_get_builddir (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->builddir;
+}
+
+/**
+ * ide_build_pipeline_get_srcdir:
+ * @self: An #IdeBuildPipeline
+ *
+ * Gets the "srcdir" of the project. This is equivalent to the
+ * IdeVcs:working-directory property as a string.
+ *
+ * Returns: the path of the source directory
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_build_pipeline_get_srcdir (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->srcdir;
+}
+
+static gchar *
+ide_build_pipeline_build_path_va_list (const gchar *prefix,
+                                       const gchar *first_part,
+                                       va_list      args)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+
+  g_assert (prefix != NULL);
+  g_assert (first_part != NULL);
+
+  ar = g_ptr_array_new ();
+  g_ptr_array_add (ar, (gchar *)prefix);
+  do
+    g_ptr_array_add (ar, (gchar *)first_part);
+  while (NULL != (first_part = va_arg (args, const gchar *)));
+  g_ptr_array_add (ar, NULL);
+
+  return g_build_filenamev ((gchar **)ar->pdata);
+}
+
+/**
+ * ide_build_pipeline_build_srcdir_path:
+ *
+ * This is a convenience function to create a new path that starts with
+ * the source directory of the project.
+ *
+ * This is functionally equivalent to calling g_build_filename() with the
+ * working directory of the source tree.
+ *
+ * Returns: (transfer full): A newly allocated string.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_build_pipeline_build_srcdir_path (IdeBuildPipeline *self,
+                                      const gchar      *first_part,
+                                      ...)
+{
+  gchar *ret;
+  va_list args;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+  g_return_val_if_fail (self->srcdir != NULL, NULL);
+  g_return_val_if_fail (first_part != NULL, NULL);
+
+  va_start (args, first_part);
+  ret = ide_build_pipeline_build_path_va_list (self->srcdir, first_part, args);
+  va_end (args);
+
+  return ret;
+}
+
+/**
+ * ide_build_pipeline_build_builddir_path:
+ *
+ * This is a convenience function to create a new path that starts with
+ * the build directory for this build configuration.
+ *
+ * This is functionally equivalent to calling g_build_filename() with the
+ * result of ide_build_pipeline_get_builddir() as the first parameter.
+ *
+ * Returns: (transfer full): A newly allocated string.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_build_pipeline_build_builddir_path (IdeBuildPipeline *self,
+                                        const gchar      *first_part,
+                                        ...)
+{
+  gchar *ret;
+  va_list args;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+  g_return_val_if_fail (self->builddir != NULL, NULL);
+  g_return_val_if_fail (first_part != NULL, NULL);
+
+  va_start (args, first_part);
+  ret = ide_build_pipeline_build_path_va_list (self->builddir, first_part, args);
+  va_end (args);
+
+  return ret;
+}
+
+/**
+ * ide_build_pipeline_detach:
+ * @self: An #IdeBuildPipeline
+ * @stage_id: An identifier returned from adding a stage
+ *
+ * This removes the stage matching @stage_id. You are returned a @stage_id when
+ * inserting a stage with functions such as ide_build_pipeline_attach()
+ * or ide_build_pipeline_attach_launcher().
+ *
+ * Plugins should use this function to remove their stages when the plugin
+ * is unloading.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_pipeline_detach (IdeBuildPipeline *self,
+                               guint             stage_id)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (self->pipeline != NULL);
+  g_return_if_fail (stage_id != 0);
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if (entry->id == stage_id)
+        {
+          ide_object_destroy (IDE_OBJECT (entry->stage));
+          g_array_remove_index (self->pipeline, i);
+          g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+          break;
+        }
+    }
+}
+
+/**
+ * ide_build_pipeline_invalidate_phase:
+ * @self: An #IdeBuildPipeline
+ * @phases: The phases to invalidate
+ *
+ * Invalidates the phases matching @phases flags.
+ *
+ * If the requested phases include the phases invalidated here, the next
+ * execution of the pipeline will execute thse phases.
+ *
+ * This should be used by plugins to ensure a particular phase is re-executed
+ * upon discovering its state is no longer valid. Such an example might be
+ * invalidating the %IDE_BUILD_PHASE_AUTOGEN phase when the an autotools
+ * projects autogen.sh file has been changed.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_pipeline_invalidate_phase (IdeBuildPipeline *self,
+                                     IdeBuildPhase     phases)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if ((entry->phase & IDE_BUILD_PHASE_MASK) & phases)
+        ide_build_stage_set_completed (entry->stage, FALSE);
+    }
+}
+
+/**
+ * ide_build_pipeline_get_stage_by_id:
+ * @self: An #IdeBuildPipeline
+ * @stage_id: the identfier of the stage
+ *
+ * Gets the stage matching the identifier @stage_id as returned from
+ * ide_build_pipeline_attach().
+ *
+ * Returns: (transfer none) (nullable): An #IdeBuildStage or %NULL if the
+ *   stage could not be found.
+ *
+ * Since: 3.32
+ */
+IdeBuildStage *
+ide_build_pipeline_get_stage_by_id (IdeBuildPipeline *self,
+                                    guint             stage_id)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if (entry->id == stage_id)
+        return entry->stage;
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_build_pipeline_get_runtime:
+ * @self: An #IdeBuildPipeline
+ *
+ * A convenience function to get the runtime for a build pipeline.
+ *
+ * Returns: (transfer none) (nullable): An #IdeRuntime or %NULL
+ *
+ * Since: 3.32
+ */
+IdeRuntime *
+ide_build_pipeline_get_runtime (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->runtime;
+}
+
+/**
+ * ide_build_pipeline_get_toolchain:
+ * @self: An #IdeBuildPipeline
+ *
+ * A convenience function to get the toolchain for a build pipeline.
+ *
+ * Returns: (transfer none): An #IdeToolchain
+ *
+ * Since: 3.32
+ */
+IdeToolchain *
+ide_build_pipeline_get_toolchain (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->toolchain;
+}
+
+/**
+ * ide_build_pipeline_create_launcher:
+ * @self: An #IdeBuildPipeline
+ *
+ * This is a convenience function to create a new #IdeSubprocessLauncher
+ * using the configuration and runtime associated with the pipeline.
+ *
+ * Returns: (transfer full): An #IdeSubprocessLauncher.
+ *
+ * Since: 3.32
+ */
+IdeSubprocessLauncher *
+ide_build_pipeline_create_launcher (IdeBuildPipeline  *self,
+                                    GError           **error)
+{
+  g_autoptr(IdeSubprocessLauncher) ret = NULL;
+  IdeRuntime *runtime;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  runtime = ide_configuration_get_runtime (self->configuration);
+
+  if (runtime == NULL)
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_FAILED,
+                   "The runtime %s is missing",
+                   ide_configuration_get_runtime_id (self->configuration));
+      return NULL;
+    }
+
+  ret = ide_runtime_create_launcher (runtime, error);
+
+  if (ret != NULL)
+    {
+      IdeEnvironment *env = ide_configuration_get_environment (self->configuration);
+
+      ide_subprocess_launcher_set_clear_env (ret, TRUE);
+      ide_subprocess_launcher_overlay_environment (ret, env);
+      /* Always ignore V=1 from configurations */
+      ide_subprocess_launcher_setenv (ret, "V", "0", TRUE);
+      ide_subprocess_launcher_set_cwd (ret, ide_build_pipeline_get_builddir (self));
+      ide_subprocess_launcher_set_flags (ret,
+                                         (G_SUBPROCESS_FLAGS_STDERR_PIPE |
+                                          G_SUBPROCESS_FLAGS_STDOUT_PIPE));
+      ide_configuration_apply_path (self->configuration, ret);
+    }
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_build_pipeline_attach_pty:
+ * @self: an #IdeBuildPipeline
+ * @launcher: an #IdeSubprocessLauncher
+ *
+ * Attaches a PTY to stdin/stdout/stderr of the #IdeSubprocessLauncher.
+ * This is useful if the application can take advantage of a PTY for
+ * features like colors and other escape sequences.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_pipeline_attach_pty (IdeBuildPipeline      *self,
+                               IdeSubprocessLauncher *launcher)
+{
+  GSubprocessFlags flags;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (launcher));
+
+  if (self->pty_slave == -1)
+    {
+      IdePtyFd master_fd = ide_pty_intercept_get_fd (&self->intercept);
+      self->pty_slave = ide_pty_intercept_create_slave (master_fd, TRUE);
+    }
+
+  if (self->pty_slave == -1)
+    {
+      ide_object_warning (self, _("Pseudo terminal creation failed. Terminal features will be limited."));
+      return;
+    }
+
+  /* Turn off built in pipes if set */
+  flags = ide_subprocess_launcher_get_flags (launcher);
+  flags &= ~(G_SUBPROCESS_FLAGS_STDERR_PIPE |
+             G_SUBPROCESS_FLAGS_STDOUT_PIPE |
+             G_SUBPROCESS_FLAGS_STDIN_PIPE);
+  ide_subprocess_launcher_set_flags (launcher, flags);
+
+  /* Assign slave device */
+  ide_subprocess_launcher_take_stdin_fd (launcher, dup (self->pty_slave));
+  ide_subprocess_launcher_take_stdout_fd (launcher, dup (self->pty_slave));
+  ide_subprocess_launcher_take_stderr_fd (launcher, dup (self->pty_slave));
+
+  /* Ensure a terminal type is set */
+  ide_subprocess_launcher_setenv (launcher, "TERM", "xterm-256color", FALSE);
+}
+
+/**
+ * ide_build_pipeline_get_pty:
+ * @self: a #IdeBuildPipeline
+ *
+ * Gets the #VtePty for the pipeline, if set.
+ *
+ * This will not be set until the pipeline has been initialized. That is not
+ * guaranteed to happen at object creation time.
+ *
+ * Returns: (transfer none) (nullable): a #VtePty or %NULL
+ *
+ * Since: 3.32
+ */
+VtePty *
+ide_build_pipeline_get_pty (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->pty;
+}
+
+guint
+ide_build_pipeline_add_log_observer (IdeBuildPipeline    *self,
+                                     IdeBuildLogObserver  observer,
+                                     gpointer             observer_data,
+                                     GDestroyNotify       observer_data_destroy)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+  g_return_val_if_fail (observer != NULL, 0);
+
+  return ide_build_log_add_observer (self->log, observer, observer_data, observer_data_destroy);
+
+}
+
+gboolean
+ide_build_pipeline_remove_log_observer (IdeBuildPipeline *self,
+                                        guint             observer_id)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+  g_return_val_if_fail (observer_id > 0, FALSE);
+
+  return ide_build_log_remove_observer (self->log, observer_id);
+}
+
+void
+ide_build_pipeline_emit_diagnostic (IdeBuildPipeline *self,
+                                    IdeDiagnostic    *diagnostic)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (diagnostic != NULL);
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+
+  g_signal_emit (self, signals[DIAGNOSTIC], 0, diagnostic);
+}
+
+/**
+ * ide_build_pipeline_add_error_format:
+ * @self: an #IdeBuildPipeline
+ * @regex: A regex to be compiled
+ *
+ * This can be used to add a regex that will extract errors from
+ * standard output. This is similar to the "errorformat" feature
+ * of vim to extract warnings from standard output.
+ *
+ * The regex should used named capture groups to pass information
+ * to the extraction process.
+ *
+ * Supported group names are:
+ *
+ *  • filename (a string path)
+ *  • line (an integer)
+ *  • column (an integer)
+ *  • level (a string)
+ *  • message (a string)
+ *
+ * For example, to extract warnings from GCC you might do something
+ * like the following:
+ *
+ *   "(?&lt;filename&gt;[a-zA-Z0-9\\-\\.\\/_]+):"
+ *   "(?&lt;line&gt;\\d+):"
+ *   "(?&lt;column&gt;\\d+): "
+ *   "(?&lt;level&gt;[\\w\\s]+): "
+ *   "(?&lt;message&gt;.*)"
+ *
+ * To remove the regex, use the ide_build_pipeline_remove_error_format()
+ * function with the resulting format id returned from this function.
+ *
+ * The resulting format id will be &gt; 0 if successful.
+ *
+ * Returns: an error format id that may be passed to
+ *   ide_build_pipeline_remove_error_format().
+ *
+ * Since: 3.32
+ */
+guint
+ide_build_pipeline_add_error_format (IdeBuildPipeline   *self,
+                                     const gchar        *regex,
+                                     GRegexCompileFlags  flags)
+{
+  ErrorFormat errfmt = { 0 };
+  g_autoptr(GError) error = NULL;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+
+  errfmt.regex = g_regex_new (regex, G_REGEX_OPTIMIZE | flags, 0, &error);
+
+  if (errfmt.regex == NULL)
+    {
+      g_warning ("%s", error->message);
+      return 0;
+    }
+
+  errfmt.id = ++self->errfmt_seqnum;
+
+  g_array_append_val (self->errfmts, errfmt);
+
+  return errfmt.id;
+}
+
+/**
+ * ide_build_pipeline_remove_error_format:
+ * @self: An #IdeBuildPipeline
+ * @error_format_id: an identifier for the error format.
+ *
+ * Removes an error format that was registered with
+ * ide_build_pipeline_add_error_format().
+ *
+ * Returns: %TRUE if the error format was removed.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_pipeline_remove_error_format (IdeBuildPipeline *self,
+                                        guint             error_format_id)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+  g_return_val_if_fail (error_format_id > 0, FALSE);
+
+  for (guint i = 0; i < self->errfmts->len; i++)
+    {
+      const ErrorFormat *errfmt = &g_array_index (self->errfmts, ErrorFormat, i);
+
+      if (errfmt->id == error_format_id)
+        {
+          g_array_remove_index (self->errfmts, i);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+gboolean
+ide_build_pipeline_get_busy (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+
+  return self->busy;
+}
+
+/**
+ * ide_build_pipeline_get_message:
+ * @self: An #IdeBuildPipeline
+ *
+ * Gets the current message for the build pipeline. This can be
+ * shown to users in UI elements to signify progress in the
+ * build.
+ *
+ * Returns: (nullable) (transfer full): A string representing the
+ *   current stage of the build, or %NULL.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_build_pipeline_get_message (IdeBuildPipeline *self)
+{
+  IdeBuildPhase phase;
+  const gchar *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  /* Use any message the Pty has given us while building. */
+  if (self->busy && self->message != NULL)
+    return g_strdup (self->message);
+
+  if (self->in_clean)
+    return g_strdup (_("Cleaning…"));
+
+  /* Not active, use simple messaging */
+  if (self->failed)
+    return g_strdup (_("Failed"));
+  else if (!self->busy)
+    return g_strdup (_("Ready"));
+
+  if (self->current_stage != NULL)
+    {
+      const gchar *name = ide_build_stage_get_name (self->current_stage);
+
+      if (!ide_str_empty0 (name))
+        return g_strdup (name);
+    }
+
+  phase = ide_build_pipeline_get_phase (self);
+
+  switch (phase)
+    {
+    case IDE_BUILD_PHASE_DOWNLOADS:
+      ret = _("Downloading…");
+      break;
+
+    case IDE_BUILD_PHASE_DEPENDENCIES:
+      ret = _("Building dependencies…");
+      break;
+
+    case IDE_BUILD_PHASE_AUTOGEN:
+      ret = _("Bootstrapping…");
+      break;
+
+    case IDE_BUILD_PHASE_CONFIGURE:
+      ret = _("Configuring…");
+      break;
+
+    case IDE_BUILD_PHASE_BUILD:
+      ret = _("Building…");
+      break;
+
+    case IDE_BUILD_PHASE_INSTALL:
+      ret = _("Installing…");
+      break;
+
+    case IDE_BUILD_PHASE_COMMIT:
+      ret = _("Committing…");
+      break;
+
+    case IDE_BUILD_PHASE_EXPORT:
+      ret = _("Exporting…");
+      break;
+
+    case IDE_BUILD_PHASE_FINAL:
+      ret = _("Success");
+      break;
+
+    case IDE_BUILD_PHASE_FINISHED:
+      ret = _("Success");
+      break;
+
+    case IDE_BUILD_PHASE_FAILED:
+      ret = _("Failed");
+      break;
+
+    case IDE_BUILD_PHASE_PREPARE:
+      ret = _("Preparing…");
+      break;
+
+    case IDE_BUILD_PHASE_NONE:
+      ret = _("Ready");
+      break;
+
+    case IDE_BUILD_PHASE_AFTER:
+    case IDE_BUILD_PHASE_BEFORE:
+    default:
+      g_assert_not_reached ();
+    }
+
+  return g_strdup (ret);
+}
+
+/**
+ * ide_build_pipeline_foreach_stage:
+ * @self: An #IdeBuildPipeline
+ * @stage_callback: (scope call): A callback for each #IdePipelineStage
+ * @user_data: user data for @stage_callback
+ *
+ * This function will call @stage_callback for every #IdeBuildStage registered
+ * in the pipeline.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_pipeline_foreach_stage (IdeBuildPipeline *self,
+                                  GFunc             stage_callback,
+                                  gpointer          user_data)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (stage_callback != NULL);
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      stage_callback (entry->stage, user_data);
+    }
+}
+
+static void
+ide_build_pipeline_clean_cb (GObject      *object,
+                             GAsyncResult *result,
+                             gpointer      user_data)
+{
+  IdeBuildStage *stage = (IdeBuildStage *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeBuildPipeline *self;
+  GPtrArray *stages;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  td = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (td != NULL);
+  g_assert (td->type == TASK_CLEAN);
+  g_assert (td->task == task);
+  g_assert (td->clean.stages != NULL);
+
+  stages = td->clean.stages;
+
+  g_assert (stages != NULL);
+  g_assert (stages->len > 0);
+  g_assert (g_ptr_array_index (stages, stages->len - 1) == stage);
+
+  if (!ide_build_stage_clean_finish (stage, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  g_ptr_array_remove_index (stages, stages->len - 1);
+
+  ide_build_pipeline_tick_clean (self, task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_tick_clean (IdeBuildPipeline *self,
+                               IdeTask          *task)
+{
+  GCancellable *cancellable;
+  GPtrArray *stages;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_TASK (task));
+
+  td = ide_task_get_task_data (task);
+  cancellable = ide_task_get_cancellable (task);
+
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (td != NULL);
+  g_assert (td->type == TASK_CLEAN);
+  g_assert (td->task == task);
+  g_assert (td->clean.stages != NULL);
+
+  stages = td->clean.stages;
+
+  if (stages->len != 0)
+    {
+      IdeBuildStage *stage = g_ptr_array_index (stages, stages->len - 1);
+
+      self->current_stage = stage;
+
+      ide_build_stage_clean_async (stage,
+                                   self,
+                                   cancellable,
+                                   ide_build_pipeline_clean_cb,
+                                   g_object_ref (task));
+
+      IDE_GOTO (notify);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+
+notify:
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PHASE]);
+
+  IDE_EXIT;
+}
+
+void
+ide_build_pipeline_clean_async (IdeBuildPipeline    *self,
+                                IdeBuildPhase        phase,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GCancellable) local_cancellable = NULL;
+  g_autoptr(GPtrArray) stages = NULL;
+  IdeBuildPhase min_phase = IDE_BUILD_PHASE_FINAL;
+  IdeBuildPhase phase_mask;
+  GFlagsClass *phase_class;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (cancellable == NULL)
+    cancellable = local_cancellable = g_cancellable_new ();
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_source_tag (task, ide_build_pipeline_clean_async);
+
+  if (!ide_build_pipeline_check_ready (self, task))
+    return;
+
+  dzl_cancellable_chain (cancellable, self->cancellable);
+
+  td = task_data_new (task, TASK_CLEAN);
+  td->phase = phase;
+  ide_task_set_task_data (task, td, task_data_free);
+
+  /*
+   * To clean the project, we go through each stage and call it's clean async
+   * vfunc pairs if they have been set. Afterwards, we ensure their
+   * IdeBuildStage:completed bit is cleared so they will run as part of the
+   * next build operation.
+   *
+   * Also, when performing a clean we walk backwards from the last stage to the
+   * present so that they can rely on things being semi-up-to-date from their
+   * point of view.
+   *
+   * To simplify the case of walking through the affected stages, we create a
+   * copy of the affected stages up front. We store them in the opposite order
+   * they need to be ran so that we only have to pop the last item after
+   * completing each stage. Otherwise we would additionally need a position
+   * variable.
+   *
+   * To calculate the phases that are affected, we subtract 1 from the min
+   * phase that was given to us. We then twos-compliment that and use it as our
+   * mask (so only our min and higher stages are cleaned).
+   */
+
+  phase_class = g_type_class_peek (IDE_TYPE_BUILD_PHASE);
+
+  for (guint i = 0; i < phase_class->n_values; i++)
+    {
+      const GFlagsValue *value = &phase_class->values [i];
+
+      if (value->value & phase)
+        {
+          if (value->value < (guint)min_phase)
+            min_phase = value->value;
+        }
+    }
+
+  phase_mask = ~(min_phase - 1);
+
+  stages = g_ptr_array_new_with_free_func (g_object_unref);
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if ((entry->phase & IDE_BUILD_PHASE_MASK) & phase_mask)
+        g_ptr_array_add (stages, g_object_ref (entry->stage));
+    }
+
+  /*
+   * Short-circuit if we don't have any stages to clean.
+   */
+  if (stages->len == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  td->clean.stages = g_steal_pointer (&stages);
+
+  g_queue_push_tail (&self->task_queue, g_steal_pointer (&task));
+
+  ide_build_pipeline_queue_flush (self);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_build_pipeline_clean_finish (IdeBuildPipeline  *self,
+                                 GAsyncResult      *result,
+                                 GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+can_remove_builddir (IdeBuildPipeline *self)
+{
+  g_autofree gchar *_build = NULL;
+  g_autoptr(GFile) builddir = NULL;
+  g_autoptr(GFile) cache = NULL;
+  IdeContext *context;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  /*
+   * Only remove builddir if it is in ~/.cache/ or our XDG data dirs
+   * equivalent. We don't want to accidentally remove data that might
+   * be important to the user.
+   *
+   * However, if the build dir is our special case "_build" inside the
+   * project directory, we'll allow that too.
+   */
+
+  cache = g_file_new_for_path (g_get_user_cache_dir ());
+  builddir = g_file_new_for_path (self->builddir);
+  if (g_file_has_prefix (builddir, cache))
+    return TRUE;
+
+  /* If this is _build in the project tree, we will allow that too
+   * since we create those sometimes.
+   */
+  context = ide_object_get_context (IDE_OBJECT (self));
+  _build = ide_context_build_filename (context, "_build", NULL);
+  if (g_str_equal (_build, self->builddir) &&
+      g_file_test (_build, G_FILE_TEST_IS_DIR) &&
+      !g_file_test (_build, G_FILE_TEST_IS_SYMLINK))
+    return TRUE;
+
+  g_debug ("%s is not in a cache directory, will not delete it", self->builddir);
+
+  return FALSE;
+}
+
+static void
+ide_build_pipeline_reaper_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  DzlDirectoryReaper *reaper = (DzlDirectoryReaper *)object;
+  IdeBuildPipeline *self;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_assert (DZL_IS_DIRECTORY_REAPER (reaper));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  td = ide_task_get_task_data (task);
+
+  g_assert (td != NULL);
+  g_assert (td->task == task);
+  g_assert (td->type == TASK_REBUILD);
+
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  /* Make sure our reaper completed or else we bail */
+  if (!dzl_directory_reaper_execute_finish (reaper, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (td->phase == IDE_BUILD_PHASE_NONE)
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  /* Perform a build using the same task and skipping the build queue. */
+  ide_build_pipeline_tick_execute (self, task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_pipeline_tick_rebuild (IdeBuildPipeline *self,
+                                 IdeTask          *task)
+{
+  g_autoptr(DzlDirectoryReaper) reaper = NULL;
+  GCancellable *cancellable;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_TASK (task));
+
+#ifndef G_DISABLE_ASSERT
+  {
+    TaskData *td = ide_task_get_task_data (task);
+
+    g_assert (td != NULL);
+    g_assert (td->type == TASK_REBUILD);
+    g_assert (td->task == task);
+  }
+#endif
+
+  reaper = dzl_directory_reaper_new ();
+
+  /*
+   * Check if we can remove the builddir. We don't want to do this if it is the
+   * same as the srcdir (in-tree builds).
+   */
+  if (can_remove_builddir (self))
+    {
+      g_autoptr(GFile) builddir = g_file_new_for_path (self->builddir);
+
+      dzl_directory_reaper_add_directory (reaper, builddir, 0);
+    }
+
+  /*
+   * Now let the build stages add any files they might want to reap as part of
+   * the rebuild process.
+   */
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      ide_build_stage_emit_reap (entry->stage, reaper);
+      ide_build_stage_set_completed (entry->stage, FALSE);
+    }
+
+  cancellable = ide_task_get_cancellable (task);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* Now execute the reaper to clean up the build files. */
+  dzl_directory_reaper_execute_async (reaper,
+                                      cancellable,
+                                      ide_build_pipeline_reaper_cb,
+                                      g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_pipeline_rebuild_async:
+ * @self: A @IdeBuildPipeline
+ * @phase: the requested build phase
+ * @targets: (element-type IdeBuildTarget) (nullable): an array of
+ *   #IdeBuildTarget or %NULL
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: data for @callback
+ *
+ * Asynchronously starts the build pipeline after cleaning any
+ * existing build artifacts.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_pipeline_rebuild_async (IdeBuildPipeline    *self,
+                                  IdeBuildPhase        phase,
+                                  GPtrArray           *targets,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  TaskData *td;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail ((phase & ~IDE_BUILD_PHASE_MASK) == 0);
+
+  cancellable = dzl_cancellable_chain (cancellable, self->cancellable);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_source_tag (task, ide_build_pipeline_rebuild_async);
+
+  if (!ide_build_pipeline_check_ready (self, task))
+    return;
+
+  td = task_data_new (task, TASK_REBUILD);
+  td->phase = phase;
+  td->rebuild.targets = _g_ptr_array_copy_objects (targets);
+  ide_task_set_task_data (task, td, task_data_free);
+
+  g_queue_push_tail (&self->task_queue, g_steal_pointer (&task));
+
+  ide_build_pipeline_queue_flush (self);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_build_pipeline_rebuild_finish (IdeBuildPipeline  *self,
+                                   GAsyncResult      *result,
+                                   GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_build_pipeline_get_can_export:
+ * @self: a #IdeBuildPipeline
+ *
+ * This function is useful to discover if there are any pipeline addins
+ * which implement the export phase. UI or GAction implementations may
+ * want to use this value to set the enabled state of the action or
+ * sensitivity of a button.
+ *
+ * Returns: %TRUE if there are export pipeline stages.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_pipeline_get_can_export (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+
+  if (self->broken)
+    return FALSE;
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if ((entry->phase & IDE_BUILD_PHASE_EXPORT) != 0)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+void
+_ide_build_pipeline_set_message (IdeBuildPipeline *self,
+                                 const gchar      *message)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+
+  if (message != NULL)
+    {
+      /*
+       * Special case to deal with messages coming from systems we
+       * know prefix the build tooling information to the message.
+       * It's easier to just do this here rather than provide some
+       * sort of API for plugins to do this for us.
+       */
+      if (g_str_has_prefix (message, "flatpak-builder: "))
+        message += strlen ("flatpak-builder: ");
+      else if (g_str_has_prefix (message, "jhbuild:"))
+        message += strlen ("jhbuild:");
+    }
+
+  if (!ide_str_equal0 (message, self->message))
+    {
+      g_free (self->message);
+      self->message = g_strdup (message);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MESSAGE]);
+    }
+}
+
+void
+_ide_build_pipeline_cancel (IdeBuildPipeline *self)
+{
+  g_autoptr(GCancellable) cancellable = NULL;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+
+  cancellable = g_steal_pointer (&self->cancellable);
+  self->cancellable = g_cancellable_new ();
+  g_cancellable_cancel (cancellable);
+}
+
+/**
+ * ide_build_pipeline_has_configured:
+ * @self: a #IdeBuildPipeline
+ *
+ * Checks to see if the pipeline has advanced far enough to ensure that
+ * the configure stage has been reached.
+ *
+ * Returns: %TRUE if %IDE_BUILD_PHASE_CONFIGURE has been reached.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_pipeline_has_configured (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+
+  if (self->broken)
+    return FALSE;
+
+  /*
+   * We need to walk from beginning towards end (instead of
+   * taking a cleaner approach that would be to walk from the
+   * end forward) because it's possible for some items to be
+   * marked completed before they've ever been run.
+   *
+   * So just walk forward and we have configured if we hit
+   * any phase that is CONFIGURE and has completed, or no
+   * configure phases were found.
+   */
+
+  for (guint i = 0; i < self->pipeline->len; i++)
+    {
+      const PipelineEntry *entry = &g_array_index (self->pipeline, PipelineEntry, i);
+
+      if ((entry->phase & IDE_BUILD_PHASE_MASK) < IDE_BUILD_PHASE_CONFIGURE)
+        continue;
+
+      if (entry->phase & IDE_BUILD_PHASE_CONFIGURE)
+        {
+          /*
+           * This is a configure phase, ensure that it has been
+           * completed, or we have not really configured.
+           */
+          if (!ide_build_stage_get_completed (entry->stage))
+            return FALSE;
+
+          /*
+           * Check the next pipeline entry to ensure that it too
+           * has been configured.
+           */
+          continue;
+        }
+
+      /*
+       * We've advanced past CONFIGURE, so anything at this point
+       * can be considered configured.
+       */
+
+      return TRUE;
+    }
+
+  /*
+   * Technically we could have a build system that only supports
+   * up to configure. But I don't really care about that case. If
+   * that ever happens, we need an additional check here that the
+   * last pipeline entry completed.
+   */
+
+  return FALSE;
+}
+
+void
+_ide_build_pipeline_mark_broken (IdeBuildPipeline *self)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+
+  self->broken = TRUE;
+}
+
+static GType
+ide_build_pipeline_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_BUILD_STAGE;
+}
+
+static guint
+ide_build_pipeline_get_n_items (GListModel *model)
+{
+  IdeBuildPipeline *self = (IdeBuildPipeline *)model;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+
+  return self->pipeline != NULL ? self->pipeline->len : 0;
+}
+
+static gpointer
+ide_build_pipeline_get_item (GListModel *model,
+                             guint       position)
+{
+  IdeBuildPipeline *self = (IdeBuildPipeline *)model;
+  const PipelineEntry *entry;
+
+  g_assert (IDE_IS_BUILD_PIPELINE (self));
+  g_assert (self->pipeline != NULL);
+  g_assert (position < self->pipeline->len);
+
+  entry = &g_array_index (self->pipeline, PipelineEntry, position);
+
+  return g_object_ref (entry->stage);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item = ide_build_pipeline_get_item;
+  iface->get_item_type = ide_build_pipeline_get_item_type;
+  iface->get_n_items = ide_build_pipeline_get_n_items;
+}
+
+/**
+ * ide_build_pipeline_get_requested_phase:
+ * @self: a #IdeBuildPipeline
+ *
+ * Gets the phase that has been requested. This can be useful when you want to
+ * get an idea of where the build pipeline will attempt to advance.
+ *
+ * Returns: an #IdeBuildPhase
+ *
+ * Since: 3.32
+ */
+IdeBuildPhase
+ide_build_pipeline_get_requested_phase (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), 0);
+
+  return self->requested_mask & IDE_BUILD_PHASE_MASK;
+}
+
+void
+_ide_build_pipeline_set_pty_size (IdeBuildPipeline *self,
+                                  guint             rows,
+                                  guint             columns)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+
+  if (self->pty_slave != IDE_PTY_FD_INVALID)
+    ide_pty_intercept_set_size (&self->intercept, rows, columns);
+}
+
+void
+_ide_build_pipeline_set_runtime (IdeBuildPipeline *self,
+                                 IdeRuntime       *runtime)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (!runtime || IDE_IS_RUNTIME (runtime));
+
+  if (g_set_object (&self->runtime, runtime))
+    {
+      IdeBuildSystem *build_system;
+      IdeContext *context;
+
+      context = ide_object_get_context (IDE_OBJECT (self));
+      build_system = ide_build_system_from_context (context);
+
+      g_clear_pointer (&self->builddir, g_free);
+      self->builddir = ide_build_system_get_builddir (build_system, self);
+    }
+}
+
+void
+_ide_build_pipeline_set_toolchain (IdeBuildPipeline *self,
+                                   IdeToolchain     *toolchain)
+{
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (!toolchain || IDE_IS_TOOLCHAIN (toolchain));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (g_set_object (&self->toolchain, toolchain))
+    ide_configuration_set_toolchain (self->configuration, toolchain);
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+/**
+ * ide_build_pipeline_ref_toolchain:
+ * @self: a #IdeBuildPipeline
+ *
+ * Thread-safe variant of ide_build_pipeline_get_toolchain().
+ *
+ * Returns: (transfer full) (nullable): an #IdeToolchain or %NULL
+ *
+ * Since: 3.32
+ */
+IdeToolchain *
+ide_build_pipeline_ref_toolchain (IdeBuildPipeline *self)
+{
+  IdeToolchain *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  g_set_object (&ret, self->toolchain);
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+void
+_ide_build_pipeline_check_toolchain (IdeBuildPipeline *self,
+                                     IdeDeviceInfo     *info)
+{
+  g_autoptr(IdeToolchain) toolchain = NULL;
+  g_autoptr(IdeTriplet) toolchain_triplet = NULL;
+  IdeContext *context;
+  IdeRuntime *runtime;
+  IdeTriplet *device_triplet;
+  IdeToolchainManager *manager;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (self));
+  g_return_if_fail (IDE_IS_DEVICE_INFO (info));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  manager = ide_toolchain_manager_from_context (context);
+  g_return_if_fail (IDE_IS_TOOLCHAIN_MANAGER (manager));
+
+  toolchain = ide_configuration_get_toolchain (self->configuration);
+  runtime = ide_configuration_get_runtime (self->configuration);
+  device_triplet = ide_device_info_get_host_triplet (info);
+  toolchain_triplet = ide_toolchain_get_host_triplet (toolchain);
+
+  if (self->host_triplet != device_triplet)
+    {
+      g_clear_pointer (&self->host_triplet, ide_triplet_unref);
+      self->host_triplet = ide_triplet_ref (device_triplet);
+    }
+
+  /* Don't try to initialize too early */
+  if (ide_toolchain_manager_is_loaded (manager))
+    IDE_EXIT;
+
+  /* TODO: fallback to most compatible toolchain instead of default */
+
+  if (toolchain == NULL ||
+      g_strcmp0 (ide_triplet_get_arch (device_triplet),
+                 ide_triplet_get_arch (toolchain_triplet)) != 0 ||
+      !ide_runtime_supports_toolchain (runtime, toolchain))
+    {
+      g_autoptr(IdeToolchain) default_toolchain = NULL;
+
+      default_toolchain = ide_toolchain_manager_get_toolchain (manager, "default");
+      _ide_build_pipeline_set_toolchain (self, default_toolchain);
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_pipeline_get_device:
+ * @self: a #IdeBuildPipeline
+ *
+ * Gets the device that the pipeline is building for.
+ *
+ * Returns: (transfer none): an #IdeDevice.
+ *
+ * Since: 3.32
+ */
+IdeDevice *
+ide_build_pipeline_get_device (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->device;
+}
+
+/**
+ * ide_build_pipeline_is_ready:
+ * @self: a #IdeBuildPipeline
+ *
+ * Checks to see if the pipeline has been loaded. Loading may be delayed
+ * due to various initialization routines that need to complete.
+ *
+ * Returns: %TRUE if the pipeline has loaded, otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_pipeline_is_ready (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+
+  return self->loaded;
+}
+
+/**
+ * ide_build_pipeline_get_host_triplet:
+ * @self: a #IdeBuildPipeline
+ *
+ * Gets the "host" triplet which specifies where the build results will run.
+ *
+ * This is a convenience wrapper around getting the triplet from the device
+ * set for the build pipeline.
+ *
+ * Returns: (transfer none): an #IdeTriplet
+ *
+ * Since: 3.32
+ */
+IdeTriplet *
+ide_build_pipeline_get_host_triplet (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), NULL);
+
+  return self->host_triplet;
+}
+
+/**
+ * ide_build_pipeline_is_native:
+ * @self: a #IdeBuildPipeline
+ *
+ * This is a helper to check if the triplet that we are compiling
+ * for matches the host system. That allows some plugins to do less
+ * work by avoiding some cross-compiling work.
+ *
+ * Returns: %FALSE if we're possibly cross-compiling, otherwise %TRUE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_pipeline_is_native (IdeBuildPipeline *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (self), FALSE);
+
+  if (self->host_triplet != NULL)
+    return ide_triplet_is_system (self->host_triplet);
+
+  return FALSE;
+}
diff --git a/src/libide/foundry/ide-build-pipeline.h b/src/libide/foundry/ide-build-pipeline.h
new file mode 100644
index 000000000..37065956a
--- /dev/null
+++ b/src/libide/foundry/ide-build-pipeline.h
@@ -0,0 +1,223 @@
+/* ide-build-pipeline.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <libide-code.h>
+#include <libide-threading.h>
+#include <vte/vte.h>
+
+#include "ide-foundry-types.h"
+
+#include "ide-build-log.h"
+#include "ide-build-stage.h"
+#include "ide-configuration.h"
+#include "ide-runtime.h"
+#include "ide-triplet.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_PIPELINE     (ide_build_pipeline_get_type())
+#define IDE_BUILD_PHASE_MASK        (0xFFFFFF)
+#define IDE_BUILD_PHASE_WHENCE_MASK (IDE_BUILD_PHASE_BEFORE | IDE_BUILD_PHASE_AFTER)
+#define IDE_BUILD_ERROR             (ide_build_error_quark())
+
+typedef enum
+{
+  IDE_BUILD_PHASE_NONE         = 0,
+  IDE_BUILD_PHASE_PREPARE      = 1 << 0,
+  IDE_BUILD_PHASE_DOWNLOADS    = 1 << 1,
+  IDE_BUILD_PHASE_DEPENDENCIES = 1 << 2,
+  IDE_BUILD_PHASE_AUTOGEN      = 1 << 3,
+  IDE_BUILD_PHASE_CONFIGURE    = 1 << 4,
+  IDE_BUILD_PHASE_BUILD        = 1 << 6,
+  IDE_BUILD_PHASE_INSTALL      = 1 << 7,
+  IDE_BUILD_PHASE_COMMIT       = 1 << 8,
+  IDE_BUILD_PHASE_EXPORT       = 1 << 9,
+  IDE_BUILD_PHASE_FINAL        = 1 << 10,
+  IDE_BUILD_PHASE_BEFORE       = 1 << 28,
+  IDE_BUILD_PHASE_AFTER        = 1 << 29,
+  IDE_BUILD_PHASE_FINISHED     = 1 << 30,
+  IDE_BUILD_PHASE_FAILED       = 1 << 31,
+} IdeBuildPhase;
+
+typedef enum
+{
+  IDE_BUILD_ERROR_UNKNOWN = 0,
+  IDE_BUILD_ERROR_BROKEN,
+  IDE_BUILD_ERROR_NOT_LOADED,
+  IDE_BUILD_ERROR_NEEDS_REBUILD,
+} IdeBuildError;
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeBuildPipeline, ide_build_pipeline, IDE, BUILD_PIPELINE, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+GQuark                 ide_build_error_quark                      (void) G_GNUC_CONST;
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_is_native               (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_is_ready                (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_get_busy                (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+IdeConfiguration      *ide_build_pipeline_get_configuration       (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+IdeDevice             *ide_build_pipeline_get_device              (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+IdeTriplet            *ide_build_pipeline_get_host_triplet        (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+IdeRuntime            *ide_build_pipeline_get_runtime             (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+IdeToolchain          *ide_build_pipeline_get_toolchain           (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+IdeToolchain          *ide_build_pipeline_ref_toolchain           (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+const gchar           *ide_build_pipeline_get_builddir            (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+const gchar           *ide_build_pipeline_get_srcdir              (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+gchar                 *ide_build_pipeline_get_message             (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+IdeBuildPhase          ide_build_pipeline_get_phase               (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_get_can_export          (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+VtePty                *ide_build_pipeline_get_pty                 (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+IdeSubprocessLauncher *ide_build_pipeline_create_launcher         (IdeBuildPipeline       *self,
+                                                                   GError                **error);
+IDE_AVAILABLE_IN_3_32
+gchar                 *ide_build_pipeline_build_srcdir_path       (IdeBuildPipeline       *self,
+                                                                   const gchar            *first_part,
+                                                                   ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+gchar                 *ide_build_pipeline_build_builddir_path     (IdeBuildPipeline       *self,
+                                                                   const gchar            *first_part,
+                                                                   ...) G_GNUC_NULL_TERMINATED;
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_pipeline_invalidate_phase        (IdeBuildPipeline       *self,
+                                                                   IdeBuildPhase           phases);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_request_phase           (IdeBuildPipeline       *self,
+                                                                   IdeBuildPhase           phase);
+IDE_AVAILABLE_IN_3_32
+guint                  ide_build_pipeline_attach                 (IdeBuildPipeline       *self,
+                                                                   IdeBuildPhase           phase,
+                                                                   gint                    priority,
+                                                                   IdeBuildStage          *stage);
+IDE_AVAILABLE_IN_3_32
+guint                  ide_build_pipeline_attach_launcher        (IdeBuildPipeline       *self,
+                                                                   IdeBuildPhase           phase,
+                                                                   gint                    priority,
+                                                                   IdeSubprocessLauncher  *launcher);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_pipeline_detach              (IdeBuildPipeline       *self,
+                                                                   guint                   stage_id);
+IDE_AVAILABLE_IN_3_32
+IdeBuildStage         *ide_build_pipeline_get_stage_by_id         (IdeBuildPipeline       *self,
+                                                                   guint                   stage_id);
+IDE_AVAILABLE_IN_3_32
+guint                  ide_build_pipeline_add_log_observer        (IdeBuildPipeline       *self,
+                                                                   IdeBuildLogObserver     observer,
+                                                                   gpointer                observer_data,
+                                                                   GDestroyNotify          
observer_data_destroy);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_remove_log_observer     (IdeBuildPipeline       *self,
+                                                                   guint                   observer_id);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_pipeline_emit_diagnostic         (IdeBuildPipeline       *self,
+                                                                   IdeDiagnostic          *diagnostic);
+IDE_AVAILABLE_IN_3_32
+guint                  ide_build_pipeline_add_error_format        (IdeBuildPipeline       *self,
+                                                                   const gchar            *regex,
+                                                                   GRegexCompileFlags      flags);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_remove_error_format     (IdeBuildPipeline       *self,
+                                                                   guint                   error_format_id);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_pipeline_build_async             (IdeBuildPipeline       *self,
+                                                                   IdeBuildPhase           phase,
+                                                                   GCancellable           *cancellable,
+                                                                   GAsyncReadyCallback     callback,
+                                                                   gpointer                user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_build_finish            (IdeBuildPipeline       *self,
+                                                                   GAsyncResult           *result,
+                                                                   GError                **error);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_pipeline_build_targets_async     (IdeBuildPipeline       *self,
+                                                                   IdeBuildPhase           phase,
+                                                                   GPtrArray              *targets,
+                                                                   GCancellable           *cancellable,
+                                                                   GAsyncReadyCallback     callback,
+                                                                   gpointer                user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_build_targets_finish    (IdeBuildPipeline       *self,
+                                                                   GAsyncResult           *result,
+                                                                   GError                **error);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_pipeline_execute_async           (IdeBuildPipeline       *self,
+                                                                   GCancellable           *cancellable,
+                                                                   GAsyncReadyCallback     callback,
+                                                                   gpointer                user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_execute_finish          (IdeBuildPipeline       *self,
+                                                                   GAsyncResult           *result,
+                                                                   GError                **error);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_pipeline_foreach_stage           (IdeBuildPipeline       *self,
+                                                                   GFunc                   stage_callback,
+                                                                   gpointer                user_data);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_pipeline_clean_async             (IdeBuildPipeline       *self,
+                                                                   IdeBuildPhase           phase,
+                                                                   GCancellable           *cancellable,
+                                                                   GAsyncReadyCallback     callback,
+                                                                   gpointer                user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_clean_finish            (IdeBuildPipeline       *self,
+                                                                   GAsyncResult           *result,
+                                                                   GError                **error);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_pipeline_rebuild_async           (IdeBuildPipeline       *self,
+                                                                   IdeBuildPhase           phase,
+                                                                   GPtrArray              *targets,
+                                                                   GCancellable           *cancellable,
+                                                                   GAsyncReadyCallback     callback,
+                                                                   gpointer                user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_rebuild_finish          (IdeBuildPipeline       *self,
+                                                                   GAsyncResult           *result,
+                                                                   GError                **error);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_pipeline_attach_pty              (IdeBuildPipeline       *self,
+                                                                   IdeSubprocessLauncher  *launcher);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_pipeline_has_configured          (IdeBuildPipeline       *self);
+IDE_AVAILABLE_IN_3_32
+IdeBuildPhase          ide_build_pipeline_get_requested_phase     (IdeBuildPipeline       *self);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-private.h b/src/libide/foundry/ide-build-private.h
new file mode 100644
index 000000000..a9360e952
--- /dev/null
+++ b/src/libide/foundry/ide-build-private.h
@@ -0,0 +1,46 @@
+/* ide-build-private.h
+ *
+ * Copyright 2017-2019 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 <vte/vte.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+guint8 *_ide_build_utils_filter_color_codes (const guint8 *data,
+                                             gsize         len,
+                                             gsize        *out_len);
+void _ide_build_pipeline_cancel          (IdeBuildPipeline *self);
+void _ide_build_pipeline_set_runtime     (IdeBuildPipeline *self,
+                                          IdeRuntime       *runtime);
+void _ide_build_pipeline_set_toolchain   (IdeBuildPipeline *self,
+                                          IdeToolchain     *toolchain);
+void _ide_build_pipeline_set_message     (IdeBuildPipeline *self,
+                                          const gchar      *message);
+void _ide_build_pipeline_mark_broken     (IdeBuildPipeline *self);
+void _ide_build_pipeline_check_toolchain (IdeBuildPipeline *self,
+                                          IdeDeviceInfo    *info);
+void _ide_build_pipeline_set_pty_size    (IdeBuildPipeline *self,
+                                          guint             rows,
+                                          guint             columns);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-stage-launcher.c b/src/libide/foundry/ide-build-stage-launcher.c
new file mode 100644
index 000000000..05786617a
--- /dev/null
+++ b/src/libide/foundry/ide-build-stage-launcher.c
@@ -0,0 +1,635 @@
+/* ide-build-stage-launcher.c
+ *
+ * Copyright 2016-2019 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 "ide-build-stage-launcher"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-build-log.h"
+#include "ide-build-pipeline.h"
+#include "ide-build-stage-launcher.h"
+
+typedef struct
+{
+  IdeSubprocessLauncher *launcher;
+  IdeSubprocessLauncher *clean_launcher;
+  guint                  ignore_exit_status : 1;
+  guint                  use_pty : 1;
+} IdeBuildStageLauncherPrivate;
+
+enum {
+  PROP_0,
+  PROP_CLEAN_LAUNCHER,
+  PROP_USE_PTY,
+  PROP_IGNORE_EXIT_STATUS,
+  PROP_LAUNCHER,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeBuildStageLauncher, ide_build_stage_launcher, IDE_TYPE_BUILD_STAGE)
+
+static GParamSpec *properties [N_PROPS];
+
+static inline gboolean
+needs_quoting (const gchar *str)
+{
+  for (; *str; str = g_utf8_next_char (str))
+    {
+      gunichar ch = g_utf8_get_char (str);
+
+      switch (ch)
+        {
+        case '\'':
+        case '"':
+        case '\\':
+          return TRUE;
+
+        default:
+          if (g_unichar_isspace (ch))
+            return TRUE;
+          break;
+        }
+    }
+
+  return FALSE;
+}
+
+static gchar *
+pretty_print_args (IdeSubprocessLauncher *launcher)
+{
+  const gchar * const *argv;
+  g_autoptr(GString) command = NULL;
+
+  g_assert (IDE_IS_SUBPROCESS_LAUNCHER (launcher));
+
+  if (!(argv = ide_subprocess_launcher_get_argv (launcher)))
+    return NULL;
+
+  command = g_string_new (NULL);
+
+  for (guint i = 0; argv[i] != NULL; i++)
+    {
+      if (command->len > 0)
+        g_string_append_c (command, ' ');
+
+      if (needs_quoting (argv[i]))
+        {
+          g_autofree gchar *quoted = g_shell_quote (argv[i]);
+          g_string_append (command, quoted);
+        }
+      else
+        g_string_append (command, argv[i]);
+    }
+
+  return g_string_free (g_steal_pointer (&command), FALSE);
+}
+
+static void
+ide_build_stage_launcher_wait_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  IdeBuildStageLauncher *self = NULL;
+  IdeBuildStageLauncherPrivate *priv;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  gint exit_status;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SUBPROCESS (subprocess));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+
+  priv = ide_build_stage_launcher_get_instance_private (self);
+
+  IDE_TRACE_MSG ("  %s.ignore_exit_status=%u",
+                 G_OBJECT_TYPE_NAME (self),
+                 priv->ignore_exit_status);
+
+  if (!ide_subprocess_wait_finish (subprocess, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (ide_subprocess_get_if_signaled (subprocess))
+    {
+      ide_task_return_new_error (task,
+                                 G_SPAWN_ERROR,
+                                 G_SPAWN_ERROR_FAILED,
+                                 "The process was terminated by signal %d",
+                                 ide_subprocess_get_term_sig (subprocess));
+      IDE_EXIT;
+    }
+
+  exit_status = ide_subprocess_get_exit_status (subprocess);
+
+  if (priv->ignore_exit_status)
+    IDE_GOTO (ignore_exit_failures);
+
+  if (!g_spawn_check_exit_status (exit_status, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+ignore_exit_failures:
+  ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_stage_launcher_notify_completed_cb (IdeTask               *task,
+                                              GParamSpec            *pspec,
+                                              IdeBuildStageLauncher *launcher)
+{
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_BUILD_STAGE_LAUNCHER (launcher));
+
+  ide_build_stage_set_active (IDE_BUILD_STAGE (launcher), FALSE);
+}
+
+static void
+ide_build_stage_launcher_run (IdeBuildStage         *stage,
+                              IdeSubprocessLauncher *launcher,
+                              IdeBuildPipeline      *pipeline,
+                              GCancellable          *cancellable,
+                              GAsyncReadyCallback    callback,
+                              gpointer               user_data)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)stage;
+  G_GNUC_UNUSED IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  GSubprocessFlags flags;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!launcher || IDE_IS_SUBPROCESS_LAUNCHER (launcher));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_build_stage_launcher_run);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  g_signal_connect (task,
+                    "notify::completed",
+                    G_CALLBACK (ide_build_stage_launcher_notify_completed_cb),
+                    self);
+
+  ide_build_stage_set_active (IDE_BUILD_STAGE (self), TRUE);
+
+  if (launcher == NULL)
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  if (priv->use_pty)
+    {
+      ide_build_pipeline_attach_pty (pipeline, launcher);
+    }
+  else
+    {
+      flags = ide_subprocess_launcher_get_flags (launcher);
+
+      /* Disable flags we do not want set for build pipeline stuff */
+
+      if (flags & G_SUBPROCESS_FLAGS_STDERR_SILENCE)
+        flags &= ~G_SUBPROCESS_FLAGS_STDERR_SILENCE;
+
+      if (flags & G_SUBPROCESS_FLAGS_STDERR_MERGE)
+        flags &= ~G_SUBPROCESS_FLAGS_STDERR_MERGE;
+
+      if (flags & G_SUBPROCESS_FLAGS_STDIN_INHERIT)
+        flags &= ~G_SUBPROCESS_FLAGS_STDIN_INHERIT;
+
+      /* Ensure we have access to stdin/stdout streams */
+
+      flags |= G_SUBPROCESS_FLAGS_STDOUT_PIPE;
+      flags |= G_SUBPROCESS_FLAGS_STDERR_PIPE;
+
+      ide_subprocess_launcher_set_flags (launcher, flags);
+    }
+
+  if (priv->use_pty)
+    {
+      g_autofree gchar *command = pretty_print_args (launcher);
+
+      if (command != NULL)
+        ide_build_stage_log (IDE_BUILD_STAGE (self), IDE_BUILD_LOG_STDOUT, command, -1);
+    }
+
+  /* Now launch the process */
+
+  subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error);
+
+  if (subprocess == NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (!priv->use_pty)
+    ide_build_stage_log_subprocess (IDE_BUILD_STAGE (self), subprocess);
+
+  IDE_TRACE_MSG ("Waiting for process %s to complete, %s exit status",
+                 ide_subprocess_get_identifier (subprocess),
+                 priv->ignore_exit_status ? "ignoring" : "checking");
+
+  ide_subprocess_wait_async (subprocess,
+                             cancellable,
+                             ide_build_stage_launcher_wait_cb,
+                             g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_stage_launcher_execute_async (IdeBuildStage       *stage,
+                                        IdeBuildPipeline    *pipeline,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)stage;
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+
+  ide_build_stage_launcher_run (stage, priv->launcher, pipeline, cancellable, callback, user_data);
+}
+
+static gboolean
+ide_build_stage_launcher_execute_finish (IdeBuildStage  *stage,
+                                         GAsyncResult   *result,
+                                         GError        **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_LAUNCHER (stage));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_build_stage_launcher_clean_async (IdeBuildStage       *stage,
+                                      IdeBuildPipeline    *pipeline,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)stage;
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+
+  ide_build_stage_launcher_run (stage, priv->clean_launcher, pipeline, cancellable, callback, user_data);
+}
+
+static gboolean
+ide_build_stage_launcher_clean_finish (IdeBuildStage  *stage,
+                                       GAsyncResult   *result,
+                                       GError        **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_LAUNCHER (stage));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_build_stage_launcher_finalize (GObject *object)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)object;
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_clear_object (&priv->launcher);
+  g_clear_object (&priv->clean_launcher);
+
+  G_OBJECT_CLASS (ide_build_stage_launcher_parent_class)->finalize (object);
+}
+
+static void
+ide_build_stage_launcher_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)object;
+
+  switch (prop_id)
+    {
+    case PROP_CLEAN_LAUNCHER:
+      g_value_set_object (value, ide_build_stage_launcher_get_clean_launcher (self));
+      break;
+
+    case PROP_USE_PTY:
+      g_value_set_boolean (value, ide_build_stage_launcher_get_use_pty (self));
+      break;
+
+    case PROP_IGNORE_EXIT_STATUS:
+      g_value_set_boolean (value, ide_build_stage_launcher_get_ignore_exit_status (self));
+      break;
+
+    case PROP_LAUNCHER:
+      g_value_set_object (value, ide_build_stage_launcher_get_launcher (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_launcher_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  IdeBuildStageLauncher *self = (IdeBuildStageLauncher *)object;
+
+  switch (prop_id)
+    {
+    case PROP_CLEAN_LAUNCHER:
+      ide_build_stage_launcher_set_clean_launcher (self, g_value_get_object (value));
+      break;
+
+    case PROP_USE_PTY:
+      ide_build_stage_launcher_set_use_pty (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_IGNORE_EXIT_STATUS:
+      ide_build_stage_launcher_set_ignore_exit_status (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_LAUNCHER:
+      ide_build_stage_launcher_set_launcher (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_launcher_class_init (IdeBuildStageLauncherClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeBuildStageClass *build_stage_class = IDE_BUILD_STAGE_CLASS (klass);
+
+  object_class->finalize = ide_build_stage_launcher_finalize;
+  object_class->get_property = ide_build_stage_launcher_get_property;
+  object_class->set_property = ide_build_stage_launcher_set_property;
+
+  build_stage_class->execute_async = ide_build_stage_launcher_execute_async;
+  build_stage_class->execute_finish = ide_build_stage_launcher_execute_finish;
+  build_stage_class->clean_async = ide_build_stage_launcher_clean_async;
+  build_stage_class->clean_finish = ide_build_stage_launcher_clean_finish;
+
+  properties [PROP_CLEAN_LAUNCHER] =
+    g_param_spec_object ("clean-launcher",
+                         "Clean Launcher",
+                         "The subprocess launcher for cleaning",
+                         IDE_TYPE_SUBPROCESS_LAUNCHER,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_USE_PTY] =
+    g_param_spec_boolean ("use-pty",
+                          "Use Pty",
+                          "If the subprocess should have a Pty attached",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_IGNORE_EXIT_STATUS] =
+    g_param_spec_boolean ("ignore-exit-status",
+                          "Ignore Exit Status",
+                          "If the exit status of the subprocess should be ignored",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LAUNCHER] =
+    g_param_spec_object ("launcher",
+                         "Launcher",
+                         "The subprocess launcher to execute",
+                         IDE_TYPE_SUBPROCESS_LAUNCHER,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_build_stage_launcher_init (IdeBuildStageLauncher *self)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  priv->use_pty = TRUE;
+}
+
+/**
+ * ide_build_stage_launcher_get_launcher:
+ *
+ * Returns: (transfer none): An #IdeSubprocessLauncher
+ *
+ * Since: 3.32
+ */
+IdeSubprocessLauncher *
+ide_build_stage_launcher_get_launcher (IdeBuildStageLauncher *self)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self), NULL);
+
+  return priv->launcher;
+}
+
+void
+ide_build_stage_launcher_set_launcher (IdeBuildStageLauncher *self,
+                                       IdeSubprocessLauncher *launcher)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+  g_return_if_fail (!launcher || IDE_IS_SUBPROCESS_LAUNCHER (launcher));
+
+  if (g_set_object (&priv->launcher, launcher))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LAUNCHER]);
+}
+
+/**
+ * ide_build_stage_launcher_new:
+ * @context: An #IdeContext
+ * @launcher: (nullable): An #IdeSubprocessLauncher or %NULL
+ *
+ * Creates a new #IdeBuildStageLauncher that can be attached to an
+ * #IdeBuildPipeline.
+ *
+ * Returns: (transfer full): An #IdeBuildStageLauncher
+ *
+ * Since: 3.32
+ */
+IdeBuildStage *
+ide_build_stage_launcher_new (IdeContext            *context,
+                              IdeSubprocessLauncher *launcher)
+{
+  return g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
+                       "launcher", launcher,
+                       NULL);
+}
+
+/**
+ * ide_build_stage_launcher_get_ignore_exit_status:
+ *
+ * Gets the "ignore-exit-status" property.
+ *
+ * If set to %TRUE, a non-zero exit status from the subprocess will not cause
+ * the build stage to fail.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_stage_launcher_get_ignore_exit_status (IdeBuildStageLauncher *self)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self), FALSE);
+
+  return priv->ignore_exit_status;
+}
+
+/**
+ * ide_build_stage_launcher_set_ignore_exit_status:
+ *
+ * Sets the "ignore-exit-status" property.
+ *
+ * If set to %TRUE, a non-zero exit status from the subprocess will not cause
+ * the build stage to fail.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_stage_launcher_set_ignore_exit_status (IdeBuildStageLauncher *self,
+                                                 gboolean               ignore_exit_status)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+
+  ignore_exit_status = !!ignore_exit_status;
+
+  if (priv->ignore_exit_status != ignore_exit_status)
+    {
+      priv->ignore_exit_status = ignore_exit_status;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IGNORE_EXIT_STATUS]);
+      IDE_EXIT;
+    }
+
+  IDE_EXIT;
+}
+
+void
+ide_build_stage_launcher_set_clean_launcher (IdeBuildStageLauncher *self,
+                                             IdeSubprocessLauncher *clean_launcher)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (clean_launcher));
+
+  if (g_set_object (&priv->clean_launcher, clean_launcher))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLEAN_LAUNCHER]);
+}
+
+/**
+ * ide_build_stage_launcher_get_clean_launcher:
+ *
+ * Returns: (nullable) (transfer none): An #IdeSubprocessLauncher or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeSubprocessLauncher *
+ide_build_stage_launcher_get_clean_launcher (IdeBuildStageLauncher *self)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self), NULL);
+
+  return priv->clean_launcher;
+}
+
+gboolean
+ide_build_stage_launcher_get_use_pty (IdeBuildStageLauncher *self)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self), FALSE);
+
+  return priv->use_pty;
+}
+
+/**
+ * ide_build_stage_launcher_set_use_pty:
+ * @self: a #IdeBuildStageLauncher
+ * @use_pty: If a Pty should be used
+ *
+ * If @use_pty is set to %TRUE, a Pty will be attached to the process.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_stage_launcher_set_use_pty (IdeBuildStageLauncher *self,
+                                      gboolean               use_pty)
+{
+  IdeBuildStageLauncherPrivate *priv = ide_build_stage_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_LAUNCHER (self));
+
+  use_pty = !!use_pty;
+
+  if (use_pty != priv->use_pty)
+    {
+      priv->use_pty = use_pty;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_USE_PTY]);
+    }
+}
diff --git a/src/libide/foundry/ide-build-stage-launcher.h b/src/libide/foundry/ide-build-stage-launcher.h
new file mode 100644
index 000000000..e454343e7
--- /dev/null
+++ b/src/libide/foundry/ide-build-stage-launcher.h
@@ -0,0 +1,71 @@
+/* ide-build-stage-launcher.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#include "ide-build-stage.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_STAGE_LAUNCHER (ide_build_stage_launcher_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeBuildStageLauncher, ide_build_stage_launcher, IDE, BUILD_STAGE_LAUNCHER, 
IdeBuildStage)
+
+struct _IdeBuildStageLauncherClass
+{
+  IdeBuildStageClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeBuildStage         *ide_build_stage_launcher_new                    (IdeContext            *context,
+                                                                        IdeSubprocessLauncher *launcher);
+IDE_AVAILABLE_IN_3_32
+IdeSubprocessLauncher *ide_build_stage_launcher_get_launcher           (IdeBuildStageLauncher *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_stage_launcher_set_launcher           (IdeBuildStageLauncher *self,
+                                                                        IdeSubprocessLauncher *launcher);
+IDE_AVAILABLE_IN_3_32
+IdeSubprocessLauncher *ide_build_stage_launcher_get_clean_launcher     (IdeBuildStageLauncher *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_stage_launcher_set_clean_launcher     (IdeBuildStageLauncher *self,
+                                                                        IdeSubprocessLauncher 
*clean_launcher);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_stage_launcher_get_ignore_exit_status (IdeBuildStageLauncher *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_stage_launcher_set_ignore_exit_status (IdeBuildStageLauncher *self,
+                                                                        gboolean               
ignore_exit_status);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_build_stage_launcher_get_use_pty            (IdeBuildStageLauncher *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_build_stage_launcher_set_use_pty            (IdeBuildStageLauncher *self,
+                                                                        gboolean               use_pty);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-stage-mkdirs.c b/src/libide/foundry/ide-build-stage-mkdirs.c
new file mode 100644
index 000000000..900300c7b
--- /dev/null
+++ b/src/libide/foundry/ide-build-stage-mkdirs.c
@@ -0,0 +1,222 @@
+/* ide-build-stage-mkdirs.c
+ *
+ * Copyright 2016-2019 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 "ide-build-stage-mkdirs"
+
+#include "config.h"
+
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "ide-build-pipeline.h"
+#include "ide-build-stage-mkdirs.h"
+
+typedef struct
+{
+  gchar    *path;
+  gboolean  with_parents;
+  gint      mode;
+  guint     remove_on_rebuild : 1;
+} Path;
+
+typedef struct
+{
+  GArray *paths;
+} IdeBuildStageMkdirsPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeBuildStageMkdirs, ide_build_stage_mkdirs, IDE_TYPE_BUILD_STAGE)
+
+static void
+clear_path (gpointer data)
+{
+  Path *p = data;
+
+  g_clear_pointer (&p->path, g_free);
+}
+
+static void
+ide_build_stage_mkdirs_query (IdeBuildStage    *stage,
+                              IdeBuildPipeline *pipeline,
+                              GPtrArray        *targets,
+                              GCancellable     *cancellable)
+{
+  IdeBuildStageMkdirs *self = (IdeBuildStageMkdirs *)stage;
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_MKDIRS (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  for (guint i = 0; i < priv->paths->len; i++)
+    {
+      const Path *path = &g_array_index (priv->paths, Path, i);
+
+      if (!g_file_test (path->path, G_FILE_TEST_EXISTS))
+        {
+          ide_build_stage_set_completed (stage, FALSE);
+          IDE_EXIT;
+        }
+    }
+
+  ide_build_stage_set_completed (stage, TRUE);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_build_stage_mkdirs_execute (IdeBuildStage     *stage,
+                                IdeBuildPipeline  *pipeline,
+                                GCancellable      *cancellable,
+                                GError           **error)
+{
+  IdeBuildStageMkdirs *self = (IdeBuildStageMkdirs *)stage;
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_MKDIRS (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_build_stage_set_active (stage, TRUE);
+
+  for (guint i = 0; i < priv->paths->len; i++)
+    {
+      const Path *path = &g_array_index (priv->paths, Path, i);
+      g_autofree gchar *message = NULL;
+      gboolean r;
+
+      if (g_file_test (path->path, G_FILE_TEST_IS_DIR))
+        continue;
+
+      message = g_strdup_printf ("Creating directory “%s”", path->path);
+      ide_build_stage_log (IDE_BUILD_STAGE (stage), IDE_BUILD_LOG_STDOUT, message, -1);
+
+      if (path->with_parents)
+        r = g_mkdir_with_parents (path->path, path->mode);
+      else
+        r = g_mkdir (path->path, path->mode);
+
+      if (r != 0)
+        {
+          g_set_error_literal (error,
+                               G_FILE_ERROR,
+                               g_file_error_from_errno (errno),
+                               g_strerror (errno));
+          IDE_RETURN (FALSE);
+        }
+    }
+
+  ide_build_stage_set_active (stage, FALSE);
+
+  IDE_RETURN (TRUE);
+}
+
+static void
+ide_build_stage_mkdirs_reap (IdeBuildStage      *stage,
+                             DzlDirectoryReaper *reaper)
+{
+  IdeBuildStageMkdirs *self = (IdeBuildStageMkdirs *)stage;
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+
+  g_assert (IDE_IS_BUILD_STAGE_MKDIRS (self));
+  g_assert (DZL_IS_DIRECTORY_REAPER (reaper));
+
+  ide_build_stage_set_active (stage, TRUE);
+
+  for (guint i = 0; i < priv->paths->len; i++)
+    {
+      const Path *path = &g_array_index (priv->paths, Path, i);
+
+      if (path->remove_on_rebuild)
+        {
+          g_autoptr(GFile) file = g_file_new_for_path (path->path);
+          dzl_directory_reaper_add_directory (reaper, file, 0);
+        }
+    }
+
+  ide_build_stage_set_active (stage, FALSE);
+}
+
+static void
+ide_build_stage_mkdirs_finalize (GObject *object)
+{
+  IdeBuildStageMkdirs *self = (IdeBuildStageMkdirs *)object;
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+
+  g_clear_pointer (&priv->paths, g_array_unref);
+
+  G_OBJECT_CLASS (ide_build_stage_mkdirs_parent_class)->finalize (object);
+}
+
+static void
+ide_build_stage_mkdirs_class_init (IdeBuildStageMkdirsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeBuildStageClass *stage_class = IDE_BUILD_STAGE_CLASS (klass);
+
+  object_class->finalize = ide_build_stage_mkdirs_finalize;
+
+  stage_class->execute = ide_build_stage_mkdirs_execute;
+  stage_class->query = ide_build_stage_mkdirs_query;
+  stage_class->reap = ide_build_stage_mkdirs_reap;
+}
+
+static void
+ide_build_stage_mkdirs_init (IdeBuildStageMkdirs *self)
+{
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+
+  priv->paths = g_array_new (FALSE, FALSE, sizeof (Path));
+  g_array_set_clear_func (priv->paths, clear_path);
+}
+
+IdeBuildStage *
+ide_build_stage_mkdirs_new (IdeContext *context)
+{
+  return g_object_new (IDE_TYPE_BUILD_STAGE_MKDIRS,
+                       NULL);
+}
+
+void
+ide_build_stage_mkdirs_add_path (IdeBuildStageMkdirs *self,
+                                 const gchar         *path,
+                                 gboolean             with_parents,
+                                 gint                 mode,
+                                 gboolean             remove_on_rebuild)
+{
+  IdeBuildStageMkdirsPrivate *priv = ide_build_stage_mkdirs_get_instance_private (self);
+  Path ele = { 0 };
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE_MKDIRS (self));
+  g_return_if_fail (path != NULL);
+
+  ele.path = g_strdup (path);
+  ele.with_parents = with_parents;
+  ele.mode = mode;
+  ele.remove_on_rebuild = !!remove_on_rebuild;
+
+  g_array_append_val (priv->paths, ele);
+}
diff --git a/src/libide/foundry/ide-build-stage-mkdirs.h b/src/libide/foundry/ide-build-stage-mkdirs.h
new file mode 100644
index 000000000..c8bb3d6f4
--- /dev/null
+++ b/src/libide/foundry/ide-build-stage-mkdirs.h
@@ -0,0 +1,55 @@
+/* ide-build-stage-mkdirs.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-build-stage.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_STAGE_MKDIRS (ide_build_stage_mkdirs_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeBuildStageMkdirs, ide_build_stage_mkdirs, IDE, BUILD_STAGE_MKDIRS, 
IdeBuildStage)
+
+struct _IdeBuildStageMkdirsClass
+{
+  IdeBuildStageClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeBuildStage *ide_build_stage_mkdirs_new      (IdeContext          *context);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_mkdirs_add_path (IdeBuildStageMkdirs *self,
+                                                const gchar         *path,
+                                                gboolean             with_parents,
+                                                gint                 mode,
+                                                gboolean             remove_on_rebuild);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-stage-private.h b/src/libide/foundry/ide-build-stage-private.h
new file mode 100644
index 000000000..ec203c35e
--- /dev/null
+++ b/src/libide/foundry/ide-build-stage-private.h
@@ -0,0 +1,41 @@
+/* ide-build-stage-private.h
+ *
+ * Copyright 2016-2019 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 "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+gboolean      _ide_build_stage_has_query                 (IdeBuildStage        *self);
+IdeBuildPhase _ide_build_stage_get_phase                 (IdeBuildStage        *self);
+void          _ide_build_stage_set_phase                 (IdeBuildStage        *self,
+                                                          IdeBuildPhase         phase);
+void          _ide_build_stage_execute_with_query_async  (IdeBuildStage        *self,
+                                                          IdeBuildPipeline     *pipeline,
+                                                          GPtrArray            *targets,
+                                                          GCancellable         *cancellable,
+                                                          GAsyncReadyCallback   callback,
+                                                          gpointer              user_data);
+gboolean      _ide_build_stage_execute_with_query_finish (IdeBuildStage        *self,
+                                                          GAsyncResult         *result,
+                                                          GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-stage-transfer.c b/src/libide/foundry/ide-build-stage-transfer.c
new file mode 100644
index 000000000..c27e46b24
--- /dev/null
+++ b/src/libide/foundry/ide-build-stage-transfer.c
@@ -0,0 +1,270 @@
+/* ide-build-stage-transfer.c
+ *
+ * Copyright 2016-2019 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 "ide-build-stage-transfer"
+
+#include "config.h"
+
+#include <libide-threading.h>
+#include <glib/gi18n.h>
+
+#include "ide-build-stage-transfer.h"
+#include "ide-build-pipeline.h"
+#include "ide-transfer.h"
+
+struct _IdeBuildStageTransfer
+{
+  IdeBuildStage  parent_instance;
+  IdeTransfer   *transfer;
+  guint          disable_when_metered : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_TRANSFER,
+  PROP_DISABLE_WHEN_METERED,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeBuildStageTransfer, ide_build_stage_transfer, IDE_TYPE_BUILD_STAGE)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_build_stage_transfer_execute_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  IdeTransferManager *transfer_manager = (IdeTransferManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeNotification *notif;
+
+  g_assert (IDE_IS_TRANSFER_MANAGER (transfer_manager));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  notif = ide_task_get_task_data (task);
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  ide_notification_withdraw (notif);
+
+  if (!ide_transfer_manager_execute_finish (transfer_manager, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_build_stage_transfer_notify_completed_cb (IdeTask               *task,
+                                              GParamSpec            *pspec,
+                                              IdeBuildStageTransfer *transfer)
+{
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_BUILD_STAGE_TRANSFER (transfer));
+
+  ide_build_stage_set_active (IDE_BUILD_STAGE (transfer), FALSE);
+}
+
+static void
+ide_build_stage_transfer_execute_async (IdeBuildStage       *stage,
+                                        IdeBuildPipeline    *pipeline,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  IdeBuildStageTransfer *self = (IdeBuildStageTransfer *)stage;
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(IdeNotifications) notifs = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  const gchar *icon_name;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_STAGE_TRANSFER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_build_stage_transfer_execute_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  g_signal_connect (task,
+                    "notify::completed",
+                    G_CALLBACK (ide_build_stage_transfer_notify_completed_cb),
+                    self);
+
+  ide_build_stage_set_active (stage, TRUE);
+
+  if (ide_transfer_get_completed (self->transfer))
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  if (self->disable_when_metered)
+    {
+      GNetworkMonitor *monitor = g_network_monitor_get_default ();
+
+      if (g_network_monitor_get_network_metered (monitor))
+        {
+          g_autoptr(GSettings) settings = g_settings_new ("org.gnome.builder.build");
+
+          if (!g_settings_get_boolean (settings, "allow-network-when-metered"))
+            {
+              ide_task_return_new_error (task,
+                                         IDE_TRANSFER_ERROR,
+                                         IDE_TRANSFER_ERROR_CONNECTION_IS_METERED,
+                                         _("Cannot execute transfer while on metered connection"));
+              IDE_EXIT;
+            }
+        }
+    }
+
+  notif = ide_notification_new ();
+  ide_notification_set_has_progress (notif, TRUE);
+  g_object_bind_property (self->transfer, "title", notif, "title", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (self->transfer, "status", notif, "body", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (self->transfer, "progress", notif, "progress", G_BINDING_SYNC_CREATE);
+
+  if ((icon_name = ide_transfer_get_icon_name (self->transfer)))
+    {
+      g_autoptr(GIcon) icon = g_themed_icon_new (icon_name);
+      ide_notification_set_icon (notif, icon);
+    }
+
+  context = ide_object_ref_context (IDE_OBJECT (self));
+  notifs = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_NOTIFICATIONS);
+  ide_notifications_add_notification (notifs, notif);
+
+  ide_task_set_task_data (task, g_steal_pointer (&notif), g_object_unref);
+
+  ide_transfer_manager_execute_async (NULL,
+                                      self->transfer,
+                                      cancellable,
+                                      ide_build_stage_transfer_execute_cb,
+                                      g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_build_stage_transfer_execute_finish (IdeBuildStage  *stage,
+                                         GAsyncResult   *result,
+                                         GError        **error)
+{
+  g_assert (IDE_IS_BUILD_STAGE_TRANSFER (stage));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_build_stage_transfer_finalize (GObject *object)
+{
+  IdeBuildStageTransfer *self = (IdeBuildStageTransfer *)object;
+
+  g_clear_object (&self->transfer);
+
+  G_OBJECT_CLASS (ide_build_stage_transfer_parent_class)->finalize (object);
+}
+
+static void
+ide_build_stage_transfer_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  IdeBuildStageTransfer *self = (IdeBuildStageTransfer *)object;
+
+  switch (prop_id)
+    {
+    case PROP_DISABLE_WHEN_METERED:
+      g_value_set_boolean (value, self->disable_when_metered);
+      break;
+
+    case PROP_TRANSFER:
+      g_value_set_object (value, self->transfer);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_transfer_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  IdeBuildStageTransfer *self = (IdeBuildStageTransfer *)object;
+
+  switch (prop_id)
+    {
+    case PROP_DISABLE_WHEN_METERED:
+      self->disable_when_metered = g_value_get_boolean (value);
+      break;
+
+    case PROP_TRANSFER:
+      self->transfer = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_transfer_class_init (IdeBuildStageTransferClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeBuildStageClass *build_stage_class = IDE_BUILD_STAGE_CLASS (klass);
+
+  object_class->finalize = ide_build_stage_transfer_finalize;
+  object_class->get_property = ide_build_stage_transfer_get_property;
+  object_class->set_property = ide_build_stage_transfer_set_property;
+
+  build_stage_class->execute_async = ide_build_stage_transfer_execute_async;
+  build_stage_class->execute_finish = ide_build_stage_transfer_execute_finish;
+
+  properties [PROP_DISABLE_WHEN_METERED] =
+    g_param_spec_boolean ("disable-when-metered",
+                          "Disable when Metered",
+                          "If the transfer should fail when on a metered connection",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TRANSFER] =
+    g_param_spec_object ("transfer",
+                         "Transfer",
+                         "The transfer to perform as part of the stage",
+                         IDE_TYPE_TRANSFER,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_build_stage_transfer_init (IdeBuildStageTransfer *self)
+{
+  self->disable_when_metered = TRUE;
+}
diff --git a/src/libide/foundry/ide-build-stage-transfer.h b/src/libide/foundry/ide-build-stage-transfer.h
new file mode 100644
index 000000000..bb6ee2304
--- /dev/null
+++ b/src/libide/foundry/ide-build-stage-transfer.h
@@ -0,0 +1,42 @@
+/* ide-build-stage-transfer.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-build-stage.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_STAGE_TRANSFER (ide_build_stage_transfer_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeBuildStageTransfer, ide_build_stage_transfer, IDE, BUILD_STAGE_TRANSFER, 
IdeBuildStage)
+
+IDE_AVAILABLE_IN_3_32
+IdeBuildStageTransfer *ide_build_stage_transfer_new (IdeContext  *context,
+                                                     IdeTransfer *transfer);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-stage.c b/src/libide/foundry/ide-build-stage.c
new file mode 100644
index 000000000..872ca4c74
--- /dev/null
+++ b/src/libide/foundry/ide-build-stage.c
@@ -0,0 +1,1220 @@
+/* ide-build-stage.c
+ *
+ * Copyright 2016-2019 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 "ide-build-stage"
+
+#include "config.h"
+
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-build-pipeline.h"
+#include "ide-build-stage.h"
+#include "ide-build-stage-private.h"
+
+typedef struct
+{
+  gchar               *name;
+  IdeBuildLogObserver  observer;
+  gpointer             observer_data;
+  GDestroyNotify       observer_data_destroy;
+  IdeTask             *queued_execute;
+  gchar               *stdout_path;
+  GOutputStream       *stdout_stream;
+  gint                 n_pause;
+  IdeBuildPhase        phase;
+  guint                completed : 1;
+  guint                disabled : 1;
+  guint                transient : 1;
+  guint                check_stdout : 1;
+  guint                active : 1;
+} IdeBuildStagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeBuildStage, ide_build_stage, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_ACTIVE,
+  PROP_CHECK_STDOUT,
+  PROP_COMPLETED,
+  PROP_DISABLED,
+  PROP_NAME,
+  PROP_STDOUT_PATH,
+  PROP_TRANSIENT,
+  N_PROPS
+};
+
+enum {
+  CHAIN,
+  QUERY,
+  REAP,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+typedef struct
+{
+  IdeBuildStage     *self;
+  GOutputStream     *stream;
+  IdeBuildLogStream  stream_type;
+} Tail;
+
+static Tail *
+tail_new (IdeBuildStage     *self,
+          GOutputStream     *stream,
+          IdeBuildLogStream  stream_type)
+{
+  Tail *tail;
+
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (!stream || G_IS_OUTPUT_STREAM (stream));
+  g_assert (stream_type == IDE_BUILD_LOG_STDOUT || stream_type == IDE_BUILD_LOG_STDERR);
+
+  tail = g_slice_new0 (Tail);
+  tail->self = g_object_ref (self);
+  tail->stream = stream ? g_object_ref (stream) : NULL;
+  tail->stream_type = stream_type;
+
+  return tail;
+}
+
+static void
+tail_free (Tail *tail)
+{
+  IDE_ENTRY;
+
+  g_clear_object (&tail->self);
+  g_clear_object (&tail->stream);
+  g_slice_free (Tail, tail);
+
+  IDE_EXIT;
+}
+
+static void
+ide_build_stage_clear_observer (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+  GDestroyNotify notify = priv->observer_data_destroy;
+  gpointer data = priv->observer_data;
+
+  priv->observer_data_destroy = NULL;
+  priv->observer_data = NULL;
+  priv->observer = NULL;
+
+  if (notify != NULL)
+    notify (data);
+}
+
+static gboolean
+ide_build_stage_real_execute (IdeBuildStage     *self,
+                              IdeBuildPipeline  *pipeline,
+                              GCancellable      *cancellable,
+                              GError           **error)
+{
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  return TRUE;
+}
+
+static void
+ide_build_stage_real_execute_worker (IdeTask      *task,
+                                     gpointer      source_object,
+                                     gpointer      task_data,
+                                     GCancellable *cancellable)
+{
+  IdeBuildStage *self = source_object;
+  IdeBuildPipeline *pipeline = task_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (IDE_BUILD_STAGE_GET_CLASS (self)->execute (self, pipeline, cancellable, &error))
+    ide_task_return_boolean (task, TRUE);
+  else
+    ide_task_return_error (task, g_steal_pointer (&error));
+}
+
+static void
+ide_build_stage_real_execute_async (IdeBuildStage       *self,
+                                    IdeBuildPipeline    *pipeline,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_build_stage_real_execute_async);
+  ide_task_set_task_data (task, g_object_ref (pipeline), g_object_unref);
+  ide_task_run_in_thread (task, ide_build_stage_real_execute_worker);
+}
+
+static gboolean
+ide_build_stage_real_execute_finish (IdeBuildStage  *self,
+                                     GAsyncResult   *result,
+                                     GError        **error)
+{
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+const gchar *
+ide_build_stage_get_name (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), NULL);
+
+  return priv->name;
+}
+
+void
+ide_build_stage_set_name (IdeBuildStage *self,
+                          const gchar   *name)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  if (g_strcmp0 (name, priv->name) != 0)
+    {
+      g_free (priv->name);
+      priv->name = g_strdup (name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_NAME]);
+    }
+}
+
+static void
+ide_build_stage_real_clean_async (IdeBuildStage       *self,
+                                  IdeBuildPipeline    *pipeline,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_build_stage_real_clean_async);
+
+  ide_build_stage_set_completed (self, FALSE);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_build_stage_real_clean_finish (IdeBuildStage  *self,
+                                   GAsyncResult   *result,
+                                   GError        **error)
+{
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static gboolean
+ide_build_stage_real_chain (IdeBuildStage *self,
+                            IdeBuildStage *next)
+{
+  return FALSE;
+}
+
+static void
+ide_build_stage_finalize (GObject *object)
+{
+  IdeBuildStage *self = (IdeBuildStage *)object;
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  ide_build_stage_clear_observer (self);
+
+  g_clear_pointer (&priv->name, g_free);
+  g_clear_pointer (&priv->stdout_path, g_free);
+  g_clear_object (&priv->queued_execute);
+  g_clear_object (&priv->stdout_stream);
+
+  G_OBJECT_CLASS (ide_build_stage_parent_class)->finalize (object);
+}
+
+static void
+ide_build_stage_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeBuildStage *self = IDE_BUILD_STAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVE:
+      g_value_set_boolean (value, ide_build_stage_get_active (self));
+      break;
+
+    case PROP_CHECK_STDOUT:
+      g_value_set_boolean (value, ide_build_stage_get_check_stdout (self));
+      break;
+
+    case PROP_COMPLETED:
+      g_value_set_boolean (value, ide_build_stage_get_completed (self));
+      break;
+
+    case PROP_DISABLED:
+      g_value_set_boolean (value, ide_build_stage_get_disabled (self));
+      break;
+
+    case PROP_NAME:
+      g_value_set_string (value, ide_build_stage_get_name (self));
+      break;
+
+    case PROP_STDOUT_PATH:
+      g_value_set_string (value, ide_build_stage_get_stdout_path (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeBuildStage *self = IDE_BUILD_STAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACTIVE:
+      ide_build_stage_set_active (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_CHECK_STDOUT:
+      ide_build_stage_set_check_stdout (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_COMPLETED:
+      ide_build_stage_set_completed (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_DISABLED:
+      ide_build_stage_set_disabled (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_NAME:
+      ide_build_stage_set_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_STDOUT_PATH:
+      ide_build_stage_set_stdout_path (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_build_stage_class_init (IdeBuildStageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_build_stage_finalize;
+  object_class->get_property = ide_build_stage_get_property;
+  object_class->set_property = ide_build_stage_set_property;
+
+  klass->execute = ide_build_stage_real_execute;
+  klass->execute_async = ide_build_stage_real_execute_async;
+  klass->execute_finish = ide_build_stage_real_execute_finish;
+  klass->clean_async = ide_build_stage_real_clean_async;
+  klass->clean_finish = ide_build_stage_real_clean_finish;
+  klass->chain = ide_build_stage_real_chain;
+
+  /**
+   * IdeBuildStage:active:
+   *
+   * This property is set to %TRUE when the build stage is actively
+   * running or cleaning.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ACTIVE] =
+    g_param_spec_boolean ("active",
+                          "Active",
+                          "If the stage is actively running",
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeBuildStage:check-stdout:
+   *
+   * Most build systems will preserve stderr for the processes they call, such
+   * as gcc, clang, and others. However, if your build system redirects all
+   * output to stdout, you may need to set this property to %TRUE to ensure
+   * that Builder will extract errors from stdout.
+   *
+   * One such example is Ninja.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CHECK_STDOUT] =
+    g_param_spec_boolean ("check-stdout",
+                         "Check STDOUT",
+                         "If STDOUT should be checked for errors using error regexes",
+                         FALSE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildStage:completed:
+   *
+   * The "completed" property is set to %TRUE after the pipeline has
+   * completed processing the stage. When the pipeline invalidates
+   * phases, completed may be reset to %FALSE.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_COMPLETED] =
+    g_param_spec_boolean ("completed",
+                          "Completed",
+                          "If the stage has been completed",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildStage:disabled:
+   *
+   * If the build stage is disabled. This allows you to have a stage that is
+   * attached but will not be activated during execution.
+   *
+   * You may enable it later and then re-execute the pipeline.
+   *
+   * If the stage is both transient and disabled, it will not be removed during
+   * the transient cleanup phase.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_DISABLED] =
+    g_param_spec_boolean ("disabled",
+                          "Disabled",
+                          "If the stage has been disabled",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildStage:name:
+   *
+   * The name of the build stage. This is only used by UI to view
+   * the build pipeline.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "The user visible name of the stage",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildStage:stdout-path:
+   *
+   * The "stdout-path" property allows a build stage to redirect its log
+   * messages to a stdout file. Instead of passing stdout along to the
+   * build pipeline, they will be redirected to this file.
+   *
+   * For safety reasons, the contents are first redirected to a temporary
+   * file and will be redirected to the stdout-path location after the
+   * build stage has completed executing.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_STDOUT_PATH] =
+    g_param_spec_string ("stdout-path",
+                         "Stdout Path",
+                         "Redirect standard output to this path",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeBuildStage:transient:
+   *
+   * If the build stage is transient.
+   *
+   * A transient build stage is removed after the completion of
+   * ide_build_pipeline_execute_async(). This can be a convenient
+   * way to add a temporary item to a build pipeline that should
+   * be immediately discarded.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_TRANSIENT] =
+    g_param_spec_boolean ("transient",
+                          "Transient",
+                          "If the stage should be removed after execution",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeBuildStage:chain:
+   *
+   * We might want to be able to "chain" multiple stages into a single stage
+   * so that we can avoid duplicate work. For example, if we have a "make"
+   * stage immediately follwed by a "make install" stage, it does not make
+   * sense to perform them both individually.
+   *
+   * Returns: %TRUE if @next's work was chained into @self for the next
+   *    execution of the pipeline.
+   *
+   * Since: 3.32
+   */
+  signals [CHAIN] =
+    g_signal_new ("chain",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeBuildStageClass, chain),
+                  g_signal_accumulator_true_handled,
+                  NULL,
+                  NULL,
+                  G_TYPE_BOOLEAN, 1, IDE_TYPE_BUILD_STAGE);
+
+  /**
+   * IdeBuildStage::query:
+   * @self: An #IdeBuildStage
+   * @pipeline: An #IdeBuildPipeline
+   * @targets: (element-type IdeBuildTarget) (nullable): an array
+   *   of #IdeBuildTarget or %NULL
+   * @cancellable: (nullable): a #GCancellable or %NULL
+   *
+   * The #IdeBuildStage::query signal is emitted to request that the
+   * build stage update its completed stage from any external resources.
+   *
+   * This can be useful if you want to use an existing build stage instances
+   * and use a signal to pause forward progress until an external system
+   * has been checked.
+   *
+   * The targets that the user would like to ensure are built are provided
+   * as @targets. Some #IdeBuildStage may use this to reduce the amount
+   * of work they perform
+   *
+   * For example, in a signal handler, you may call ide_build_stage_pause()
+   * and perform an external operation. Forward progress of the stage will
+   * be paused until a matching number of ide_build_stage_unpause() calls
+   * have been made.
+   *
+   * Since: 3.32
+   */
+  signals [QUERY] =
+    g_signal_new ("query",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeBuildStageClass, query),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  3,
+                  IDE_TYPE_BUILD_PIPELINE,
+                  G_TYPE_PTR_ARRAY,
+                  G_TYPE_CANCELLABLE);
+
+  /**
+   * IdeBuildStage::reap:
+   * @self: An #IdeBuildStage
+   * @reaper: An #DzlDirectoryReaper
+   *
+   * This signal is emitted when a request to rebuild the project has
+   * occurred. This allows build stages to ensure that certain files are
+   * removed from the system. For example, an autotools build stage might
+   * request that "configure" is removed so that autogen.sh will be executed
+   * as part of the next build.
+   *
+   * Since: 3.32
+   */
+  signals [REAP] =
+    g_signal_new ("reap",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeBuildStageClass, reap),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 1, DZL_TYPE_DIRECTORY_REAPER);
+}
+
+static void
+ide_build_stage_init (IdeBuildStage *self)
+{
+}
+
+void
+ide_build_stage_execute_async (IdeBuildStage       *self,
+                               IdeBuildPipeline    *pipeline,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if G_UNLIKELY (priv->stdout_path != NULL)
+    {
+      g_autoptr(GFileOutputStream) stream = NULL;
+      g_autoptr(GFile) file = NULL;
+      g_autoptr(GError) error = NULL;
+
+      file = g_file_new_for_path (priv->stdout_path);
+      stream = g_file_replace (file, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, cancellable, &error);
+
+      if (stream == NULL)
+        {
+          g_task_report_error (self, callback, user_data,
+                               ide_build_stage_execute_async,
+                               g_steal_pointer (&error));
+          return;
+        }
+
+      g_clear_object (&priv->stdout_stream);
+
+      priv->stdout_stream = G_OUTPUT_STREAM (g_steal_pointer (&stream));
+    }
+
+  IDE_BUILD_STAGE_GET_CLASS (self)->execute_async (self, pipeline, cancellable, callback, user_data);
+}
+
+gboolean
+ide_build_stage_execute_finish (IdeBuildStage  *self,
+                                GAsyncResult   *result,
+                                GError        **error)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  /*
+   * If for some reason execute_finish() is not called (likely due to use of
+   * the build stage without a pipeline, so sort of a programming error) then
+   * we won't clean up the stdout stream. But it gets cleaned up in finalize
+   * anyway, so its safe (if only delayed rename()).
+   *
+   * We can just unref the stream, and the close will happen silently. We need
+   * to do this as some async reads to be proxied to the stream may occur after
+   * the execute_finish() completes.
+   *
+   * The Tail structure has it's own reference to stdout_stream.
+   */
+  g_clear_object (&priv->stdout_stream);
+
+  return IDE_BUILD_STAGE_GET_CLASS (self)->execute_finish (self, result, error);
+}
+
+/**
+ * ide_build_stage_set_log_observer:
+ * @self: An #IdeBuildStage
+ * @observer: (scope async): The observer for the log entries
+ * @observer_data: data for @observer
+ * @observer_data_destroy: destroy callback for @observer_data
+ *
+ * Sets the log observer to handle calls to the various stage logging
+ * functions. This will be set by the pipeline to mux logs from all
+ * stages into a unified build log.
+ *
+ * Plugins that need to handle logging from a build stage should set
+ * an observer on the pipeline so that log distribution may be fanned
+ * out to all observers.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_stage_set_log_observer (IdeBuildStage       *self,
+                                  IdeBuildLogObserver  observer,
+                                  gpointer             observer_data,
+                                  GDestroyNotify       observer_data_destroy)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  ide_build_stage_clear_observer (self);
+
+  priv->observer = observer;
+  priv->observer_data = observer_data;
+  priv->observer_data_destroy = observer_data_destroy;
+}
+
+static void
+ide_build_stage_log_internal (IdeBuildStage     *self,
+                              IdeBuildLogStream  stream_type,
+                              GOutputStream     *stream,
+                              const gchar       *message,
+                              gssize             message_len)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  /*
+   * If we are logging to a file instead of the build pipeline, handle that
+   * specially now and then exit without calling the observer.
+   */
+  if (stream != NULL)
+    {
+      gsize count;
+
+      if G_UNLIKELY (message_len < 0)
+        message_len = strlen (message);
+
+      g_output_stream_write_all (stream, message, message_len, &count, NULL, NULL);
+      g_output_stream_write_all (stream, "\n", 1, &count, NULL, NULL);
+
+      return;
+    }
+
+  if G_LIKELY (priv->observer != NULL)
+    priv->observer (stream_type, message, message_len, priv->observer_data);
+}
+
+void
+ide_build_stage_log (IdeBuildStage     *self,
+                     IdeBuildLogStream  stream_type,
+                     const gchar       *message,
+                     gssize             message_len)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  if (stream_type == IDE_BUILD_LOG_STDOUT)
+    ide_build_stage_log_internal (self, stream_type, priv->stdout_stream, message, message_len);
+  else
+    ide_build_stage_log_internal (self, stream_type, NULL, message, message_len);
+}
+
+gboolean
+ide_build_stage_get_completed (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+
+  return priv->completed;
+}
+
+void
+ide_build_stage_set_completed (IdeBuildStage *self,
+                               gboolean       completed)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  completed = !!completed;
+
+  if (completed != priv->completed)
+    {
+      priv->completed = completed;
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_COMPLETED]);
+    }
+}
+
+void
+ide_build_stage_set_transient (IdeBuildStage *self,
+                               gboolean       transient)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  transient = !!transient;
+
+  if (priv->transient != transient)
+    {
+      priv->transient = transient;
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TRANSIENT]);
+    }
+}
+
+gboolean
+ide_build_stage_get_transient (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+
+  return priv->transient;
+}
+
+static void
+ide_build_stage_observe_stream_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  GDataInputStream *stream = (GDataInputStream *)object;
+  g_autofree gchar *line = NULL;
+  g_autoptr(GError) error = NULL;
+  Tail *tail = user_data;
+  gsize n_read = 0;
+
+  g_assert (G_IS_DATA_INPUT_STREAM (stream));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (tail != NULL);
+
+  line = g_data_input_stream_read_line_finish_utf8 (stream, result, &n_read, &error);
+
+  if (error == NULL)
+    {
+      if (line == NULL)
+        goto cleanup;
+
+      ide_build_stage_log_internal (tail->self, tail->stream_type, tail->stream, line, (gssize)n_read);
+
+      if G_UNLIKELY (g_input_stream_is_closed (G_INPUT_STREAM (stream)))
+        goto cleanup;
+
+      g_data_input_stream_read_line_async (stream,
+                                           G_PRIORITY_DEFAULT,
+                                           NULL,
+                                           ide_build_stage_observe_stream_cb,
+                                           tail);
+
+      return;
+    }
+
+  g_debug ("%s", error->message);
+
+cleanup:
+  tail_free (tail);
+}
+
+
+static void
+ide_build_stage_observe_stream (IdeBuildStage     *self,
+                                IdeBuildLogStream  stream_type,
+                                GInputStream      *stream)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+  g_autoptr(GDataInputStream) data_stream = NULL;
+  Tail *tail;
+
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (stream_type == IDE_BUILD_LOG_STDOUT || stream_type == IDE_BUILD_LOG_STDERR);
+  g_assert (G_IS_INPUT_STREAM (stream));
+
+  if (G_IS_DATA_INPUT_STREAM (stream))
+    data_stream = g_object_ref (G_DATA_INPUT_STREAM (stream));
+  else
+    data_stream = g_data_input_stream_new (stream);
+
+  IDE_TRACE_MSG ("Logging subprocess stream of type %s as %s",
+                 G_OBJECT_TYPE_NAME (data_stream),
+                 stream_type == IDE_BUILD_LOG_STDOUT ? "stdout" : "stderr");
+
+  if (stream_type == IDE_BUILD_LOG_STDOUT)
+    tail = tail_new (self, priv->stdout_stream, stream_type);
+  else
+    tail = tail_new (self, NULL, stream_type);
+
+  g_data_input_stream_read_line_async (data_stream,
+                                       G_PRIORITY_DEFAULT,
+                                       NULL,
+                                       ide_build_stage_observe_stream_cb,
+                                       tail);
+}
+
+/**
+ * ide_build_stage_log_subprocess:
+ * @self: An #IdeBuildStage
+ * @subprocess: An #IdeSubprocess
+ *
+ * This function will begin logging @subprocess by reading from the
+ * stdout and stderr streams of the subprocess. You must have created
+ * the subprocess with %G_SUBPROCESS_FLAGS_STDERR_PIPE and
+ * %G_SUBPROCESS_FLAGS_STDOUT_PIPE so that the streams may be read.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_stage_log_subprocess (IdeBuildStage *self,
+                                IdeSubprocess *subprocess)
+{
+  GInputStream *stdout_stream;
+  GInputStream *stderr_stream;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (IDE_IS_SUBPROCESS (subprocess));
+
+  stderr_stream = ide_subprocess_get_stderr_pipe (subprocess);
+  stdout_stream = ide_subprocess_get_stdout_pipe (subprocess);
+
+  if (stderr_stream != NULL)
+    ide_build_stage_observe_stream (self, IDE_BUILD_LOG_STDERR, stderr_stream);
+
+  if (stdout_stream != NULL)
+    ide_build_stage_observe_stream (self, IDE_BUILD_LOG_STDOUT, stdout_stream);
+
+  IDE_EXIT;
+}
+
+void
+ide_build_stage_pause (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  g_atomic_int_inc (&priv->n_pause);
+}
+
+static void
+ide_build_stage_unpause_execute_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  IdeBuildStage *self = (IdeBuildStage *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_BUILD_STAGE (self));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_build_stage_execute_finish (self, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+void
+ide_build_stage_unpause (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (priv->n_pause > 0);
+
+  if (g_atomic_int_dec_and_test (&priv->n_pause) && priv->queued_execute != NULL)
+    {
+      g_autoptr(IdeTask) task = g_steal_pointer (&priv->queued_execute);
+      GCancellable *cancellable = ide_task_get_cancellable (task);
+      IdeBuildPipeline *pipeline = ide_task_get_task_data (task);
+
+      g_assert (IDE_IS_TASK (task));
+      g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+      g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+      if (priv->completed)
+        {
+          ide_task_return_boolean (task, TRUE);
+          return;
+        }
+
+      ide_build_stage_execute_async (self,
+                                     pipeline,
+                                     cancellable,
+                                     ide_build_stage_unpause_execute_cb,
+                                     g_steal_pointer (&task));
+    }
+}
+
+/**
+ * _ide_build_stage_execute_with_query_async: (skip)
+ *
+ * This function is used to execute the build stage after emitting the
+ * query signal. If the stage is paused after the query, execute will
+ * be delayed until the correct number of ide_build_stage_unpause() calls
+ * have occurred.
+ *
+ * Since: 3.32
+ */
+void
+_ide_build_stage_execute_with_query_async (IdeBuildStage       *self,
+                                           IdeBuildPipeline    *pipeline,
+                                           GPtrArray           *targets,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+  g_autoptr(GPtrArray) local_targets = NULL;
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, _ide_build_stage_execute_with_query_async);
+  ide_task_set_task_data (task, g_object_ref (pipeline), g_object_unref);
+
+  if (targets == NULL)
+    targets = local_targets = g_ptr_array_new_with_free_func (g_object_unref);
+
+  if (priv->queued_execute != NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_PENDING,
+                                 "A build is already in progress");
+      return;
+    }
+
+  priv->queued_execute = g_steal_pointer (&task);
+
+  /*
+   * Pause the pipeline around our query call so that any call to
+   * pause/unpause does not cause the stage to make progress. This allows
+   * us to share the code-path to make progress on the build stage.
+   */
+  ide_build_stage_pause (self);
+  g_signal_emit (self, signals [QUERY], 0, pipeline, targets, cancellable);
+  ide_build_stage_unpause (self);
+}
+
+gboolean
+_ide_build_stage_execute_with_query_finish (IdeBuildStage  *self,
+                                            GAsyncResult   *result,
+                                            GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+void
+ide_build_stage_set_stdout_path (IdeBuildStage *self,
+                                 const gchar   *stdout_path)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  if (g_strcmp0 (stdout_path, priv->stdout_path) != 0)
+    {
+      g_free (priv->stdout_path);
+      priv->stdout_path = g_strdup (stdout_path);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_STDOUT_PATH]);
+    }
+}
+
+const gchar *
+ide_build_stage_get_stdout_path (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), NULL);
+
+  return priv->stdout_path;
+}
+
+gboolean
+_ide_build_stage_has_query (IdeBuildStage *self)
+{
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+
+  if (g_signal_has_handler_pending (self, signals [QUERY], 0, FALSE))
+    IDE_RETURN (TRUE);
+
+  if (IDE_BUILD_STAGE_GET_CLASS (self)->query)
+    IDE_RETURN (TRUE);
+
+  IDE_RETURN (FALSE);
+}
+
+void
+ide_build_stage_clean_async (IdeBuildStage       *self,
+                             IdeBuildPipeline    *pipeline,
+                             GCancellable        *cancellable,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_BUILD_STAGE_GET_CLASS (self)->clean_async (self, pipeline, cancellable, callback, user_data);
+}
+
+gboolean
+ide_build_stage_clean_finish (IdeBuildStage  *self,
+                              GAsyncResult   *result,
+                              GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return IDE_BUILD_STAGE_GET_CLASS (self)->clean_finish (self, result, error);
+}
+
+void
+ide_build_stage_emit_reap (IdeBuildStage      *self,
+                           DzlDirectoryReaper *reaper)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+  g_return_if_fail (DZL_IS_DIRECTORY_REAPER (reaper));
+
+  g_signal_emit (self, signals [REAP], 0, reaper);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_build_stage_chain (IdeBuildStage *self,
+                       IdeBuildStage *next)
+{
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (next), FALSE);
+
+  if (ide_build_stage_get_disabled (next))
+    return FALSE;
+
+  g_signal_emit (self, signals[CHAIN], 0, next, &ret);
+
+  return ret;
+}
+
+gboolean
+ide_build_stage_get_disabled (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+
+  return priv->disabled;
+}
+
+void
+ide_build_stage_set_disabled (IdeBuildStage *self,
+                              gboolean       disabled)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  disabled = !!disabled;
+
+  if (priv->disabled != disabled)
+    {
+      priv->disabled = disabled;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISABLED]);
+    }
+}
+
+gboolean
+ide_build_stage_get_check_stdout (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+
+  return priv->check_stdout;
+}
+
+void
+ide_build_stage_set_check_stdout (IdeBuildStage *self,
+                                  gboolean       check_stdout)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  check_stdout = !!check_stdout;
+
+  if (check_stdout != priv->check_stdout)
+    {
+      priv->check_stdout = check_stdout;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHECK_STDOUT]);
+    }
+}
+
+/**
+ * ide_build_stage_get_active:
+ * @self: a #IdeBuildStage
+ *
+ * Gets the "active" property, which is set to %TRUE when the
+ * build stage is actively executing or cleaning.
+ *
+ * Returns: %TRUE if the stage is actively executing or cleaning.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_stage_get_active (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), FALSE);
+
+  return priv->active;
+}
+
+void
+ide_build_stage_set_active (IdeBuildStage *self,
+                            gboolean       active)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  active = !!active;
+
+  if (priv->active != active)
+    {
+      priv->active = active;
+      ide_object_notify_in_main (IDE_OBJECT (self), properties [PROP_ACTIVE]);
+    }
+}
+
+IdeBuildPhase
+_ide_build_stage_get_phase (IdeBuildStage *self)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (self), 0);
+
+  return priv->phase;
+}
+
+void
+_ide_build_stage_set_phase (IdeBuildStage *self,
+                            IdeBuildPhase  phase)
+{
+  IdeBuildStagePrivate *priv = ide_build_stage_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_BUILD_STAGE (self));
+
+  priv->phase = phase;
+}
diff --git a/src/libide/foundry/ide-build-stage.h b/src/libide/foundry/ide-build-stage.h
new file mode 100644
index 000000000..1dbb2818c
--- /dev/null
+++ b/src/libide/foundry/ide-build-stage.h
@@ -0,0 +1,215 @@
+/* ide-build-stage.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+#include "ide-build-log.h"
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_STAGE (ide_build_stage_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeBuildStage, ide_build_stage, IDE, BUILD_STAGE, IdeObject)
+
+struct _IdeBuildStageClass
+{
+  IdeObjectClass parent_class;
+
+  /**
+   * IdeBuildStage::execute:
+   *
+   * This vfunc will be run in a thread by the default
+   * IdeBuildStage::execute_async() and IdeBuildStage::execute_finish()
+   * vfuncs.
+   *
+   * Only use thread-safe API from this function.
+   *
+   * Since: 3.32
+   */
+  gboolean (*execute)        (IdeBuildStage        *self,
+                              IdeBuildPipeline     *pipeline,
+                              GCancellable         *cancellable,
+                              GError              **error);
+
+  /**
+   * IdeBuildStage::execute_async:
+   *
+   * Asynchronous version of the #IdeBuildStage API. This is the preferred
+   * way to subclass #IdeBuildStage.
+   *
+   * Since: 3.32
+   */
+  void     (*execute_async)  (IdeBuildStage        *self,
+                              IdeBuildPipeline     *pipeline,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+
+  /**
+   * IdeBuildStage::execute_finish:
+   *
+   * Completes an asynchronous call to ide_build_stage_execute_async().
+   *
+   * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+   *   Upon failure, the pipeline will be stopped.
+   *
+   * Since: 3.32
+   */
+  gboolean (*execute_finish) (IdeBuildStage        *self,
+                              GAsyncResult         *result,
+                              GError              **error);
+
+  /**
+   * IdeBuildStage::clean_async:
+   * @self: an #IdeBuildStage
+   * @pipeline: An #IdeBuildPipeline
+   * @cancellable: (nullable): a #GCancellable or %NULL
+   * @callback: An async callback
+   * @user_data: user data for @callback
+   *
+   * This function will perform the clean operation.
+   *
+   * Since: 3.32
+   */
+  void     (*clean_async)    (IdeBuildStage        *self,
+                              IdeBuildPipeline     *pipeline,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+
+  /**
+   * IdeBuildStage::clean_finish:
+   * @self: an #IdeBuildStage
+   * @result: a #GErrorResult
+   * @error: A location for a #GError or %NULL.
+   *
+   * Completes an async operation to ide_build_stage_clean_async().
+   *
+   * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+   *
+   * Since: 3.32
+   */
+  gboolean (*clean_finish)   (IdeBuildStage        *self,
+                              GAsyncResult         *result,
+                              GError              **error);
+
+  /* Signals */
+  void     (*query)          (IdeBuildStage        *self,
+                              IdeBuildPipeline     *pipeline,
+                              GPtrArray            *targets,
+                              GCancellable         *cancellable);
+  void     (*reap)           (IdeBuildStage        *self,
+                              DzlDirectoryReaper   *reaper);
+  gboolean (*chain)          (IdeBuildStage        *self,
+                              IdeBuildStage        *next);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_build_stage_get_active       (IdeBuildStage        *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_set_active       (IdeBuildStage        *self,
+                                                 gboolean              active);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_build_stage_get_name         (IdeBuildStage        *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_set_name         (IdeBuildStage        *self,
+                                                 const gchar          *name);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_log              (IdeBuildStage        *self,
+                                                 IdeBuildLogStream     stream,
+                                                 const gchar          *message,
+                                                 gssize                message_len);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_log_subprocess   (IdeBuildStage        *self,
+                                                 IdeSubprocess        *subprocess);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_set_log_observer (IdeBuildStage        *self,
+                                                 IdeBuildLogObserver   observer,
+                                                 gpointer              observer_data,
+                                                 GDestroyNotify        observer_data_destroy);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_set_stdout_path  (IdeBuildStage        *self,
+                                                 const gchar          *path);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_build_stage_get_stdout_path  (IdeBuildStage        *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_build_stage_get_completed    (IdeBuildStage        *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_set_completed    (IdeBuildStage        *self,
+                                                 gboolean              completed);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_build_stage_get_disabled     (IdeBuildStage        *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_set_disabled     (IdeBuildStage        *self,
+                                                 gboolean              disabled);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_build_stage_get_check_stdout (IdeBuildStage        *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_set_check_stdout (IdeBuildStage        *self,
+                                                 gboolean              check_stdout);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_build_stage_get_transient    (IdeBuildStage        *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_set_transient    (IdeBuildStage        *self,
+                                                 gboolean              transient);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_execute_async    (IdeBuildStage        *self,
+                                                 IdeBuildPipeline     *pipeline,
+                                                 GCancellable         *cancellable,
+                                                 GAsyncReadyCallback   callback,
+                                                 gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_build_stage_execute_finish   (IdeBuildStage        *self,
+                                                 GAsyncResult         *result,
+                                                 GError              **error);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_clean_async      (IdeBuildStage        *self,
+                                                 IdeBuildPipeline     *pipeline,
+                                                 GCancellable         *cancellable,
+                                                 GAsyncReadyCallback   callback,
+                                                 gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_build_stage_clean_finish     (IdeBuildStage        *self,
+                                                 GAsyncResult         *result,
+                                                 GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_build_stage_chain            (IdeBuildStage        *self,
+                                                 IdeBuildStage        *next);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_pause            (IdeBuildStage        *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_unpause          (IdeBuildStage        *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_build_stage_emit_reap        (IdeBuildStage        *self,
+                                                 DzlDirectoryReaper   *reaper);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-system-discovery.c b/src/libide/foundry/ide-build-system-discovery.c
new file mode 100644
index 000000000..63477135e
--- /dev/null
+++ b/src/libide/foundry/ide-build-system-discovery.c
@@ -0,0 +1,75 @@
+/* ide-build-system-discovery.c
+ *
+ * Copyright 2017-2019 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 "ide-build-system-discovery"
+
+#include "config.h"
+
+#include "ide-build-system-discovery.h"
+
+G_DEFINE_INTERFACE (IdeBuildSystemDiscovery, ide_build_system_discovery, G_TYPE_OBJECT)
+
+static void
+ide_build_system_discovery_default_init (IdeBuildSystemDiscoveryInterface *iface)
+{
+}
+
+/**
+ * ide_build_system_discovery_discover:
+ * @self: An #IdeBuildSystemDiscovery
+ * @project_file: a #GFile containing the project file (a directory)
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @priority: (out): A location for the priority
+ * @error: a location for a #GError or %NULL
+ *
+ * This virtual method can be used to try to discover the build system to use for
+ * a particular project. This might be used in cases like Flatpak where the build
+ * system can be determined from the .json manifest rather than auto-discovery
+ * by locating project files.
+ *
+ * Returns: (transfer full): The hint for the build system, which should match what
+ *   the build system returns from ide_build_system_get_id().
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_build_system_discovery_discover (IdeBuildSystemDiscovery  *self,
+                                     GFile                    *project_file,
+                                     GCancellable             *cancellable,
+                                     gint                     *priority,
+                                     GError                  **error)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM_DISCOVERY (self), NULL);
+  g_return_val_if_fail (G_IS_FILE (project_file), NULL);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL);
+
+  if (priority != NULL)
+    *priority = G_MAXINT;
+
+  if (IDE_BUILD_SYSTEM_DISCOVERY_GET_IFACE (self)->discover)
+    return IDE_BUILD_SYSTEM_DISCOVERY_GET_IFACE (self)->discover (self, project_file, cancellable, priority, 
error);
+
+  g_set_error (error,
+               G_IO_ERROR,
+               G_IO_ERROR_NOT_SUPPORTED,
+               "Discovery is not supported");
+
+  return NULL;
+}
diff --git a/src/libide/foundry/ide-build-system-discovery.h b/src/libide/foundry/ide-build-system-discovery.h
new file mode 100644
index 000000000..6d9a8f201
--- /dev/null
+++ b/src/libide/foundry/ide-build-system-discovery.h
@@ -0,0 +1,56 @@
+/* ide-build-system-discovery.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_SYSTEM_DISCOVERY (ide_build_system_discovery_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeBuildSystemDiscovery, ide_build_system_discovery, IDE, BUILD_SYSTEM_DISCOVERY, 
GObject)
+
+struct _IdeBuildSystemDiscoveryInterface
+{
+  GTypeInterface parent_iface;
+
+  gchar *(*discover) (IdeBuildSystemDiscovery  *self,
+                      GFile                    *project_file,
+                      GCancellable             *cancellable,
+                      gint                     *priority,
+                      GError                  **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+gchar *ide_build_system_discovery_discover (IdeBuildSystemDiscovery  *self,
+                                            GFile                    *project_file,
+                                            GCancellable             *cancellable,
+                                            gint                     *priority,
+                                            GError                  **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-system.c b/src/libide/foundry/ide-build-system.c
new file mode 100644
index 000000000..d2820d7fd
--- /dev/null
+++ b/src/libide/foundry/ide-build-system.c
@@ -0,0 +1,674 @@
+/* ide-build-system.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-build-system"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-projects.h>
+#include <libide-threading.h>
+#include <libide-vcs.h>
+#include <string.h>
+
+#include "ide-build-manager.h"
+#include "ide-build-pipeline.h"
+#include "ide-build-system.h"
+#include "ide-configuration.h"
+#include "ide-device.h"
+#include "ide-foundry-compat.h"
+#include "ide-toolchain.h"
+
+G_DEFINE_INTERFACE (IdeBuildSystem, ide_build_system, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_PROJECT_FILE,
+  N_PROPS
+};
+
+typedef struct
+{
+  GPtrArray   *files;
+  GHashTable  *flags;
+  guint        index;
+} GetBuildFlagsData;
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+get_build_flags_data_free (GetBuildFlagsData *data)
+{
+  g_clear_pointer (&data->files, g_ptr_array_unref);
+  g_clear_pointer (&data->flags, g_hash_table_unref);
+  g_slice_free (GetBuildFlagsData, data);
+}
+
+gint
+ide_build_system_get_priority (IdeBuildSystem *self)
+{
+  IdeBuildSystemInterface *iface;
+
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), 0);
+
+  iface = IDE_BUILD_SYSTEM_GET_IFACE (self);
+
+  if (iface->get_priority != NULL)
+    return iface->get_priority (self);
+
+  return 0;
+}
+
+static void
+ide_build_system_real_get_build_flags_async (IdeBuildSystem      *self,
+                                             GFile               *file,
+                                             GCancellable        *cancellable,
+                                             GAsyncReadyCallback  callback,
+                                             gpointer             user_data)
+{
+  ide_task_report_new_error (self,
+                             callback,
+                             user_data,
+                             ide_build_system_real_get_build_flags_async,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Fetching build flags is not supported");
+}
+
+static gchar **
+ide_build_system_real_get_build_flags_finish (IdeBuildSystem  *self,
+                                              GAsyncResult    *result,
+                                              GError         **error)
+{
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+ide_build_system_get_build_flags_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  IdeBuildSystem *self = (IdeBuildSystem *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_auto(GStrv) flags = NULL;
+  GetBuildFlagsData *data;
+  GFile *file;
+
+  g_assert (IDE_IS_BUILD_SYSTEM (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  data = ide_task_get_task_data (task);
+  g_assert (data != NULL);
+  g_assert (data->files != NULL);
+  g_assert (data->files->len > 0);
+  g_assert (data->index < data->files->len);
+  g_assert (data->flags != NULL);
+
+  file = g_ptr_array_index (data->files, data->index);
+  g_assert (G_IS_FILE (file));
+
+  data->index++;
+
+  flags = ide_build_system_get_build_flags_finish (self, result, &error);
+
+  if (error != NULL)
+    g_debug ("Failed to load build flags for \"%s\": %s",
+             g_file_peek_path (file),
+             error->message);
+  else
+    g_hash_table_insert (data->flags, g_object_ref (file), g_steal_pointer (&flags));
+
+  if (ide_task_return_error_if_cancelled (task))
+    return;
+
+  if (data->index < data->files->len)
+    {
+      GCancellable *cancellable = ide_task_get_cancellable (task);
+
+      file = g_ptr_array_index (data->files, data->index);
+      g_assert (G_IS_FILE (file));
+
+      ide_build_system_get_build_flags_async (self,
+                                              file,
+                                              cancellable,
+                                              ide_build_system_get_build_flags_cb,
+                                              g_steal_pointer (&task));
+      return;
+    }
+
+  ide_task_return_pointer (task,
+                           g_steal_pointer (&data->flags),
+                           (GDestroyNotify)g_hash_table_unref);
+}
+
+static GPtrArray *
+copy_files (GPtrArray *in)
+{
+  GPtrArray *out = g_ptr_array_new_full (in->len, g_object_unref);
+  for (guint i = 0; i < in->len; i++)
+    g_ptr_array_add (out, g_file_dup (g_ptr_array_index (in, i)));
+  return g_steal_pointer (&out);
+}
+
+static void
+ide_build_system_real_get_build_flags_for_files_async (IdeBuildSystem       *self,
+                                                       GPtrArray            *files,
+                                                       GCancellable         *cancellable,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  GetBuildFlagsData *data;
+
+  g_return_if_fail (IDE_IS_BUILD_SYSTEM (self));
+  g_return_if_fail (files != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_build_system_real_get_build_flags_for_files_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  if (files->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVAL,
+                                 "No files were provided");
+      return;
+    }
+
+  g_assert (files->len > 0);
+  g_assert (G_IS_FILE (g_ptr_array_index (files, 0)));
+
+  if (ide_task_return_error_if_cancelled (task))
+    return;
+
+  data = g_slice_new0 (GetBuildFlagsData);
+  data->files = copy_files (files);
+  data->flags = g_hash_table_new_full ((GHashFunc)g_file_hash,
+                                       (GEqualFunc)g_file_equal,
+                                       g_object_unref,
+                                       (GDestroyNotify)g_strfreev);
+  ide_task_set_task_data (task, data, get_build_flags_data_free);
+
+  ide_build_system_get_build_flags_async (self,
+                                          g_ptr_array_index (files, 0),
+                                          cancellable,
+                                          ide_build_system_get_build_flags_cb,
+                                          g_steal_pointer (&task));
+}
+
+static GHashTable *
+ide_build_system_real_get_build_flags_for_files_finish (IdeBuildSystem       *self,
+                                                        GAsyncResult         *result,
+                                                        GError              **error)
+{
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+ide_build_system_default_init (IdeBuildSystemInterface *iface)
+{
+  iface->get_build_flags_async = ide_build_system_real_get_build_flags_async;
+  iface->get_build_flags_finish = ide_build_system_real_get_build_flags_finish;
+  iface->get_build_flags_for_files_async = ide_build_system_real_get_build_flags_for_files_async;
+  iface->get_build_flags_for_files_finish = ide_build_system_real_get_build_flags_for_files_finish;
+
+  properties [PROP_PROJECT_FILE] =
+    g_param_spec_object ("project-file",
+                         "Project File",
+                         "The project file.",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_interface_install_property (iface, properties [PROP_PROJECT_FILE]);
+}
+
+static gchar *
+ide_build_system_translate (IdeBuildSystem   *self,
+                            IdeBuildPipeline *pipeline,
+                            const gchar      *prefix,
+                            const gchar      *path)
+{
+  g_autofree gchar *freeme = NULL;
+  g_autofree gchar *translated_path = NULL;
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(GFile) translated = NULL;
+  IdeRuntime *runtime;
+
+  g_assert (IDE_IS_BUILD_SYSTEM (self));
+  g_assert (!pipeline || IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (prefix != NULL);
+  g_assert (path != NULL);
+
+  if (NULL == pipeline ||
+      NULL == (runtime = ide_build_pipeline_get_runtime (pipeline)))
+    return g_strdup_printf ("%s%s", prefix, path);
+
+  if (!g_path_is_absolute (path))
+    path = freeme = ide_build_pipeline_build_builddir_path (pipeline, path, NULL);
+
+  file = g_file_new_for_path (path);
+  translated = ide_runtime_translate_file (runtime, file);
+  translated_path = g_file_get_path (translated);
+
+  return g_strdup_printf ("%s%s", prefix, translated_path);
+}
+
+static void
+ide_build_system_post_process_build_flags (IdeBuildSystem  *self,
+                                           gchar          **flags)
+{
+  IdeBuildPipeline *pipeline;
+  IdeBuildManager *build_manager;
+  IdeContext *context;
+
+  g_assert (IDE_IS_BUILD_SYSTEM (self));
+
+  if (flags == NULL || flags[0] == NULL)
+    return;
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_manager = ide_build_manager_from_context (context);
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+
+  for (guint i = 0; flags[i] != NULL; i++)
+    {
+      gchar *flag = flags[i];
+      gchar *translated;
+
+      if (flag[0] != '-')
+        continue;
+
+      switch (flag[1])
+        {
+        case 'I':
+          if (flag[2] == '\0')
+            {
+              if (flags[i+1] != NULL)
+                {
+                  translated = ide_build_system_translate (self, pipeline, "", flags[++i]);
+                  flags[i] = translated;
+                  g_free (flag);
+                }
+            }
+          else
+            {
+              translated = ide_build_system_translate (self, pipeline, "-I", &flag[2]);
+              flags[i] = translated;
+              g_free (flag);
+            }
+          break;
+
+        case 'D':
+        case 'x':
+          if (strlen (flag) == 2)
+            i++;
+          break;
+
+        case 'f': /* -fPIC */
+        case 'W': /* -Werror... */
+        case 'm': /* -m64 -mtune=native */
+        default:
+          break;
+        }
+    }
+}
+
+void
+ide_build_system_get_build_flags_async (IdeBuildSystem      *self,
+                                        GFile               *file,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_SYSTEM (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_BUILD_SYSTEM_GET_IFACE (self)->get_build_flags_async (self, file, cancellable, callback, user_data);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_system_get_build_flags_finish:
+ *
+ * Returns: (transfer full):
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_build_system_get_build_flags_finish (IdeBuildSystem  *self,
+                                         GAsyncResult    *result,
+                                         GError         **error)
+{
+  gchar **ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  ret = IDE_BUILD_SYSTEM_GET_IFACE (self)->get_build_flags_finish (self, result, error);
+  if (ret != NULL)
+    ide_build_system_post_process_build_flags (self, ret);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_build_system_get_build_flags_for_files_async:
+ * @self: An #IdeBuildSystem instance.
+ * @files: (element-type GFile) (transfer none): array of files whose build flags has to be retrieved.
+ * @cancellable: (allow-none): a #GCancellable to cancel getting build flags.
+ * @callback: function to be called after getting build flags.
+ * @user_data: data to pass to @callback.
+ *
+ * This function will get build flags for all files and returns
+ * map of file and its build flags as #GHashTable.
+ *
+ * Since: 3.32
+ */
+void
+ide_build_system_get_build_flags_for_files_async (IdeBuildSystem       *self,
+                                                  GPtrArray            *files,
+                                                  GCancellable         *cancellable,
+                                                  GAsyncReadyCallback   callback,
+                                                  gpointer              user_data)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_BUILD_SYSTEM (self));
+  g_return_if_fail ( files != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_BUILD_SYSTEM_GET_IFACE (self)->get_build_flags_for_files_async (self, files, cancellable, callback, 
user_data);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_build_system_get_build_flags_for_files_finish:
+ * @self: an #IdeBuildSystem
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError or %NULL
+ *
+ * Returns: (element-type Ide.File GStrv) (transfer full): a #GHashTable or #GFile to #GStrv
+ *
+ * Since: 3.32
+ */
+GHashTable *
+ide_build_system_get_build_flags_for_files_finish (IdeBuildSystem  *self,
+                                                   GAsyncResult    *result,
+                                                   GError         **error)
+{
+  GHashTable *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  ret = IDE_BUILD_SYSTEM_GET_IFACE (self)->get_build_flags_for_files_finish (self, result, error);
+
+  if (ret != NULL)
+    {
+      GHashTableIter iter;
+      gchar **flags;
+
+      g_hash_table_iter_init (&iter, ret);
+
+      while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&flags))
+        ide_build_system_post_process_build_flags (self, flags);
+    }
+
+  IDE_RETURN (ret);
+}
+
+gchar *
+ide_build_system_get_builddir (IdeBuildSystem   *self,
+                               IdeBuildPipeline *pipeline)
+{
+  gchar *ret = NULL;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), NULL);
+  g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (pipeline), NULL);
+
+  if (IDE_BUILD_SYSTEM_GET_IFACE (self)->get_builddir)
+    ret = IDE_BUILD_SYSTEM_GET_IFACE (self)->get_builddir (self, pipeline);
+
+  if (ret == NULL)
+    {
+      g_autofree gchar *name = NULL;
+      g_autofree gchar *branch = NULL;
+      IdeConfiguration *config;
+      const gchar *config_id;
+      const gchar *runtime_id;
+      IdeRuntime *runtime;
+      IdeContext *context;
+      IdeVcs *vcs;
+
+      context = ide_object_get_context (IDE_OBJECT (self));
+      vcs = ide_vcs_from_context (context);
+      config = ide_build_pipeline_get_configuration (pipeline);
+      config_id = ide_configuration_get_id (config);
+      runtime = ide_build_pipeline_get_runtime (pipeline);
+      runtime_id = ide_runtime_get_id (runtime);
+      branch = ide_vcs_get_branch_name (vcs);
+
+      if (branch != NULL)
+        name = g_strdup_printf ("%s-%s-%s", config_id, runtime_id, branch);
+      else
+        name = g_strdup_printf ("%s-%s", config_id, runtime_id);
+
+      g_strdelimit (name, "@:/ ", '-');
+
+      ret = ide_context_cache_filename (context, "builds", name, NULL);
+    }
+
+  IDE_RETURN (ret);
+}
+
+gchar *
+ide_build_system_get_id (IdeBuildSystem *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), NULL);
+
+  if (IDE_BUILD_SYSTEM_GET_IFACE (self)->get_id)
+    return IDE_BUILD_SYSTEM_GET_IFACE (self)->get_id (self);
+
+  return g_strdup (G_OBJECT_TYPE_NAME (self));
+}
+
+gchar *
+ide_build_system_get_display_name (IdeBuildSystem *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), NULL);
+
+  if (IDE_BUILD_SYSTEM_GET_IFACE (self)->get_display_name)
+    return IDE_BUILD_SYSTEM_GET_IFACE (self)->get_display_name (self);
+
+  return ide_build_system_get_id (self);
+}
+
+static void
+ide_build_system_get_build_flags_for_dir_cb2 (GObject      *object,
+                                              GAsyncResult *result,
+                                              gpointer      user_data)
+{
+  IdeBuildSystem *build_system = (IdeBuildSystem *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GHashTable) ret = NULL;
+
+  g_assert (IDE_IS_BUILD_SYSTEM (build_system));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  ret = ide_build_system_get_build_flags_for_files_finish (build_system, result, &error);
+
+  if (ret == NULL)
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task,
+                             g_steal_pointer (&ret),
+                             (GDestroyNotify)g_hash_table_unref);
+}
+
+static void
+ide_build_system_get_build_flags_for_dir_cb (GObject      *object,
+                                             GAsyncResult *result,
+                                             gpointer      user_data)
+{
+  GFile *dir = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GPtrArray) infos = NULL;
+  g_autoptr(GPtrArray) files = NULL;
+  IdeBuildSystem *self;
+  GCancellable *cancellable;
+  IdeContext *context;
+  IdeVcs *vcs;
+
+  g_assert (G_IS_FILE (dir));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  infos = ide_g_file_get_children_finish (dir, result, &error);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (infos, g_object_unref);
+
+  if (infos == NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self = ide_task_get_source_object (task);
+  context = ide_object_get_context (IDE_OBJECT (self));
+  vcs = ide_vcs_from_context (context);
+  cancellable = ide_task_get_cancellable (task);
+  files = g_ptr_array_new_with_free_func (g_object_unref);
+
+  for (guint i = 0; i < infos->len; i++)
+    {
+      GFileInfo *file_info = g_ptr_array_index (infos, i);
+      GFileType file_type = g_file_info_get_file_type (file_info);
+
+      if (file_type == G_FILE_TYPE_REGULAR)
+        {
+          const gchar *name = g_file_info_get_name (file_info);
+          g_autoptr(GFile) child = g_file_get_child (dir, name);
+
+          if (!ide_vcs_is_ignored (vcs, child, NULL))
+            g_ptr_array_add (files, g_steal_pointer (&child));
+        }
+    }
+
+  ide_build_system_get_build_flags_for_files_async (self,
+                                                    files,
+                                                    cancellable,
+                                                    ide_build_system_get_build_flags_for_dir_cb2,
+                                                    g_steal_pointer (&task));
+}
+
+void
+ide_build_system_get_build_flags_for_dir_async (IdeBuildSystem      *self,
+                                                GFile               *directory,
+                                                GCancellable        *cancellable,
+                                                GAsyncReadyCallback  callback,
+                                                gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_BUILD_SYSTEM (self));
+  g_return_if_fail (G_IS_FILE (directory));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_build_system_get_build_flags_for_dir_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  ide_g_file_get_children_async (directory,
+                                 G_FILE_ATTRIBUTE_STANDARD_NAME","
+                                 G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                                 G_FILE_QUERY_INFO_NONE,
+                                 G_PRIORITY_LOW,
+                                 cancellable,
+                                 ide_build_system_get_build_flags_for_dir_cb,
+                                 g_steal_pointer (&task));
+}
+
+/**
+ * ide_build_system_get_build_flags_for_dir_finish:
+ * @self: an #IdeBuildSystem
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError or %NULL
+ *
+ * Returns: (element-type Ide.File GStrv) (transfer full): a #GHashTable of #GFile to #GStrv
+ *
+ * Since: 3.32
+ */
+GHashTable *
+ide_build_system_get_build_flags_for_dir_finish (IdeBuildSystem  *self,
+                                                 GAsyncResult    *result,
+                                                 GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+/**
+ * ide_build_system_supports_toolchain:
+ * @self: an #IdeBuildSystem
+ * @toolchain: a #IdeToolchain
+ *
+ * Checks whether the build system supports the given toolchain.
+ *
+ * Returns: %TRUE if the toolchain is supported by the build system, %FALSE otherwise
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_system_supports_toolchain (IdeBuildSystem *self,
+                                     IdeToolchain   *toolchain)
+{
+  const gchar *toolchain_id;
+
+  g_return_val_if_fail (IDE_IS_BUILD_SYSTEM (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN (toolchain), FALSE);
+
+  toolchain_id = ide_toolchain_get_id (toolchain);
+  if (g_strcmp0 (toolchain_id, "default") == 0)
+    return TRUE;
+
+  if (IDE_BUILD_SYSTEM_GET_IFACE (self)->supports_toolchain)
+    return IDE_BUILD_SYSTEM_GET_IFACE (self)->supports_toolchain (self, toolchain);
+
+  return FALSE;
+}
diff --git a/src/libide/foundry/ide-build-system.h b/src/libide/foundry/ide-build-system.h
new file mode 100644
index 000000000..fe2b5c6a1
--- /dev/null
+++ b/src/libide/foundry/ide-build-system.h
@@ -0,0 +1,115 @@
+/* ide-build-system.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <libide-code.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_SYSTEM (ide_build_system_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeBuildSystem, ide_build_system, IDE, BUILD_SYSTEM, IdeObject)
+
+struct _IdeBuildSystemInterface
+{
+  GTypeInterface parent_iface;
+
+  gint        (*get_priority)                      (IdeBuildSystem       *self);
+  void        (*get_build_flags_async)             (IdeBuildSystem       *self,
+                                                    GFile                *file,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+  gchar     **(*get_build_flags_finish)            (IdeBuildSystem       *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+  void        (*get_build_flags_for_files_async)   (IdeBuildSystem       *self,
+                                                    GPtrArray            *files,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+  GHashTable *(*get_build_flags_for_files_finish)  (IdeBuildSystem       *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+  gchar      *(*get_builddir)                      (IdeBuildSystem       *self,
+                                                    IdeBuildPipeline     *pipeline);
+  gchar      *(*get_id)                            (IdeBuildSystem       *self);
+  gchar      *(*get_display_name)                  (IdeBuildSystem       *self);
+  gboolean    (*supports_toolchain)                (IdeBuildSystem       *self,
+                                                    IdeToolchain         *toolchain);
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeBuildSystem *ide_build_system_from_context                      (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+gchar          *ide_build_system_get_id                            (IdeBuildSystem       *self);
+IDE_AVAILABLE_IN_3_32
+gchar          *ide_build_system_get_display_name                  (IdeBuildSystem       *self);
+IDE_AVAILABLE_IN_3_32
+gint            ide_build_system_get_priority                      (IdeBuildSystem       *self);
+IDE_AVAILABLE_IN_3_32
+gchar          *ide_build_system_get_builddir                      (IdeBuildSystem       *self,
+                                                                    IdeBuildPipeline     *pipeline);
+IDE_AVAILABLE_IN_3_32
+void            ide_build_system_get_build_flags_async             (IdeBuildSystem       *self,
+                                                                    GFile                *file,
+                                                                    GCancellable         *cancellable,
+                                                                    GAsyncReadyCallback   callback,
+                                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gchar         **ide_build_system_get_build_flags_finish            (IdeBuildSystem       *self,
+                                                                    GAsyncResult         *result,
+                                                                    GError              **error);
+IDE_AVAILABLE_IN_3_32
+void            ide_build_system_get_build_flags_for_files_async   (IdeBuildSystem       *self,
+                                                                    GPtrArray            *files,
+                                                                    GCancellable         *cancellable,
+                                                                    GAsyncReadyCallback   callback,
+                                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GHashTable     *ide_build_system_get_build_flags_for_files_finish  (IdeBuildSystem       *self,
+                                                                    GAsyncResult         *result,
+                                                                    GError              **error);
+IDE_AVAILABLE_IN_3_32
+void            ide_build_system_get_build_flags_for_dir_async     (IdeBuildSystem       *self,
+                                                                    GFile                *directory,
+                                                                    GCancellable         *cancellable,
+                                                                    GAsyncReadyCallback   callback,
+                                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GHashTable     *ide_build_system_get_build_flags_for_dir_finish    (IdeBuildSystem       *self,
+                                                                    GAsyncResult         *result,
+                                                                    GError              **error);
+void            _ide_build_system_set_project_file                 (IdeBuildSystem       *self,
+                                                                    GFile                *project_file) 
G_GNUC_INTERNAL;
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_build_system_supports_toolchain                (IdeBuildSystem       *self,
+                                                                    IdeToolchain         *toolchain);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-target-provider.c b/src/libide/foundry/ide-build-target-provider.c
new file mode 100644
index 000000000..d351bee05
--- /dev/null
+++ b/src/libide/foundry/ide-build-target-provider.c
@@ -0,0 +1,115 @@
+/* ide-build-target-provider.c
+ *
+ * Copyright 2017-2019 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 "ide-build-target-provider"
+
+#include "config.h"
+
+#include "ide-build-target-provider.h"
+
+G_DEFINE_INTERFACE (IdeBuildTargetProvider, ide_build_target_provider, G_TYPE_OBJECT)
+
+static void
+ide_build_target_provider_real_get_targets_async (IdeBuildTargetProvider *provider,
+                                                  GCancellable           *cancellable,
+                                                  GAsyncReadyCallback     callback,
+                                                  gpointer                user_data)
+{
+  g_task_report_new_error (provider, callback, user_data,
+                           ide_build_target_provider_real_get_targets_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Loading targets is not supported by %s",
+                           G_OBJECT_TYPE_NAME (provider));
+}
+
+static GPtrArray *
+ide_build_target_provider_real_get_targets_finish (IdeBuildTargetProvider  *provider,
+                                                   GAsyncResult            *result,
+                                                   GError                 **error)
+{
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+ide_build_target_provider_default_init (IdeBuildTargetProviderInterface *iface)
+{
+  iface->get_targets_async = ide_build_target_provider_real_get_targets_async;
+  iface->get_targets_finish = ide_build_target_provider_real_get_targets_finish;
+}
+
+/**
+ * ide_build_target_provider_get_targets_async:
+ * @self: an #IdeBuildTargetProvider
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: (scope async): a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously requests that the provider fetch all of the known build
+ * targets that are part of the project. Generally this should be limited to
+ * executables that Builder might be interested in potentially running.
+ *
+ * @callback should call ide_build_target_provider_get_targets_finish() to
+ * complete the asynchronous operation.
+ *
+ * See also: ide_build_target_provider_get_targets_finish()
+ *
+ * Since: 3.32
+ */
+void
+ide_build_target_provider_get_targets_async (IdeBuildTargetProvider *self,
+                                             GCancellable           *cancellable,
+                                             GAsyncReadyCallback     callback,
+                                             gpointer                user_data)
+{
+  g_return_if_fail (IDE_IS_BUILD_TARGET_PROVIDER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_BUILD_TARGET_PROVIDER_GET_IFACE (self)->get_targets_async (self,
+                                                                 cancellable,
+                                                                 callback,
+                                                                 user_data);
+}
+
+/**
+ * ide_build_target_provider_get_targets_finish:
+ * @self: an #IdeBuildTargetProvider
+ * @result: a #GAsyncResult provided to the callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes a request to get the targets for the project.
+ *
+ * See also: ide_build_target_provider_get_targets_async()
+ *
+ * Returns: (transfer full) (element-type Ide.BuildTarget): The array of
+ *   build targets or %NULL upon failure and @error is set.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_build_target_provider_get_targets_finish (IdeBuildTargetProvider  *self,
+                                              GAsyncResult            *result,
+                                              GError                 **error)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_TARGET_PROVIDER (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_BUILD_TARGET_PROVIDER_GET_IFACE (self)->get_targets_finish (self, result, error);
+}
diff --git a/src/libide/foundry/ide-build-target-provider.h b/src/libide/foundry/ide-build-target-provider.h
new file mode 100644
index 000000000..067347da7
--- /dev/null
+++ b/src/libide/foundry/ide-build-target-provider.h
@@ -0,0 +1,59 @@
+/* ide-build-target-provider.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_TARGET_PROVIDER (ide_build_target_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeBuildTargetProvider, ide_build_target_provider, IDE, BUILD_TARGET_PROVIDER, 
IdeObject)
+
+struct _IdeBuildTargetProviderInterface
+{
+  GTypeInterface parent_iface;
+
+  void       (*get_targets_async)  (IdeBuildTargetProvider  *self,
+                                    GCancellable            *cancellable,
+                                    GAsyncReadyCallback      callback,
+                                    gpointer                 user_data);
+  GPtrArray *(*get_targets_finish) (IdeBuildTargetProvider  *self,
+                                    GAsyncResult            *result,
+                                    GError                 **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void       ide_build_target_provider_get_targets_async  (IdeBuildTargetProvider  *self,
+                                                         GCancellable            *cancellable,
+                                                         GAsyncReadyCallback      callback,
+                                                         gpointer                 user_data);
+IDE_AVAILABLE_IN_3_32
+GPtrArray *ide_build_target_provider_get_targets_finish (IdeBuildTargetProvider  *self,
+                                                         GAsyncResult            *result,
+                                                         GError                 **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-target.c b/src/libide/foundry/ide-build-target.c
new file mode 100644
index 000000000..291597716
--- /dev/null
+++ b/src/libide/foundry/ide-build-target.c
@@ -0,0 +1,253 @@
+/* ide-build-target.c
+ *
+ * Copyright 2016-2019 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 "ide-build-target"
+
+#include "config.h"
+
+#include "ide-build-target.h"
+
+G_DEFINE_INTERFACE (IdeBuildTarget, ide_build_target, IDE_TYPE_OBJECT)
+
+static gchar*
+ide_build_target_real_get_cwd (IdeBuildTarget *self)
+{
+  return NULL;
+}
+
+static gchar*
+ide_build_target_real_get_language (IdeBuildTarget *self)
+{
+  return g_strdup ("asm");
+}
+
+
+static void
+ide_build_target_default_init (IdeBuildTargetInterface *iface)
+{
+  iface->get_cwd = ide_build_target_real_get_cwd;
+  iface->get_language = ide_build_target_real_get_language;
+}
+
+/**
+ * ide_build_target_get_install_directory:
+ *
+ * Returns: (nullable) (transfer full): a #GFile or %NULL.
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_build_target_get_install_directory (IdeBuildTarget *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_TARGET (self), NULL);
+
+  if (IDE_BUILD_TARGET_GET_IFACE (self)->get_install_directory)
+    return IDE_BUILD_TARGET_GET_IFACE (self)->get_install_directory (self);
+
+  return NULL;
+}
+
+/**
+ * ide_build_target_get_install:
+ * @self: an #IdeBuildTarget
+ *
+ * Checks if the #IdeBuildTarget gets installed.
+ *
+ * Returns: %TRUE if the build target is installed
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_build_target_get_install (IdeBuildTarget *self)
+{
+  g_autoptr(GFile) dir = NULL;
+
+  g_return_val_if_fail (IDE_IS_BUILD_TARGET (self), FALSE);
+
+  if ((dir = ide_build_target_get_install_directory (self)))
+    return TRUE;
+
+  return FALSE;
+}
+
+/**
+ * ide_build_target_get_name:
+ *
+ * Returns: (nullable) (transfer full): A filename or %NULL.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_build_target_get_name (IdeBuildTarget *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_TARGET (self), NULL);
+
+  if (IDE_BUILD_TARGET_GET_IFACE (self)->get_name)
+    return IDE_BUILD_TARGET_GET_IFACE (self)->get_name (self);
+
+  return NULL;
+}
+
+/**
+ * ide_build_target_get_priority:
+ * @self: an #IdeBuildTarget
+ *
+ * Gets the priority of the build target. This is used to sort build targets by
+ * their importance. The lowest value (negative values are allowed) will be run
+ * as the default run target by Builder.
+ *
+ * Returns: the priority of the build target
+ *
+ * Since: 3.32
+ */
+gint
+ide_build_target_get_priority (IdeBuildTarget *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_TARGET (self), 0);
+
+  if (IDE_BUILD_TARGET_GET_IFACE (self)->get_priority)
+    return IDE_BUILD_TARGET_GET_IFACE (self)->get_priority (self);
+  return 0;
+}
+
+/**
+ * ide_build_target_get_kind:
+ * @self: a #IdeBuildTarget
+ *
+ * Gets the kind of artifact.
+ *
+ * Returns: an #IdeArtifactKind
+ *
+ * Since: 3.32
+ */
+IdeArtifactKind
+ide_build_target_get_kind (IdeBuildTarget *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_TARGET (self), 0);
+
+  if (IDE_BUILD_TARGET_GET_IFACE (self)->get_kind)
+    return IDE_BUILD_TARGET_GET_IFACE (self)->get_kind (self);
+
+  return IDE_ARTIFACT_KIND_NONE;
+}
+
+gint
+ide_build_target_compare (const IdeBuildTarget *left,
+                          const IdeBuildTarget *right)
+{
+  return ide_build_target_get_priority ((IdeBuildTarget *)left) -
+         ide_build_target_get_priority ((IdeBuildTarget *)right);
+}
+
+/**
+ * ide_build_target_get_argv:
+ * @self: a #IdeBuildTarget
+ *
+ * Gets the arguments used to run the target.
+ *
+ * Returns: (transfer full): A #GStrv containing the arguments to
+ *   run the target.
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_build_target_get_argv (IdeBuildTarget *self)
+{
+  g_auto(GStrv) argv = NULL;
+
+  g_return_val_if_fail (IDE_IS_BUILD_TARGET (self), NULL);
+
+  if (IDE_BUILD_TARGET_GET_IFACE (self)->get_argv)
+    argv = IDE_BUILD_TARGET_GET_IFACE (self)->get_argv (self);
+
+  if (argv == NULL || *argv == NULL)
+    {
+      g_autofree gchar *name = ide_build_target_get_name (self);
+      g_autoptr(GFile) dir = ide_build_target_get_install_directory (self);
+
+      g_clear_pointer (&argv, g_strfreev);
+
+      if (!g_path_is_absolute (name) && dir != NULL && g_file_is_native (dir))
+        {
+          g_autofree gchar *tmp = g_steal_pointer (&name);
+          g_autoptr(GFile) child = g_file_get_child (dir, tmp);
+
+          name = g_file_get_path (child);
+        }
+
+      argv = g_new (gchar *, 2);
+      argv[0] = g_steal_pointer (&name);
+      argv[1] = NULL;
+    }
+
+  return g_steal_pointer (&argv);
+}
+
+/**
+ * ide_build_target_get_cwd:
+ * @self: a #IdeBuildTarget
+ *
+ * For build systems and build target providers that insist to be run in
+ * a specific place, this method gets the correct working directory.
+ *
+ * If this method returns %NULL, the runtime will pick a default working
+ * directory for the spawned process (usually, the user home directory
+ * in the host system, or the flatpak sandbox home under flatpak).
+ *
+ * Returns: (nullable) (transfer full): the working directory to use for this target
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_build_target_get_cwd (IdeBuildTarget *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_TARGET (self), NULL);
+
+  return IDE_BUILD_TARGET_GET_IFACE (self)->get_cwd (self);
+}
+
+/**
+ * ide_build_target_get_language:
+ * @self: a #IdeBuildTarget
+ *
+ * Return the main programming language that was used to
+ * write this build target.
+ *
+ * This method is primarily used to choose an appropriate
+ * debugger. Therefore, if a build target is composed of
+ * components in multiple language (eg. a GJS app with
+ * GObject Introspection libraries, or a Java app with JNI
+ * libraries), this should return the language that is
+ * most likely to be appropriate for debugging.
+ *
+ * The default implementation returns "asm", which indicates
+ * an unspecified language that compiles to native code.
+ *
+ * Returns: (transfer full): the programming language of this target
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_build_target_get_language (IdeBuildTarget *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_TARGET (self), NULL);
+
+  return IDE_BUILD_TARGET_GET_IFACE (self)->get_language (self);
+}
diff --git a/src/libide/foundry/ide-build-target.h b/src/libide/foundry/ide-build-target.h
new file mode 100644
index 000000000..8da2fdb68
--- /dev/null
+++ b/src/libide/foundry/ide-build-target.h
@@ -0,0 +1,80 @@
+/* ide-build-target.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILD_TARGET (ide_build_target_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeBuildTarget, ide_build_target, IDE, BUILD_TARGET, IdeObject)
+
+typedef enum
+{
+  IDE_ARTIFACT_KIND_NONE,
+  IDE_ARTIFACT_KIND_EXECUTABLE,
+  IDE_ARTIFACT_KIND_SHARED_LIBRARY,
+  IDE_ARTIFACT_KIND_STATIC_LIBRARY,
+  IDE_ARTIFACT_KIND_FILE,
+} IdeArtifactKind;
+
+struct _IdeBuildTargetInterface
+{
+  GTypeInterface parent_iface;
+
+  GFile            *(*get_install_directory) (IdeBuildTarget *self);
+  gchar            *(*get_name)              (IdeBuildTarget *self);
+  gint              (*get_priority)          (IdeBuildTarget *self);
+  gchar           **(*get_argv)              (IdeBuildTarget *self);
+  gchar            *(*get_cwd)               (IdeBuildTarget *self);
+  gchar            *(*get_language)          (IdeBuildTarget *self);
+  IdeArtifactKind   (*get_kind)              (IdeBuildTarget *self);
+};
+
+IDE_AVAILABLE_IN_3_32
+GFile            *ide_build_target_get_install_directory (IdeBuildTarget       *self);
+IDE_AVAILABLE_IN_3_32
+gchar            *ide_build_target_get_name              (IdeBuildTarget       *self);
+IDE_AVAILABLE_IN_3_32
+gint              ide_build_target_get_priority          (IdeBuildTarget       *self);
+IDE_AVAILABLE_IN_3_32
+gchar           **ide_build_target_get_argv              (IdeBuildTarget       *self);
+IDE_AVAILABLE_IN_3_32
+gchar            *ide_build_target_get_cwd               (IdeBuildTarget       *self);
+IDE_AVAILABLE_IN_3_32
+gchar            *ide_build_target_get_language          (IdeBuildTarget       *self);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_build_target_get_install           (IdeBuildTarget       *self);
+IDE_AVAILABLE_IN_3_32
+IdeArtifactKind   ide_build_target_get_kind              (IdeBuildTarget       *self);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_build_target_compare               (const IdeBuildTarget *left,
+                                                          const IdeBuildTarget *right);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-build-utils.c b/src/libide/foundry/ide-build-utils.c
new file mode 100644
index 000000000..18683cede
--- /dev/null
+++ b/src/libide/foundry/ide-build-utils.c
@@ -0,0 +1,87 @@
+/* ide-build-utils.c
+ *
+ * Copyright 2017 Sebastien Lafargue <slafargue gnome org>
+ *
+ * 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 "ide-build-utils"
+
+#include "config.h"
+
+#include "ide-build-private.h"
+
+guint8 *
+_ide_build_utils_filter_color_codes (const guint8 *data,
+                                     gsize         len,
+                                     gsize        *out_len)
+{
+  g_autoptr(GByteArray) dst = NULL;
+
+  g_return_val_if_fail (out_len != NULL, NULL);
+
+  *out_len = 0;
+
+  if (data == NULL)
+    return NULL;
+  else if (len == 0)
+    return (guint8 *)g_strdup ("");
+
+  dst = g_byte_array_sized_new (len);
+
+  for (gsize i = 0; i < len; i++)
+    {
+      guint8 ch = data[i];
+      guint8 next = (i+1) < len ? data[i+1] : 0;
+
+      if (ch == '\\' && next == 'e')
+        {
+          i += 2;
+        }
+      else if (ch == '\033')
+        {
+          i++;
+        }
+      else
+        {
+          g_byte_array_append (dst, &ch, 1);
+          continue;
+        }
+
+      if (i >= len)
+        break;
+
+      if (data[i] == '[')
+        i++;
+
+      if (i >= len)
+        break;
+
+      for (; i < len; i++)
+        {
+          ch = data[i];
+
+          if (g_ascii_isdigit (ch) || ch == ' ' || ch == ';')
+            continue;
+
+          break;
+        }
+    }
+
+  *out_len = dst->len;
+
+  return g_byte_array_free (g_steal_pointer (&dst), FALSE);
+}
diff --git a/src/libide/foundry/ide-compile-commands.c b/src/libide/foundry/ide-compile-commands.c
new file mode 100644
index 000000000..76a4dd3cb
--- /dev/null
+++ b/src/libide/foundry/ide-compile-commands.c
@@ -0,0 +1,738 @@
+/* ide-compile-commands.c
+ *
+ * Copyright 2017-2019 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 "ide-compile-commands"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <json-glib/json-glib.h>
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-compile-commands.h"
+
+/**
+ * SECTION:ide-compile-commands
+ * @title: IdeCompileCommands
+ * @short_description: Integration with compile_commands.json
+ *
+ * The #IdeCompileCommands object provides a simplified interface to
+ * interact with compile_commands.json files which are generated by a
+ * number of build systems, including Clang tooling, Meson and CMake.
+ *
+ * Create a new #IdeCompileCommands instance, and then asynchronously
+ * load the file using ide_compile_commands_load_async(). After the
+ * database has been loaded, you can access build commands using
+ * ide_compile_commands_lookup().
+ *
+ * Due to the rather unfortunate design of JSON, this file holds on
+ * to a number of strings during the lifetime of the object, for each
+ * of the compile commands. On larger projects, this can be the order
+ * of a couple of megabytes.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeCompileCommands
+{
+  GObject parent_instance;
+
+  /*
+   * The info_by_file field contains a hashtable whose keys are #GFile
+   * matching the file that is to be compiled. It contains as a value
+   * the CompileInfo struct describing how to compile that file.
+   */
+  GHashTable *info_by_file;
+
+  /*
+   * The vala_info field contains an array of every vala like file we've
+   * discovered while parsing the database. This is used so because some
+   * compile_commands.json only have a single valac command which wont
+   * match the file we want to lookup (Notably Meson-based).
+   */
+  GPtrArray *vala_info;
+
+  /*
+   * The has_loaded field determines if we've had a load (async or sync
+   * variant) operation called. We can only do this safely once because
+   * we assign state in the task worker. Callers must discard the object
+   * if the load operation fails.
+   */
+  guint has_loaded : 1;
+};
+
+typedef struct
+{
+  GFile *directory;
+  GFile *file;
+  gchar *command;
+} CompileInfo;
+
+G_DEFINE_TYPE (IdeCompileCommands, ide_compile_commands, G_TYPE_OBJECT)
+
+static void
+compile_info_free (gpointer data)
+{
+  CompileInfo *info = data;
+
+  if (info != NULL)
+    {
+      g_clear_object (&info->directory);
+      g_clear_object (&info->file);
+      g_clear_pointer (&info->command, g_free);
+      g_slice_free (CompileInfo, info);
+    }
+}
+
+static void
+ide_compile_commands_finalize (GObject *object)
+{
+  IdeCompileCommands *self = (IdeCompileCommands *)object;
+
+  g_clear_pointer (&self->info_by_file, g_hash_table_unref);
+  g_clear_pointer (&self->vala_info, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (ide_compile_commands_parent_class)->finalize (object);
+}
+
+static void
+ide_compile_commands_class_init (IdeCompileCommandsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_compile_commands_finalize;
+}
+
+static void
+ide_compile_commands_init (IdeCompileCommands *self)
+{
+}
+
+/**
+ * ide_compile_commands_new:
+ *
+ * Creates a new #IdeCompileCommands object which can be used to parse
+ * clang-style compile commands database files (compile_commands.json).
+ *
+ * Returns: The newly created #IdeCompileCommands
+ *
+ * Since: 3.32
+ */
+IdeCompileCommands *
+ide_compile_commands_new (void)
+{
+  return g_object_new (IDE_TYPE_COMPILE_COMMANDS, NULL);
+}
+
+static void
+ide_compile_commands_load_worker (IdeTask      *task,
+                                  gpointer      source_object,
+                                  gpointer      task_data,
+                                  GCancellable *cancellable)
+{
+  IdeCompileCommands *self = source_object;
+  GFile *gfile = task_data;
+  g_autoptr(JsonParser) parser = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GHashTable) info_by_file = NULL;
+  g_autoptr(GHashTable) directories_by_path = NULL;
+  g_autoptr(GPtrArray) vala_info = NULL;
+  g_autofree gchar *contents = NULL;
+  JsonNode *root;
+  JsonArray *ar;
+  gsize len = 0;
+  guint n_items;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_COMPILE_COMMANDS (self));
+  g_assert (G_IS_FILE (gfile));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  parser = json_parser_new ();
+
+  if (!g_file_load_contents (gfile, cancellable, &contents, &len, NULL, &error) ||
+      !json_parser_load_from_data (parser, contents, len, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (NULL == (root = json_parser_get_root (parser)) ||
+      !JSON_NODE_HOLDS_ARRAY (root) ||
+      NULL == (ar = json_node_get_array (root)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_DATA,
+                                 "Failed to extract commands, invalid json");
+      IDE_EXIT;
+    }
+
+  info_by_file = g_hash_table_new_full (g_file_hash,
+                                        (GEqualFunc)g_file_equal,
+                                        NULL,
+                                        compile_info_free);
+
+  directories_by_path = g_hash_table_new_full (g_str_hash,
+                                               g_str_equal,
+                                               NULL,
+                                               g_object_unref);
+
+  vala_info = g_ptr_array_new_with_free_func (compile_info_free);
+
+  n_items = json_array_get_length (ar);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      CompileInfo *info;
+      JsonNode *item;
+      JsonNode *value;
+      JsonObject *obj;
+      GFile *dir;
+      const gchar *directory = NULL;
+      const gchar *file = NULL;
+      const gchar *command = NULL;
+
+      item = json_array_get_element (ar, i);
+
+      /* Skip past this node if its invalid for some reason, so we
+       * can try to be tolerante of errors created by broken tooling.
+       */
+      if (item == NULL ||
+          !JSON_NODE_HOLDS_OBJECT (item) ||
+          NULL == (obj = json_node_get_object (item)))
+        continue;
+
+      if (json_object_has_member (obj, "file") &&
+          NULL != (value = json_object_get_member (obj, "file")) &&
+          JSON_NODE_HOLDS_VALUE (value))
+        file = json_node_get_string (value);
+
+      if (json_object_has_member (obj, "directory") &&
+          NULL != (value = json_object_get_member (obj, "directory")) &&
+          JSON_NODE_HOLDS_VALUE (value))
+        directory = json_node_get_string (value);
+
+      if (json_object_has_member (obj, "command") &&
+          NULL != (value = json_object_get_member (obj, "command")) &&
+          JSON_NODE_HOLDS_VALUE (value))
+        command = json_node_get_string (value);
+
+      /* Ignore items that are missing something or other */
+      if (file == NULL || command == NULL || directory == NULL)
+        continue;
+
+      /* Try to reduce the number of GFile we have for directories */
+      if (NULL == (dir = g_hash_table_lookup (directories_by_path, directory)))
+        {
+          dir = g_file_new_for_path (directory);
+          g_hash_table_insert (directories_by_path, (gchar *)directory, dir);
+        }
+
+      info = g_slice_new0 (CompileInfo);
+      info->file = g_file_resolve_relative_path (dir, file);
+      info->directory = g_object_ref (dir);
+      info->command = g_strdup (command);
+      g_hash_table_replace (info_by_file, info->file, info);
+
+      /*
+       * We might need to keep a special copy of this for resolving .vala
+       * builds which won't be able ot be matched based on the filename. We
+       * keep all of them around right now in case we want to later on find
+       * the closest match based on directory.
+       */
+      if (g_str_has_suffix (file, ".vala"))
+        {
+          info = g_slice_new0 (CompileInfo);
+          info->file = g_file_resolve_relative_path (dir, file);
+          info->directory = g_object_ref (dir);
+          info->command = g_strdup (command);
+          g_ptr_array_add (vala_info, info);
+        }
+    }
+
+  self->info_by_file = g_steal_pointer (&info_by_file);
+  self->vala_info = g_steal_pointer (&vala_info);
+
+  ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_compile_commands_load:
+ * @self: An #IdeCompileCommands
+ * @file: a #GFile
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @error: A location for a #GError, or %NULL
+ *
+ * Synchronously loads the contents of the requested @file and parses
+ * the JSON command database contained within.
+ *
+ * You may only call this function once on an #IdeCompileCommands object.
+ * If there is a failure, you must create a new #IdeCompileCommands instance
+ * instead of calling this function again.
+ *
+ * See also: ide_compile_commands_load_async()
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_compile_commands_load (IdeCompileCommands  *self,
+                           GFile               *file,
+                           GCancellable        *cancellable,
+                           GError             **error)
+{
+  g_autoptr(IdeTask) task = NULL;
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_COMPILE_COMMANDS (self), FALSE);
+  g_return_val_if_fail (self->has_loaded == FALSE, FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  self->has_loaded = TRUE;
+
+  task = ide_task_new (self, cancellable, NULL, NULL);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_source_tag (task, ide_compile_commands_load);
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
+  ide_compile_commands_load_worker (task, self, file, cancellable);
+
+  ret = ide_task_propagate_boolean (task, error);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_compile_commands_load_async:
+ * @self: An #IdeCompileCommands
+ * @file: a #GFile
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @callback: the callback for the async operation
+ * @user_data: user data for @callback
+ *
+ * Asynchronously loads the contents of the requested @file and parses
+ * the JSON command database contained within.
+ *
+ * You may only call this function once on an #IdeCompileCommands object.
+ * If there is a failure, you must create a new #IdeCompileCommands instance
+ * instead of calling this function again.
+ *
+ * See also: ide_compile_commands_load_finish()
+ *
+ * Since: 3.32
+ */
+void
+ide_compile_commands_load_async (IdeCompileCommands  *self,
+                                 GFile               *file,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPILE_COMMANDS (self));
+  g_return_if_fail (self->has_loaded == FALSE);
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  self->has_loaded = TRUE;
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_source_tag (task, ide_compile_commands_load_async);
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
+  ide_task_run_in_thread (task, ide_compile_commands_load_worker);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_compile_commands_load_finish:
+ * @self: An #IdeCompileCommands
+ * @result: a #GAsyncResult provided to the callback
+ * @error: A location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_compile_commands_load_async().
+ *
+ * See also: ide_compile_commands_load_async()
+ *
+ * Returns: %TRUE if the file was loaded successfully; otherwise %FALSE
+ *   and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_compile_commands_load_finish (IdeCompileCommands  *self,
+                                  GAsyncResult        *result,
+                                  GError             **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_COMPILE_COMMANDS (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+suffix_is_c_like (const gchar *suffix)
+{
+  if (suffix == NULL)
+    return FALSE;
+
+  return !!strstr (suffix, ".c") || !!strstr (suffix, ".h") ||
+         !!strstr (suffix, ".cc") || !!strstr (suffix, ".hh") ||
+         !!strstr (suffix, ".cxx") || !!strstr (suffix, ".hxx") ||
+         !!strstr (suffix, ".cpp") || !!strstr (suffix, ".hpp");
+}
+
+static gboolean
+suffix_is_vala (const gchar *suffix)
+{
+  if (suffix == NULL)
+    return FALSE;
+
+  return !!strstr (suffix, ".vala");
+}
+
+static gchar *
+ide_compile_commands_resolve (IdeCompileCommands *self,
+                              const CompileInfo  *info,
+                              const gchar        *path)
+{
+  g_autoptr(GFile) file = NULL;
+
+  g_assert (IDE_IS_COMPILE_COMMANDS (self));
+  g_assert (info != NULL);
+
+  if (path == NULL)
+    return NULL;
+
+  if (g_path_is_absolute (path))
+    return g_strdup (path);
+
+  file = g_file_resolve_relative_path (info->directory, path);
+  if (file != NULL)
+    return g_file_get_path (file);
+
+  return NULL;
+}
+
+static void
+ide_compile_commands_filter_c (IdeCompileCommands   *self,
+                               const CompileInfo    *info,
+                               const gchar * const  *system_includes,
+                               gchar              ***argv)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+
+  g_assert (IDE_IS_COMPILE_COMMANDS (self));
+  g_assert (info != NULL);
+  g_assert (argv != NULL);
+
+  if (*argv == NULL)
+    return;
+
+  ar = g_ptr_array_new_with_free_func (g_free);
+
+  if (system_includes != NULL)
+    {
+      for (guint i = 0; system_includes[i]; i++)
+        g_ptr_array_add (ar, g_strdup_printf ("-I%s", system_includes[i]));
+    }
+
+  for (guint i = 0; (*argv)[i] != NULL; i++)
+    {
+      const gchar *param = (*argv)[i];
+      const gchar *next = (*argv)[i+1];
+      g_autofree gchar *resolved = NULL;
+
+      if (param[0] != '-')
+        continue;
+
+      switch (param[1])
+        {
+        case 'I': /* -I/usr/include, -I /usr/include */
+          if (param[2] != '\0')
+            next = &param[2];
+          resolved = ide_compile_commands_resolve (self, info, next);
+          if (resolved != NULL)
+            g_ptr_array_add (ar, g_strdup_printf ("-I%s", resolved));
+          break;
+
+        case 'f': /* -fPIC */
+        case 'W': /* -Werror... */
+        case 'm': /* -m64 -mtune=native */
+        case 'O': /* -O2 */
+          g_ptr_array_add (ar, g_strdup (param));
+          break;
+
+        case 'M': /* -MMD -MQ -MT -MF <file> */
+          /* ignore the -M class of commands */
+          break;
+
+        case 'D': /* -DFOO, -D FOO */
+        case 'x': /* -xc++ */
+          g_ptr_array_add (ar, g_strdup (param));
+          if (param[2] == '\0')
+            g_ptr_array_add (ar, g_strdup (next));
+          break;
+
+        default:
+          if (g_str_has_prefix (param, "-std=") ||
+              ide_str_equal0 (param, "-pthread") ||
+              g_str_has_prefix (param, "-isystem"))
+            {
+              g_ptr_array_add (ar, g_strdup (param));
+            }
+          else if (next != NULL && ide_str_equal0 (param, "-include"))
+            {
+              g_ptr_array_add (ar, g_strdup (param));
+              g_ptr_array_add (ar, ide_compile_commands_resolve (self, info, next));
+            }
+          break;
+        }
+    }
+
+  g_ptr_array_add (ar, NULL);
+
+  g_strfreev (*argv);
+  *argv = (gchar **)g_ptr_array_free (g_steal_pointer (&ar), FALSE);
+}
+
+static void
+ide_compile_commands_filter_vala (IdeCompileCommands   *self,
+                                  const CompileInfo    *info,
+                                  gchar              ***argv)
+{
+  GPtrArray *ar;
+
+  g_assert (IDE_IS_COMPILE_COMMANDS (self));
+  g_assert (info != NULL);
+  g_assert (argv != NULL);
+
+  if (*argv == NULL)
+    return;
+
+  ar = g_ptr_array_new ();
+
+  for (guint i = 0; (*argv)[i] != NULL; i++)
+    {
+      const gchar *param = (*argv)[i];
+      const gchar *next = (*argv)[i+1];
+
+      if (g_str_has_prefix (param, "--pkg=") ||
+          g_str_has_prefix (param, "--target-glib=") ||
+          !!strstr (param, ".vapi"))
+        {
+          g_ptr_array_add (ar, g_strdup (param));
+        }
+      else if (g_str_has_prefix (param, "--vapidir=") ||
+               g_str_has_prefix (param, "--girdir=") ||
+               g_str_has_prefix (param, "--metadatadir="))
+        {
+          g_autofree gchar *resolved = NULL;
+          gchar *eq = strchr (param, '=');
+
+          next = eq + 1;
+          *eq = '\0';
+
+          resolved = ide_compile_commands_resolve (self, info, next);
+          g_ptr_array_add (ar, g_strdup_printf ("%s=%s", param, resolved));
+        }
+      else if (next != NULL &&
+               (g_str_has_prefix (param, "--pkg") ||
+                g_str_has_prefix (param, "--target-glib")))
+        {
+          g_ptr_array_add (ar, g_strdup (param));
+          g_ptr_array_add (ar, g_strdup (next));
+          i++;
+        }
+      else if (next != NULL &&
+               (g_str_has_prefix (param, "--vapidir") ||
+                g_str_has_prefix (param, "--girdir") ||
+                g_str_has_prefix (param, "--metadatadir")))
+        {
+          g_ptr_array_add (ar, g_strdup (param));
+          g_ptr_array_add (ar, ide_compile_commands_resolve (self, info, next));
+          i++;
+        }
+    }
+
+  g_strfreev (*argv);
+
+  g_ptr_array_add (ar, NULL);
+  *argv = (gchar **)g_ptr_array_free (ar, FALSE);
+}
+
+static const CompileInfo *
+find_with_alternates (IdeCompileCommands *self,
+                      GFile              *file)
+{
+  const CompileInfo *info;
+
+  g_assert (IDE_IS_COMPILE_COMMANDS (self));
+  g_assert (G_IS_FILE (file));
+
+  if (self->info_by_file == NULL)
+    return NULL;
+
+  if (NULL != (info = g_hash_table_lookup (self->info_by_file, file)))
+    return info;
+
+  {
+    g_autofree gchar *path = g_file_get_path (file);
+    gsize len = strlen (path);
+
+    if (g_str_has_suffix (path, "-private.h"))
+      {
+        g_autofree gchar *other_path = NULL;
+        g_autoptr(GFile) other = NULL;
+
+        path[len - strlen ("-private.h")] = 0;
+
+        other_path = g_strconcat (path, ".c", NULL);
+        other = g_file_new_for_path (other_path);
+
+        if (NULL != (info = g_hash_table_lookup (self->info_by_file, other)))
+          return info;
+      }
+    else if (g_str_has_suffix (path, ".h"))
+      {
+        static const gchar *tries[] = { "c", "cc", "cpp" };
+        path[--len] = 0;
+
+        for (guint i = 0; i < G_N_ELEMENTS (tries); i++)
+          {
+            g_autofree gchar *other_path = g_strconcat (path, tries[i], NULL);
+            g_autoptr(GFile) other = g_file_new_for_path (other_path);
+
+            if (NULL != (info = g_hash_table_lookup (self->info_by_file, other)))
+              return info;
+          }
+      }
+  }
+
+  return NULL;
+}
+
+/**
+ * ide_compile_commands_lookup:
+ * @self: An #IdeCompileCommands
+ * @file: a #GFile representing the file to lookup
+ * @system_includes: system include dirs if any
+ * @directory: (out) (optional) (transfer full): A location for a #GFile, or %NULL
+ * @error: A location for a #GError, or %NULL
+ *
+ * Locates the commands to compile the @file requested.
+ *
+ * If @directory is non-NULL, then the directory to run the command from
+ * is placed in @directory.
+ *
+ * Returns: (nullable) (transfer full): A string array or %NULL if
+ *   there was a failure to locate or parse the command.
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_compile_commands_lookup (IdeCompileCommands   *self,
+                             GFile                *file,
+                             const gchar * const  *system_includes,
+                             GFile               **directory,
+                             GError              **error)
+{
+  g_autofree gchar *base = NULL;
+  const CompileInfo *info;
+  const gchar *dot;
+
+  g_return_val_if_fail (IDE_IS_COMPILE_COMMANDS (self), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  base = g_file_get_basename (file);
+  dot = strrchr (base, '.');
+
+  if (NULL != (info = find_with_alternates (self, file)))
+    {
+      g_auto(GStrv) argv = NULL;
+      gint argc = 0;
+
+      if (!g_shell_parse_argv (info->command, &argc, &argv, error))
+        return NULL;
+
+      if (suffix_is_c_like (dot))
+        ide_compile_commands_filter_c (self, info, system_includes, &argv);
+      else if (suffix_is_vala (dot))
+        ide_compile_commands_filter_vala (self, info, &argv);
+
+      if (directory != NULL)
+        *directory = g_file_dup (info->directory);
+
+      return g_steal_pointer (&argv);
+    }
+
+  /*
+   * Some compile-commands databases will give us info about .vala, but there
+   * may only be a single valac command to run. While we parsed the JSON
+   * document we stored information about each of the Vala files in a special
+   * list for exactly this purpose.
+   */
+  if (ide_str_equal0 (dot, ".vala") && self->vala_info != NULL)
+    {
+      for (guint i = 0; i < self->vala_info->len; i++)
+        {
+          g_auto(GStrv) argv = NULL;
+          gint argc = 0;
+
+          info = g_ptr_array_index (self->vala_info, i);
+
+          if (!g_shell_parse_argv (info->command, &argc, &argv, NULL))
+            continue;
+
+          ide_compile_commands_filter_vala (self, info, &argv);
+
+          if (directory != NULL)
+            *directory = g_object_ref (info->directory);
+
+          return g_steal_pointer (&argv);
+        }
+    }
+
+  g_set_error_literal (error,
+                       G_IO_ERROR,
+                       G_IO_ERROR_NOT_FOUND,
+                       "Failed to locate command for requested file");
+
+  return NULL;
+}
diff --git a/src/libide/foundry/ide-compile-commands.h b/src/libide/foundry/ide-compile-commands.h
new file mode 100644
index 000000000..8c88171a0
--- /dev/null
+++ b/src/libide/foundry/ide-compile-commands.h
@@ -0,0 +1,60 @@
+/* ide-compile-commands.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPILE_COMMANDS (ide_compile_commands_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeCompileCommands, ide_compile_commands, IDE, COMPILE_COMMANDS, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeCompileCommands  *ide_compile_commands_new         (void);
+IDE_AVAILABLE_IN_3_32
+gboolean             ide_compile_commands_load        (IdeCompileCommands   *self,
+                                                       GFile                *file,
+                                                       GCancellable         *cancellable,
+                                                       GError              **error);
+IDE_AVAILABLE_IN_3_32
+void                 ide_compile_commands_load_async  (IdeCompileCommands   *self,
+                                                       GFile                *file,
+                                                       GCancellable         *cancellable,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean             ide_compile_commands_load_finish (IdeCompileCommands   *self,
+                                                       GAsyncResult         *result,
+                                                       GError              **error);
+IDE_AVAILABLE_IN_3_32
+gchar              **ide_compile_commands_lookup      (IdeCompileCommands   *self,
+                                                       GFile                *file,
+                                                       const gchar * const  *system_includes,
+                                                       GFile               **directory,
+                                                       GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-configuration-manager.c b/src/libide/foundry/ide-configuration-manager.c
new file mode 100644
index 000000000..d5c712f12
--- /dev/null
+++ b/src/libide/foundry/ide-configuration-manager.c
@@ -0,0 +1,1149 @@
+/* ide-configuration-manager.c
+ *
+ * Copyright 2016-2019 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 "ide-configuration-manager"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+
+#include "ide-configuration-manager.h"
+#include "ide-configuration-private.h"
+#include "ide-configuration.h"
+#include "ide-configuration-provider.h"
+
+#define WRITEBACK_DELAY_SEC 3
+
+struct _IdeConfigurationManager
+{
+  GObject           parent_instance;
+
+  GCancellable     *cancellable;
+  GArray           *configs;
+  IdeConfiguration *current;
+  PeasExtensionSet *providers;
+  GSettings        *project_settings;
+
+  guint             queued_save_source;
+
+  guint             propagate_to_settings : 1;
+};
+
+typedef struct
+{
+  IdeConfigurationProvider *provider;
+  IdeConfiguration         *config;
+} ConfigInfo;
+
+static void async_initable_iface_init                   (GAsyncInitableIface     *iface);
+static void list_model_iface_init                       (GListModelInterface     *iface);
+static void ide_configuration_manager_save_tick         (IdeTask                 *task);
+static void ide_configuration_manager_actions_current   (IdeConfigurationManager *self,
+                                                         GVariant                *param);
+static void ide_configuration_manager_actions_delete    (IdeConfigurationManager *self,
+                                                         GVariant                *param);
+static void ide_configuration_manager_actions_duplicate (IdeConfigurationManager *self,
+                                                         GVariant                *param);
+
+DZL_DEFINE_ACTION_GROUP (IdeConfigurationManager, ide_configuration_manager, {
+  { "current", ide_configuration_manager_actions_current, "s" },
+  { "delete", ide_configuration_manager_actions_delete, "s" },
+  { "duplicate", ide_configuration_manager_actions_duplicate, "s" },
+})
+
+G_DEFINE_TYPE_EXTENDED (IdeConfigurationManager, ide_configuration_manager, IDE_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)
+                        G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init)
+                        G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
+                                               ide_configuration_manager_init_action_group))
+
+enum {
+  PROP_0,
+  PROP_CURRENT,
+  PROP_CURRENT_DISPLAY_NAME,
+  PROP_READY,
+  LAST_PROP
+};
+
+enum {
+  INVALIDATE,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [N_SIGNALS];
+
+static void
+config_info_clear (gpointer data)
+{
+  ConfigInfo *info = data;
+
+  g_clear_object (&info->config);
+  g_clear_object (&info->provider);
+}
+
+static void
+ide_configuration_manager_actions_current (IdeConfigurationManager *self,
+                                           GVariant                *param)
+{
+  IdeConfiguration *config;
+  const gchar *id;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+
+  id = g_variant_get_string (param, NULL);
+
+  if ((config = ide_configuration_manager_get_configuration (self, id)))
+    ide_configuration_manager_set_current (self, config);
+}
+
+static void
+ide_configuration_manager_actions_duplicate (IdeConfigurationManager *self,
+                                             GVariant                *param)
+{
+  IdeConfiguration *config;
+  const gchar *id;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+
+  id = g_variant_get_string (param, NULL);
+
+  if ((config = ide_configuration_manager_get_configuration (self, id)))
+    ide_configuration_manager_duplicate (self, config);
+}
+
+static void
+ide_configuration_manager_actions_delete (IdeConfigurationManager *self,
+                                          GVariant                *param)
+{
+  IdeConfiguration *config;
+  const gchar *id;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+
+  id = g_variant_get_string (param, NULL);
+
+  if ((config = ide_configuration_manager_get_configuration (self, id)))
+    ide_configuration_manager_delete (self, config);
+}
+
+static void
+ide_configuration_manager_collect_providers (PeasExtensionSet *set,
+                                             PeasPluginInfo   *plugin_info,
+                                             PeasExtension    *exten,
+                                             gpointer          user_data)
+{
+  IdeConfigurationProvider *provider = (IdeConfigurationProvider *)exten;
+  GPtrArray *providers = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+  g_assert (providers != NULL);
+
+  g_ptr_array_add (providers, g_object_ref (provider));
+}
+
+static void
+ide_configuration_manager_save_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeConfigurationProvider *provider = (IdeConfigurationProvider *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_configuration_provider_save_finish (provider, result, &error))
+    g_warning ("%s: %s", G_OBJECT_TYPE_NAME (provider), error->message);
+
+  ide_configuration_manager_save_tick (task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_configuration_manager_save_tick (IdeTask *task)
+{
+  IdeConfigurationProvider *provider;
+  GCancellable *cancellable;
+  GPtrArray *providers;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TASK (task));
+
+  providers = ide_task_get_task_data (task);
+  cancellable = ide_task_get_cancellable (task);
+
+  if (providers->len == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  provider = g_ptr_array_index (providers, providers->len - 1);
+
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+
+  ide_configuration_provider_save_async (provider,
+                                         cancellable,
+                                         ide_configuration_manager_save_cb,
+                                         g_object_ref (task));
+
+  g_ptr_array_remove_index (providers, providers->len - 1);
+
+  IDE_EXIT;
+}
+
+void
+ide_configuration_manager_save_async (IdeConfigurationManager *self,
+                                      GCancellable            *cancellable,
+                                      GAsyncReadyCallback      callback,
+                                      gpointer                 user_data)
+{
+  g_autoptr(GPtrArray) providers = NULL;
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_configuration_manager_save_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  providers = g_ptr_array_new_with_free_func (g_object_unref);
+  peas_extension_set_foreach (self->providers,
+                              ide_configuration_manager_collect_providers,
+                              providers);
+  ide_task_set_task_data (task, g_ptr_array_ref (providers), g_ptr_array_unref);
+
+  if (providers->len == 0)
+    ide_task_return_boolean (task, TRUE);
+  else
+    ide_configuration_manager_save_tick (task);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_configuration_manager_save_finish (IdeConfigurationManager  *self,
+                                       GAsyncResult             *result,
+                                       GError                  **error)
+{
+  g_return_val_if_fail (IDE_IS_CONFIGURATION_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_configuration_manager_get_configuration:
+ * @self: An #IdeConfigurationManager
+ * @id: The string identifier of the configuration
+ *
+ * Gets the #IdeConfiguration by id. See ide_configuration_get_id().
+ *
+ * Returns: (transfer none) (nullable): An #IdeConfiguration or %NULL if
+ *   the configuration could not be found.
+ *
+ * Since: 3.32
+ */
+IdeConfiguration *
+ide_configuration_manager_get_configuration (IdeConfigurationManager *self,
+                                             const gchar             *id)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONFIGURATION_MANAGER (self), NULL);
+  g_return_val_if_fail (id != NULL, NULL);
+
+  for (guint i = 0; i < self->configs->len; i++)
+    {
+      const ConfigInfo *info = &g_array_index (self->configs, ConfigInfo, i);
+
+      g_assert (IDE_IS_CONFIGURATION (info->config));
+
+      if (ide_str_equal0 (id, ide_configuration_get_id (info->config)))
+        return info->config;
+    }
+
+  return NULL;
+}
+
+static const gchar *
+ide_configuration_manager_get_display_name (IdeConfigurationManager *self)
+{
+  g_return_val_if_fail (IDE_IS_CONFIGURATION_MANAGER (self), NULL);
+
+  if (self->current != NULL)
+    return ide_configuration_get_display_name (self->current);
+
+  return "";
+}
+
+static void
+ide_configuration_manager_notify_display_name (IdeConfigurationManager *self,
+                                               GParamSpec              *pspec,
+                                               IdeConfiguration        *configuration)
+{
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (IDE_IS_CONFIGURATION (configuration));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_DISPLAY_NAME]);
+}
+
+static void
+ide_configuration_manager_notify_ready (IdeConfigurationManager *self,
+                                        GParamSpec              *pspec,
+                                        IdeConfiguration        *configuration)
+{
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (IDE_IS_CONFIGURATION (configuration));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_READY]);
+}
+
+static void
+ide_configuration_manager_dispose (GObject *object)
+{
+  IdeConfigurationManager *self = (IdeConfigurationManager *)object;
+
+  if (self->current != NULL)
+    {
+      g_signal_handlers_disconnect_by_func (self->current,
+                                            G_CALLBACK (ide_configuration_manager_notify_display_name),
+                                            self);
+      g_signal_handlers_disconnect_by_func (self->current,
+                                            G_CALLBACK (ide_configuration_manager_notify_ready),
+                                            self);
+    }
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->project_settings);
+
+  G_OBJECT_CLASS (ide_configuration_manager_parent_class)->dispose (object);
+}
+
+static void
+ide_configuration_manager_finalize (GObject *object)
+{
+  IdeConfigurationManager *self = (IdeConfigurationManager *)object;
+
+  g_clear_object (&self->current);
+  g_clear_object (&self->cancellable);
+  g_clear_pointer (&self->configs, g_array_unref);
+
+  G_OBJECT_CLASS (ide_configuration_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_configuration_manager_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  IdeConfigurationManager *self = IDE_CONFIGURATION_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CURRENT:
+      g_value_set_object (value, ide_configuration_manager_get_current (self));
+      break;
+
+    case PROP_CURRENT_DISPLAY_NAME:
+      g_value_set_string (value, ide_configuration_manager_get_display_name (self));
+      break;
+
+    case PROP_READY:
+      g_value_set_boolean (value, ide_configuration_manager_get_ready (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_configuration_manager_set_property (GObject      *object,
+                                        guint         prop_id,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  IdeConfigurationManager *self = IDE_CONFIGURATION_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CURRENT:
+      ide_configuration_manager_set_current (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_configuration_manager_class_init (IdeConfigurationManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_configuration_manager_dispose;
+  object_class->finalize = ide_configuration_manager_finalize;
+  object_class->get_property = ide_configuration_manager_get_property;
+  object_class->set_property = ide_configuration_manager_set_property;
+
+  properties [PROP_CURRENT] =
+    g_param_spec_object ("current",
+                         "Current",
+                         "The current configuration for the context",
+                         IDE_TYPE_CONFIGURATION,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CURRENT_DISPLAY_NAME] =
+    g_param_spec_string ("current-display-name",
+                         "Current Display Name",
+                         "The display name of the current configuration",
+                         NULL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_READY] =
+    g_param_spec_boolean ("ready",
+                          "Ready",
+                          "If the current configuration is ready",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  /**
+   * IdeConfigurationManager::invalidate:
+   * @self: an #IdeConfigurationManager
+   *
+   * This signal is emitted any time a new configuration is selected or the
+   * currently selected configurations state changes.
+   *
+   * Since: 3.32
+   */
+  signals [INVALIDATE] =
+    g_signal_new ("invalidate",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE, 0);
+}
+
+static void
+ide_configuration_manager_init (IdeConfigurationManager *self)
+{
+  self->cancellable = g_cancellable_new ();
+  self->configs = g_array_new (FALSE, FALSE, sizeof (ConfigInfo));
+  g_array_set_clear_func (self->configs, config_info_clear);
+}
+
+static GType
+ide_configuration_manager_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_CONFIGURATION;
+}
+
+static guint
+ide_configuration_manager_get_n_items (GListModel *model)
+{
+  IdeConfigurationManager *self = (IdeConfigurationManager *)model;
+
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (self->configs != NULL);
+
+  return self->configs->len;
+}
+
+static gpointer
+ide_configuration_manager_get_item (GListModel *model,
+                                    guint       position)
+{
+  IdeConfigurationManager *self = (IdeConfigurationManager *)model;
+  const ConfigInfo *info;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION_MANAGER (self), NULL);
+  g_return_val_if_fail (position < self->configs->len, NULL);
+
+  info = &g_array_index (self->configs, ConfigInfo, position);
+
+  return g_object_ref (info->config);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_configuration_manager_get_item_type;
+  iface->get_n_items = ide_configuration_manager_get_n_items;
+  iface->get_item = ide_configuration_manager_get_item;
+}
+
+static gboolean
+ide_configuration_manager_do_save (gpointer data)
+{
+  IdeConfigurationManager *self = data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+
+  self->queued_save_source = 0;
+
+  g_signal_emit (self, signals [INVALIDATE], 0);
+
+  ide_configuration_manager_save_async (self, NULL, NULL, NULL);
+
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+ide_configuration_manager_changed (IdeConfigurationManager *self,
+                                   IdeConfiguration        *config)
+{
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  g_clear_handle_id (&self->queued_save_source, g_source_remove);
+  self->queued_save_source =
+    g_timeout_add_seconds_full (G_PRIORITY_LOW,
+                                WRITEBACK_DELAY_SEC,
+                                ide_configuration_manager_do_save,
+                                g_object_ref (self),
+                                g_object_unref);
+}
+
+static void
+ide_configuration_manager_config_added (IdeConfigurationManager  *self,
+                                        IdeConfiguration         *config,
+                                        IdeConfigurationProvider *provider)
+{
+  ConfigInfo info = {0};
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+
+  g_signal_connect_object (config,
+                           "changed",
+                           G_CALLBACK (ide_configuration_manager_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  info.provider = g_object_ref (provider);
+  info.config = g_object_ref (config);
+  g_array_append_val (self->configs, info);
+
+  g_list_model_items_changed (G_LIST_MODEL (self), self->configs->len - 1, 0, 1);
+
+  if (self->current == NULL)
+    ide_configuration_manager_set_current (self, config);
+
+  _ide_configuration_attach (config);
+
+  IDE_EXIT;
+}
+
+static void
+ide_configuration_manager_config_removed (IdeConfigurationManager  *self,
+                                          IdeConfiguration         *config,
+                                          IdeConfigurationProvider *provider)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+
+  for (guint i = 0; i < self->configs->len; i++)
+    {
+      const ConfigInfo *info = &g_array_index (self->configs, ConfigInfo, i);
+
+      if (info->provider == provider && info->config == config)
+        {
+          g_signal_handlers_disconnect_by_func (config,
+                                                G_CALLBACK (ide_configuration_manager_changed),
+                                                self);
+          g_array_remove_index (self->configs, i);
+          g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+          break;
+        }
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_configuration_manager_provider_load_cb (GObject      *object,
+                                            GAsyncResult *result,
+                                            gpointer      user_data)
+{
+  IdeConfigurationProvider *provider = (IdeConfigurationProvider *)object;
+  IdeContext *context;
+  g_autoptr(IdeConfigurationManager) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  if (!ide_configuration_provider_load_finish (provider, result, &error))
+    ide_context_warning (context,
+                         "Failed to initialize config provider: %s: %s",
+                         G_OBJECT_TYPE_NAME (provider), error->message);
+
+  IDE_EXIT;
+}
+
+static void
+provider_connect (IdeConfigurationManager  *self,
+                  IdeConfigurationProvider *provider)
+{
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+
+  g_signal_connect_object (provider,
+                           "added",
+                           G_CALLBACK (ide_configuration_manager_config_added),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (provider,
+                           "removed",
+                           G_CALLBACK (ide_configuration_manager_config_removed),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+provider_disconnect (IdeConfigurationManager  *self,
+                     IdeConfigurationProvider *provider)
+{
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+
+  g_signal_handlers_disconnect_by_func (provider,
+                                        G_CALLBACK (ide_configuration_manager_config_added),
+                                        self);
+  g_signal_handlers_disconnect_by_func (provider,
+                                        G_CALLBACK (ide_configuration_manager_config_removed),
+                                        self);
+}
+
+static void
+ide_configuration_manager_provider_added (PeasExtensionSet *set,
+                                          PeasPluginInfo   *plugin_info,
+                                          PeasExtension    *exten,
+                                          gpointer          user_data)
+{
+  IdeConfigurationManager *self = user_data;
+  IdeConfigurationProvider *provider = (IdeConfigurationProvider *)exten;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+
+  provider_connect (self, provider);
+
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (provider));
+
+  ide_configuration_provider_load_async (provider,
+                                         self->cancellable,
+                                         ide_configuration_manager_provider_load_cb,
+                                         g_object_ref (self));
+}
+
+static void
+ide_configuration_manager_provider_removed (PeasExtensionSet *set,
+                                            PeasPluginInfo   *plugin_info,
+                                            PeasExtension    *exten,
+                                            gpointer          user_data)
+{
+  IdeConfigurationManager *self = user_data;
+  IdeConfigurationProvider *provider = (IdeConfigurationProvider *)exten;
+  g_autoptr(IdeConfigurationProvider) hold = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+
+  hold = g_object_ref (provider);
+
+  ide_configuration_provider_unload (provider);
+
+  provider_disconnect (self, provider);
+
+  for (guint i = self->configs->len; i > 0; i--)
+    {
+      const ConfigInfo *info = &g_array_index (self->configs, ConfigInfo, i - 1);
+
+      if (info->provider == provider)
+        {
+          g_warning ("%s failed to remove configuration \"%s\"",
+                     G_OBJECT_TYPE_NAME (provider),
+                     ide_configuration_get_id (info->config));
+          g_array_remove_index (self->configs, i);
+        }
+    }
+
+  ide_object_destroy (IDE_OBJECT (provider));
+}
+
+static void
+notify_providers_loaded (IdeConfigurationManager *self,
+                         GParamSpec              *pspec,
+                         IdeTask                 *task)
+{
+  g_autoptr(GVariant) user_value = NULL;
+
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_assert (IDE_IS_TASK (task));
+
+  if (self->project_settings == NULL)
+    return;
+
+  /*
+   * At this point, all of our configuratin providers have returned from
+   * their asynchronous loading. So we should have all of the configs we
+   * can know about at this point.
+   *
+   * We need to read our config-id from project_settings, and if we find
+   * a match, make that our active configuration.
+   *
+   * We want to avoid applying the value if the value is unchanged
+   * according to g_settings_get_user_value() so that we don't override
+   * any provider that set_current() during it's load, unless the user
+   * has manually set this config in the past.
+   *
+   * Once we have updated the current config, we can start propagating
+   * new values to the settings when set_current() is called.
+   */
+
+  user_value = g_settings_get_user_value (self->project_settings, "config-id");
+
+  if (user_value != NULL)
+    {
+      const gchar *str = g_variant_get_string (user_value, NULL);
+      IdeConfiguration *config;
+
+      if ((config = ide_configuration_manager_get_configuration (self, str)))
+        {
+          if (config != self->current)
+            ide_configuration_manager_set_current (self, config);
+        }
+    }
+
+  self->propagate_to_settings = TRUE;
+}
+
+static void
+ide_configuration_manager_init_load_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  IdeConfigurationProvider *provider = (IdeConfigurationProvider *)object;
+  IdeConfigurationManager *self;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  GPtrArray *providers;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (self));
+
+  if (!ide_configuration_provider_load_finish (provider, result, &error))
+    {
+      g_assert (error != NULL);
+      g_warning ("Failed to initialize config provider: %s: %s",
+                 G_OBJECT_TYPE_NAME (provider), error->message);
+    }
+
+  providers = ide_task_get_task_data (task);
+  g_assert (providers != NULL);
+  g_assert (providers->len > 0);
+
+  if (!g_ptr_array_remove (providers, provider))
+    g_critical ("Failed to locate provider in active set");
+
+  if (providers->len == 0)
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_configuration_manager_init_async (GAsyncInitable      *initable,
+                                      gint                 priority,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  IdeConfigurationManager *self = (IdeConfigurationManager *)initable;
+  g_autoptr(GPtrArray) providers = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  IdeContext *context;
+
+  g_assert (G_IS_ASYNC_INITABLE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_configuration_manager_init_async);
+  ide_task_set_priority (task, priority);
+
+  g_signal_connect_swapped (task,
+                            "notify::completed",
+                            G_CALLBACK (notify_providers_loaded),
+                            self);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  self->project_settings = ide_context_ref_project_settings (context);
+
+  self->providers = peas_extension_set_new (peas_engine_get_default (),
+                                            IDE_TYPE_CONFIGURATION_PROVIDER,
+                                            NULL);
+
+  g_signal_connect (self->providers,
+                    "extension-added",
+                    G_CALLBACK (ide_configuration_manager_provider_added),
+                    self);
+
+  g_signal_connect (self->providers,
+                    "extension-removed",
+                    G_CALLBACK (ide_configuration_manager_provider_removed),
+                    self);
+
+  /* We don't call ide_configuration_manager_provider_added() here for each
+   * of our providers because we want to be in control of the async lifetime
+   * and delay our init_async() completion until loaders have finished
+   */
+
+  providers = g_ptr_array_new_with_free_func (g_object_unref);
+  peas_extension_set_foreach (self->providers,
+                              ide_configuration_manager_collect_providers,
+                              providers);
+  ide_task_set_task_data (task, g_ptr_array_ref (providers), g_ptr_array_unref);
+
+  for (guint i = 0; i < providers->len; i++)
+    {
+      IdeConfigurationProvider *provider = g_ptr_array_index (providers, i);
+
+      g_assert (IDE_IS_CONFIGURATION_PROVIDER (provider));
+
+      provider_connect (self, provider);
+
+      ide_object_append (IDE_OBJECT (self), IDE_OBJECT (provider));
+
+      ide_configuration_provider_load_async (provider,
+                                             cancellable,
+                                             ide_configuration_manager_init_load_cb,
+                                             g_object_ref (task));
+    }
+
+  if (providers->len == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_configuration_manager_init_finish (GAsyncInitable  *initable,
+                                       GAsyncResult    *result,
+                                       GError         **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (initable));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+async_initable_iface_init (GAsyncInitableIface *iface)
+{
+  iface->init_async = ide_configuration_manager_init_async;
+  iface->init_finish = ide_configuration_manager_init_finish;
+}
+
+void
+ide_configuration_manager_set_current (IdeConfigurationManager *self,
+                                       IdeConfiguration        *current)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_return_if_fail (!current || IDE_IS_CONFIGURATION (current));
+
+  if (self->current != current)
+    {
+      if (self->current != NULL)
+        {
+          g_signal_handlers_disconnect_by_func (self->current,
+                                                G_CALLBACK (ide_configuration_manager_notify_display_name),
+                                                self);
+          g_signal_handlers_disconnect_by_func (self->current,
+                                                G_CALLBACK (ide_configuration_manager_notify_ready),
+                                                self);
+          g_clear_object (&self->current);
+        }
+
+      if (current != NULL)
+        {
+          self->current = g_object_ref (current);
+
+          g_signal_connect_object (current,
+                                   "notify::display-name",
+                                   G_CALLBACK (ide_configuration_manager_notify_display_name),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+          g_signal_connect_object (current,
+                                   "notify::ready",
+                                   G_CALLBACK (ide_configuration_manager_notify_ready),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+          if (self->propagate_to_settings && self->project_settings != NULL)
+            {
+              g_autofree gchar *new_id = g_strdup (ide_configuration_get_id (current));
+              g_settings_set_string (self->project_settings, "config-id", new_id);
+            }
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_DISPLAY_NAME]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_READY]);
+
+      g_signal_emit (self, signals [INVALIDATE], 0);
+    }
+}
+
+/**
+ * ide_configuration_manager_ref_current:
+ * @self: An #IdeConfigurationManager
+ *
+ * Gets the current configuration to use for building.
+ *
+ * Many systems allow you to pass a configuration in instead of relying on the
+ * default configuration. This gets the default configuration that various
+ * background items might use, such as tags builders which need to discover
+ * settings.
+ *
+ * Returns: (transfer full): An #IdeConfiguration
+ *
+ * Since: 3.32
+ */
+IdeConfiguration *
+ide_configuration_manager_ref_current (IdeConfigurationManager *self)
+{
+  g_autoptr(IdeConfiguration) ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION_MANAGER (self), NULL);
+  g_return_val_if_fail (self->current != NULL || self->configs->len > 0, NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+
+  if (self->current != NULL)
+    ret = g_object_ref (self->current);
+  else if (self->configs->len > 0)
+    {
+      const ConfigInfo *info = &g_array_index (self->configs, ConfigInfo, 0);
+
+      g_assert (IDE_IS_CONFIGURATION_PROVIDER (info->provider));
+      g_assert (IDE_IS_CONFIGURATION (info->config));
+
+      ret = g_object_ref (info->config);
+    }
+
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_configuration_manager_get_current:
+ * @self: An #IdeConfigurationManager
+ *
+ * Gets the current configuration to use for building.
+ *
+ * Many systems allow you to pass a configuration in instead of relying on the
+ * default configuration. This gets the default configuration that various
+ * background items might use, such as tags builders which need to discover
+ * settings.
+ *
+ * Returns: (transfer none): An #IdeConfiguration
+ *
+ * Since: 3.32
+ */
+IdeConfiguration *
+ide_configuration_manager_get_current (IdeConfigurationManager *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONFIGURATION_MANAGER (self), NULL);
+  g_return_val_if_fail (self->current != NULL || self->configs->len > 0, NULL);
+
+  if (self->current != NULL)
+    return self->current;
+
+  if (self->configs->len > 0)
+    {
+      const ConfigInfo *info = &g_array_index (self->configs, ConfigInfo, 0);
+
+      g_assert (IDE_IS_CONFIGURATION_PROVIDER (info->provider));
+      g_assert (IDE_IS_CONFIGURATION (info->config));
+
+      return info->config;
+    }
+
+  g_critical ("Failed to locate activate configuration. This should not happen.");
+
+  return NULL;
+}
+
+void
+ide_configuration_manager_duplicate (IdeConfigurationManager *self,
+                                     IdeConfiguration        *config)
+{
+  g_return_if_fail (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_return_if_fail (IDE_IS_CONFIGURATION (config));
+
+  for (guint i = 0; i < self->configs->len; i++)
+    {
+      const ConfigInfo *info = &g_array_index (self->configs, ConfigInfo, i);
+
+      g_assert (IDE_IS_CONFIGURATION_PROVIDER (info->provider));
+      g_assert (IDE_IS_CONFIGURATION (info->config));
+
+      if (info->config == config)
+        {
+          g_autoptr(IdeConfigurationProvider) provider = g_object_ref (info->provider);
+
+          info = NULL; /* info becomes invalid */
+          ide_configuration_provider_duplicate (provider, config);
+          ide_configuration_provider_save_async (provider, NULL, NULL, NULL);
+          break;
+        }
+    }
+}
+
+void
+ide_configuration_manager_delete (IdeConfigurationManager *self,
+                                  IdeConfiguration        *config)
+{
+  g_autoptr(IdeConfiguration) hold = NULL;
+
+  g_return_if_fail (IDE_IS_CONFIGURATION_MANAGER (self));
+  g_return_if_fail (IDE_IS_CONFIGURATION (config));
+
+  hold = g_object_ref (config);
+
+  for (guint i = 0; i < self->configs->len; i++)
+    {
+      const ConfigInfo *info = &g_array_index (self->configs, ConfigInfo, i);
+      g_autoptr(IdeConfigurationProvider) provider = NULL;
+
+      g_assert (IDE_IS_CONFIGURATION_PROVIDER (info->provider));
+      g_assert (IDE_IS_CONFIGURATION (info->config));
+
+      provider = g_object_ref (info->provider);
+
+      if (info->config == config)
+        {
+          info = NULL; /* info becomes invalid */
+          ide_configuration_provider_delete (provider, config);
+          ide_configuration_provider_save_async (provider, NULL, NULL, NULL);
+          break;
+        }
+    }
+}
+
+/**
+ * ide_configuration_manager_get_ready:
+ * @self: an #IdeConfigurationManager
+ *
+ * This returns %TRUE if the current configuration is ready for usage.
+ *
+ * This is equivalent to checking the ready property of the current
+ * configuration. It allows consumers to not need to track changes to
+ * the current configuration.
+ *
+ * Returns: %TRUE if the current configuration is ready for usage;
+ *   otherwise %FALSE.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_configuration_manager_get_ready (IdeConfigurationManager *self)
+{
+  IdeConfiguration *config;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION_MANAGER (self), FALSE);
+
+  if ((config = ide_configuration_manager_get_current (self)))
+    return ide_configuration_get_ready (config);
+
+  return FALSE;
+}
+
+/**
+ * ide_configuration_manager_ref_from_context:
+ * @context: an #IdeContext
+ *
+ * Thread-safe version of ide_configuration_manager_from_context().
+ *
+ * Returns: (transfer full): an #IdeConfigurationManager
+ *
+ * Since: 3.32
+ */
+IdeConfigurationManager *
+ide_configuration_manager_ref_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return ide_object_ensure_child_typed (IDE_OBJECT (context),
+                                        IDE_TYPE_CONFIGURATION_MANAGER);
+}
diff --git a/src/libide/foundry/ide-configuration-manager.h b/src/libide/foundry/ide-configuration-manager.h
new file mode 100644
index 000000000..040f58295
--- /dev/null
+++ b/src/libide/foundry/ide-configuration-manager.h
@@ -0,0 +1,70 @@
+/* ide-configuration-manager.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONFIGURATION_MANAGER (ide_configuration_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeConfigurationManager, ide_configuration_manager, IDE, CONFIGURATION_MANAGER, 
IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeConfigurationManager *ide_configuration_manager_from_context (IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+IdeConfigurationManager *ide_configuration_manager_ref_from_context (IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+IdeConfiguration *ide_configuration_manager_get_current       (IdeConfigurationManager  *self);
+IDE_AVAILABLE_IN_3_32
+IdeConfiguration *ide_configuration_manager_ref_current       (IdeConfigurationManager  *self);
+IDE_AVAILABLE_IN_3_32
+void              ide_configuration_manager_set_current       (IdeConfigurationManager  *self,
+                                                               IdeConfiguration         *configuration);
+IDE_AVAILABLE_IN_3_32
+IdeConfiguration *ide_configuration_manager_get_configuration (IdeConfigurationManager  *self,
+                                                               const gchar              *id);
+IDE_AVAILABLE_IN_3_32
+void              ide_configuration_manager_duplicate         (IdeConfigurationManager  *self,
+                                                               IdeConfiguration         *config);
+IDE_AVAILABLE_IN_3_32
+void              ide_configuration_manager_delete            (IdeConfigurationManager  *self,
+                                                               IdeConfiguration         *config);
+IDE_AVAILABLE_IN_3_32
+void              ide_configuration_manager_save_async        (IdeConfigurationManager  *self,
+                                                               GCancellable             *cancellable,
+                                                               GAsyncReadyCallback       callback,
+                                                               gpointer                  user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_configuration_manager_save_finish       (IdeConfigurationManager  *self,
+                                                               GAsyncResult             *result,
+                                                               GError                  **error);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_configuration_manager_get_ready         (IdeConfigurationManager  *self);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-configuration-private.h b/src/libide/foundry/ide-configuration-private.h
new file mode 100644
index 000000000..633ec9115
--- /dev/null
+++ b/src/libide/foundry/ide-configuration-private.h
@@ -0,0 +1,29 @@
+/* ide-configuration-private.h
+ *
+ * Copyright 2018-2019 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 "ide-configuration.h"
+
+G_BEGIN_DECLS
+
+void _ide_configuration_attach (IdeConfiguration *self);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-configuration-provider.c b/src/libide/foundry/ide-configuration-provider.c
new file mode 100644
index 000000000..d95347a1e
--- /dev/null
+++ b/src/libide/foundry/ide-configuration-provider.c
@@ -0,0 +1,397 @@
+/* ide-configuration-provider.c
+ *
+ * Copyright 2016 Matthew Leeds <mleeds 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 "ide-configuration-provider"
+
+#include "config.h"
+
+#include "ide-configuration.h"
+#include "ide-configuration-manager.h"
+#include "ide-configuration-provider.h"
+
+G_DEFINE_INTERFACE (IdeConfigurationProvider, ide_configuration_provider, IDE_TYPE_OBJECT)
+
+enum {
+  ADDED,
+  REMOVED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+ide_configuration_provider_real_load_async (IdeConfigurationProvider *self,
+                                            GCancellable             *cancellable,
+                                            GAsyncReadyCallback       callback,
+                                            gpointer                  user_data)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_task_report_new_error (self, callback, user_data,
+                           ide_configuration_provider_real_load_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "%s does not implement load_async",
+                           G_OBJECT_TYPE_NAME (self));
+}
+
+static gboolean
+ide_configuration_provider_real_load_finish (IdeConfigurationProvider  *self,
+                                             GAsyncResult              *result,
+                                             GError                   **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_assert (G_IS_TASK (result));
+  g_assert (g_task_is_valid (G_TASK (result), self));
+
+  return g_task_propagate_boolean (G_TASK (self), error);
+}
+
+static void
+ide_configuration_provider_real_duplicate (IdeConfigurationProvider *self,
+                                           IdeConfiguration         *config)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+}
+
+static void
+ide_configuration_provider_real_unload (IdeConfigurationProvider *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (self));
+
+}
+
+static void
+ide_configuration_provider_real_save_async (IdeConfigurationProvider *self,
+                                            GCancellable             *cancellable,
+                                            GAsyncReadyCallback       callback,
+                                            gpointer                  user_data)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_task_report_new_error (self, callback, user_data,
+                           ide_configuration_provider_real_save_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "%s does not implement save_async",
+                           G_OBJECT_TYPE_NAME (self));
+}
+
+static gboolean
+ide_configuration_provider_real_save_finish (IdeConfigurationProvider  *self,
+                                             GAsyncResult              *result,
+                                             GError                   **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_assert (G_IS_TASK (result));
+  g_assert (g_task_is_valid (G_TASK (result), self));
+
+  return g_task_propagate_boolean (G_TASK (self), error);
+}
+
+static void
+ide_configuration_provider_default_init (IdeConfigurationProviderInterface *iface)
+{
+  iface->load_async = ide_configuration_provider_real_load_async;
+  iface->load_finish = ide_configuration_provider_real_load_finish;
+  iface->duplicate = ide_configuration_provider_real_duplicate;
+  iface->unload = ide_configuration_provider_real_unload;
+  iface->save_async = ide_configuration_provider_real_save_async;
+  iface->save_finish = ide_configuration_provider_real_save_finish;
+
+  /**
+   * IdeConfigurationProvider:added:
+   * @self: an #IdeConfigurationProvider
+   * @config: an #IdeConfiguration
+   *
+   * The "added" signal is emitted when a configuration
+   * has been added to a configuration provider.
+   *
+   * Since: 3.32
+   */
+  signals [ADDED] =
+    g_signal_new ("added",
+                  G_TYPE_FROM_INTERFACE (iface),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeConfigurationProviderInterface, added),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_CONFIGURATION);
+  g_signal_set_va_marshaller (signals [ADDED],
+                              G_TYPE_FROM_INTERFACE (iface),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  /**
+   * IdeConfigurationProvider:removed:
+   * @self: an #IdeConfigurationProvider
+   * @config: an #IdeConfiguration
+   *
+   * The "removed" signal is emitted when a configuration
+   * has been removed from a configuration provider.
+   *
+   * Since: 3.32
+   */
+  signals [REMOVED] =
+    g_signal_new ("removed",
+                  G_TYPE_FROM_INTERFACE (iface),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeConfigurationProviderInterface, removed),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_CONFIGURATION);
+  g_signal_set_va_marshaller (signals [REMOVED],
+                              G_TYPE_FROM_INTERFACE (iface),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+}
+
+/**
+ * ide_configuration_provider_load_async:
+ * @self: a #IdeConfigurationProvider
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * This function is called to initialize the configuration provider after
+ * the plugin instance has been created. The provider should locate any
+ * build configurations within the project and call
+ * ide_configuration_provider_emit_added() before completing the
+ * asynchronous function so that the configuration manager may be made
+ * aware of the configurations.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_provider_load_async (IdeConfigurationProvider *self,
+                                       GCancellable             *cancellable,
+                                       GAsyncReadyCallback       callback,
+                                       gpointer                  user_data)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_CONFIGURATION_PROVIDER_GET_IFACE (self)->load_async (self, cancellable, callback, user_data);
+}
+
+/**
+ * ide_configuration_provider_load_finish:
+ * @self: a #IdeConfigurationProvider
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_configuration_provider_load_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_configuration_provider_load_finish (IdeConfigurationProvider  *self,
+                                        GAsyncResult              *result,
+                                        GError                   **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_CONFIGURATION_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_CONFIGURATION_PROVIDER_GET_IFACE (self)->load_finish (self, result, error);
+}
+
+/**
+ * ide_configuration_provider_unload:
+ * @self: a #IdeConfigurationProvider
+ *
+ * Requests that the configuration provider unload any state. This is called
+ * shortly before the configuration provider is finalized.
+ *
+ * Implementations of #IdeConfigurationProvider should emit removed
+ * for every configuration they have registered so that the
+ * #IdeConfigurationManager has correct information.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_provider_unload (IdeConfigurationProvider *self)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CONFIGURATION_PROVIDER (self));
+
+  IDE_CONFIGURATION_PROVIDER_GET_IFACE (self)->unload (self);
+}
+
+/**
+ * ide_configuration_provider_save_async:
+ * @self: a #IdeConfigurationProvider
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * This function is called to request that the configuration provider
+ * persist any changed configurations back to disk.
+ *
+ * This function will be called before unloading the configuration provider
+ * so that it has a chance to persist any outstanding changes.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_provider_save_async (IdeConfigurationProvider *self,
+                                       GCancellable             *cancellable,
+                                       GAsyncReadyCallback       callback,
+                                       gpointer                  user_data)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_CONFIGURATION_PROVIDER_GET_IFACE (self)->save_async (self, cancellable, callback, user_data);
+}
+
+/**
+ * ide_configuration_provider_save_finish:
+ * @self: a #IdeConfigurationProvider
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_configuration_provider_save_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_configuration_provider_save_finish (IdeConfigurationProvider  *self,
+                                        GAsyncResult              *result,
+                                        GError                   **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_CONFIGURATION_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_CONFIGURATION_PROVIDER_GET_IFACE (self)->save_finish (self, result, error);
+}
+
+/**
+ * ide_configuration_provider_emit_added:
+ * @self: an #IdeConfigurationProvider
+ * @config: an #IdeConfiguration
+ *
+ * #IdeConfigurationProvider implementations should call this function with
+ * a @config when it has discovered a new configuration.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_provider_emit_added (IdeConfigurationProvider *self,
+                                       IdeConfiguration         *config)
+{
+  g_return_if_fail (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_return_if_fail (IDE_IS_CONFIGURATION (config));
+
+  g_signal_emit (self, signals [ADDED], 0, config);
+}
+
+/**
+ * ide_configuration_provider_emit_removed:
+ * @self: an #IdeConfigurationProvider
+ * @config: an #IdeConfiguration
+ *
+ * #IdeConfigurationProvider implementations should call this function with
+ * a @config when it has discovered it was removed.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_provider_emit_removed (IdeConfigurationProvider *self,
+                                         IdeConfiguration         *config)
+{
+  g_return_if_fail (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_return_if_fail (IDE_IS_CONFIGURATION (config));
+
+  g_signal_emit (self, signals [REMOVED], 0, config);
+}
+
+/**
+ * ide_configuration_provider_delete:
+ * @self: a #IdeConfigurationProvider
+ * @config: an #IdeConfiguration owned by the provider
+ *
+ * Requests that the configuration provider delete the configuration.
+ *
+ * ide_configuration_provider_save_async() will be called by the
+ * #IdeConfigurationManager after calling this function.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_provider_delete (IdeConfigurationProvider *self,
+                                   IdeConfiguration         *config)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_return_if_fail (IDE_IS_CONFIGURATION (config));
+
+  if (IDE_CONFIGURATION_PROVIDER_GET_IFACE (self)->delete)
+    IDE_CONFIGURATION_PROVIDER_GET_IFACE (self)->delete (self, config);
+  else
+    g_warning ("Cannot delete configuration %s",
+               ide_configuration_get_id (config));
+}
+
+/**
+ * ide_configuration_provider_duplicate:
+ * @self: an #IdeConfigurationProvider
+ * @config: an #IdeConfiguration
+ *
+ * Requests that the configuration provider duplicate the configuration.
+ *
+ * This is useful when the user wants to experiment with alternate settings
+ * without breaking a previous configuration.
+ *
+ * The configuration provider does not need to persist the configuration
+ * in this function, ide_configuration_provider_save_async() will be called
+ * afterwards to persist configurations to disk.
+ *
+ * It is expected that the #IdeConfigurationProvider will emit
+ * #IdeConfigurationProvider::added with the new configuration.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_provider_duplicate (IdeConfigurationProvider *self,
+                                      IdeConfiguration         *config)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CONFIGURATION_PROVIDER (self));
+  g_return_if_fail (IDE_IS_CONFIGURATION (config));
+
+  IDE_CONFIGURATION_PROVIDER_GET_IFACE (self)->duplicate (self, config);
+}
diff --git a/src/libide/foundry/ide-configuration-provider.h b/src/libide/foundry/ide-configuration-provider.h
new file mode 100644
index 000000000..5ff84b7c3
--- /dev/null
+++ b/src/libide/foundry/ide-configuration-provider.h
@@ -0,0 +1,100 @@
+/* ide-configuration-provider.h
+ *
+ * Copyright 2016 Matthew Leeds <mleeds redhat com>
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONFIGURATION_PROVIDER (ide_configuration_provider_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeConfigurationProvider, ide_configuration_provider, IDE, CONFIGURATION_PROVIDER, 
IdeObject)
+
+struct _IdeConfigurationProviderInterface
+{
+  GTypeInterface parent_iface;
+
+  void     (*added)          (IdeConfigurationProvider  *self,
+                              IdeConfiguration          *config);
+  void     (*removed)        (IdeConfigurationProvider  *self,
+                              IdeConfiguration          *config);
+  void     (*load_async)     (IdeConfigurationProvider  *self,
+                              GCancellable              *cancellable,
+                              GAsyncReadyCallback        callback,
+                              gpointer                   user_data);
+  gboolean (*load_finish)    (IdeConfigurationProvider  *self,
+                              GAsyncResult              *result,
+                              GError                   **error);
+  void     (*save_async)     (IdeConfigurationProvider  *self,
+                              GCancellable              *cancellable,
+                              GAsyncReadyCallback        callback,
+                              gpointer                   user_data);
+  gboolean (*save_finish)    (IdeConfigurationProvider  *self,
+                              GAsyncResult              *result,
+                              GError                   **error);
+  void     (*delete)         (IdeConfigurationProvider  *self,
+                              IdeConfiguration          *config);
+  void     (*duplicate)      (IdeConfigurationProvider  *self,
+                              IdeConfiguration          *config);
+  void     (*unload)         (IdeConfigurationProvider  *self);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_configuration_provider_emit_added    (IdeConfigurationProvider  *self,
+                                                   IdeConfiguration          *config);
+IDE_AVAILABLE_IN_3_32
+void     ide_configuration_provider_emit_removed  (IdeConfigurationProvider  *self,
+                                                   IdeConfiguration          *config);
+IDE_AVAILABLE_IN_3_32
+void     ide_configuration_provider_load_async    (IdeConfigurationProvider  *self,
+                                                   GCancellable              *cancellable,
+                                                   GAsyncReadyCallback        callback,
+                                                   gpointer                   user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_configuration_provider_load_finish   (IdeConfigurationProvider  *self,
+                                                   GAsyncResult              *result,
+                                                   GError                   **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_configuration_provider_save_async    (IdeConfigurationProvider  *self,
+                                                   GCancellable              *cancellable,
+                                                   GAsyncReadyCallback        callback,
+                                                   gpointer                   user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_configuration_provider_save_finish   (IdeConfigurationProvider  *self,
+                                                   GAsyncResult              *result,
+                                                   GError                   **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_configuration_provider_delete        (IdeConfigurationProvider  *self,
+                                                   IdeConfiguration          *config);
+void     ide_configuration_provider_duplicate     (IdeConfigurationProvider  *self,
+                                                   IdeConfiguration          *config);
+IDE_AVAILABLE_IN_3_32
+void     ide_configuration_provider_unload        (IdeConfigurationProvider  *self);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-configuration.c b/src/libide/foundry/ide-configuration.c
new file mode 100644
index 000000000..3efafce34
--- /dev/null
+++ b/src/libide/foundry/ide-configuration.c
@@ -0,0 +1,1724 @@
+/* ide-configuration.c
+ *
+ * Copyright 2016-2019 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 "ide-configuration"
+
+#include "config.h"
+
+#include <string.h>
+
+#include "ide-configuration-manager.h"
+#include "ide-configuration-private.h"
+#include "ide-configuration.h"
+#include "ide-foundry-enums.h"
+#include "ide-foundry-compat.h"
+#include "ide-runtime-manager.h"
+#include "ide-runtime.h"
+#include "ide-toolchain-manager.h"
+#include "ide-toolchain.h"
+
+typedef struct
+{
+  gchar          *app_id;
+  gchar         **build_commands;
+  gchar          *config_opts;
+  gchar          *display_name;
+  gchar          *id;
+  gchar         **post_install_commands;
+  gchar          *prefix;
+  gchar          *run_opts;
+  gchar          *runtime_id;
+  gchar          *toolchain_id;
+  gchar          *append_path;
+
+  GFile          *build_commands_dir;
+
+  IdeEnvironment *environment;
+
+  GHashTable     *internal;
+
+  gint            parallelism;
+  guint           sequence;
+
+  guint           block_changed;
+
+  guint           dirty : 1;
+  guint           debug : 1;
+  guint           has_attached : 1;
+
+  /*
+   * This is used to determine if we can make progress building
+   * with this configuration. When runtimes are added/removed, the
+   * IdeConfiguration:ready property will be notified.
+   */
+  guint           runtime_ready : 1;
+
+  IdeBuildLocality locality : 3;
+} IdeConfigurationPrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (IdeConfiguration, ide_configuration, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_APPEND_PATH,
+  PROP_APP_ID,
+  PROP_BUILD_COMMANDS,
+  PROP_BUILD_COMMANDS_DIR,
+  PROP_CONFIG_OPTS,
+  PROP_DEBUG,
+  PROP_DIRTY,
+  PROP_DISPLAY_NAME,
+  PROP_ENVIRON,
+  PROP_ID,
+  PROP_LOCALITY,
+  PROP_PARALLELISM,
+  PROP_POST_INSTALL_COMMANDS,
+  PROP_PREFIX,
+  PROP_READY,
+  PROP_RUNTIME,
+  PROP_RUNTIME_ID,
+  PROP_TOOLCHAIN_ID,
+  PROP_TOOLCHAIN,
+  PROP_RUN_OPTS,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+_value_free (gpointer data)
+{
+  GValue *value = data;
+
+  if (value != NULL)
+    {
+      g_value_unset (value);
+      g_slice_free (GValue, value);
+    }
+}
+
+static GValue *
+_value_new (GType type)
+{
+  GValue *value;
+
+  value = g_slice_new0 (GValue);
+  g_value_init (value, type);
+
+  return value;
+}
+
+static void
+ide_configuration_block_changed (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_assert (IDE_IS_CONFIGURATION (self));
+
+  priv->block_changed++;
+}
+
+static void
+ide_configuration_unblock_changed (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_assert (IDE_IS_CONFIGURATION (self));
+
+  priv->block_changed--;
+}
+
+static void
+ide_configuration_emit_changed (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_assert (IDE_IS_CONFIGURATION (self));
+
+  if (priv->block_changed == 0)
+    g_signal_emit (self, signals [CHANGED], 0);
+}
+
+static IdeRuntime *
+ide_configuration_real_get_runtime (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  if (priv->runtime_id != NULL)
+    {
+      IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
+      IdeRuntimeManager *runtime_manager = ide_runtime_manager_from_context (context);
+      return ide_runtime_manager_get_runtime (runtime_manager, priv->runtime_id);;
+    }
+
+  return NULL;
+}
+
+static void
+ide_configuration_set_id (IdeConfiguration *self,
+                          const gchar      *id)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (id != NULL);
+
+  if (g_strcmp0 (id, priv->id) != 0)
+    {
+      g_free (priv->id);
+      priv->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+    }
+}
+
+static void
+ide_configuration_runtime_manager_items_changed (IdeConfiguration  *self,
+                                                 guint              position,
+                                                 guint              added,
+                                                 guint              removed,
+                                                 IdeRuntimeManager *runtime_manager)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+  IdeRuntime *runtime;
+  gboolean runtime_ready;
+
+  g_assert (IDE_IS_CONFIGURATION (self));
+
+  if (ide_object_in_destruction (IDE_OBJECT (self)))
+    return;
+
+  g_assert (IDE_IS_RUNTIME_MANAGER (runtime_manager));
+
+  runtime = ide_runtime_manager_get_runtime (runtime_manager, priv->runtime_id);
+  runtime_ready = !!runtime;
+
+  if (!priv->runtime_ready && runtime_ready)
+    ide_runtime_prepare_configuration (runtime, self);
+
+  if (runtime_ready != priv->runtime_ready)
+    {
+      priv->runtime_ready = runtime_ready;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_READY]);
+    }
+}
+
+static void
+ide_configuration_environment_changed (IdeConfiguration *self,
+                                       IdeEnvironment   *environment)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_CONFIGURATION (self));
+  g_assert (IDE_IS_ENVIRONMENT (environment));
+
+  if (ide_object_in_destruction (IDE_OBJECT (self)))
+    return;
+
+  ide_configuration_set_dirty (self, TRUE);
+  ide_configuration_emit_changed (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_configuration_real_set_runtime (IdeConfiguration *self,
+                                    IdeRuntime       *runtime)
+{
+  const gchar *runtime_id = "host";
+
+  g_assert (IDE_IS_CONFIGURATION (self));
+  g_assert (!runtime || IDE_IS_RUNTIME (runtime));
+
+  if (runtime != NULL)
+    runtime_id = ide_runtime_get_id (runtime);
+
+  ide_configuration_set_runtime_id (self, runtime_id);
+}
+
+static gchar *
+ide_configuration_repr (IdeObject *object)
+{
+  IdeConfiguration *self = (IdeConfiguration *)object;
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION (self));
+
+  return g_strdup_printf ("%s id=\"%s\" name=\"%s\" runtime=\"%s\"",
+                          G_OBJECT_TYPE_NAME (self),
+                          priv->id,
+                          priv->display_name,
+                          priv->runtime_id);
+}
+
+static void
+ide_configuration_finalize (GObject *object)
+{
+  IdeConfiguration *self = (IdeConfiguration *)object;
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_clear_object (&priv->build_commands_dir);
+  g_clear_object (&priv->environment);
+
+  g_clear_pointer (&priv->build_commands, g_strfreev);
+  g_clear_pointer (&priv->internal, g_hash_table_unref);
+  g_clear_pointer (&priv->config_opts, g_free);
+  g_clear_pointer (&priv->display_name, g_free);
+  g_clear_pointer (&priv->id, g_free);
+  g_clear_pointer (&priv->post_install_commands, g_strfreev);
+  g_clear_pointer (&priv->prefix, g_free);
+  g_clear_pointer (&priv->runtime_id, g_free);
+  g_clear_pointer (&priv->app_id, g_free);
+  g_clear_pointer (&priv->toolchain_id, g_free);
+
+  G_OBJECT_CLASS (ide_configuration_parent_class)->finalize (object);
+}
+
+static void
+ide_configuration_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeConfiguration *self = IDE_CONFIGURATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONFIG_OPTS:
+      g_value_set_string (value, ide_configuration_get_config_opts (self));
+      break;
+
+    case PROP_BUILD_COMMANDS:
+      g_value_set_boxed (value, ide_configuration_get_build_commands (self));
+      break;
+
+    case PROP_BUILD_COMMANDS_DIR:
+      g_value_set_object (value, ide_configuration_get_build_commands_dir (self));
+      break;
+
+    case PROP_DEBUG:
+      g_value_set_boolean (value, ide_configuration_get_debug (self));
+      break;
+
+    case PROP_DIRTY:
+      g_value_set_boolean (value, ide_configuration_get_dirty (self));
+      break;
+
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, ide_configuration_get_display_name (self));
+      break;
+
+    case PROP_ENVIRON:
+      g_value_set_boxed (value, ide_configuration_get_environ (self));
+      break;
+
+    case PROP_ID:
+      g_value_set_string (value, ide_configuration_get_id (self));
+      break;
+
+    case PROP_PARALLELISM:
+      g_value_set_int (value, ide_configuration_get_parallelism (self));
+      break;
+
+    case PROP_READY:
+      g_value_set_boolean (value, ide_configuration_get_ready (self));
+      break;
+
+    case PROP_POST_INSTALL_COMMANDS:
+      g_value_set_boxed (value, ide_configuration_get_post_install_commands (self));
+      break;
+
+    case PROP_PREFIX:
+      g_value_set_string (value, ide_configuration_get_prefix (self));
+      break;
+
+    case PROP_RUNTIME:
+      g_value_set_object (value, ide_configuration_get_runtime (self));
+      break;
+
+    case PROP_RUNTIME_ID:
+      g_value_set_string (value, ide_configuration_get_runtime_id (self));
+      break;
+
+    case PROP_TOOLCHAIN:
+      g_value_take_object (value, ide_configuration_get_toolchain (self));
+      break;
+
+    case PROP_TOOLCHAIN_ID:
+      g_value_set_string (value, ide_configuration_get_toolchain_id (self));
+      break;
+
+    case PROP_RUN_OPTS:
+      g_value_set_string (value, ide_configuration_get_run_opts (self));
+      break;
+
+    case PROP_APP_ID:
+      g_value_set_string (value, ide_configuration_get_app_id (self));
+      break;
+
+    case PROP_APPEND_PATH:
+      g_value_set_string (value, ide_configuration_get_append_path (self));
+      break;
+
+    case PROP_LOCALITY:
+      g_value_set_flags (value, ide_configuration_get_locality (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_configuration_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeConfiguration *self = IDE_CONFIGURATION (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONFIG_OPTS:
+      ide_configuration_set_config_opts (self, g_value_get_string (value));
+      break;
+
+    case PROP_BUILD_COMMANDS:
+      ide_configuration_set_build_commands (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_BUILD_COMMANDS_DIR:
+      ide_configuration_set_build_commands_dir (self, g_value_get_object (value));
+      break;
+
+    case PROP_DEBUG:
+      ide_configuration_set_debug (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_DIRTY:
+      ide_configuration_set_dirty (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_DISPLAY_NAME:
+      ide_configuration_set_display_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ID:
+      ide_configuration_set_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_POST_INSTALL_COMMANDS:
+      ide_configuration_set_post_install_commands (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_PREFIX:
+      ide_configuration_set_prefix (self, g_value_get_string (value));
+      break;
+
+    case PROP_PARALLELISM:
+      ide_configuration_set_parallelism (self, g_value_get_int (value));
+      break;
+
+    case PROP_RUNTIME:
+      ide_configuration_set_runtime (self, g_value_get_object (value));
+      break;
+
+    case PROP_RUNTIME_ID:
+      ide_configuration_set_runtime_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_TOOLCHAIN:
+      ide_configuration_set_toolchain (self, g_value_get_object (value));
+      break;
+
+    case PROP_TOOLCHAIN_ID:
+      ide_configuration_set_toolchain_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_RUN_OPTS:
+      ide_configuration_set_run_opts (self, g_value_get_string (value));
+      break;
+
+    case PROP_APP_ID:
+      ide_configuration_set_app_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_APPEND_PATH:
+      ide_configuration_set_append_path (self, g_value_get_string (value));
+      break;
+
+    case PROP_LOCALITY:
+      ide_configuration_set_locality (self, g_value_get_flags (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_configuration_class_init (IdeConfigurationClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_configuration_finalize;
+  object_class->get_property = ide_configuration_get_property;
+  object_class->set_property = ide_configuration_set_property;
+
+  i_object_class->repr = ide_configuration_repr;
+
+  klass->get_runtime = ide_configuration_real_get_runtime;
+  klass->set_runtime = ide_configuration_real_set_runtime;
+
+  properties [PROP_APPEND_PATH] =
+    g_param_spec_string ("append-path",
+                         "Append Path",
+                         "Append to PATH environment variable",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_BUILD_COMMANDS] =
+    g_param_spec_boxed ("build-commands",
+                        "Build commands",
+                        "Build commands",
+                        G_TYPE_STRV,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_BUILD_COMMANDS_DIR] =
+    g_param_spec_object ("build-commands-dir",
+                        "Build commands Dir",
+                        "Directory to run build commands from",
+                        G_TYPE_FILE,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CONFIG_OPTS] =
+    g_param_spec_string ("config-opts",
+                         "Config Options",
+                         "Parameters to bootstrap the project",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DEBUG] =
+    g_param_spec_boolean ("debug",
+                          "Debug",
+                          "Debug",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DIRTY] =
+    g_param_spec_boolean ("dirty",
+                          "Dirty",
+                          "If the configuration has been changed.",
+                          FALSE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DISPLAY_NAME] =
+    g_param_spec_string ("display-name",
+                         "Display Name",
+                         "Display Name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ENVIRON] =
+    g_param_spec_boxed ("environ",
+                        "Environ",
+                        "Environ",
+                        G_TYPE_STRV,
+                        (G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "Id",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PARALLELISM] =
+    g_param_spec_int ("parallelism",
+                      "Parallelism",
+                      "Parallelism",
+                      -1,
+                      G_MAXINT,
+                      -1,
+                      (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_POST_INSTALL_COMMANDS] =
+    g_param_spec_boxed ("post-install-commands",
+                        "Post install commands",
+                        "Post install commands",
+                        G_TYPE_STRV,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PREFIX] =
+    g_param_spec_string ("prefix",
+                         "Prefix",
+                         "Prefix",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_READY] =
+    g_param_spec_boolean ("ready",
+                          "Ready",
+                          "If the configuration can be used for building",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RUN_OPTS] =
+    g_param_spec_string ("run-opts",
+                         "Run Options",
+                         "The options for running the target application",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RUNTIME] =
+    g_param_spec_object ("runtime",
+                         "Runtime",
+                         "Runtime",
+                         IDE_TYPE_RUNTIME,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RUNTIME_ID] =
+    g_param_spec_string ("runtime-id",
+                         "Runtime Id",
+                         "The identifier of the runtime",
+                         "host",
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TOOLCHAIN] =
+    g_param_spec_object ("toolchain",
+                         "Toolchain",
+                         "Toolchain",
+                         IDE_TYPE_TOOLCHAIN,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TOOLCHAIN_ID] =
+    g_param_spec_string ("toolchain-id",
+                         "Toolchain Id",
+                         "The identifier of the toolchain",
+                         "default",
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_APP_ID] =
+    g_param_spec_string ("app-id",
+                         "App ID",
+                         "The application ID (such as org.gnome.Builder)",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LOCALITY] =
+    g_param_spec_flags ("locality",
+                        "Locality",
+                        "Where the build may occur",
+                        IDE_TYPE_BUILD_LOCALITY,
+                        IDE_BUILD_LOCALITY_DEFAULT,
+                        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+static void
+ide_configuration_init (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+  g_autoptr(IdeEnvironment) env = ide_environment_new ();
+
+  priv->runtime_id = g_strdup ("host");
+  priv->toolchain_id = g_strdup ("default");
+  priv->debug = TRUE;
+  priv->parallelism = -1;
+  priv->locality = IDE_BUILD_LOCALITY_DEFAULT;
+  priv->internal = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, _value_free);
+
+  ide_configuration_set_environment (self, env);
+}
+
+/**
+ * ide_configuration_get_app_id:
+ * @self: An #IdeConfiguration
+ *
+ * Gets the application ID for the configuration.
+ *
+ * Returns: (transfer none) (nullable): A string.
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_configuration_get_app_id (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->app_id;
+}
+
+void
+ide_configuration_set_app_id (IdeConfiguration *self,
+                              const gchar      *app_id)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (priv->app_id != app_id)
+    {
+      g_free (priv->app_id);
+      priv->app_id = g_strdup (app_id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_APP_ID]);
+    }
+}
+
+const gchar *
+ide_configuration_get_runtime_id (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->runtime_id;
+}
+
+void
+ide_configuration_set_runtime_id (IdeConfiguration *self,
+                                  const gchar      *runtime_id)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (runtime_id == NULL)
+    runtime_id = "host";
+
+  if (g_strcmp0 (runtime_id, priv->runtime_id) != 0)
+    {
+      priv->runtime_ready = FALSE;
+      g_free (priv->runtime_id);
+      priv->runtime_id = g_strdup (runtime_id);
+
+      ide_configuration_set_dirty (self, TRUE);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNTIME_ID]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUNTIME]);
+
+      if (priv->has_attached)
+        {
+          IdeRuntimeManager *runtime_manager;
+          IdeContext *context;
+
+          g_assert (IDE_IS_MAIN_THREAD ());
+
+          context = ide_object_get_context (IDE_OBJECT (self));
+          runtime_manager = ide_runtime_manager_from_context (context);
+          ide_configuration_runtime_manager_items_changed (self, 0, 0, 0, runtime_manager);
+
+          ide_configuration_emit_changed (self);
+        }
+    }
+}
+
+/**
+ * ide_configuration_get_toolchain_id:
+ * @self: An #IdeConfiguration
+ *
+ * Gets the toolchain id for the configuration.
+ *
+ * Returns: (transfer none) (nullable): The id of an #IdeToolchain or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_configuration_get_toolchain_id (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->toolchain_id;
+}
+
+/**
+ * ide_configuration_set_toolchain_id:
+ * @self: An #IdeConfiguration
+ * @toolchain_id: The id of an #IdeToolchain
+ *
+ * Sets the toolchain id for the configuration.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_set_toolchain_id (IdeConfiguration *self,
+                                    const gchar      *toolchain_id)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (toolchain_id == NULL)
+    toolchain_id = "default";
+
+  if (g_strcmp0 (toolchain_id, priv->toolchain_id) != 0)
+    {
+      g_free (priv->toolchain_id);
+      priv->toolchain_id = g_strdup (toolchain_id);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TOOLCHAIN_ID]);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TOOLCHAIN]);
+
+      ide_configuration_set_dirty (self, TRUE);
+      ide_configuration_emit_changed (self);
+    }
+}
+
+/**
+ * ide_configuration_get_runtime:
+ * @self: An #IdeConfiguration
+ *
+ * Gets the runtime for the configuration.
+ *
+ * Returns: (transfer none) (nullable): An #IdeRuntime
+ *
+ * Since: 3.32
+ */
+IdeRuntime *
+ide_configuration_get_runtime (IdeConfiguration *self)
+{
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return IDE_CONFIGURATION_GET_CLASS (self)->get_runtime (self);
+}
+
+void
+ide_configuration_set_runtime (IdeConfiguration *self,
+                               IdeRuntime       *runtime)
+{
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (!runtime || IDE_IS_RUNTIME (runtime));
+
+  IDE_CONFIGURATION_GET_CLASS (self)->set_runtime (self, runtime);
+}
+
+/**
+ * ide_configuration_get_toolchain:
+ * @self: An #IdeConfiguration
+ *
+ * Gets the toolchain for the configuration.
+ *
+ * Returns: (transfer full) (nullable): An #IdeToolchain
+ *
+ * Since: 3.32
+ */
+IdeToolchain *
+ide_configuration_get_toolchain (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  if (priv->toolchain_id != NULL)
+    {
+      IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
+      IdeToolchainManager *toolchain_manager = ide_toolchain_manager_from_context (context);
+      g_autoptr (IdeToolchain) toolchain = ide_toolchain_manager_get_toolchain (toolchain_manager, 
priv->toolchain_id);
+
+      if (toolchain != NULL)
+        return g_steal_pointer (&toolchain);
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_configuration_set_toolchain:
+ * @self: An #IdeConfiguration
+ * @toolchain: (nullable): An #IdeToolchain or %NULL to use the default one
+ *
+ * Sets the toolchain for the configuration.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_set_toolchain (IdeConfiguration *self,
+                                 IdeToolchain     *toolchain)
+{
+  const gchar *toolchain_id = "default";
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (!toolchain || IDE_IS_TOOLCHAIN (toolchain));
+
+  if (toolchain != NULL)
+    toolchain_id = ide_toolchain_get_id (toolchain);
+
+  ide_configuration_set_toolchain_id (self, toolchain_id);
+}
+
+/**
+ * ide_configuration_get_environ:
+ * @self: An #IdeConfiguration
+ *
+ * Gets the environment to use when spawning processes.
+ *
+ * Returns: (transfer full): An array of key=value environment variables.
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_configuration_get_environ (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return ide_environment_get_environ (priv->environment);
+}
+
+const gchar *
+ide_configuration_getenv (IdeConfiguration *self,
+                          const gchar      *key)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  return ide_environment_getenv (priv->environment, key);
+}
+
+void
+ide_configuration_setenv (IdeConfiguration *self,
+                          const gchar      *key,
+                          const gchar      *value)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (key != NULL);
+
+  ide_environment_setenv (priv->environment, key, value);
+}
+
+const gchar *
+ide_configuration_get_id (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->id;
+}
+
+const gchar *
+ide_configuration_get_prefix (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->prefix;
+}
+
+void
+ide_configuration_set_prefix (IdeConfiguration *self,
+                              const gchar      *prefix)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (g_strcmp0 (prefix, priv->prefix) != 0)
+    {
+      g_free (priv->prefix);
+      priv->prefix = g_strdup (prefix);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PREFIX]);
+      ide_configuration_set_dirty (self, TRUE);
+    }
+}
+
+gint
+ide_configuration_get_parallelism (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), -1);
+
+  if (priv->parallelism == -1)
+    {
+      g_autoptr(GSettings) settings = g_settings_new ("org.gnome.builder.build");
+
+      return g_settings_get_int (settings, "parallel");
+    }
+
+  return priv->parallelism;
+}
+
+void
+ide_configuration_set_parallelism (IdeConfiguration *self,
+                                   gint              parallelism)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (parallelism >= -1);
+
+  if (parallelism != priv->parallelism)
+    {
+      priv->parallelism = parallelism;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PARALLELISM]);
+    }
+}
+
+gboolean
+ide_configuration_get_debug (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), FALSE);
+
+  return priv->debug;
+}
+
+void
+ide_configuration_set_debug (IdeConfiguration *self,
+                             gboolean          debug)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  debug = !!debug;
+
+  if (debug != priv->debug)
+    {
+      priv->debug = debug;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DEBUG]);
+      ide_configuration_set_dirty (self, TRUE);
+    }
+}
+
+const gchar *
+ide_configuration_get_display_name (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->display_name;
+}
+
+void
+ide_configuration_set_display_name (IdeConfiguration *self,
+                                    const gchar      *display_name)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (g_strcmp0 (display_name, priv->display_name) != 0)
+    {
+      g_free (priv->display_name);
+      priv->display_name = g_strdup (display_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
+      ide_configuration_emit_changed (self);
+    }
+}
+
+gboolean
+ide_configuration_get_dirty (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), FALSE);
+
+  return priv->dirty;
+}
+
+void
+ide_configuration_set_dirty (IdeConfiguration *self,
+                             gboolean          dirty)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (priv->block_changed)
+    IDE_EXIT;
+
+  dirty = !!dirty;
+
+  if (dirty != priv->dirty)
+    {
+      priv->dirty = dirty;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DIRTY]);
+    }
+
+  if (dirty)
+    {
+      /*
+       * Emit the changed signal so that the configuration manager
+       * can queue a writeback of the configuration. If we are
+       * clearing the dirty bit, then we don't need to do this.
+       */
+      priv->sequence++;
+      IDE_TRACE_MSG ("configuration set dirty with sequence %u", priv->sequence);
+      ide_configuration_emit_changed (self);
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_configuration_get_environment:
+ *
+ * Returns: (transfer none): An #IdeEnvironment.
+ *
+ * Since: 3.32
+ */
+IdeEnvironment *
+ide_configuration_get_environment (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->environment;
+}
+
+void
+ide_configuration_set_environment (IdeConfiguration *self,
+                                   IdeEnvironment   *environment)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (!environment || IDE_IS_ENVIRONMENT (environment));
+
+  if (priv->environment != environment)
+    {
+      if (priv->environment != NULL)
+        {
+          g_signal_handlers_disconnect_by_func (priv->environment,
+                                                G_CALLBACK (ide_configuration_environment_changed),
+                                                self);
+          g_clear_object (&priv->environment);
+        }
+
+      if (environment != NULL)
+        {
+          priv->environment = g_object_ref (environment);
+          g_signal_connect_object (priv->environment,
+                                   "changed",
+                                   G_CALLBACK (ide_configuration_environment_changed),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ENVIRON]);
+    }
+}
+
+const gchar *
+ide_configuration_get_config_opts (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->config_opts;
+}
+
+void
+ide_configuration_set_config_opts (IdeConfiguration *self,
+                                   const gchar      *config_opts)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (g_strcmp0 (config_opts, priv->config_opts) != 0)
+    {
+      g_free (priv->config_opts);
+      priv->config_opts = g_strdup (config_opts);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONFIG_OPTS]);
+      ide_configuration_set_dirty (self, TRUE);
+    }
+}
+
+const gchar * const *
+ide_configuration_get_build_commands (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return (const gchar * const *)priv->build_commands;
+}
+
+void
+ide_configuration_set_build_commands (IdeConfiguration *self,
+                                      const gchar * const     *build_commands)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (priv->build_commands != (gchar **)build_commands)
+    {
+      g_strfreev (priv->build_commands);
+      priv->build_commands = g_strdupv ((gchar **)build_commands);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUILD_COMMANDS]);
+    }
+}
+
+const gchar * const *
+ide_configuration_get_post_install_commands (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return (const gchar * const *)priv->post_install_commands;
+}
+
+void
+ide_configuration_set_post_install_commands (IdeConfiguration    *self,
+                                             const gchar * const *post_install_commands)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (priv->post_install_commands != (gchar **)post_install_commands)
+    {
+      g_strfreev (priv->post_install_commands);
+      priv->post_install_commands = g_strdupv ((gchar **)post_install_commands);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POST_INSTALL_COMMANDS]);
+    }
+}
+
+/**
+ * ide_configuration_get_sequence:
+ * @self: An #IdeConfiguration
+ *
+ * This returns a sequence number for the configuration. This is useful
+ * for build systems that want to clear the "dirty" bit on the configuration
+ * so that they need not bootstrap a second time. This should be done by
+ * checking the sequence number before executing the bootstrap, and only
+ * cleared if the sequence number matches after performing the bootstrap.
+ * This indicates no changes have been made to the configuration in the
+ * mean time.
+ *
+ * Returns: A monotonic sequence number.
+ *
+ * Since: 3.32
+ */
+guint
+ide_configuration_get_sequence (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), 0);
+
+  return priv->sequence;
+}
+
+static GValue *
+ide_configuration_reset_internal_value (IdeConfiguration *self,
+                                        const gchar      *key,
+                                        GType             type)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+  GValue *v;
+
+  g_assert (IDE_IS_CONFIGURATION (self));
+  g_assert (key != NULL);
+  g_assert (type != G_TYPE_INVALID);
+
+  v = g_hash_table_lookup (priv->internal, key);
+
+  if (v == NULL)
+    {
+      v = _value_new (type);
+      g_hash_table_insert (priv->internal, g_strdup (key), v);
+    }
+  else
+    {
+      g_value_unset (v);
+      g_value_init (v, type);
+    }
+
+  return v;
+}
+
+const gchar *
+ide_configuration_get_internal_string (IdeConfiguration *self,
+                                       const gchar      *key)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+  const GValue *v;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  v = g_hash_table_lookup (priv->internal, key);
+
+  if (v != NULL && G_VALUE_HOLDS_STRING (v))
+    return g_value_get_string (v);
+
+  return NULL;
+}
+
+void
+ide_configuration_set_internal_string (IdeConfiguration *self,
+                                       const gchar      *key,
+                                       const gchar      *value)
+{
+  GValue *v;
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (key != NULL);
+
+  v = ide_configuration_reset_internal_value (self, key, G_TYPE_STRING);
+  g_value_set_string (v, value);
+}
+
+const gchar * const *
+ide_configuration_get_internal_strv (IdeConfiguration *self,
+                                     const gchar      *key)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+  const GValue *v;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  v = g_hash_table_lookup (priv->internal, key);
+
+  if (v != NULL && G_VALUE_HOLDS (v, G_TYPE_STRV))
+    return g_value_get_boxed (v);
+
+  return NULL;
+}
+
+void
+ide_configuration_set_internal_strv (IdeConfiguration    *self,
+                                     const gchar         *key,
+                                     const gchar * const *value)
+{
+  GValue *v;
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (key != NULL);
+
+  v = ide_configuration_reset_internal_value (self, key, G_TYPE_STRV);
+  g_value_set_boxed (v, value);
+}
+
+gboolean
+ide_configuration_get_internal_boolean (IdeConfiguration *self,
+                                        const gchar      *key)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+  const GValue *v;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), FALSE);
+  g_return_val_if_fail (key != NULL, FALSE);
+
+  v = g_hash_table_lookup (priv->internal, key);
+
+  if (v != NULL && G_VALUE_HOLDS_BOOLEAN (v))
+    return g_value_get_boolean (v);
+
+  return FALSE;
+}
+
+void
+ide_configuration_set_internal_boolean (IdeConfiguration  *self,
+                                        const gchar       *key,
+                                        gboolean           value)
+{
+  GValue *v;
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (key != NULL);
+
+  v = ide_configuration_reset_internal_value (self, key, G_TYPE_BOOLEAN);
+  g_value_set_boolean (v, value);
+}
+
+gint
+ide_configuration_get_internal_int (IdeConfiguration *self,
+                                    const gchar      *key)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+  const GValue *v;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), -1);
+  g_return_val_if_fail (key != NULL, -1);
+
+  v = g_hash_table_lookup (priv->internal, key);
+
+  if (v != NULL && G_VALUE_HOLDS_INT (v))
+    return g_value_get_int (v);
+
+  return 0;
+}
+
+void
+ide_configuration_set_internal_int (IdeConfiguration *self,
+                                    const gchar      *key,
+                                    gint              value)
+{
+  GValue *v;
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (key != NULL);
+
+  v = ide_configuration_reset_internal_value (self, key, G_TYPE_INT);
+  g_value_set_int (v, value);
+}
+
+gint64
+ide_configuration_get_internal_int64 (IdeConfiguration *self,
+                                      const gchar      *key)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+  const GValue *v;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), -1);
+  g_return_val_if_fail (key != NULL, -1);
+
+  v = g_hash_table_lookup (priv->internal, key);
+
+  if (v != NULL && G_VALUE_HOLDS_INT64 (v))
+    return g_value_get_int64 (v);
+
+  return 0;
+}
+
+void
+ide_configuration_set_internal_int64 (IdeConfiguration *self,
+                                      const gchar      *key,
+                                      gint64            value)
+{
+  GValue *v;
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (key != NULL);
+
+  v = ide_configuration_reset_internal_value (self, key, G_TYPE_INT64);
+  g_value_set_int64 (v, value);
+}
+
+/**
+ * ide_configuration_get_internal_object:
+ * @self: An #IdeConfiguration
+ * @key: The key to get
+ *
+ * Gets the value associated with @key if it is a #GObject.
+ *
+ * Returns: (nullable) (transfer none) (type GObject.Object): a #GObject or %NULL.
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_configuration_get_internal_object (IdeConfiguration *self,
+                                       const gchar      *key)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+  const GValue *v;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  v = g_hash_table_lookup (priv->internal, key);
+
+  if (v != NULL && G_VALUE_HOLDS_OBJECT (v))
+    return g_value_get_object (v);
+
+  return NULL;
+}
+
+/**
+ * ide_configuration_set_internal_object:
+ * @self: an #IdeConfiguration
+ * @key: the key to set
+ * @instance: (type GObject.Object) (nullable): a #GObject or %NULL
+ *
+ * Sets the value for @key to @instance.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_set_internal_object (IdeConfiguration *self,
+                                       const gchar      *key,
+                                       gpointer          instance)
+{
+  GValue *v;
+  GType type;
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (key != NULL);
+
+  if (instance != NULL)
+    type = G_OBJECT_TYPE (instance);
+  else
+    type = G_TYPE_OBJECT;
+
+  v = ide_configuration_reset_internal_value (self, key, type);
+  g_value_set_object (v, instance);
+}
+
+/**
+ * ide_configuration_get_ready:
+ * @self: An #IdeConfiguration
+ *
+ * Determines if the configuration is ready for use.
+ *
+ * Returns: %TRUE if the configuration is ready for use.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_configuration_get_ready (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), FALSE);
+
+  return priv->runtime_ready;
+}
+
+gboolean
+ide_configuration_supports_runtime (IdeConfiguration *self,
+                                    IdeRuntime       *runtime)
+{
+  gboolean ret = TRUE;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), FALSE);
+  g_return_val_if_fail (IDE_IS_RUNTIME (runtime), FALSE);
+
+  if (IDE_CONFIGURATION_GET_CLASS (self)->supports_runtime)
+    ret = IDE_CONFIGURATION_GET_CLASS (self)->supports_runtime (self, runtime);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_configuration_get_run_opts:
+ * @self: a #IdeConfiguration
+ *
+ * Gets the command line options to use when running the target application.
+ * The result should be parsed with g_shell_parse_argv() to convert the run
+ * options to an array suitable for use in argv.
+ *
+ * Returns: (transfer none) (nullable): A string containing the run options
+ *   or %NULL if none have been set.
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_configuration_get_run_opts (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->run_opts;
+}
+
+/**
+ * ide_configuration_set_run_opts:
+ * @self: a #IdeConfiguration
+ * @run_opts: (nullable): the run options for the target application
+ *
+ * Sets the run options to use when running the target application.
+ * See ide_configuration_get_run_opts() for more information.
+ *
+ * Since: 3.32
+ */
+void
+ide_configuration_set_run_opts (IdeConfiguration *self,
+                                const gchar      *run_opts)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (g_strcmp0 (run_opts, priv->run_opts) != 0)
+    {
+      g_free (priv->run_opts);
+      priv->run_opts = g_strdup (run_opts);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUN_OPTS]);
+    }
+}
+
+const gchar *
+ide_configuration_get_append_path (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->append_path;
+}
+
+void
+ide_configuration_set_append_path (IdeConfiguration *self,
+                                   const gchar      *append_path)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+
+  if (priv->append_path != append_path)
+    {
+      g_free (priv->append_path);
+      priv->append_path = g_strdup (append_path);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_APPEND_PATH]);
+    }
+}
+
+void
+ide_configuration_apply_path (IdeConfiguration      *self,
+                              IdeSubprocessLauncher *launcher)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (launcher));
+
+  if (priv->append_path != NULL)
+    ide_subprocess_launcher_append_path (launcher, priv->append_path);
+}
+
+IdeBuildLocality
+ide_configuration_get_locality (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), 0);
+
+  return priv->locality;
+}
+
+void
+ide_configuration_set_locality (IdeConfiguration *self,
+                                IdeBuildLocality  locality)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (locality > 0);
+  g_return_if_fail (locality <= IDE_BUILD_LOCALITY_DEFAULT);
+
+  if (priv->locality != locality)
+    {
+      priv->locality = locality;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LOCALITY]);
+    }
+}
+
+/**
+ * ide_configuration_get_build_commands_dir:
+ * @self: a #IdeConfiguration
+ *
+ * Returns: (transfer none) (nullable): a #GFile or %NULL
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_configuration_get_build_commands_dir (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (self), NULL);
+
+  return priv->build_commands_dir;
+}
+
+void
+ide_configuration_set_build_commands_dir (IdeConfiguration *self,
+                                          GFile            *build_commands_dir)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (!build_commands_dir || G_IS_FILE (build_commands_dir));
+
+  if (g_set_object (&priv->build_commands_dir, build_commands_dir))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUILD_COMMANDS_DIR]);
+}
+
+void
+_ide_configuration_attach (IdeConfiguration *self)
+{
+  IdeConfigurationPrivate *priv = ide_configuration_get_instance_private (self);
+  IdeRuntimeManager *runtime_manager;
+  IdeContext *context;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CONFIGURATION (self));
+  g_return_if_fail (priv->has_attached == FALSE);
+
+  priv->has_attached = TRUE;
+
+  /*
+   * We don't start monitoring changed events until we've gotten back
+   * to the main loop (in case of threaded loaders) which happens from
+   * the point where the configuration is added ot the config manager.
+   */
+
+  if (!(context = ide_object_get_context (IDE_OBJECT (self))))
+    {
+      g_critical ("Attempt to register configuration without a context");
+      return;
+    }
+
+  runtime_manager = ide_runtime_manager_from_context (context);
+
+  g_signal_connect_object (runtime_manager,
+                           "items-changed",
+                           G_CALLBACK (ide_configuration_runtime_manager_items_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* Update the runtime and potentially set prefix, but do not emit changed */
+  ide_configuration_block_changed (self);
+  ide_configuration_runtime_manager_items_changed (self, 0, 0, 0, runtime_manager);
+  ide_configuration_unblock_changed (self);
+}
diff --git a/src/libide/foundry/ide-configuration.h b/src/libide/foundry/ide-configuration.h
new file mode 100644
index 000000000..62344ef48
--- /dev/null
+++ b/src/libide/foundry/ide-configuration.h
@@ -0,0 +1,214 @@
+/* ide-configuration.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONFIGURATION (ide_configuration_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeConfiguration, ide_configuration, IDE, CONFIGURATION, IdeObject)
+
+typedef enum
+{
+  IDE_BUILD_LOCALITY_IN_TREE     = 1 << 0,
+  IDE_BUILD_LOCALITY_OUT_OF_TREE = 1 << 1,
+  IDE_BUILD_LOCALITY_DEFAULT     = IDE_BUILD_LOCALITY_IN_TREE | IDE_BUILD_LOCALITY_OUT_OF_TREE,
+} IdeBuildLocality;
+
+struct _IdeConfigurationClass
+{
+  IdeObjectClass parent;
+
+  IdeRuntime *(*get_runtime)      (IdeConfiguration *self);
+  void        (*set_runtime)      (IdeConfiguration *self,
+                                   IdeRuntime       *runtime);
+  gboolean    (*supports_runtime) (IdeConfiguration *self,
+                                   IdeRuntime       *runtime);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_get_append_path           (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_append_path           (IdeConfiguration      *self,
+                                                                   const gchar           *append_path);
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_get_id                    (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_get_runtime_id            (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_runtime_id            (IdeConfiguration      *self,
+                                                                   const gchar           *runtime_id);
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_get_toolchain_id          (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_toolchain_id          (IdeConfiguration      *self,
+                                                                   const gchar           *toolchain_id);
+IDE_AVAILABLE_IN_3_32
+gboolean              ide_configuration_get_dirty                 (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_dirty                 (IdeConfiguration      *self,
+                                                                   gboolean               dirty);
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_get_display_name          (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_display_name          (IdeConfiguration      *self,
+                                                                   const gchar           *display_name);
+IDE_AVAILABLE_IN_3_32
+IdeBuildLocality      ide_configuration_get_locality              (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_locality              (IdeConfiguration      *self,
+                                                                   IdeBuildLocality       locality);
+IDE_AVAILABLE_IN_3_32
+gboolean              ide_configuration_get_ready                 (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+IdeRuntime           *ide_configuration_get_runtime               (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_runtime               (IdeConfiguration      *self,
+                                                                   IdeRuntime            *runtime);
+IDE_AVAILABLE_IN_3_32
+IdeToolchain         *ide_configuration_get_toolchain             (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_toolchain             (IdeConfiguration      *self,
+                                                                   IdeToolchain          *toolchain);
+IDE_AVAILABLE_IN_3_32
+gchar               **ide_configuration_get_environ               (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_getenv                    (IdeConfiguration      *self,
+                                                                   const gchar           *key);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_setenv                    (IdeConfiguration      *self,
+                                                                   const gchar           *key,
+                                                                   const gchar           *value);
+IDE_AVAILABLE_IN_3_32
+gboolean              ide_configuration_get_debug                 (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_debug                 (IdeConfiguration      *self,
+                                                                   gboolean               debug);
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_get_prefix                (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_prefix                (IdeConfiguration      *self,
+                                                                   const gchar           *prefix);
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_get_config_opts           (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_config_opts           (IdeConfiguration      *self,
+                                                                   const gchar           *config_opts);
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_get_run_opts              (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_run_opts              (IdeConfiguration      *self,
+                                                                   const gchar           *run_opts);
+IDE_AVAILABLE_IN_3_32
+const gchar * const  *ide_configuration_get_build_commands        (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_build_commands        (IdeConfiguration      *self,
+                                                                   const gchar *const    *build_commands);
+IDE_AVAILABLE_IN_3_32
+GFile                *ide_configuration_get_build_commands_dir    (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_build_commands_dir    (IdeConfiguration      *self,
+                                                                   GFile                 
*build_commands_dir);
+IDE_AVAILABLE_IN_3_32
+const gchar * const  *ide_configuration_get_post_install_commands (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_post_install_commands (IdeConfiguration      *self,
+                                                                   const gchar *const    
*post_install_commands);
+IDE_AVAILABLE_IN_3_32
+gint                  ide_configuration_get_parallelism           (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_parallelism           (IdeConfiguration      *self,
+                                                                   gint                   parallelism);
+IDE_AVAILABLE_IN_3_32
+IdeEnvironment       *ide_configuration_get_environment           (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_environment           (IdeConfiguration      *self,
+                                                                   IdeEnvironment        *environment);
+IDE_AVAILABLE_IN_3_32
+guint                 ide_configuration_get_sequence              (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_get_app_id                (IdeConfiguration      *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_app_id                (IdeConfiguration      *self,
+                                                                   const gchar           *app_id);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_apply_path                (IdeConfiguration      *self,
+                                                                   IdeSubprocessLauncher *launcher);
+IDE_AVAILABLE_IN_3_32
+gboolean              ide_configuration_supports_runtime          (IdeConfiguration      *self,
+                                                                   IdeRuntime            *runtime);
+IDE_AVAILABLE_IN_3_32
+const gchar          *ide_configuration_get_internal_string       (IdeConfiguration      *self,
+                                                                   const gchar           *key);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_internal_string       (IdeConfiguration      *self,
+                                                                   const gchar           *key,
+                                                                   const gchar           *value);
+IDE_AVAILABLE_IN_3_32
+const gchar * const  *ide_configuration_get_internal_strv         (IdeConfiguration      *self,
+                                                                   const gchar           *key);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_internal_strv         (IdeConfiguration      *self,
+                                                                   const gchar           *key,
+                                                                   const gchar *const    *value);
+IDE_AVAILABLE_IN_3_32
+gboolean              ide_configuration_get_internal_boolean      (IdeConfiguration      *self,
+                                                                   const gchar           *key);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_internal_boolean      (IdeConfiguration      *self,
+                                                                   const gchar           *key,
+                                                                   gboolean               value);
+IDE_AVAILABLE_IN_3_32
+gint                  ide_configuration_get_internal_int          (IdeConfiguration      *self,
+                                                                   const gchar           *key);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_internal_int          (IdeConfiguration      *self,
+                                                                   const gchar           *key,
+                                                                   gint                   value);
+IDE_AVAILABLE_IN_3_32
+gint64                ide_configuration_get_internal_int64        (IdeConfiguration      *self,
+                                                                   const gchar           *key);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_internal_int64        (IdeConfiguration      *self,
+                                                                   const gchar           *key,
+                                                                   gint64                 value);
+IDE_AVAILABLE_IN_3_32
+gpointer              ide_configuration_get_internal_object       (IdeConfiguration      *self,
+                                                                   const gchar           *key);
+IDE_AVAILABLE_IN_3_32
+void                  ide_configuration_set_internal_object       (IdeConfiguration      *self,
+                                                                   const gchar           *key,
+                                                                   gpointer               instance);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-dependency-updater.c b/src/libide/foundry/ide-dependency-updater.c
new file mode 100644
index 000000000..f54460ca5
--- /dev/null
+++ b/src/libide/foundry/ide-dependency-updater.c
@@ -0,0 +1,83 @@
+/* ide-dependency-updater.c
+ *
+ * Copyright 2017-2019 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 "ide-dependency-updater"
+
+#include "config.h"
+
+#include "ide-dependency-updater.h"
+
+G_DEFINE_INTERFACE (IdeDependencyUpdater, ide_dependency_updater, IDE_TYPE_OBJECT)
+
+static void
+ide_dependency_updater_real_update_async (IdeDependencyUpdater *self,
+                                          GCancellable         *cancellable,
+                                          GAsyncReadyCallback   callback,
+                                          gpointer              user_data)
+{
+  g_task_report_new_error (self,
+                           callback,
+                           user_data,
+                           ide_dependency_updater_real_update_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "update_async is not supported");
+}
+
+static gboolean
+ide_dependency_updater_real_update_finish (IdeDependencyUpdater  *self,
+                                           GAsyncResult          *result,
+                                           GError               **error)
+{
+  g_assert (IDE_IS_DEPENDENCY_UPDATER (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_dependency_updater_default_init (IdeDependencyUpdaterInterface *iface)
+{
+  iface->update_async = ide_dependency_updater_real_update_async;
+  iface->update_finish = ide_dependency_updater_real_update_finish;
+}
+
+void
+ide_dependency_updater_update_async (IdeDependencyUpdater *self,
+                                     GCancellable         *cancellable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              user_data)
+{
+  g_return_if_fail (IDE_IS_DEPENDENCY_UPDATER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_DEPENDENCY_UPDATER_GET_IFACE (self)->update_async (self, cancellable, callback, user_data);
+}
+
+gboolean
+ide_dependency_updater_update_finish (IdeDependencyUpdater  *self,
+                                      GAsyncResult          *result,
+                                      GError               **error)
+{
+  g_return_val_if_fail (IDE_IS_DEPENDENCY_UPDATER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_DEPENDENCY_UPDATER_GET_IFACE (self)->update_finish (self, result, error);
+}
diff --git a/src/libide/foundry/ide-dependency-updater.h b/src/libide/foundry/ide-dependency-updater.h
new file mode 100644
index 000000000..e585c8f1d
--- /dev/null
+++ b/src/libide/foundry/ide-dependency-updater.h
@@ -0,0 +1,59 @@
+/* ide-dependency-updater.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEPENDENCY_UPDATER (ide_dependency_updater_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeDependencyUpdater, ide_dependency_updater, IDE, DEPENDENCY_UPDATER, IdeObject)
+
+struct _IdeDependencyUpdaterInterface
+{
+  GTypeInterface parent;
+
+  void     (*update_async)  (IdeDependencyUpdater  *self,
+                             GCancellable          *cancellable,
+                             GAsyncReadyCallback    callback,
+                             gpointer               user_data);
+  gboolean (*update_finish) (IdeDependencyUpdater  *self,
+                             GAsyncResult          *result,
+                             GError               **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_dependency_updater_update_async  (IdeDependencyUpdater  *self,
+                                               GCancellable          *cancellable,
+                                               GAsyncReadyCallback    callback,
+                                               gpointer               user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_dependency_updater_update_finish (IdeDependencyUpdater  *self,
+                                               GAsyncResult          *result,
+                                               GError               **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-deploy-strategy.c b/src/libide/foundry/ide-deploy-strategy.c
new file mode 100644
index 000000000..fcfddcb40
--- /dev/null
+++ b/src/libide/foundry/ide-deploy-strategy.c
@@ -0,0 +1,248 @@
+/* ide-deploy-strategy.c
+ *
+ * Copyright 2018-2019 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 "ide-deploy-strategy"
+
+#include "config.h"
+
+#include "ide-debug.h"
+
+#include "ide-build-pipeline.h"
+#include "ide-deploy-strategy.h"
+
+G_DEFINE_ABSTRACT_TYPE (IdeDeployStrategy, ide_deploy_strategy, IDE_TYPE_OBJECT)
+
+static void
+ide_deploy_strategy_real_load_async (IdeDeployStrategy   *self,
+                                     IdeBuildPipeline    *pipeline,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_deploy_strategy_real_load_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "%s does not support the current pipeline",
+                           G_OBJECT_TYPE_NAME (self));
+}
+
+static gboolean
+ide_deploy_strategy_real_load_finish (IdeDeployStrategy  *self,
+                                      GAsyncResult       *result,
+                                      GError            **error)
+{
+  g_assert (IDE_IS_DEPLOY_STRATEGY (self));
+  g_assert (G_IS_TASK (result));
+  g_assert (g_task_is_valid (G_TASK (result), self));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_deploy_strategy_real_deploy_async (IdeDeployStrategy     *self,
+                                       IdeBuildPipeline      *pipeline,
+                                       GFileProgressCallback  progress,
+                                       gpointer               progress_data,
+                                       GDestroyNotify         progress_data_destroy,
+                                       GCancellable          *cancellable,
+                                       GAsyncReadyCallback    callback,
+                                       gpointer               user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_deploy_strategy_real_deploy_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "%s does not support the current pipeline",
+                           G_OBJECT_TYPE_NAME (self));
+}
+
+static gboolean
+ide_deploy_strategy_real_deploy_finish (IdeDeployStrategy  *self,
+                                        GAsyncResult       *result,
+                                        GError            **error)
+{
+  g_assert (IDE_IS_DEPLOY_STRATEGY (self));
+  g_assert (G_IS_TASK (result));
+  g_assert (g_task_is_valid (G_TASK (result), self));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_deploy_strategy_class_init (IdeDeployStrategyClass *klass)
+{
+  klass->load_async = ide_deploy_strategy_real_load_async;
+  klass->load_finish = ide_deploy_strategy_real_load_finish;
+  klass->deploy_async = ide_deploy_strategy_real_deploy_async;
+  klass->deploy_finish = ide_deploy_strategy_real_deploy_finish;
+}
+
+static void
+ide_deploy_strategy_init (IdeDeployStrategy *self)
+{
+}
+
+/**
+ * ide_deploy_strategy_load_async:
+ * @self: an #IdeDeployStrategy
+ * @pipeline: an #IdeBuildPipeline
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously requests that the #IdeDeployStrategy load anything
+ * necessary to support deployment for @pipeline. If the strategy cannot
+ * support the pipeline, it should fail with %G_IO_ERROR error domain
+ * and %G_IO_ERROR_NOT_SUPPORTED error code.
+ *
+ * Generally, the deployment strategy is responsible for checking if
+ * it can support deployment to the given device, and determine how to
+ * get the install data out of the pipeline. Given so many moving parts
+ * in build systems, how to determine that is an implementation detail of
+ * the specific #IdeDeployStrategy.
+ *
+ * Since: 3.32
+ */
+void
+ide_deploy_strategy_load_async (IdeDeployStrategy   *self,
+                                IdeBuildPipeline    *pipeline,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEPLOY_STRATEGY (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_DEPLOY_STRATEGY_GET_CLASS (self)->load_async (self, pipeline, cancellable, callback, user_data);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_deploy_strategy_load_finish:
+ * @self: an #IdeDeployStrategy
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to load the #IdeDeployStrategy.
+ *
+ * Returns: %TRUE if successful and the pipeline was supported; otherwise
+ *   %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_deploy_strategy_load_finish (IdeDeployStrategy  *self,
+                                 GAsyncResult       *result,
+                                 GError            **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEPLOY_STRATEGY (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  ret = IDE_DEPLOY_STRATEGY_GET_CLASS (self)->load_finish (self, result, error);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_deploy_strategy_deploy_async:
+ * @self: a #IdeDeployStrategy
+ * @pipeline: an #IdeBuildPipeline
+ * @progress: (nullable) (closure progress_data) (scope notified):
+ *   a #GFileProgressCallback or %NULL
+ * @progress_data: (nullable): closure data for @progress or %NULL
+ * @progress_data_destroy: (nullable): destroy callback for @progress_data
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: (closure user_data): a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Requests that the #IdeDeployStrategy deploy the application to the
+ * configured device in the build pipeline.
+ *
+ * If supported, the strategy will call @progress with periodic updates as
+ * the application is deployed.
+ *
+ * Since: 3.32
+ */
+void
+ide_deploy_strategy_deploy_async (IdeDeployStrategy     *self,
+                                  IdeBuildPipeline      *pipeline,
+                                  GFileProgressCallback  progress,
+                                  gpointer               progress_data,
+                                  GDestroyNotify         progress_data_destroy,
+                                  GCancellable          *cancellable,
+                                  GAsyncReadyCallback    callback,
+                                  gpointer               user_data)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEPLOY_STRATEGY (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_DEPLOY_STRATEGY_GET_CLASS (self)->deploy_async (self,
+                                                      pipeline,
+                                                      progress,
+                                                      progress_data,
+                                                      progress_data_destroy,
+                                                      cancellable,
+                                                      callback,
+                                                      user_data);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_deploy_strategy_deploy_finish:
+ * @self: an #IdeDeployStrategy
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError or %NULL
+ *
+ * Completes an asynchronous request to deploy the application to the
+ * build pipeline's device.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_deploy_strategy_deploy_finish (IdeDeployStrategy  *self,
+                                   GAsyncResult       *result,
+                                   GError            **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEPLOY_STRATEGY (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  ret = IDE_DEPLOY_STRATEGY_GET_CLASS (self)->deploy_finish (self, result, error);
+
+  IDE_RETURN (ret);
+}
diff --git a/src/libide/foundry/ide-deploy-strategy.h b/src/libide/foundry/ide-deploy-strategy.h
new file mode 100644
index 000000000..74159f93b
--- /dev/null
+++ b/src/libide/foundry/ide-deploy-strategy.h
@@ -0,0 +1,87 @@
+/* ide-deploy-strategy.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEPLOY_STRATEGY (ide_deploy_strategy_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeDeployStrategy, ide_deploy_strategy, IDE, DEPLOY_STRATEGY, IdeObject)
+
+struct _IdeDeployStrategyClass
+{
+  IdeObjectClass parent;
+
+  void     (*load_async)    (IdeDeployStrategy     *self,
+                             IdeBuildPipeline      *pipeline,
+                             GCancellable          *cancellable,
+                             GAsyncReadyCallback    callback,
+                             gpointer               user_data);
+  gboolean (*load_finish)   (IdeDeployStrategy     *self,
+                             GAsyncResult          *result,
+                             GError               **error);
+  void     (*deploy_async)  (IdeDeployStrategy     *self,
+                             IdeBuildPipeline      *pipeline,
+                             GFileProgressCallback  progress,
+                             gpointer               progress_data,
+                             GDestroyNotify         progress_data_destroy,
+                             GCancellable          *cancellable,
+                             GAsyncReadyCallback    callback,
+                             gpointer               user_data);
+  gboolean (*deploy_finish) (IdeDeployStrategy     *self,
+                             GAsyncResult          *result,
+                             GError               **error);
+
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_deploy_strategy_load_async    (IdeDeployStrategy      *self,
+                                            IdeBuildPipeline       *pipeline,
+                                            GCancellable           *cancellable,
+                                            GAsyncReadyCallback     callback,
+                                            gpointer                user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_deploy_strategy_load_finish   (IdeDeployStrategy      *self,
+                                            GAsyncResult           *result,
+                                            GError                **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_deploy_strategy_deploy_async  (IdeDeployStrategy      *self,
+                                            IdeBuildPipeline       *pipeline,
+                                            GFileProgressCallback   progress,
+                                            gpointer                progress_data,
+                                            GDestroyNotify          progress_data_destroy,
+                                            GCancellable           *cancellable,
+                                            GAsyncReadyCallback     callback,
+                                            gpointer                user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_deploy_strategy_deploy_finish (IdeDeployStrategy      *self,
+                                            GAsyncResult           *result,
+                                            GError                **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-device-info.c b/src/libide/foundry/ide-device-info.c
new file mode 100644
index 000000000..51674cafe
--- /dev/null
+++ b/src/libide/foundry/ide-device-info.c
@@ -0,0 +1,222 @@
+/* ide-device-info.c
+ *
+ * Copyright 2018-2019 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 "ide-device-info"
+
+#include "config.h"
+
+#include "ide-device-info.h"
+#include "ide-foundry-enums.h"
+#include "ide-triplet.h"
+
+struct _IdeDeviceInfo
+{
+  GObject parent_instance;
+  IdeTriplet *host_triplet;
+  IdeDeviceKind kind;
+};
+
+G_DEFINE_TYPE (IdeDeviceInfo, ide_device_info, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_KIND,
+  PROP_HOST_TRIPLET,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_device_info_finalize (GObject *object)
+{
+  IdeDeviceInfo *self = (IdeDeviceInfo *)object;
+
+  g_clear_pointer (&self->host_triplet, ide_triplet_unref);
+
+  G_OBJECT_CLASS (ide_device_info_parent_class)->finalize (object);
+}
+
+static void
+ide_device_info_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeDeviceInfo *self = IDE_DEVICE_INFO (object);
+
+  switch (prop_id)
+    {
+    case PROP_KIND:
+      g_value_set_enum (value, ide_device_info_get_kind (self));
+      break;
+
+    case PROP_HOST_TRIPLET:
+      g_value_set_boxed (value, ide_device_info_get_host_triplet (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_device_info_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeDeviceInfo *self = IDE_DEVICE_INFO (object);
+
+  switch (prop_id)
+    {
+    case PROP_KIND:
+      ide_device_info_set_kind (self, g_value_get_enum (value));
+      break;
+
+    case PROP_HOST_TRIPLET:
+      ide_device_info_set_host_triplet (self, g_value_get_boxed (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_device_info_class_init (IdeDeviceInfoClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_device_info_finalize;
+  object_class->get_property = ide_device_info_get_property;
+  object_class->set_property = ide_device_info_set_property;
+
+  properties [PROP_KIND] =
+    g_param_spec_enum ("kind",
+                       "Kind",
+                       "The device kind",
+                       IDE_TYPE_DEVICE_KIND,
+                       IDE_DEVICE_KIND_COMPUTER,
+                       G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_HOST_TRIPLET] =
+    g_param_spec_boxed ("host-triplet",
+                        "Host Triplet",
+                        "The #IdeTriplet object holding all the configuration name values",
+                        IDE_TYPE_TRIPLET,
+                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_device_info_init (IdeDeviceInfo *self)
+{
+  self->host_triplet = ide_triplet_new_from_system ();
+}
+
+IdeDeviceInfo *
+ide_device_info_new (void)
+{
+  return g_object_new (IDE_TYPE_DEVICE_INFO, NULL);
+}
+
+/**
+ * ide_device_info_get_kind:
+ * @self: An #IdeDeviceInfo
+ *
+ * Get the #IdeDeviceKind of the device describing the type of device @self refers to
+ *
+ * Returns: An #IdeDeviceKind.
+ *
+ * Since: 3.32
+ */
+IdeDeviceKind
+ide_device_info_get_kind (IdeDeviceInfo *self)
+{
+  g_return_val_if_fail (IDE_IS_DEVICE_INFO (self), 0);
+
+  return self->kind;
+}
+
+
+/**
+ * ide_device_info_set_kind:
+ * @self: An #IdeDeviceInfo
+ * @kind: An #IdeDeviceKind
+ *
+ * Set the #IdeDeviceKind of the device describing the type of device @self refers to
+ *
+ * Since: 3.32
+ */
+void
+ide_device_info_set_kind (IdeDeviceInfo *self,
+                          IdeDeviceKind  kind)
+{
+  g_return_if_fail (IDE_IS_DEVICE_INFO (self));
+
+  if (self->kind != kind)
+    {
+      self->kind = kind;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_KIND]);
+    }
+}
+
+/**
+ * ide_device_info_get_host_triplet:
+ * @self: An #IdeDeviceInfo
+ *
+ * Get the #IdeTriplet object describing the configuration name
+ * of the Device (its architecture…)
+ *
+ * Returns: (transfer none) (nullable): An #IdeTriplet.
+ *
+ * Since: 3.32
+ */
+IdeTriplet *
+ide_device_info_get_host_triplet (IdeDeviceInfo *self)
+{
+  g_return_val_if_fail (IDE_IS_DEVICE_INFO (self), NULL);
+
+  return self->host_triplet;
+}
+
+/**
+ * ide_device_info_set_host_triplet:
+ * @self: An #IdeDeviceInfo
+ *
+ * Set the #IdeTriplet object describing the configuration name
+ *
+ * Since: 3.32
+ */
+void
+ide_device_info_set_host_triplet (IdeDeviceInfo *self,
+                                  IdeTriplet    *host_triplet)
+{
+  g_return_if_fail (IDE_IS_DEVICE_INFO (self));
+
+  if (host_triplet != self->host_triplet)
+    {
+      g_clear_pointer (&self->host_triplet, ide_triplet_unref);
+      self->host_triplet = host_triplet ? ide_triplet_ref (host_triplet) : NULL;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HOST_TRIPLET]);
+    }
+}
diff --git a/src/libide/foundry/ide-device-info.h b/src/libide/foundry/ide-device-info.h
new file mode 100644
index 000000000..7b84200d7
--- /dev/null
+++ b/src/libide/foundry/ide-device-info.h
@@ -0,0 +1,59 @@
+/* ide-device-info.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_DEVICE_KIND_COMPUTER,
+  IDE_DEVICE_KIND_PHONE,
+  IDE_DEVICE_KIND_TABLET,
+  IDE_DEVICE_KIND_MICRO_CONTROLLER,
+} IdeDeviceKind;
+
+#define IDE_TYPE_DEVICE_INFO (ide_device_info_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeDeviceInfo, ide_device_info, IDE, DEVICE_INFO, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeDeviceInfo *ide_device_info_new              (void);
+IDE_AVAILABLE_IN_3_32
+IdeDeviceKind  ide_device_info_get_kind         (IdeDeviceInfo *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_device_info_set_kind         (IdeDeviceInfo *self,
+                                                 IdeDeviceKind  kind);
+IDE_AVAILABLE_IN_3_32
+IdeTriplet    *ide_device_info_get_host_triplet (IdeDeviceInfo *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_device_info_set_host_triplet (IdeDeviceInfo *self,
+                                                 IdeTriplet    *host_triplet);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-device-manager.c b/src/libide/foundry/ide-device-manager.c
new file mode 100644
index 000000000..37cee92ea
--- /dev/null
+++ b/src/libide/foundry/ide-device-manager.c
@@ -0,0 +1,1137 @@
+/* ide-device-manager.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-device-manager"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+
+#include "ide-build-manager.h"
+#include "ide-build-pipeline.h"
+#include "ide-deploy-strategy.h"
+#include "ide-device-manager.h"
+#include "ide-device-private.h"
+#include "ide-device-provider.h"
+#include "ide-device.h"
+#include "ide-foundry-compat.h"
+#include "ide-local-device.h"
+
+struct _IdeDeviceManager
+{
+  IdeObject parent_instance;
+
+  /*
+   * The currently selected device. Various subsystems will track this
+   * to update necessary changes for the device type. For example, the
+   * build pipeline will need to adjust things based on the current
+   * device to ensure we are building for the right architecture.
+   */
+  IdeDevice *device;
+
+  /*
+   * The devices that have been registered by IdeDeviceProvier plugins.
+   * It always has at least one device, the "local" device (IdeLocalDevice).
+   */
+  GPtrArray *devices;
+
+  /* Providers that are registered in plugins supporting IdeDeviceProvider. */
+  IdeExtensionSetAdapter *providers;
+
+  /*
+   * Our menu that contains our list of devices for the user to select. This
+   * is "per-IdeContext" so that it is not global to the system (which would
+   * result in duplicates for each workbench opened).
+   */
+  GMenu *menu;
+  GMenu *menu_section;
+
+  /*
+   * Our progress in a deployment. Simplifies binding to the progress bar
+   * in the omnibar.
+   */
+  gdouble progress;
+
+  guint loading : 1;
+};
+
+typedef struct
+{
+  IdeObjectArray   *strategies;
+  IdeBuildPipeline *pipeline;
+} DeployState;
+
+typedef struct
+{
+  gint n_active;
+} InitState;
+
+static void list_model_init_interface        (GListModelInterface *iface);
+static void async_initable_init_iface        (GAsyncInitableIface *iface);
+static void ide_device_manager_action_device (IdeDeviceManager    *self,
+                                              GVariant            *param);
+static void ide_device_manager_action_deploy (IdeDeviceManager    *self,
+                                              GVariant            *param);
+static void ide_device_manager_deploy_tick   (IdeTask             *task);
+
+DZL_DEFINE_ACTION_GROUP (IdeDeviceManager, ide_device_manager, {
+  { "device", ide_device_manager_action_device, "s", "'local'" },
+  { "deploy", ide_device_manager_action_deploy },
+})
+
+G_DEFINE_TYPE_WITH_CODE (IdeDeviceManager, ide_device_manager, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
+                                                ide_device_manager_init_action_group)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_init_iface)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_init_interface))
+
+enum {
+  PROP_0,
+  PROP_DEVICE,
+  PROP_PROGRESS,
+  N_PROPS
+};
+
+enum {
+  DEPLOY_STARTED,
+  DEPLOY_FINISHED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+deploy_state_free (DeployState *state)
+{
+  g_clear_object (&state->pipeline);
+  g_clear_pointer (&state->strategies, ide_object_array_unref);
+  g_slice_free (DeployState, state);
+}
+
+static void
+ide_device_manager_provider_device_added_cb (IdeDeviceManager  *self,
+                                             IdeDevice         *device,
+                                             IdeDeviceProvider *provider)
+{
+  g_autoptr(GMenuItem) menu_item = NULL;
+  const gchar *display_name;
+  const gchar *icon_name;
+  const gchar *device_id;
+  guint position;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+  g_assert (IDE_IS_DEVICE (device));
+  g_assert (!provider || IDE_IS_DEVICE_PROVIDER (provider));
+
+  device_id = ide_device_get_id (device);
+  icon_name = ide_device_get_icon_name (device);
+  display_name = ide_device_get_display_name (device);
+
+  IDE_TRACE_MSG ("Discovered device %s", device_id);
+
+  /* Notify user of new device if this is after initial loading */
+  if (!self->loading)
+    {
+      g_autoptr(IdeNotification) notif = NULL;
+      g_autofree gchar *title = NULL;
+
+      /* translators: %s is replaced with the external device name */
+      title = g_strdup_printf (_("Discovered device “%s”"), display_name);
+      notif = g_object_new (IDE_TYPE_NOTIFICATION,
+                            "id", "org.gnome.builder.device-manager.added",
+                            "title", title,
+                            "icon-name", icon_name,
+                            NULL);
+
+      ide_notification_attach (notif, IDE_OBJECT (self));
+      ide_notification_withdraw_in_seconds (notif, -1);
+    }
+
+  /* First add the device to the array, we'll notify observers later */
+  position = self->devices->len;
+  g_ptr_array_add (self->devices, g_object_ref (device));
+
+  /* Now add a new menu item to our selection model */
+  menu_item = g_menu_item_new (display_name, NULL);
+  g_menu_item_set_attribute (menu_item, "id", "s", device_id);
+  g_menu_item_set_attribute (menu_item, "verb-icon-name", "s", icon_name ?: "computer-symbolic");
+  g_menu_item_set_action_and_target_value (menu_item,
+                                           "device-manager.device",
+                                           g_variant_new_string (device_id));
+  g_menu_append_item (self->menu_section, menu_item);
+
+  /* Now notify about the new device */
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+
+  IDE_EXIT;
+}
+
+static void
+ide_device_manager_provider_device_removed_cb (IdeDeviceManager  *self,
+                                               IdeDevice         *device,
+                                               IdeDeviceProvider *provider)
+{
+  const gchar *device_id;
+  GMenu *menu;
+  guint n_items;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+  g_assert (IDE_IS_DEVICE (device));
+  g_assert (IDE_IS_DEVICE_PROVIDER (provider));
+
+  device_id = ide_device_get_id (device);
+
+  menu = self->menu_section;
+  n_items = g_menu_model_get_n_items (G_MENU_MODEL (menu));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autofree gchar *id = NULL;
+
+      if (g_menu_model_get_item_attribute (G_MENU_MODEL (menu), i, "id", "s", &id) &&
+          g_strcmp0 (id, device_id) == 0)
+        {
+          g_menu_remove (menu, i);
+          break;
+        }
+    }
+
+  for (guint i = 0; i < self->devices->len; i++)
+    {
+      IdeDevice *element = g_ptr_array_index (self->devices, i);
+
+      if (element == device)
+        {
+          g_ptr_array_remove_index (self->devices, i);
+          g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+          break;
+        }
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_device_manager_provider_load_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  IdeDeviceProvider *provider = (IdeDeviceProvider *)object;
+  g_autoptr(IdeDeviceManager) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEVICE_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+
+  if (!ide_device_provider_load_finish (provider, result, &error))
+    g_warning ("%s failed to load: %s",
+               G_OBJECT_TYPE_NAME (provider),
+               error->message);
+
+  IDE_EXIT;
+}
+
+static void
+ide_device_manager_provider_added_cb (IdeExtensionSetAdapter *set,
+                                      PeasPluginInfo         *plugin_info,
+                                      PeasExtension          *exten,
+                                      gpointer                user_data)
+{
+  IdeDeviceManager *self = user_data;
+  IdeDeviceProvider *provider = (IdeDeviceProvider *)exten;
+  g_autoptr(GPtrArray) devices = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DEVICE_PROVIDER (provider));
+
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (provider));
+
+  g_signal_connect_object (provider,
+                           "device-added",
+                           G_CALLBACK (ide_device_manager_provider_device_added_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (provider,
+                           "device-removed",
+                           G_CALLBACK (ide_device_manager_provider_device_removed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  devices = ide_device_provider_get_devices (provider);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (devices, g_object_unref);
+
+  for (guint i = 0; i < devices->len; i++)
+    {
+      IdeDevice *device = g_ptr_array_index (devices, i);
+
+      g_assert (IDE_IS_DEVICE (device));
+
+      ide_device_manager_provider_device_added_cb (self, device, provider);
+    }
+
+  ide_device_provider_load_async (provider,
+                                  NULL,
+                                  ide_device_manager_provider_load_cb,
+                                  g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+ide_device_manager_provider_removed_cb (IdeExtensionSetAdapter *set,
+                                        PeasPluginInfo         *plugin_info,
+                                        PeasExtension          *exten,
+                                        gpointer                user_data)
+{
+  IdeDeviceManager *self = user_data;
+  IdeDeviceProvider *provider = (IdeDeviceProvider *)exten;
+  g_autoptr(GPtrArray) devices = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DEVICE_PROVIDER (provider));
+
+  devices = ide_device_provider_get_devices (provider);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (devices, g_object_unref);
+
+  for (guint i = 0; i < devices->len; i++)
+    {
+      IdeDevice *removed_device = g_ptr_array_index (devices, i);
+
+      for (guint j = 0; j < self->devices->len; j++)
+        {
+          IdeDevice *device = g_ptr_array_index (self->devices, j);
+
+          if (device == removed_device)
+            {
+              g_ptr_array_remove_index (self->devices, j);
+              g_list_model_items_changed (G_LIST_MODEL (self), j, 1, 0);
+              break;
+            }
+        }
+    }
+
+  g_signal_handlers_disconnect_by_func (provider,
+                                        G_CALLBACK (ide_device_manager_provider_device_added_cb),
+                                        self);
+
+  g_signal_handlers_disconnect_by_func (provider,
+                                        G_CALLBACK (ide_device_manager_provider_device_removed_cb),
+                                        self);
+
+  ide_object_destroy (IDE_OBJECT (provider));
+
+  IDE_EXIT;
+}
+
+static void
+ide_device_manager_add_local (IdeDeviceManager *self)
+{
+  g_autoptr(IdeDevice) device = NULL;
+  g_autoptr(IdeTriplet) triplet = NULL;
+  g_autofree gchar *arch = NULL;
+
+  g_return_if_fail (IDE_IS_DEVICE_MANAGER (self));
+
+  triplet = ide_triplet_new_from_system ();
+  device = g_object_new (IDE_TYPE_LOCAL_DEVICE,
+                         "triplet", triplet,
+                         NULL);
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (device));
+  ide_device_manager_provider_device_added_cb (self, device, NULL);
+}
+
+static GType
+ide_device_manager_get_item_type (GListModel *list_model)
+{
+  return IDE_TYPE_DEVICE;
+}
+
+static guint
+ide_device_manager_get_n_items (GListModel *list_model)
+{
+  IdeDeviceManager *self = (IdeDeviceManager *)list_model;
+
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+
+  return self->devices->len;
+}
+
+static gpointer
+ide_device_manager_get_item (GListModel *list_model,
+                             guint       position)
+{
+  IdeDeviceManager *self = (IdeDeviceManager *)list_model;
+
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+  g_assert (position < self->devices->len);
+
+  return g_object_ref (g_ptr_array_index (self->devices, position));
+}
+
+static void
+ide_device_manager_destroy (IdeObject *object)
+{
+  IdeDeviceManager *self = (IdeDeviceManager *)object;
+
+  g_assert (IDE_IS_OBJECT (object));
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  ide_clear_and_destroy_object (&self->providers);
+
+  IDE_OBJECT_CLASS (ide_device_manager_parent_class)->destroy (object);
+
+  if (self->devices->len > 0)
+    g_ptr_array_remove_range (self->devices, 0, self->devices->len);
+
+  g_clear_object (&self->device);
+}
+
+static void
+ide_device_manager_finalize (GObject *object)
+{
+  IdeDeviceManager *self = (IdeDeviceManager *)object;
+
+  g_clear_pointer (&self->devices, g_ptr_array_unref);
+  g_clear_object (&self->menu);
+  g_clear_object (&self->menu_section);
+
+  G_OBJECT_CLASS (ide_device_manager_parent_class)->finalize (object);
+}
+
+static void
+ide_device_manager_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeDeviceManager *self = IDE_DEVICE_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEVICE:
+      g_value_set_object (value, ide_device_manager_get_device (self));
+      break;
+
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_device_manager_get_progress (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_device_manager_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeDeviceManager *self = IDE_DEVICE_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEVICE:
+      ide_device_manager_set_device (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_device_manager_class_init (IdeDeviceManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_device_manager_finalize;
+  object_class->get_property = ide_device_manager_get_property;
+  object_class->set_property = ide_device_manager_set_property;
+
+  i_object_class->destroy = ide_device_manager_destroy;
+
+  /**
+   * IdeDeviceManager:device:
+   *
+   * The "device" property indicates the currently selected device by the
+   * user. This is the device we will try to deploy to when running, and
+   * execute the application on.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_DEVICE] =
+    g_param_spec_object ("device",
+                         "Device",
+                         "The currently selected device to build for",
+                         IDE_TYPE_DEVICE,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeDeviceManager:progress:
+   *
+   * The "progress" property is updated with a value between 0.0 and 1.0 while
+   * the deployment is in progress.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "Deployment progress",
+                         0.0, 1.0, 0.0,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [DEPLOY_STARTED] =
+    g_signal_new ("deploy-started",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  signals [DEPLOY_FINISHED] =
+    g_signal_new ("deploy-finished",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+static void
+list_model_init_interface (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_device_manager_get_item_type;
+  iface->get_n_items = ide_device_manager_get_n_items;
+  iface->get_item = ide_device_manager_get_item;
+}
+
+static void
+ide_device_manager_init (IdeDeviceManager *self)
+{
+  self->devices = g_ptr_array_new_with_free_func (g_object_unref);
+
+  self->menu = g_menu_new ();
+  self->menu_section = g_menu_new ();
+  g_menu_append_section (self->menu, _("Devices"), G_MENU_MODEL (self->menu_section));
+}
+
+/**
+ * ide_device_manager_get_device_by_id:
+ * @self: an #IdeDeviceManager
+ * @device_id: The device identifier string.
+ *
+ * Fetches the first device that matches the device identifier @device_id.
+ *
+ * Returns: (transfer none): An #IdeDevice or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeDevice *
+ide_device_manager_get_device_by_id (IdeDeviceManager *self,
+                                     const gchar      *device_id)
+{
+  g_return_val_if_fail (IDE_IS_DEVICE_MANAGER (self), NULL);
+
+  for (guint i = 0; i < self->devices->len; i++)
+    {
+      IdeDevice *device;
+      const gchar *id;
+
+      device = g_ptr_array_index (self->devices, i);
+      id = ide_device_get_id (device);
+
+      if (0 == g_strcmp0 (id, device_id))
+        return device;
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_device_manager_get_device:
+ * @self: a #IdeDeviceManager
+ *
+ * Gets the currently selected device.
+ * Usually, this is an #IdeLocalDevice.
+ *
+ * Returns: (transfer none) (not nullable): an #IdeDevice
+ *
+ * Since: 3.32
+ */
+IdeDevice *
+ide_device_manager_get_device (IdeDeviceManager *self)
+{
+  g_return_val_if_fail (IDE_IS_DEVICE_MANAGER (self), NULL);
+  g_return_val_if_fail (self->devices->len > 0, NULL);
+
+  if (self->device == NULL)
+    {
+      for (guint i = 0; i < self->devices->len; i++)
+        {
+          IdeDevice *device = g_ptr_array_index (self->devices, i);
+
+          if (IDE_IS_LOCAL_DEVICE (device))
+            return device;
+        }
+
+      g_assert_not_reached ();
+    }
+
+  return self->device;
+}
+
+/**
+ * ide_device_manager_set_device:
+ * @self: an #IdeDeviceManager
+ * @device: (nullable): an #IdeDevice or %NULL
+ *
+ * Sets the #IdeDeviceManager:device property, which is the currently selected
+ * device. Builder uses this to determine how to build the current project for
+ * the devices architecture and operating system.
+ *
+ * If @device is %NULL, the local device will be used.
+ *
+ * Since: 3.32
+ */
+void
+ide_device_manager_set_device (IdeDeviceManager *self,
+                               IdeDevice        *device)
+{
+  g_return_if_fail (IDE_IS_DEVICE_MANAGER (self));
+  g_return_if_fail (!device || IDE_IS_DEVICE (device));
+
+  if (g_set_object (&self->device, device))
+    {
+      const gchar *device_id = NULL;
+
+      if (device != NULL)
+        device_id = ide_device_get_id (device);
+
+      if (device_id == NULL)
+        device_id = "local";
+
+      ide_device_manager_set_action_state (self, "device", g_variant_new_string (device_id));
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DEVICE]);
+    }
+}
+
+static void
+ide_device_manager_action_device (IdeDeviceManager *self,
+                                  GVariant         *param)
+{
+  const gchar *device_id;
+  IdeDevice *device;
+
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+
+  if (!(device_id = g_variant_get_string (param, NULL)))
+    device_id = "local";
+
+  if ((device = ide_device_manager_get_device_by_id (self, device_id)))
+    ide_device_manager_set_device (self, device);
+}
+
+static void
+log_deploy_error (GObject      *object,
+                  GAsyncResult *result,
+                  gpointer      user_data)
+{
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_DEVICE_MANAGER (object));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (!ide_device_manager_deploy_finish (IDE_DEVICE_MANAGER (object), result, &error))
+    ide_object_warning (object, "%s", error->message);
+}
+
+static void
+ide_device_manager_action_deploy (IdeDeviceManager *self,
+                                  GVariant         *param)
+{
+  IdeBuildPipeline *pipeline;
+  IdeBuildManager *build_manager;
+  IdeContext *context;
+
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_manager = ide_build_manager_from_context (context);
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+
+  if (!ide_build_pipeline_is_ready (pipeline))
+    ide_context_warning (context, _("Cannot deploy to device, build pipeline is not initialized"));
+  else
+    ide_device_manager_deploy_async (self, pipeline, NULL, log_deploy_error, NULL);
+}
+
+static void
+deploy_progress_cb (goffset  current_num_bytes,
+                    goffset  total_num_bytes,
+                    gpointer user_data)
+{
+  IdeDeviceManager *self = user_data;
+  gdouble progress = 0.0;
+
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+
+  if (total_num_bytes > 0)
+    progress = current_num_bytes / total_num_bytes;
+
+  self->progress = CLAMP (progress, 0.0, 1.0);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+}
+
+static void
+collect_strategies (PeasExtensionSet *set,
+                    PeasPluginInfo   *plugin_info,
+                    PeasExtension    *exten,
+                    gpointer          user_data)
+{
+  IdeObjectArray *strategies = user_data;
+  IdeDeployStrategy *strategy = (IdeDeployStrategy *)exten;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DEPLOY_STRATEGY (strategy));
+  g_assert (strategies != NULL);
+
+  ide_object_array_add (strategies, strategy);
+}
+
+static void
+ide_device_manager_deploy_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  IdeDeployStrategy *strategy = (IdeDeployStrategy *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEPLOY_STRATEGY (strategy));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_deploy_strategy_deploy_finish (strategy, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  ide_object_destroy (IDE_OBJECT (strategy));
+
+  IDE_EXIT;
+}
+
+static void
+ide_device_manager_deploy_load_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeDeployStrategy *strategy = (IdeDeployStrategy *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  IdeDeviceManager *self;
+  DeployState *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEPLOY_STRATEGY (strategy));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_deploy_strategy_load_finish (strategy, result, &error))
+    {
+      g_debug ("Deploy strategy failed to load: %s", error->message);
+      ide_object_destroy (IDE_OBJECT (strategy));
+      ide_device_manager_deploy_tick (task);
+      IDE_EXIT;
+    }
+
+  /* Okay, we found a match. Now deploy to the device. */
+
+  self = ide_task_get_source_object (task);
+  state = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+  g_assert (state != NULL);
+  g_assert (state->strategies != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE (state->pipeline));
+
+  ide_deploy_strategy_deploy_async (strategy,
+                                    state->pipeline,
+                                    deploy_progress_cb,
+                                    g_object_ref (self),
+                                    g_object_unref,
+                                    ide_task_get_cancellable (task),
+                                    ide_device_manager_deploy_cb,
+                                    g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+static void
+ide_device_manager_deploy_tick (IdeTask *task)
+{
+  g_autoptr(IdeDeployStrategy) strategy = NULL;
+  DeployState *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+
+  g_assert (state != NULL);
+  g_assert (state->strategies != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE (state->pipeline));
+
+  if (state->strategies->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Failed to locate deployment strategy for device");
+      IDE_EXIT;
+    }
+
+  strategy = ide_object_array_steal_index (state->strategies, 0);
+
+  ide_deploy_strategy_load_async (strategy,
+                                  state->pipeline,
+                                  ide_task_get_cancellable (task),
+                                  ide_device_manager_deploy_load_cb,
+                                  g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+static void
+ide_device_manager_deploy_completed (IdeDeviceManager *self,
+                                     GParamSpec       *pspec,
+                                     IdeTask          *task)
+{
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+  g_assert (IDE_IS_TASK (task));
+
+  if (self->progress < 1.0)
+    {
+      self->progress = 1.0;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+    }
+
+  g_signal_emit (self, signals [DEPLOY_FINISHED], 0);
+}
+
+/**
+ * ide_device_manager_deploy_async:
+ * @self: a #IdeDeviceManager
+ * @pipeline: an #IdeBuildPipeline
+ * @cancellable: a #GCancellable, or %NULL
+ * @callback: a #GAsyncReadyCallback
+ * @user_data: closure data for @callback
+ *
+ * Requests that the application be deployed to the device. This may need to
+ * be done before running the application so that the device has the most
+ * up to date build.
+ *
+ * Since: 3.32
+ */
+void
+ide_device_manager_deploy_async (IdeDeviceManager    *self,
+                                 IdeBuildPipeline    *pipeline,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autoptr(PeasExtensionSet) set = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  DeployState *state;
+  IdeDevice *device;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_DEVICE_MANAGER (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  self->progress = 0.0;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+
+  g_signal_emit (self, signals [DEPLOY_STARTED], 0);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_device_manager_deploy_async);
+
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_device_manager_deploy_completed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  if (!(device = ide_build_pipeline_get_device (pipeline)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "Missing device in pipeline");
+      IDE_EXIT;
+    }
+
+  if (IDE_IS_LOCAL_DEVICE (device))
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  state = g_slice_new0 (DeployState);
+  state->pipeline = g_object_ref (pipeline);
+  state->strategies = ide_object_array_new ();
+  ide_task_set_task_data (task, state, deploy_state_free);
+
+  set = peas_extension_set_new (peas_engine_get_default (),
+                                IDE_TYPE_DEPLOY_STRATEGY,
+                                NULL);
+  peas_extension_set_foreach (set, collect_strategies, state->strategies);
+
+  /* Root the addins as children of us so that they get context access */
+  for (guint i = 0; i < state->strategies->len; i++)
+    ide_object_append (IDE_OBJECT (self),
+                       ide_object_array_index (state->strategies, i));
+
+  ide_device_manager_deploy_tick (task);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_device_manager_deploy_finish:
+ * @self: a #IdeDeviceManager
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes a request to deploy the application to the device.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_device_manager_deploy_finish (IdeDeviceManager  *self,
+                                  GAsyncResult      *result,
+                                  GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_DEVICE_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+  g_return_val_if_fail (ide_task_is_valid (IDE_TASK (result), self), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+gdouble
+ide_device_manager_get_progress (IdeDeviceManager *self)
+{
+  g_return_val_if_fail (IDE_IS_DEVICE_MANAGER (self), 0.0);
+
+  return self->progress;
+}
+
+GMenu *
+_ide_device_manager_get_menu (IdeDeviceManager *self)
+{
+  g_return_val_if_fail (IDE_IS_DEVICE_MANAGER (self), NULL);
+
+  return self->menu;
+}
+
+static void
+ide_device_manager_init_provider_load_cb (GObject      *object,
+                                          GAsyncResult *result,
+                                          gpointer      user_data)
+{
+  IdeDeviceProvider *provider = (IdeDeviceProvider *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  InitState *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DEVICE_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_device_provider_load_finish (provider, result, &error))
+    g_warning ("%s: %s", G_OBJECT_TYPE_NAME (provider), error->message);
+
+  state = ide_task_get_task_data (task);
+  state->n_active--;
+
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_device_manager_init_provider_cb (IdeExtensionSetAdapter *set,
+                                     PeasPluginInfo         *plugin_info,
+                                     PeasExtension          *exten,
+                                     gpointer                user_data)
+{
+  IdeDeviceProvider *provider = (IdeDeviceProvider *)exten;
+  IdeDeviceManager *self;
+  IdeTask *task = user_data;
+  InitState *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DEVICE_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  state = ide_task_get_task_data (task);
+  state->n_active++;
+
+  g_signal_connect_object (provider,
+                           "device-added",
+                           G_CALLBACK (ide_device_manager_provider_device_added_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (provider,
+                           "device-removed",
+                           G_CALLBACK (ide_device_manager_provider_device_removed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_device_provider_load_async (provider,
+                                  ide_task_get_cancellable (task),
+                                  ide_device_manager_init_provider_load_cb,
+                                  g_object_ref (task));
+}
+
+static void
+ide_device_manager_init_async (GAsyncInitable      *initable,
+                               gint                 io_priority,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  IdeDeviceManager *self = (IdeDeviceManager *)initable;
+  g_autoptr(IdeTask) task = NULL;
+  InitState *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DEVICE_MANAGER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_device_manager_init_async);
+  ide_task_set_priority (task, io_priority);
+
+  self->loading = TRUE;
+
+  state = g_new0 (InitState, 1);
+  ide_task_set_task_data (task, state, g_free);
+
+  ide_device_manager_add_local (self);
+
+  self->providers = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                   peas_engine_get_default (),
+                                                   IDE_TYPE_DEVICE_PROVIDER,
+                                                   NULL, NULL);
+
+  g_signal_connect (self->providers,
+                    "extension-added",
+                    G_CALLBACK (ide_device_manager_provider_added_cb),
+                    self);
+
+  g_signal_connect (self->providers,
+                    "extension-removed",
+                    G_CALLBACK (ide_device_manager_provider_removed_cb),
+                    self);
+
+  ide_extension_set_adapter_foreach (self->providers,
+                                     ide_device_manager_init_provider_cb,
+                                     task);
+
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_device_manager_init_finish (GAsyncInitable  *initable,
+                                GAsyncResult    *result,
+                                GError         **error)
+{
+  IdeDeviceManager *self = (IdeDeviceManager *)initable;
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_DEVICE_MANAGER (initable));
+  g_assert (IDE_IS_TASK (result));
+
+  self->loading = FALSE;
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+async_initable_init_iface (GAsyncInitableIface *iface)
+{
+  iface->init_async = ide_device_manager_init_async;
+  iface->init_finish = ide_device_manager_init_finish;
+}
diff --git a/src/libide/foundry/ide-device-manager.h b/src/libide/foundry/ide-device-manager.h
new file mode 100644
index 000000000..096849d0d
--- /dev/null
+++ b/src/libide/foundry/ide-device-manager.h
@@ -0,0 +1,61 @@
+/* ide-device-manager.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEVICE_MANAGER (ide_device_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeDeviceManager, ide_device_manager, IDE, DEVICE_MANAGER, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeDeviceManager        *ide_device_manager_from_context        (IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+gdouble    ide_device_manager_get_progress     (IdeDeviceManager     *self);
+IDE_AVAILABLE_IN_3_32
+IdeDevice *ide_device_manager_get_device       (IdeDeviceManager     *self);
+IDE_AVAILABLE_IN_3_32
+void       ide_device_manager_set_device       (IdeDeviceManager     *self,
+                                                IdeDevice            *device);
+IDE_AVAILABLE_IN_3_32
+IdeDevice *ide_device_manager_get_device_by_id (IdeDeviceManager     *self,
+                                                const gchar          *device_id);
+IDE_AVAILABLE_IN_3_32
+void       ide_device_manager_deploy_async     (IdeDeviceManager     *self,
+                                                IdeBuildPipeline     *pipeline,
+                                                GCancellable         *cancellable,
+                                                GAsyncReadyCallback   callback,
+                                                gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_device_manager_deploy_finish    (IdeDeviceManager     *self,
+                                                GAsyncResult         *result,
+                                                GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-device-private.h b/src/libide/foundry/ide-device-private.h
new file mode 100644
index 000000000..1663ec5c8
--- /dev/null
+++ b/src/libide/foundry/ide-device-private.h
@@ -0,0 +1,29 @@
+/* ide-device-private.h
+ *
+ * Copyright 2018-2019 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 "ide-device-manager.h"
+
+G_BEGIN_DECLS
+
+GMenu *_ide_device_manager_get_menu (IdeDeviceManager *self);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-device-provider.c b/src/libide/foundry/ide-device-provider.c
new file mode 100644
index 000000000..e83787a66
--- /dev/null
+++ b/src/libide/foundry/ide-device-provider.c
@@ -0,0 +1,302 @@
+/* ide-device-provider.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-device-provider"
+
+#include "config.h"
+
+#include "ide-device.h"
+#include "ide-device-provider.h"
+
+typedef struct
+{
+  GPtrArray *devices;
+} IdeDeviceProviderPrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (IdeDeviceProvider, ide_device_provider, IDE_TYPE_OBJECT)
+
+enum {
+  DEVICE_ADDED,
+  DEVICE_REMOVED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+ide_device_provider_real_load_async (IdeDeviceProvider   *self,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_device_provider_real_load_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "%s does not implement load_async",
+                           G_OBJECT_TYPE_NAME (self));
+}
+
+static gboolean
+ide_device_provider_real_load_finish (IdeDeviceProvider  *self,
+                                      GAsyncResult       *result,
+                                      GError            **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_device_provider_real_device_added (IdeDeviceProvider *self,
+                                       IdeDevice         *device)
+{
+  IdeDeviceProviderPrivate *priv = ide_device_provider_get_instance_private (self);
+
+  g_assert (IDE_IS_DEVICE_PROVIDER (self));
+  g_assert (IDE_IS_DEVICE (device));
+
+  if (priv->devices == NULL)
+    priv->devices = g_ptr_array_new_with_free_func (g_object_unref);
+  g_ptr_array_add (priv->devices, g_object_ref (device));
+}
+
+static void
+ide_device_provider_real_device_removed (IdeDeviceProvider *self,
+                                         IdeDevice         *device)
+{
+  IdeDeviceProviderPrivate *priv = ide_device_provider_get_instance_private (self);
+
+  g_assert (IDE_IS_DEVICE_PROVIDER (self));
+  g_assert (IDE_IS_DEVICE (device));
+
+  /* Maybe we just disposed */
+  if (priv->devices == NULL)
+    return;
+
+  if (!g_ptr_array_remove (priv->devices, device))
+    g_warning ("No such device \"%s\" found in \"%s\"",
+               G_OBJECT_TYPE_NAME (device),
+               G_OBJECT_TYPE_NAME (self));
+}
+
+static void
+ide_device_provider_dispose (GObject *object)
+{
+  IdeDeviceProvider *self = (IdeDeviceProvider *)object;
+  IdeDeviceProviderPrivate *priv = ide_device_provider_get_instance_private (self);
+
+  g_clear_pointer (&priv->devices, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (ide_device_provider_parent_class)->dispose (object);
+}
+
+static void
+ide_device_provider_class_init (IdeDeviceProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_device_provider_dispose;
+
+  klass->device_added = ide_device_provider_real_device_added;
+  klass->device_removed = ide_device_provider_real_device_removed;
+  klass->load_async = ide_device_provider_real_load_async;
+  klass->load_finish = ide_device_provider_real_load_finish;
+
+  /**
+   * IdeDeviceProvider::device-added:
+   * @self: an #IdeDeviceProvider
+   * @device: an #IdeDevice
+   *
+   * The "device-added" signal is emitted when a provider has discovered
+   * a device has become available.
+   *
+   * Subclasses of #IdeDeviceManager must chain-up if they override the
+   * #IdeDeviceProviderClass.device_added vfunc.
+   *
+   * Since: 3.32
+   */
+  signals [DEVICE_ADDED] =
+    g_signal_new ("device-added",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeDeviceProviderClass, device_added),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_DEVICE);
+  g_signal_set_va_marshaller (signals [DEVICE_ADDED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  /**
+   * IdeDeviceProvider::device-removed:
+   * @self: an #IdeDeviceProvider
+   * @device: an #IdeDevice
+   *
+   * The "device-removed" signal is emitted when a provider has discovered
+   * a device is no longer available.
+   *
+   * Subclasses of #IdeDeviceManager must chain-up if they override the
+   * #IdeDeviceProviderClass.device_removed vfunc.
+   *
+   * Since: 3.32
+   */
+  signals [DEVICE_REMOVED] =
+    g_signal_new ("device-removed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeDeviceProviderClass, device_removed),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_DEVICE);
+  g_signal_set_va_marshaller (signals [DEVICE_REMOVED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+}
+
+static void
+ide_device_provider_init (IdeDeviceProvider *self)
+{
+}
+
+/**
+ * ide_device_provider_emit_device_added:
+ *
+ * Emits the #IdeDeviceProvider::device-added signal.
+ *
+ * This should only be called by subclasses of #IdeDeviceProvider when
+ * a new device has been discovered.
+ *
+ * Since: 3.32
+ */
+void
+ide_device_provider_emit_device_added (IdeDeviceProvider *provider,
+                                       IdeDevice         *device)
+{
+  g_return_if_fail (IDE_IS_DEVICE_PROVIDER (provider));
+  g_return_if_fail (IDE_IS_DEVICE (device));
+
+  g_signal_emit (provider, signals [DEVICE_ADDED], 0, device);
+}
+
+/**
+ * ide_device_provider_emit_device_removed:
+ *
+ * Emits the #IdeDeviceProvider::device-removed signal.
+ *
+ * This should only be called by subclasses of #IdeDeviceProvider when
+ * a previously added device has been removed.
+ *
+ * Since: 3.32
+ */
+void
+ide_device_provider_emit_device_removed (IdeDeviceProvider *provider,
+                                         IdeDevice         *device)
+{
+  g_return_if_fail (IDE_IS_DEVICE_PROVIDER (provider));
+  g_return_if_fail (IDE_IS_DEVICE (device));
+
+  g_signal_emit (provider, signals [DEVICE_REMOVED], 0, device);
+}
+
+/**
+ * ide_device_provider_load_async:
+ * @self: an #IdeDeviceProvider
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: (nullable): a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Requests that the #IdeDeviceProvider asynchronously load any known devices.
+ *
+ * This should only be called once on an #IdeDeviceProvider. It is an error
+ * to call this function more than once for a single #IdeDeviceProvider.
+ *
+ * #IdeDeviceProvider implementations are expected to emit the
+ * #IdeDeviceProvider::device-added signal for each device they've discovered.
+ * That should be done for known devices before returning from the asynchronous
+ * operation so that the device manager does not need to wait for additional
+ * devices to enter the "settled" state.
+ *
+ * Since: 3.32
+ */
+void
+ide_device_provider_load_async (IdeDeviceProvider   *self,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_DEVICE_PROVIDER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_DEVICE_PROVIDER_GET_CLASS (self)->load_async (self, cancellable, callback, user_data);
+}
+
+/**
+ * ide_device_provider_load_finish:
+ * @self: an #IdeDeviceProvider
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to load known devices via
+ * ide_device_provider_load_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_device_provider_load_finish (IdeDeviceProvider  *self,
+                                 GAsyncResult       *result,
+                                 GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_DEVICE_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_DEVICE_PROVIDER_GET_CLASS (self)->load_finish (self, result, error);
+}
+
+/**
+ * ide_device_provider_get_devices:
+ * @self: an #IdeDeviceProvider
+ *
+ * Gets a new #GPtrArray containing a list of #IdeDevice instances that were
+ * registered by the #IdeDeviceProvider
+ *
+ * Returns: (transfer full) (element-type Ide.Device) (not nullable):
+ *   a #GPtrArray of #IdeDevice.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_device_provider_get_devices (IdeDeviceProvider *self)
+{
+  IdeDeviceProviderPrivate *priv = ide_device_provider_get_instance_private (self);
+  g_autoptr(GPtrArray) devices = NULL;
+
+  g_return_val_if_fail (IDE_IS_DEVICE_PROVIDER (self), NULL);
+
+  devices = g_ptr_array_new ();
+
+  if (priv->devices != NULL)
+    {
+      for (guint i = 0; i < priv->devices->len; i++)
+        g_ptr_array_add (devices, g_object_ref (g_ptr_array_index (priv->devices, i)));
+    }
+
+  return g_steal_pointer (&devices);
+}
diff --git a/src/libide/foundry/ide-device-provider.h b/src/libide/foundry/ide-device-provider.h
new file mode 100644
index 000000000..13ebcc6be
--- /dev/null
+++ b/src/libide/foundry/ide-device-provider.h
@@ -0,0 +1,73 @@
+/* ide-device-provider.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEVICE_PROVIDER (ide_device_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeDeviceProvider, ide_device_provider, IDE, DEVICE_PROVIDER, IdeObject)
+
+struct _IdeDeviceProviderClass
+{
+  IdeObjectClass parent_class;
+
+  void     (*device_added)   (IdeDeviceProvider    *self,
+                              IdeDevice            *device);
+  void     (*device_removed) (IdeDeviceProvider    *self,
+                              IdeDevice            *device);
+  void     (*load_async)     (IdeDeviceProvider    *self,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+  gboolean (*load_finish)    (IdeDeviceProvider    *self,
+                              GAsyncResult         *result,
+                              GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void       ide_device_provider_emit_device_added   (IdeDeviceProvider    *self,
+                                                    IdeDevice            *device);
+IDE_AVAILABLE_IN_3_32
+void       ide_device_provider_emit_device_removed (IdeDeviceProvider    *self,
+                                                    IdeDevice            *device);
+IDE_AVAILABLE_IN_3_32
+void       ide_device_provider_load_async          (IdeDeviceProvider    *self,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_device_provider_load_finish         (IdeDeviceProvider    *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+IDE_AVAILABLE_IN_3_32
+GPtrArray *ide_device_provider_get_devices         (IdeDeviceProvider    *self);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-device.c b/src/libide/foundry/ide-device.c
new file mode 100644
index 000000000..a5fcb2b65
--- /dev/null
+++ b/src/libide/foundry/ide-device.c
@@ -0,0 +1,392 @@
+/* ide-device.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-device"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+
+#include "ide-configuration.h"
+#include "ide-device.h"
+#include "ide-device-info.h"
+
+typedef struct
+{
+  gchar *display_name;
+  gchar *icon_name;
+  gchar *id;
+} IdeDevicePrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (IdeDevice, ide_device, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_DISPLAY_NAME,
+  PROP_ICON_NAME,
+  PROP_ID,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * ide_device_get_display_name:
+ *
+ * This function returns the name of the device. If no name has been set, then
+ * %NULL is returned.
+ *
+ * In some cases, this value wont be available until additional information
+ * has been probed from the device.
+ *
+ * Returns: (nullable): A string containing the display name for the device.
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_device_get_display_name (IdeDevice *device)
+{
+  IdeDevicePrivate *priv = ide_device_get_instance_private (device);
+
+  g_return_val_if_fail (IDE_IS_DEVICE (device), NULL);
+
+  return priv->display_name;
+}
+
+void
+ide_device_set_display_name (IdeDevice   *device,
+                             const gchar *display_name)
+{
+  IdeDevicePrivate *priv = ide_device_get_instance_private (device);
+
+  g_return_if_fail (IDE_IS_DEVICE (device));
+
+  if (display_name != priv->display_name)
+    {
+      g_free (priv->display_name);
+      priv->display_name = g_strdup (display_name);
+      g_object_notify_by_pspec (G_OBJECT (device),
+                                properties [PROP_DISPLAY_NAME]);
+    }
+}
+
+/**
+ * ide_device_get_icon_name:
+ * @self: a #IdeDevice
+ *
+ * Gets the icon to use when displaying the device in UI elements.
+ *
+ * Returns: (nullable): an icon-name or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_device_get_icon_name (IdeDevice *self)
+{
+  IdeDevicePrivate *priv = ide_device_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_DEVICE (self), NULL);
+
+  return priv->icon_name;
+}
+
+/**
+ * ide_device_set_icon_name:
+ * @self: a #IdeDevice
+ *
+ * Sets the icon-name property.
+ *
+ * This is the icon that is displayed with the device name in UI elements.
+ *
+ * Since: 3.32
+ */
+void
+ide_device_set_icon_name (IdeDevice   *self,
+                          const gchar *icon_name)
+{
+  IdeDevicePrivate *priv = ide_device_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_DEVICE (self));
+
+  if (g_strcmp0 (icon_name, priv->icon_name) != 0)
+    {
+      g_free (priv->icon_name);
+      priv->icon_name = g_strdup (icon_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+    }
+}
+
+/**
+ * ide_device_get_id:
+ *
+ * Retrieves the "id" property of the #IdeDevice. This is generally not a
+ * user friendly name as it is often a guid.
+ *
+ * Returns: A unique identifier for the device.
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_device_get_id (IdeDevice *device)
+{
+  IdeDevicePrivate *priv = ide_device_get_instance_private (device);
+
+  g_return_val_if_fail (IDE_IS_DEVICE (device), NULL);
+
+  return priv->id;
+}
+
+void
+ide_device_set_id (IdeDevice   *device,
+                   const gchar *id)
+{
+  IdeDevicePrivate *priv = ide_device_get_instance_private (device);
+
+  g_return_if_fail (IDE_IS_DEVICE (device));
+
+  if (id != priv->id)
+    {
+      g_free (priv->id);
+      priv->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (device), properties [PROP_ID]);
+    }
+}
+
+static void
+ide_device_real_get_info_async (IdeDevice           *self,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_device_real_get_info_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "%s has not implemented get_info_async()",
+                           G_OBJECT_TYPE_NAME (self));
+}
+
+static IdeDeviceInfo *
+ide_device_real_get_info_finish (IdeDevice     *self,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+ide_device_finalize (GObject *object)
+{
+  IdeDevice *self = (IdeDevice *)object;
+  IdeDevicePrivate *priv = ide_device_get_instance_private (self);
+
+  g_clear_pointer (&priv->display_name, g_free);
+  g_clear_pointer (&priv->id, g_free);
+
+  G_OBJECT_CLASS (ide_device_parent_class)->finalize (object);
+}
+
+static void
+ide_device_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  IdeDevice *self = IDE_DEVICE (object);
+
+  switch (prop_id)
+    {
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, ide_device_get_display_name (self));
+      break;
+
+    case PROP_ICON_NAME:
+      g_value_set_string (value, ide_device_get_icon_name (self));
+      break;
+
+    case PROP_ID:
+      g_value_set_string (value, ide_device_get_id (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_device_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  IdeDevice *self = IDE_DEVICE (object);
+
+  switch (prop_id)
+    {
+    case PROP_DISPLAY_NAME:
+      ide_device_set_display_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ICON_NAME:
+      ide_device_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ID:
+      ide_device_set_id (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_device_class_init (IdeDeviceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_device_finalize;
+  object_class->get_property = ide_device_get_property;
+  object_class->set_property = ide_device_set_property;
+
+  klass->get_info_async = ide_device_real_get_info_async;
+  klass->get_info_finish = ide_device_real_get_info_finish;
+
+  properties [PROP_DISPLAY_NAME] =
+    g_param_spec_string ("display-name",
+                         "Display Name",
+                         "The display name of the device.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeDevice:icon-name:
+   *
+   * The "icon-name" property is the icon to display with the device in
+   * various UI elements of Builder.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "Icon Name",
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "ID",
+                         "The device identifier.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_device_init (IdeDevice *self)
+{
+}
+
+void
+ide_device_prepare_configuration (IdeDevice        *self,
+                                  IdeConfiguration *configuration)
+{
+  g_assert (IDE_IS_DEVICE (self));
+  g_assert (IDE_IS_CONFIGURATION (configuration));
+
+  if (IDE_DEVICE_GET_CLASS (self)->prepare_configuration)
+    IDE_DEVICE_GET_CLASS (self)->prepare_configuration (self, configuration);
+}
+
+GQuark
+ide_device_error_quark (void)
+{
+  static GQuark quark = 0;
+
+  if G_UNLIKELY (quark == 0)
+    quark = g_quark_from_static_string ("ide_device_error_quark");
+
+  return quark;
+}
+
+/**
+ * ide_device_get_info_async:
+ * @self: an #IdeDevice
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously requests information about the device.
+ *
+ * Some information may not be available until after a connection
+ * has been established. This allows the device to connect before
+ * fetching that information.
+ *
+ * Since: 3.32
+ */
+void
+ide_device_get_info_async (IdeDevice           *self,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_DEVICE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_DEVICE_GET_CLASS (self)->get_info_async (self, cancellable, callback, user_data);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_device_get_info_finish:
+ * @self: an #IdeDevice
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to load the information about a device.
+ *
+ * Returns: (transfer full): an #IdeDeviceInfo or %NULL and @error is set
+ *
+ * Since: 3.32
+ */
+IdeDeviceInfo *
+ide_device_get_info_finish (IdeDevice     *self,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  IdeDeviceInfo *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_DEVICE (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  ret = IDE_DEVICE_GET_CLASS (self)->get_info_finish (self, result, error);
+
+  g_return_val_if_fail (!ret || IDE_IS_DEVICE_INFO (ret), NULL);
+
+  IDE_RETURN (ret);
+}
diff --git a/src/libide/foundry/ide-device.h b/src/libide/foundry/ide-device.h
new file mode 100644
index 000000000..b14a68f24
--- /dev/null
+++ b/src/libide/foundry/ide-device.h
@@ -0,0 +1,92 @@
+/* ide-device.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_DEVICE_ERROR_NO_SUCH_DEVICE = 1,
+} IdeDeviceError;
+
+#define IDE_TYPE_DEVICE  (ide_device_get_type())
+#define IDE_DEVICE_ERROR (ide_device_error_quark())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeDevice, ide_device, IDE, DEVICE, IdeObject)
+
+struct _IdeDeviceClass
+{
+  IdeObjectClass parent;
+
+  void           (*prepare_configuration) (IdeDevice            *self,
+                                           IdeConfiguration     *configuration);
+  void           (*get_info_async)        (IdeDevice            *self,
+                                           GCancellable         *cancellable,
+                                           GAsyncReadyCallback   callback,
+                                           gpointer              user_data);
+  IdeDeviceInfo *(*get_info_finish)       (IdeDevice            *self,
+                                           GAsyncResult         *result,
+                                           GError              **error);
+
+  /*< private >*/
+  gpointer _reserved[32];
+};
+
+IDE_AVAILABLE_IN_3_32
+GQuark         ide_device_error_quark           (void) G_GNUC_CONST;
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_device_get_display_name      (IdeDevice             *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_device_set_display_name      (IdeDevice             *self,
+                                                 const gchar           *display_name);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_device_get_icon_name         (IdeDevice             *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_device_set_icon_name         (IdeDevice             *self,
+                                                 const gchar           *icon_name);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_device_get_id                (IdeDevice             *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_device_set_id                (IdeDevice             *self,
+                                                 const gchar           *id);
+IDE_AVAILABLE_IN_3_32
+void           ide_device_prepare_configuration (IdeDevice             *self,
+                                                 IdeConfiguration      *configuration);
+IDE_AVAILABLE_IN_3_32
+void           ide_device_get_info_async        (IdeDevice             *self,
+                                                 GCancellable          *cancellable,
+                                                 GAsyncReadyCallback    callback,
+                                                 gpointer               user_data);
+IDE_AVAILABLE_IN_3_32
+IdeDeviceInfo *ide_device_get_info_finish       (IdeDevice             *self,
+                                                 GAsyncResult          *result,
+                                                 GError               **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-fallback-build-system.c b/src/libide/foundry/ide-fallback-build-system.c
new file mode 100644
index 000000000..9768dbf64
--- /dev/null
+++ b/src/libide/foundry/ide-fallback-build-system.c
@@ -0,0 +1,169 @@
+/* ide-fallback-build-system.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "fallback-build-system"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-fallback-build-system.h"
+
+struct _IdeFallbackBuildSystem
+{
+  IdeObject  parent_instance;
+  GFile     *project_file;
+};
+
+static void build_system_init (IdeBuildSystemInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeFallbackBuildSystem,
+                         ide_fallback_build_system,
+                         IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_SYSTEM, build_system_init))
+
+enum {
+  PROP_0,
+  PROP_PROJECT_FILE,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+ide_fallback_build_system_finalize (GObject *object)
+{
+  IdeFallbackBuildSystem *self = (IdeFallbackBuildSystem *)object;
+
+  g_clear_object (&self->project_file);
+
+  G_OBJECT_CLASS (ide_fallback_build_system_parent_class)->finalize (object);
+}
+
+static void
+ide_fallback_build_system_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  IdeFallbackBuildSystem *self = IDE_FALLBACK_BUILD_SYSTEM (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_FILE:
+      g_value_set_object (value, self->project_file);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_fallback_build_system_set_property (GObject      *object,
+                                        guint         prop_id,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  IdeFallbackBuildSystem *self = IDE_FALLBACK_BUILD_SYSTEM (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT_FILE:
+      self->project_file = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_fallback_build_system_class_init (IdeFallbackBuildSystemClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_fallback_build_system_finalize;
+  object_class->get_property = ide_fallback_build_system_get_property;
+  object_class->set_property = ide_fallback_build_system_set_property;
+
+  /**
+   * IdeFallbackBuildSystem:project-file:
+   *
+   * The "project-file" property is the primary file representing the
+   * projects build system.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROJECT_FILE] =
+    g_param_spec_object ("project-file",
+                         "Project File",
+                         "The path of the project file.",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_fallback_build_system_init (IdeFallbackBuildSystem *self)
+{
+}
+
+static gint
+ide_fallback_build_system_get_priority (IdeBuildSystem *build_system)
+{
+  return 1000000;
+}
+
+static gchar *
+ide_fallback_build_system_get_id (IdeBuildSystem *build_system)
+{
+  return g_strdup ("fallback");
+}
+
+static gchar *
+ide_fallback_build_system_get_display_name (IdeBuildSystem *build_system)
+{
+  return g_strdup (_("Fallback"));
+}
+
+static void
+build_system_init (IdeBuildSystemInterface *iface)
+{
+  iface->get_priority = ide_fallback_build_system_get_priority;
+  iface->get_id = ide_fallback_build_system_get_id;
+  iface->get_display_name = ide_fallback_build_system_get_display_name;
+}
+
+/**
+ * ide_fallback_build_system_new:
+ *
+ * Creates a new #IdeFallbackBuildSystem.
+ *
+ * Returns: (transfer full): an #IdeBuildSystem
+ *
+ * Since: 3.32
+ */
+IdeBuildSystem *
+ide_fallback_build_system_new (void)
+{
+  return g_object_new (IDE_TYPE_FALLBACK_BUILD_SYSTEM, NULL);
+}
diff --git a/src/libide/foundry/ide-fallback-build-system.h b/src/libide/foundry/ide-fallback-build-system.h
new file mode 100644
index 000000000..b336c913a
--- /dev/null
+++ b/src/libide/foundry/ide-fallback-build-system.h
@@ -0,0 +1,35 @@
+/* ide-fallback-build-system.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-build-system.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FALLBACK_BUILD_SYSTEM (ide_fallback_build_system_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeFallbackBuildSystem, ide_fallback_build_system, IDE, FALLBACK_BUILD_SYSTEM, 
IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeBuildSystem *ide_fallback_build_system_new (void);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-foundry-compat.c b/src/libide/foundry/ide-foundry-compat.c
new file mode 100644
index 000000000..04051ef46
--- /dev/null
+++ b/src/libide/foundry/ide-foundry-compat.c
@@ -0,0 +1,227 @@
+/* ide-foundry-compat.c
+ *
+ * Copyright 2018-2019 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 "ide-foundry-compat"
+
+#include "config.h"
+
+#include "ide-build-manager.h"
+#include "ide-build-system.h"
+#include "ide-device-manager.h"
+#include "ide-configuration-manager.h"
+#include "ide-foundry-compat.h"
+#include "ide-run-manager.h"
+#include "ide-runtime-manager.h"
+#include "ide-test-manager.h"
+#include "ide-toolchain-manager.h"
+
+static gpointer
+ensure_child_typed_borrowed (IdeContext *context,
+                             GType       child_type)
+{
+  gpointer ret;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONTEXT (context));
+
+  if (!(ret = ide_context_peek_child_typed (context, child_type)))
+    {
+      g_autoptr(IdeObject) child = NULL;
+
+      if (!ide_context_has_project (context))
+        {
+          g_critical ("A plugin has attempted to access the %s foundry subsystem before a project has been 
loaded. "
+                      "This is not supported and may cause undesired behavior.",
+                      g_type_name (child_type));
+        }
+
+      child = ide_object_ensure_child_typed (IDE_OBJECT (context), child_type);
+      ret = ide_context_peek_child_typed (context, child_type);
+    }
+
+  return ret;
+}
+
+static gpointer
+get_child_typed_borrowed (IdeContext *context,
+                          GType       child_type)
+{
+  GObject *ret;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONTEXT (context));
+
+  /* We get a full ref to the child, but we want to return a borrowed
+   * reference to the manager. Since we're on the main thread, we can
+   * guarantee that no destroy will happen (since that has to happen
+   * in the main thread).
+   */
+  if ((ret = ide_object_get_child_typed (IDE_OBJECT (context), child_type)))
+    g_object_unref (ret);
+
+  return ret;
+}
+
+/**
+ * ide_build_manager_from_context:
+ * @context: a #IdeContext
+ *
+ * Returns: (transfer none): an #IdeBuildManager
+ *
+ * Since: 3.32
+ */
+IdeBuildManager *
+ide_build_manager_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return ensure_child_typed_borrowed (context, IDE_TYPE_BUILD_MANAGER);
+}
+
+/**
+ * ide_build_manager_ref_from_context:
+ * @context: a #IdeContext
+ *
+ * Returns: (transfer full): an #IdeBuildManager
+ *
+ * Since: 3.32
+ */
+IdeBuildManager *
+ide_build_manager_ref_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_BUILD_MANAGER);
+}
+
+/**
+ * ide_build_system_from_context:
+ * @context: a #IdeContext
+ *
+ * Gets the build system for the context. If no build system has been
+ * registered, then this returns %NULL.
+ *
+ * Returns: (transfer none) (nullable): an #IdeBuildSystem
+ *
+ * Since: 3.32
+ */
+IdeBuildSystem *
+ide_build_system_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return get_child_typed_borrowed (context, IDE_TYPE_BUILD_SYSTEM);
+}
+
+/**
+ * ide_configuration_manager_from_context:
+ * @context: a #IdeContext
+ *
+ * Returns: (transfer none): an #IdeConfigurationManager
+ *
+ * Since: 3.32
+ */
+IdeConfigurationManager *
+ide_configuration_manager_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return ensure_child_typed_borrowed (context, IDE_TYPE_CONFIGURATION_MANAGER);
+}
+
+/**
+ * ide_device_manager_from_context:
+ * @context: a #IdeContext
+ *
+ * Returns: (transfer none): an #IdeDeviceManager
+ *
+ * Since: 3.32
+ */
+IdeDeviceManager *
+ide_device_manager_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return ensure_child_typed_borrowed (context, IDE_TYPE_DEVICE_MANAGER);
+}
+
+/**
+ * ide_toolchain_manager_from_context:
+ * @context: a #IdeContext
+ *
+ * Returns: (transfer none): an #IdeToolchainManager
+ *
+ * Since: 3.32
+ */
+IdeToolchainManager *
+ide_toolchain_manager_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return ensure_child_typed_borrowed (context, IDE_TYPE_TOOLCHAIN_MANAGER);
+}
+
+/**
+ * ide_run_manager_from_context:
+ * @context: a #IdeContext
+ *
+ * Returns: (transfer none): an #IdeRunManager
+ *
+ * Since: 3.32
+ */
+IdeRunManager *
+ide_run_manager_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return ensure_child_typed_borrowed (context, IDE_TYPE_RUN_MANAGER);
+}
+
+/**
+ * ide_runtime_manager_from_context:
+ * @context: a #IdeContext
+ *
+ * Returns: (transfer none): an #IdeRuntimeManager
+ *
+ * Since: 3.32
+ */
+IdeRuntimeManager *
+ide_runtime_manager_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return ensure_child_typed_borrowed (context, IDE_TYPE_RUNTIME_MANAGER);
+}
+
+/**
+ * ide_test_manager_from_context:
+ * @context: a #IdeContext
+ *
+ * Returns: (transfer none): an #IdeTestManager
+ *
+ * Since: 3.32
+ */
+IdeTestManager *
+ide_test_manager_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return ensure_child_typed_borrowed (context, IDE_TYPE_TEST_MANAGER);
+}
diff --git a/src/libide/foundry/ide-foundry-compat.h b/src/libide/foundry/ide-foundry-compat.h
new file mode 100644
index 000000000..a8acb5e00
--- /dev/null
+++ b/src/libide/foundry/ide-foundry-compat.h
@@ -0,0 +1,36 @@
+/* ide-foundry-compat.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+IdeToolchainManager     *ide_toolchain_manager_from_context     (IdeContext *context);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-foundry-init.c b/src/libide/foundry/ide-foundry-init.c
new file mode 100644
index 000000000..91a04a6ca
--- /dev/null
+++ b/src/libide/foundry/ide-foundry-init.c
@@ -0,0 +1,161 @@
+/* ide-foundry-init.c
+ *
+ * Copyright 2018-2019 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 "ide-foundry-init"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-build-manager.h"
+#include "ide-device-manager.h"
+#include "ide-configuration-manager.h"
+#include "ide-foundry-init.h"
+#include "ide-run-manager.h"
+#include "ide-runtime-manager.h"
+#include "ide-test-manager.h"
+#include "ide-toolchain-manager.h"
+
+typedef struct
+{
+  GQueue to_init;
+} FoundryInit;
+
+static void
+foundry_init_free (FoundryInit *state)
+{
+  g_queue_foreach (&state->to_init, (GFunc)g_object_unref, NULL);
+  g_queue_clear (&state->to_init);
+  g_slice_free (FoundryInit, state);
+}
+
+static void
+ide_foundry_init_async_cb (GObject      *init_object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  GAsyncInitable *initable = (GAsyncInitable *)init_object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  FoundryInit *state;
+  GCancellable *cancellable;
+
+  g_assert (G_IS_ASYNC_INITABLE (initable));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!g_async_initable_init_finish (initable, result, &error))
+    g_warning ("Failed to init %s: %s",
+               G_OBJECT_TYPE_NAME (initable), error->message);
+
+  state = ide_task_get_task_data (task);
+  cancellable = ide_task_get_cancellable (task);
+
+  while (state->to_init.head)
+    {
+      g_autoptr(IdeObject) object = g_queue_pop_head (&state->to_init);
+
+      if (G_IS_ASYNC_INITABLE (object))
+        {
+          g_async_initable_init_async (G_ASYNC_INITABLE (object),
+                                       G_PRIORITY_DEFAULT,
+                                       cancellable,
+                                       ide_foundry_init_async_cb,
+                                       g_steal_pointer (&task));
+          return;
+        }
+
+      if (G_IS_INITABLE (object))
+        g_initable_init (G_INITABLE (object), NULL, NULL);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+void
+_ide_foundry_init_async (IdeContext          *context,
+                         GCancellable        *cancellable,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  FoundryInit *state;
+  GType foundry_types[] = {
+    IDE_TYPE_DEVICE_MANAGER,
+    IDE_TYPE_RUNTIME_MANAGER,
+    IDE_TYPE_TOOLCHAIN_MANAGER,
+    IDE_TYPE_CONFIGURATION_MANAGER,
+    IDE_TYPE_BUILD_MANAGER,
+    IDE_TYPE_RUN_MANAGER,
+    IDE_TYPE_TEST_MANAGER,
+  };
+
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  state = g_slice_new0 (FoundryInit);
+  g_queue_init (&state->to_init);
+
+  task = ide_task_new (context, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, _ide_foundry_init_async);
+  ide_task_set_task_data (task, state, foundry_init_free);
+
+  for (guint i = 0; i < G_N_ELEMENTS (foundry_types); i++)
+    {
+      g_autoptr(IdeObject) object = NULL;
+
+      /* Skip if plugins already forced this subsystem to load */
+      if ((object = ide_object_get_child_typed (IDE_OBJECT (context), foundry_types[i])))
+        continue;
+
+      object = g_object_new (foundry_types[i], NULL);
+      ide_object_append (IDE_OBJECT (context), object);
+      g_queue_push_tail (&state->to_init, g_steal_pointer (&object));
+    }
+
+  while (state->to_init.head)
+    {
+      g_autoptr(IdeObject) object = g_queue_pop_head (&state->to_init);
+
+      if (G_IS_ASYNC_INITABLE (object))
+        {
+          g_async_initable_init_async (G_ASYNC_INITABLE (object),
+                                       G_PRIORITY_DEFAULT,
+                                       NULL,
+                                       ide_foundry_init_async_cb,
+                                       g_steal_pointer (&task));
+          return;
+        }
+
+      if (G_IS_INITABLE (object))
+        g_initable_init (G_INITABLE (object), NULL, NULL);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+gboolean
+_ide_foundry_init_finish (GAsyncResult  *result,
+                          GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
diff --git a/src/libide/foundry/ide-foundry-init.h b/src/libide/foundry/ide-foundry-init.h
new file mode 100644
index 000000000..a12915b16
--- /dev/null
+++ b/src/libide/foundry/ide-foundry-init.h
@@ -0,0 +1,34 @@
+/* ide-foundry-init.h
+ *
+ * Copyright 2018-2019 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
+
+void     _ide_foundry_init_async  (IdeContext           *context,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data);
+gboolean _ide_foundry_init_finish (GAsyncResult         *result,
+                                   GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-foundry-types.h b/src/libide/foundry/ide-foundry-types.h
new file mode 100644
index 000000000..442f730e2
--- /dev/null
+++ b/src/libide/foundry/ide-foundry-types.h
@@ -0,0 +1,71 @@
+/* ide-foundry-types.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef struct _IdeBuildLog IdeBuildLog;
+typedef struct _IdeBuildManager IdeBuildManager;
+typedef struct _IdeBuildPipeline IdeBuildPipeline;
+typedef struct _IdeBuildPipelineAddin IdeBuildPipelineAddin;
+typedef struct _IdeBuildStage IdeBuildStage;
+typedef struct _IdeBuildStageLauncher IdeBuildStageLauncher;
+typedef struct _IdeBuildStageMkdirs IdeBuildStageMkdirs;
+typedef struct _IdeBuildStageTransfer IdeBuildStageTransfer;
+typedef struct _IdeBuildSystem IdeBuildSystem;
+typedef struct _IdeBuildSystemDiscovery IdeBuildSystemDiscovery;
+typedef struct _IdeBuildTarget IdeBuildTarget;
+typedef struct _IdeBuildTargetProvider IdeBuildTargetProvider;
+typedef struct _IdeCompileCommands IdeCompileCommands;
+typedef struct _IdeConfiguration IdeConfiguration;
+typedef struct _IdeConfigurationProvider IdeConfigurationProvider;
+typedef struct _IdeConfigurationManager IdeConfigurationManager;
+typedef struct _IdeDependencyUpdater IdeDependencyUpdater;
+typedef struct _IdeDeployStrategy IdeDeployStrategy;
+typedef struct _IdeDevice IdeDevice;
+typedef struct _IdeDeviceInfo IdeDeviceInfo;
+typedef struct _IdeDeviceManager IdeDeviceManager;
+typedef struct _IdeDeviceProvider IdeDeviceProvider;
+typedef struct _IdeLocalDevice IdeLocalDevice;
+typedef struct _IdeRunButton IdeRunButton;
+typedef struct _IdeRunManager IdeRunManager;
+typedef struct _IdeRunner IdeRunner;
+typedef struct _IdeRunnerAddin IdeRunnerAddin;
+typedef struct _IdeRuntime IdeRuntime;
+typedef struct _IdeRuntimeManager IdeRuntimeManager;
+typedef struct _IdeRuntimeProvider IdeRuntimeProvider;
+typedef struct _IdeSimpleBuildTarget IdeSimpleBuildTarget;
+typedef struct _IdeSimpleToolchain IdeSimpleToolchain;
+typedef struct _IdeTriplet IdeTriplet;
+typedef struct _IdeTest IdeTest;
+typedef struct _IdeTestManager IdeTestManager;
+typedef struct _IdeTestProvider IdeTestProvider;
+typedef struct _IdeToolchain IdeToolchain;
+typedef struct _IdeToolchainManager IdeToolchainManager;
+typedef struct _IdeToolchainProvider IdeToolchainProvider;
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-local-device.c b/src/libide/foundry/ide-local-device.c
new file mode 100644
index 000000000..50cc16a52
--- /dev/null
+++ b/src/libide/foundry/ide-local-device.c
@@ -0,0 +1,196 @@
+/* ide-local-device.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-loca-device"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+#include <string.h>
+#include <sys/utsname.h>
+
+#include "ide-local-device.h"
+#include "ide-device.h"
+#include "ide-device-info.h"
+#include "ide-triplet.h"
+
+typedef struct
+{
+  IdeTriplet *triplet;
+} IdeLocalDevicePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeLocalDevice, ide_local_device, IDE_TYPE_DEVICE)
+
+enum {
+  PROP_0,
+  PROP_TRIPLET,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_local_device_get_info_async (IdeDevice           *device,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  IdeLocalDevice *self = (IdeLocalDevice *)device;
+  IdeLocalDevicePrivate *priv = ide_local_device_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(IdeDeviceInfo) info = NULL;
+
+  g_assert (IDE_IS_LOCAL_DEVICE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (device, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_local_device_get_info_async);
+  ide_task_set_check_cancellable (task, FALSE);
+
+  info = ide_device_info_new ();
+  ide_device_info_set_host_triplet (info, priv->triplet);
+
+  ide_task_return_pointer (task, g_steal_pointer (&info), g_object_unref);
+}
+
+static IdeDeviceInfo *
+ide_local_device_get_info_finish (IdeDevice     *device,
+                                  GAsyncResult  *result,
+                                  GError       **error)
+{
+  g_assert (IDE_IS_DEVICE (device));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+ide_local_device_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  IdeLocalDevice *self = IDE_LOCAL_DEVICE (object);
+  IdeLocalDevicePrivate *priv = ide_local_device_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_TRIPLET:
+      g_value_set_boxed (value, priv->triplet);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_local_device_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  IdeLocalDevice *self = IDE_LOCAL_DEVICE (object);
+  IdeLocalDevicePrivate *priv = ide_local_device_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_TRIPLET:
+      priv->triplet = g_value_dup_boxed (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_local_device_constructed (GObject *object)
+{
+  IdeLocalDevice *self = (IdeLocalDevice *)object;
+  IdeLocalDevicePrivate *priv = ide_local_device_get_instance_private (self);
+  g_autofree gchar *name = NULL;
+
+  g_assert (IDE_IS_LOCAL_DEVICE (self));
+
+  if (priv->triplet == NULL)
+    priv->triplet = ide_triplet_new_from_system ();
+
+  if (ide_triplet_is_system (priv->triplet))
+    {
+      /* translators: %s is replaced with the host name */
+      name = g_strdup_printf (_("My Computer (%s)"), g_get_host_name ());
+      ide_device_set_display_name (IDE_DEVICE (self), name);
+      ide_device_set_id (IDE_DEVICE (self), "local");
+    }
+  else
+    {
+      const gchar *arch = ide_triplet_get_arch (priv->triplet);
+      g_autofree gchar *id = g_strdup_printf ("local:%s", arch);
+
+      /* translators: first %s is replaced with the host name, second with CPU architecture */
+      name = g_strdup_printf (_("My Computer (%s) — %s"), g_get_host_name (), arch);
+      ide_device_set_display_name (IDE_DEVICE (self), name);
+      ide_device_set_id (IDE_DEVICE (self), id);
+    }
+
+  G_OBJECT_CLASS (ide_local_device_parent_class)->constructed (object);
+}
+
+static void
+ide_local_device_finalize (GObject *object)
+{
+  IdeLocalDevice *self = (IdeLocalDevice *)object;
+  IdeLocalDevicePrivate *priv = ide_local_device_get_instance_private (self);
+
+  g_clear_pointer (&priv->triplet, ide_triplet_unref);
+
+  G_OBJECT_CLASS (ide_local_device_parent_class)->finalize (object);
+}
+
+static void
+ide_local_device_class_init (IdeLocalDeviceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeDeviceClass *device_class = IDE_DEVICE_CLASS (klass);
+
+  object_class->constructed = ide_local_device_constructed;
+  object_class->finalize = ide_local_device_finalize;
+  object_class->get_property = ide_local_device_get_property;
+  object_class->set_property = ide_local_device_set_property;
+
+  device_class->get_info_async = ide_local_device_get_info_async;
+  device_class->get_info_finish = ide_local_device_get_info_finish;
+
+  properties [PROP_TRIPLET] =
+    g_param_spec_boxed ("triplet",
+                        "Triplet",
+                        "The #IdeTriplet object describing the local device configuration name",
+                        IDE_TYPE_TRIPLET,
+                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_local_device_init (IdeLocalDevice *self)
+{
+}
diff --git a/src/libide/foundry/ide-local-device.h b/src/libide/foundry/ide-local-device.h
new file mode 100644
index 000000000..1f4e5f1b3
--- /dev/null
+++ b/src/libide/foundry/ide-local-device.h
@@ -0,0 +1,46 @@
+/* ide-local-device.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-device.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LOCAL_DEVICE (ide_local_device_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeLocalDevice, ide_local_device, IDE, LOCAL_DEVICE, IdeDevice)
+
+struct _IdeLocalDeviceClass
+{
+  IdeDeviceClass parent;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-run-manager-private.h b/src/libide/foundry/ide-run-manager-private.h
new file mode 100644
index 000000000..18c26d27b
--- /dev/null
+++ b/src/libide/foundry/ide-run-manager-private.h
@@ -0,0 +1,41 @@
+/* ide-run-manager-private.h
+ *
+ * Copyright 2016-2019 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 "ide-run-manager.h"
+
+G_BEGIN_DECLS
+
+typedef struct
+{
+  gchar          *id;
+  gchar          *title;
+  gchar          *icon_name;
+  gchar          *accel;
+  gint            priority;
+  IdeRunHandler   handler;
+  gpointer        handler_data;
+  GDestroyNotify  handler_data_destroy;
+} IdeRunHandlerInfo;
+
+const GList *_ide_run_manager_get_handlers (IdeRunManager *self);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-run-manager.c b/src/libide/foundry/ide-run-manager.c
new file mode 100644
index 000000000..82e254fb9
--- /dev/null
+++ b/src/libide/foundry/ide-run-manager.c
@@ -0,0 +1,1178 @@
+/* ide-run-manager.c
+ *
+ * Copyright 2016-2019 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 "ide-run-manager"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+#include <libpeas/peas-autocleanups.h>
+
+#include "ide-build-manager.h"
+#include "ide-build-system.h"
+#include "ide-build-target-provider.h"
+#include "ide-build-target.h"
+#include "ide-configuration-manager.h"
+#include "ide-configuration.h"
+#include "ide-foundry-compat.h"
+#include "ide-run-manager-private.h"
+#include "ide-run-manager.h"
+#include "ide-runner.h"
+#include "ide-runtime.h"
+
+struct _IdeRunManager
+{
+  IdeObject                parent_instance;
+
+  GCancellable            *cancellable;
+  IdeBuildTarget          *build_target;
+  IdeNotification         *notif;
+
+  const IdeRunHandlerInfo *handler;
+  GList                   *handlers;
+
+  guint                    busy : 1;
+};
+
+typedef struct
+{
+  GList     *providers;
+  GPtrArray *results;
+  guint      active;
+} DiscoverState;
+
+static void initable_iface_init                      (GInitableIface *iface);
+static void ide_run_manager_actions_run              (IdeRunManager  *self,
+                                                      GVariant       *param);
+static void ide_run_manager_actions_run_with_handler (IdeRunManager  *self,
+                                                      GVariant       *param);
+static void ide_run_manager_actions_stop             (IdeRunManager  *self,
+                                                      GVariant       *param);
+
+DZL_DEFINE_ACTION_GROUP (IdeRunManager, ide_run_manager, {
+  { "run", ide_run_manager_actions_run },
+  { "run-with-handler", ide_run_manager_actions_run_with_handler, "s" },
+  { "stop", ide_run_manager_actions_stop },
+})
+
+G_DEFINE_TYPE_EXTENDED (IdeRunManager, ide_run_manager, IDE_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
+                        G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
+                                               ide_run_manager_init_action_group))
+
+enum {
+  PROP_0,
+  PROP_BUSY,
+  PROP_HANDLER,
+  PROP_BUILD_TARGET,
+  N_PROPS
+};
+
+enum {
+  RUN,
+  STOPPED,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+discover_state_free (gpointer data)
+{
+  DiscoverState *state = data;
+
+  g_assert (state->active == 0);
+
+  g_list_free_full (state->providers, g_object_unref);
+  g_clear_pointer (&state->results, g_ptr_array_unref);
+  g_slice_free (DiscoverState, state);
+}
+
+static void
+ide_run_manager_real_run (IdeRunManager *self,
+                          IdeRunner     *runner)
+{
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (IDE_IS_RUNNER (runner));
+
+  /*
+   * If the current handler has a callback specified (our default "run" handler
+   * does not), then we need to allow that handler to prepare the runner.
+   */
+  if (self->handler != NULL && self->handler->handler != NULL)
+    self->handler->handler (self, runner, self->handler->handler_data);
+}
+
+static void
+ide_run_handler_info_free (gpointer data)
+{
+  IdeRunHandlerInfo *info = data;
+
+  g_free (info->id);
+  g_free (info->title);
+  g_free (info->icon_name);
+  g_free (info->accel);
+
+  if (info->handler_data_destroy)
+    info->handler_data_destroy (info->handler_data);
+
+  g_slice_free (IdeRunHandlerInfo, info);
+}
+
+static void
+ide_run_manager_dispose (GObject *object)
+{
+  IdeRunManager *self = (IdeRunManager *)object;
+
+  self->handler = NULL;
+
+  g_clear_object (&self->cancellable);
+  ide_clear_and_destroy_object (&self->build_target);
+
+  g_list_free_full (self->handlers, ide_run_handler_info_free);
+  self->handlers = NULL;
+
+  G_OBJECT_CLASS (ide_run_manager_parent_class)->dispose (object);
+}
+
+static void
+ide_run_manager_update_action_enabled (IdeRunManager *self)
+{
+  IdeBuildManager *build_manager;
+  IdeContext *context;
+  gboolean can_build;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_manager = ide_build_manager_from_context (context);
+  can_build = ide_build_manager_get_can_build (build_manager);
+
+  ide_run_manager_set_action_enabled (self, "run",
+                                      self->busy == FALSE && can_build == TRUE);
+  ide_run_manager_set_action_enabled (self, "run-with-handler",
+                                      self->busy == FALSE && can_build == TRUE);
+  ide_run_manager_set_action_enabled (self, "stop", self->busy == TRUE);
+}
+
+static void
+ide_run_manager_notify_can_build (IdeRunManager   *self,
+                                  GParamSpec      *pspec,
+                                  IdeBuildManager *build_manager)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (G_IS_PARAM_SPEC (pspec));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  ide_run_manager_update_action_enabled (self);
+
+  IDE_EXIT;
+}
+
+static gboolean
+initable_init (GInitable     *initable,
+               GCancellable  *cancellable,
+               GError       **error)
+{
+  IdeRunManager *self = (IdeRunManager *)initable;
+  IdeBuildManager *build_manager;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_manager = ide_build_manager_from_context (context);
+
+  g_signal_connect_object (build_manager,
+                           "notify::can-build",
+                           G_CALLBACK (ide_run_manager_notify_can_build),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_run_manager_update_action_enabled (self);
+
+  IDE_RETURN (TRUE);
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+  iface->init = initable_init;
+}
+
+static void
+ide_run_manager_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeRunManager *self = IDE_RUN_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUSY:
+      g_value_set_boolean (value, ide_run_manager_get_busy (self));
+      break;
+
+    case PROP_HANDLER:
+      g_value_set_string (value, ide_run_manager_get_handler (self));
+      break;
+
+    case PROP_BUILD_TARGET:
+      g_value_set_object (value, ide_run_manager_get_build_target (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_run_manager_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeRunManager *self = IDE_RUN_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUILD_TARGET:
+      ide_run_manager_set_build_target (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_run_manager_class_init (IdeRunManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_run_manager_dispose;
+  object_class->get_property = ide_run_manager_get_property;
+  object_class->set_property = ide_run_manager_set_property;
+
+  properties [PROP_BUSY] =
+    g_param_spec_boolean ("busy",
+                          "Busy",
+                          "Busy",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HANDLER] =
+    g_param_spec_string ("handler",
+                         "Handler",
+                         "Handler",
+                         "run",
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_BUILD_TARGET] =
+    g_param_spec_object ("build-target",
+                         "Build Target",
+                         "The IdeBuildTarget that will be run",
+                         IDE_TYPE_BUILD_TARGET,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeRunManager::run:
+   * @self: An #IdeRunManager
+   * @runner: An #IdeRunner
+   *
+   * This signal is emitted right before ide_runner_run_async() is called
+   * on an #IdeRunner. It can be used by plugins to tweak things right
+   * before the runner is executed.
+   *
+   * The current run handler (debugger, profiler, etc) is run as the default
+   * handler for this function. So connect with %G_SIGNAL_AFTER if you want
+   * to be nofied after the run handler has executed. It's unwise to change
+   * things that the run handler might expect. Generally if you want to
+   * change settings, do that before the run handler has exected.
+   *
+   * Since: 3.32
+   */
+  signals [RUN] =
+    g_signal_new_class_handler ("run",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_run_manager_real_run),
+                                NULL,
+                                NULL,
+                                NULL,
+                                G_TYPE_NONE,
+                                1,
+                                IDE_TYPE_RUNNER);
+
+  /**
+   * IdeRunManager::stopped:
+   *
+   * This signal is emitted when the run manager has stopped the currently
+   * executing inferior.
+   *
+   * Since: 3.32
+   */
+  signals [STOPPED] =
+    g_signal_new ("stopped",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  0);
+}
+
+gboolean
+ide_run_manager_get_busy (IdeRunManager *self)
+{
+  g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), FALSE);
+
+  return self->busy;
+}
+
+static gboolean
+ide_run_manager_check_busy (IdeRunManager  *self,
+                            GError        **error)
+{
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (error != NULL);
+
+  if (ide_run_manager_get_busy (self))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_BUSY,
+                   "%s",
+                   _("Cannot run target, another target is running"));
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_run_manager_run_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  IdeRunner *runner = (IdeRunner *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeRunManager *self;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUNNER (runner));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+
+  if (self->notif != NULL)
+    {
+      ide_notification_withdraw (self->notif);
+      g_clear_object (&self->notif);
+    }
+
+  if (!ide_runner_run_finish (runner, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  g_signal_emit (self, signals [STOPPED], 0);
+
+  ide_object_destroy (IDE_OBJECT (runner));
+
+  IDE_EXIT;
+}
+
+static void
+do_run_async (IdeRunManager *self,
+              IdeTask       *task)
+{
+  g_autofree gchar *name = NULL;
+  g_autofree gchar *title = NULL;
+  IdeBuildTarget *build_target;
+  IdeContext *context;
+  IdeConfigurationManager *config_manager;
+  IdeConfiguration *config;
+  IdeEnvironment *environment;
+  IdeRuntime *runtime;
+  g_autoptr(IdeRunner) runner = NULL;
+  GCancellable *cancellable;
+  const gchar *run_opts;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (IDE_IS_TASK (task));
+
+  build_target = ide_task_get_task_data (task);
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  g_assert (IDE_IS_BUILD_TARGET (build_target));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  config_manager = ide_configuration_manager_from_context (context);
+  config = ide_configuration_manager_get_current (config_manager);
+  runtime = ide_configuration_get_runtime (config);
+
+  if (runtime == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 IDE_RUNTIME_ERROR,
+                                 IDE_RUNTIME_ERROR_NO_SUCH_RUNTIME,
+                                 "%s “%s”",
+                                 _("Failed to locate runtime"),
+                                 ide_configuration_get_runtime_id (config));
+      IDE_EXIT;
+    }
+
+  runner = ide_runtime_create_runner (runtime, build_target);
+  cancellable = ide_task_get_cancellable (task);
+
+  g_assert (IDE_IS_RUNNER (runner));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* Add our run arguments if specified in the config. */
+  if (NULL != (run_opts = ide_configuration_get_run_opts (config)))
+    {
+      g_auto(GStrv) argv = NULL;
+      gint argc;
+
+      if (g_shell_parse_argv (run_opts, &argc, &argv, NULL))
+        {
+          for (gint i = 0; i < argc; i++)
+            ide_runner_append_argv (runner, argv[i]);
+        }
+    }
+
+  /* Add our environment variables. Currently, these are coming
+   * from the *build* environment because we do not yet have a
+   * way to differentiate between build environment and runtime
+   * for the application.
+   */
+  environment = ide_runner_get_environment (runner);
+  ide_environment_setenv (environment, "G_MESSAGES_DEBUG", "all");
+  ide_environment_copy_into (ide_configuration_get_environment (config), environment, TRUE);
+
+  g_signal_emit (self, signals [RUN], 0, runner);
+
+  if (ide_runner_get_failed (runner))
+    {
+      ide_task_return_new_error (task,
+                                 IDE_RUNTIME_ERROR,
+                                 IDE_RUNTIME_ERROR_SPAWN_FAILED,
+                                 "Failed to execute the application");
+      IDE_EXIT;
+    }
+
+  if (self->notif != NULL)
+    {
+      ide_notification_withdraw (self->notif);
+      g_clear_object (&self->notif);
+    }
+
+  self->notif = ide_notification_new ();
+  name = ide_build_target_get_name (build_target);
+  /* translators: %s is replaced with the name of the users executable */
+  title = g_strdup_printf (_("Running %s…"), name);
+  ide_notification_set_title (self->notif, title);
+  ide_notification_attach (self->notif, IDE_OBJECT (self));
+
+  ide_runner_run_async (runner,
+                        cancellable,
+                        ide_run_manager_run_cb,
+                        g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_run_discover_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  IdeRunManager *self = (IdeRunManager *)object;
+  g_autoptr(IdeBuildTarget) build_target = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  build_target = ide_run_manager_discover_default_target_finish (self, result, &error);
+
+  if (build_target == NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  ide_run_manager_set_build_target (self, build_target);
+
+  ide_task_set_task_data (task, g_steal_pointer (&build_target), g_object_unref);
+
+  do_run_async (self, task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_install_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeBuildManager *build_manager = (IdeBuildManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeRunManager *self;
+  IdeBuildTarget *build_target;
+  GCancellable *cancellable;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  if (!ide_build_manager_execute_finish (build_manager, result, &error))
+    {
+      /* We want to let the consumer know there was a build error
+       * (but don't need to pass the specific error code) so that
+       * they have an error code to check against.
+       */
+      ide_task_return_new_error (task,
+                                 IDE_RUNTIME_ERROR,
+                                 IDE_RUNTIME_ERROR_BUILD_FAILED,
+                                 /* translators: %s is replaced with the specific error reason */
+                                 _("The build target failed to build: %s"),
+                                 error->message);
+      IDE_EXIT;
+    }
+
+  build_target = ide_run_manager_get_build_target (self);
+
+  if (build_target == NULL)
+    {
+      cancellable = ide_task_get_cancellable (task);
+      g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+      ide_run_manager_discover_default_target_async (self,
+                                                     cancellable,
+                                                     ide_run_manager_run_discover_cb,
+                                                     g_steal_pointer (&task));
+      IDE_EXIT;
+    }
+
+  ide_task_set_task_data (task, g_object_ref (build_target), g_object_unref);
+
+  do_run_async (self, task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_task_completed (IdeRunManager *self,
+                                GParamSpec    *pspec,
+                                IdeTask       *task)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_TASK (task));
+
+  self->busy = FALSE;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+
+  ide_run_manager_update_action_enabled (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_do_install_before_run (IdeRunManager *self,
+                                       IdeTask       *task)
+{
+  IdeBuildManager *build_manager;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (IDE_IS_TASK (task));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_manager = ide_build_manager_from_context (context);
+
+  /*
+   * First we need to make sure the target is up to date and installed
+   * so that all the dependent resources are available.
+   */
+
+  self->busy = TRUE;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_run_manager_task_completed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_build_manager_execute_async (build_manager,
+                                   IDE_BUILD_PHASE_INSTALL,
+                                   NULL,
+                                   ide_task_get_cancellable (task),
+                                   ide_run_manager_install_cb,
+                                   g_object_ref (task));
+
+  ide_run_manager_update_action_enabled (self);
+
+  IDE_EXIT;
+}
+
+void
+ide_run_manager_run_async (IdeRunManager       *self,
+                           IdeBuildTarget      *build_target,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GCancellable) local_cancellable = NULL;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
+  g_return_if_fail (!build_target || IDE_IS_BUILD_TARGET (build_target));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (!g_cancellable_is_cancelled (self->cancellable));
+
+  if (cancellable == NULL)
+    cancellable = local_cancellable = g_cancellable_new ();
+
+  dzl_cancellable_chain (cancellable, self->cancellable);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_run_manager_run_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  if (ide_run_manager_check_busy (self, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (build_target != NULL)
+    ide_run_manager_set_build_target (self, build_target);
+
+  ide_run_manager_do_install_before_run (self, task);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_run_manager_run_finish (IdeRunManager  *self,
+                            GAsyncResult   *result,
+                            GError        **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+do_cancel_in_timeout (gpointer user_data)
+{
+  g_autoptr(GCancellable) cancellable = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_CANCELLABLE (cancellable));
+
+  if (!g_cancellable_is_cancelled (cancellable))
+    g_cancellable_cancel (cancellable);
+
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+void
+ide_run_manager_cancel (IdeRunManager *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
+
+  if (self->cancellable != NULL)
+    g_timeout_add (0, do_cancel_in_timeout, g_steal_pointer (&self->cancellable));
+  self->cancellable = g_cancellable_new ();
+
+  IDE_EXIT;
+}
+
+void
+ide_run_manager_set_handler (IdeRunManager *self,
+                             const gchar   *id)
+{
+  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
+
+  self->handler = NULL;
+
+  for (GList *iter = self->handlers; iter; iter = iter->next)
+    {
+      const IdeRunHandlerInfo *info = iter->data;
+
+      if (g_strcmp0 (info->id, id) == 0)
+        {
+          self->handler = info;
+          IDE_TRACE_MSG ("run handler set to %s", info->title);
+          g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HANDLER]);
+          break;
+        }
+    }
+}
+
+void
+ide_run_manager_add_handler (IdeRunManager  *self,
+                             const gchar    *id,
+                             const gchar    *title,
+                             const gchar    *icon_name,
+                             const gchar    *accel,
+                             IdeRunHandler   run_handler,
+                             gpointer        user_data,
+                             GDestroyNotify  user_data_destroy)
+{
+  IdeRunHandlerInfo *info;
+  DzlShortcutManager *manager;
+  DzlShortcutTheme *theme;
+  g_autofree gchar *action_name = NULL;
+  GApplication *app;
+
+  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
+  g_return_if_fail (id != NULL);
+  g_return_if_fail (title != NULL);
+
+  info = g_slice_new0 (IdeRunHandlerInfo);
+  info->id = g_strdup (id);
+  info->title = g_strdup (title);
+  info->icon_name = g_strdup (icon_name);
+  info->accel = g_strdup (accel);
+  info->handler = run_handler;
+  info->handler_data = user_data;
+  info->handler_data_destroy = user_data_destroy;
+
+  self->handlers = g_list_append (self->handlers, info);
+
+  app = g_application_get_default ();
+  manager = dzl_application_get_shortcut_manager (DZL_APPLICATION (app));
+  theme = g_object_ref (dzl_shortcut_manager_get_theme (manager));
+
+  action_name = g_strdup_printf ("run-manager.run-with-handler('%s')", id);
+
+  dzl_shortcut_manager_add_action (manager,
+                                   action_name,
+                                   NC_("shortcut window", "Workbench shortcuts"),
+                                   NC_("shortcut window", "Build and Run"),
+                                   NC_("shortcut window", title),
+                                   NULL);
+
+  dzl_shortcut_theme_set_accel_for_action (theme, action_name, accel, DZL_SHORTCUT_PHASE_DISPATCH);
+
+  if (self->handler == NULL)
+    self->handler = info;
+}
+
+void
+ide_run_manager_remove_handler (IdeRunManager *self,
+                                const gchar   *id)
+{
+  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
+  g_return_if_fail (id != NULL);
+
+  for (GList *iter = self->handlers; iter; iter = iter->next)
+    {
+      IdeRunHandlerInfo *info = iter->data;
+
+      if (g_strcmp0 (info->id, id) == 0)
+        {
+          self->handlers = g_list_delete_link (self->handlers, iter);
+
+          if (self->handler == info && self->handlers != NULL)
+            self->handler = self->handlers->data;
+          else
+            self->handler = NULL;
+
+          ide_run_handler_info_free (info);
+
+          break;
+        }
+    }
+}
+
+/**
+ * ide_run_manager_get_build_target:
+ *
+ * Gets the build target that will be executed by the run manager which
+ * was either specified to ide_run_manager_run_async() or determined by
+ * the build system.
+ *
+ * Returns: (transfer none): An #IdeBuildTarget or %NULL if no build target
+ *   has been set.
+ *
+ * Since: 3.32
+ */
+IdeBuildTarget *
+ide_run_manager_get_build_target (IdeRunManager *self)
+{
+  g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), NULL);
+
+  return self->build_target;
+}
+
+void
+ide_run_manager_set_build_target (IdeRunManager  *self,
+                                  IdeBuildTarget *build_target)
+{
+  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
+  g_return_if_fail (IDE_IS_BUILD_TARGET (build_target));
+
+  if (build_target == self->build_target)
+    return;
+
+  if (self->build_target)
+    ide_clear_and_destroy_object (&self->build_target);
+
+  if (build_target)
+    self->build_target = g_object_ref (build_target);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUILD_TARGET]);
+}
+
+static gint
+compare_targets (gconstpointer a,
+                 gconstpointer b)
+{
+  const IdeBuildTarget * const *a_target = a;
+  const IdeBuildTarget * const *b_target = b;
+
+  return ide_build_target_compare (*a_target, *b_target);
+}
+
+static void
+collect_extensions (PeasExtensionSet *set,
+                    PeasPluginInfo   *plugin_info,
+                    PeasExtension    *exten,
+                    gpointer          user_data)
+{
+  DiscoverState *state = user_data;
+
+  g_assert (state != NULL);
+  g_assert (IDE_IS_BUILD_TARGET_PROVIDER (exten));
+
+  state->providers = g_list_append (state->providers, g_object_ref (exten));
+  state->active++;
+}
+
+static void
+ide_run_manager_provider_get_targets_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  IdeBuildTargetProvider *provider = (IdeBuildTargetProvider *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GPtrArray) ret = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeRunManager *self;
+  DiscoverState *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_TARGET_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  state = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (state != NULL);
+  g_assert (state->active > 0);
+  g_assert (g_list_find (state->providers, provider) != NULL);
+
+  ret = ide_build_target_provider_get_targets_finish (provider, result, &error);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (ret, g_object_unref);
+
+  if (ret != NULL)
+    {
+      for (guint i = 0; i < ret->len; i++)
+        {
+          IdeBuildTarget *target = g_ptr_array_index (ret, i);
+
+          ide_object_append (IDE_OBJECT (self), IDE_OBJECT (target));
+
+          g_ptr_array_add (state->results, g_object_ref (target));
+        }
+    }
+
+  ide_object_destroy (IDE_OBJECT (provider));
+
+  state->active--;
+
+  if (state->active > 0)
+    return;
+
+  if (state->results->len == 0)
+    {
+      if (error != NULL)
+        ide_task_return_error (task, g_steal_pointer (&error));
+      else
+        ide_task_return_new_error (task,
+                                   IDE_RUNTIME_ERROR,
+                                   IDE_RUNTIME_ERROR_TARGET_NOT_FOUND,
+                                   _("Failed to locate a build target"));
+      IDE_EXIT;
+    }
+
+  g_ptr_array_sort (state->results, compare_targets);
+
+  ide_task_return_pointer (task,
+                           g_object_ref (g_ptr_array_index (state->results, 0)),
+                           g_object_unref);
+
+  IDE_EXIT;
+}
+
+void
+ide_run_manager_discover_default_target_async (IdeRunManager       *self,
+                                               GCancellable        *cancellable,
+                                               GAsyncReadyCallback  callback,
+                                               gpointer             user_data)
+{
+  g_autoptr(PeasExtensionSet) set = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  DiscoverState *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RUN_MANAGER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_run_manager_discover_default_target_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  set = peas_extension_set_new (peas_engine_get_default (),
+                                IDE_TYPE_BUILD_TARGET_PROVIDER,
+                                NULL);
+
+  state = g_slice_new0 (DiscoverState);
+  state->results = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_object_unref_and_destroy);
+  state->providers = NULL;
+  state->active = 0;
+
+  peas_extension_set_foreach (set, collect_extensions, state);
+
+  for (const GList *iter = state->providers; iter; iter = iter->next)
+    ide_object_append (IDE_OBJECT (self), IDE_OBJECT (iter->data));
+
+  ide_task_set_task_data (task, state, discover_state_free);
+
+  for (const GList *iter = state->providers; iter != NULL; iter = iter->next)
+    {
+      IdeBuildTargetProvider *provider = iter->data;
+
+      ide_build_target_provider_get_targets_async (provider,
+                                                   cancellable,
+                                                   ide_run_manager_provider_get_targets_cb,
+                                                   g_object_ref (task));
+    }
+
+  if (state->active == 0)
+    ide_task_return_new_error (task,
+                               IDE_RUNTIME_ERROR,
+                               IDE_RUNTIME_ERROR_TARGET_NOT_FOUND,
+                               _("Failed to locate a build target"));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_run_manager_discover_default_target_finish:
+ *
+ * Returns: (transfer full): An #IdeBuildTarget if successful; otherwise %NULL
+ *   and @error is set.
+ *
+ * Since: 3.32
+ */
+IdeBuildTarget *
+ide_run_manager_discover_default_target_finish (IdeRunManager  *self,
+                                                GAsyncResult   *result,
+                                                GError        **error)
+{
+  IdeBuildTarget *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+const GList *
+_ide_run_manager_get_handlers (IdeRunManager *self)
+{
+  g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), NULL);
+
+  return self->handlers;
+}
+
+const gchar *
+ide_run_manager_get_handler (IdeRunManager *self)
+{
+  g_return_val_if_fail (IDE_IS_RUN_MANAGER (self), NULL);
+
+  if (self->handler != NULL)
+    return self->handler->id;
+
+  return NULL;
+}
+
+static void
+ide_run_manager_run_action_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  IdeRunManager *self = (IdeRunManager *)object;
+  IdeContext *context;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  /* Propagate the error to the context */
+  if (!ide_run_manager_run_finish (self, result, &error))
+    ide_context_warning (context, "%s", error->message);
+}
+
+static void
+ide_run_manager_actions_run (IdeRunManager *self,
+                             GVariant      *param)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  ide_run_manager_run_async (self,
+                             NULL,
+                             NULL,
+                             ide_run_manager_run_action_cb,
+                             NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_actions_run_with_handler (IdeRunManager *self,
+                                          GVariant      *param)
+{
+  const gchar *handler = NULL;
+  g_autoptr(GVariant) sunk = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  if (param != NULL)
+  {
+    handler = g_variant_get_string (param, NULL);
+    if (g_variant_is_floating (param))
+      sunk = g_variant_ref_sink (param);
+  }
+
+  /* Use specified handler, if provided */
+  if (!ide_str_empty0 (handler))
+    ide_run_manager_set_handler (self, handler);
+
+  ide_run_manager_run_async (self,
+                             NULL,
+                             NULL,
+                             ide_run_manager_run_action_cb,
+                             NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_actions_stop (IdeRunManager *self,
+                              GVariant      *param)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (self));
+
+  ide_run_manager_cancel (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_run_manager_init (IdeRunManager *self)
+{
+  self->cancellable = g_cancellable_new ();
+
+  ide_run_manager_add_handler (self,
+                               "run",
+                               _("Run"),
+                               "media-playback-start-symbolic",
+                               "<primary>F5",
+                               NULL,
+                               NULL,
+                               NULL);
+}
diff --git a/src/libide/foundry/ide-run-manager.h b/src/libide/foundry/ide-run-manager.h
new file mode 100644
index 000000000..3889bb9c1
--- /dev/null
+++ b/src/libide/foundry/ide-run-manager.h
@@ -0,0 +1,90 @@
+/* ide-run-manager.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RUN_MANAGER (ide_run_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeRunManager, ide_run_manager, IDE, RUN_MANAGER, IdeObject)
+
+typedef void (*IdeRunHandler) (IdeRunManager *self,
+                               IdeRunner     *runner,
+                               gpointer       user_data);
+
+IDE_AVAILABLE_IN_3_32
+IdeRunManager  *ide_run_manager_from_context                   (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+IdeBuildTarget *ide_run_manager_get_build_target               (IdeRunManager        *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_run_manager_set_build_target               (IdeRunManager        *self,
+                                                                IdeBuildTarget       *build_target);
+IDE_AVAILABLE_IN_3_32
+void            ide_run_manager_cancel                         (IdeRunManager        *self);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_run_manager_get_busy                       (IdeRunManager        *self);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_run_manager_get_handler                    (IdeRunManager        *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_run_manager_set_handler                    (IdeRunManager        *self,
+                                                                const gchar          *id);
+IDE_AVAILABLE_IN_3_32
+void            ide_run_manager_add_handler                    (IdeRunManager        *self,
+                                                                const gchar          *id,
+                                                                const gchar          *title,
+                                                                const gchar          *icon_name,
+                                                                const gchar          *accel,
+                                                                IdeRunHandler         run_handler,
+                                                                gpointer              user_data,
+                                                                GDestroyNotify        user_data_destroy);
+IDE_AVAILABLE_IN_3_32
+void            ide_run_manager_remove_handler                 (IdeRunManager        *self,
+                                                                const gchar          *id);
+IDE_AVAILABLE_IN_3_32
+void            ide_run_manager_run_async                      (IdeRunManager        *self,
+                                                                IdeBuildTarget       *build_target,
+                                                                GCancellable         *cancellable,
+                                                                GAsyncReadyCallback   callback,
+                                                                gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_run_manager_run_finish                     (IdeRunManager        *self,
+                                                                GAsyncResult         *result,
+                                                                GError              **error);
+IDE_AVAILABLE_IN_3_32
+void            ide_run_manager_discover_default_target_async  (IdeRunManager        *self,
+                                                                GCancellable         *cancellable,
+                                                                GAsyncReadyCallback   callback,
+                                                                gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+IdeBuildTarget *ide_run_manager_discover_default_target_finish (IdeRunManager        *self,
+                                                                GAsyncResult         *result,
+                                                                GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-runner-addin.c b/src/libide/foundry/ide-runner-addin.c
new file mode 100644
index 000000000..e95223eb5
--- /dev/null
+++ b/src/libide/foundry/ide-runner-addin.c
@@ -0,0 +1,148 @@
+/* ide-runner-addin.c
+ *
+ * Copyright 2016-2019 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 "ide-runner-addin"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-runner.h"
+#include "ide-runner-addin.h"
+
+G_DEFINE_INTERFACE (IdeRunnerAddin, ide_runner_addin, G_TYPE_OBJECT)
+
+static void
+ide_runner_addin_real_load (IdeRunnerAddin *self,
+                            IdeRunner      *runner)
+{
+}
+
+static void
+ide_runner_addin_real_unload (IdeRunnerAddin *self,
+                              IdeRunner      *runner)
+{
+}
+
+static void
+dummy_async (IdeRunnerAddin      *self,
+             GCancellable        *cancellable,
+             GAsyncReadyCallback  callback,
+             gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_RUNNER_ADDIN (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (callback == NULL)
+    return;
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+dummy_finish (IdeRunnerAddin  *self,
+              GAsyncResult    *result,
+              GError         **error)
+{
+  g_assert (IDE_IS_RUNNER_ADDIN (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_runner_addin_default_init (IdeRunnerAddinInterface *iface)
+{
+  iface->load = ide_runner_addin_real_load;
+  iface->unload = ide_runner_addin_real_unload;
+  iface->prehook_async = dummy_async;
+  iface->prehook_finish = dummy_finish;
+  iface->posthook_async = dummy_async;
+  iface->posthook_finish = dummy_finish;
+}
+
+void
+ide_runner_addin_load (IdeRunnerAddin *self,
+                       IdeRunner      *runner)
+{
+  g_assert (IDE_IS_RUNNER_ADDIN (self));
+  g_assert (IDE_IS_RUNNER (runner));
+
+  IDE_RUNNER_ADDIN_GET_IFACE (self)->load (self, runner);
+}
+
+void
+ide_runner_addin_unload (IdeRunnerAddin *self,
+                         IdeRunner      *runner)
+{
+  g_assert (IDE_IS_RUNNER_ADDIN (self));
+  g_assert (IDE_IS_RUNNER (runner));
+
+  IDE_RUNNER_ADDIN_GET_IFACE (self)->unload (self, runner);
+}
+
+void
+ide_runner_addin_prehook_async (IdeRunnerAddin      *self,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_RUNNER_ADDIN (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_RUNNER_ADDIN_GET_IFACE (self)->prehook_async (self, cancellable, callback, user_data);
+}
+
+gboolean
+ide_runner_addin_prehook_finish (IdeRunnerAddin  *self,
+                                 GAsyncResult    *result,
+                                 GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_RUNNER_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_RUNNER_ADDIN_GET_IFACE (self)->prehook_finish (self, result, error);
+}
+
+void
+ide_runner_addin_posthook_async (IdeRunnerAddin      *self,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_RUNNER_ADDIN (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_RUNNER_ADDIN_GET_IFACE (self)->posthook_async (self, cancellable, callback, user_data);
+}
+
+gboolean
+ide_runner_addin_posthook_finish (IdeRunnerAddin  *self,
+                                  GAsyncResult    *result,
+                                  GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_RUNNER_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_RUNNER_ADDIN_GET_IFACE (self)->posthook_finish (self, result, error);
+}
diff --git a/src/libide/foundry/ide-runner-addin.h b/src/libide/foundry/ide-runner-addin.h
new file mode 100644
index 000000000..41daf8ca7
--- /dev/null
+++ b/src/libide/foundry/ide-runner-addin.h
@@ -0,0 +1,87 @@
+/* ide-runner-addin.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RUNNER_ADDIN (ide_runner_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeRunnerAddin, ide_runner_addin, IDE, RUNNER_ADDIN, GObject)
+
+struct _IdeRunnerAddinInterface
+{
+  GTypeInterface parent_interface;
+
+  void     (*load)            (IdeRunnerAddin       *self,
+                               IdeRunner            *runner);
+  void     (*unload)          (IdeRunnerAddin       *self,
+                               IdeRunner            *runner);
+  void     (*prehook_async)   (IdeRunnerAddin       *self,
+                               GCancellable         *cancellable,
+                               GAsyncReadyCallback   callback,
+                               gpointer              user_data);
+  gboolean (*prehook_finish)  (IdeRunnerAddin       *self,
+                               GAsyncResult         *result,
+                               GError              **error);
+  void     (*posthook_async)  (IdeRunnerAddin       *self,
+                               GCancellable         *cancellable,
+                               GAsyncReadyCallback   callback,
+                               gpointer              user_data);
+  gboolean (*posthook_finish) (IdeRunnerAddin       *self,
+                               GAsyncResult         *result,
+                               GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_runner_addin_load            (IdeRunnerAddin       *self,
+                                           IdeRunner            *runner);
+IDE_AVAILABLE_IN_3_32
+void     ide_runner_addin_unload          (IdeRunnerAddin       *self,
+                                           IdeRunner            *runner);
+IDE_AVAILABLE_IN_3_32
+void     ide_runner_addin_prehook_async   (IdeRunnerAddin       *self,
+                                           GCancellable         *cancellable,
+                                           GAsyncReadyCallback   callback,
+                                           gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_runner_addin_prehook_finish  (IdeRunnerAddin       *self,
+                                           GAsyncResult         *result,
+                                           GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_runner_addin_posthook_async  (IdeRunnerAddin       *self,
+                                           GCancellable         *cancellable,
+                                           GAsyncReadyCallback   callback,
+                                           gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_runner_addin_posthook_finish (IdeRunnerAddin       *self,
+                                           GAsyncResult         *result,
+                                           GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-runner.c b/src/libide/foundry/ide-runner.c
new file mode 100644
index 000000000..239139669
--- /dev/null
+++ b/src/libide/foundry/ide-runner.c
@@ -0,0 +1,1442 @@
+/* ide-runner.c
+ *
+ * Copyright 2016-2019 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 "ide-runner"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <errno.h>
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "ide-build-target.h"
+#include "ide-configuration-manager.h"
+#include "ide-configuration.h"
+#include "ide-foundry-compat.h"
+#include "ide-runner-addin.h"
+#include "ide-runner.h"
+#include "ide-runtime.h"
+
+typedef struct
+{
+  PeasExtensionSet *addins;
+  IdeEnvironment *env;
+  IdeBuildTarget *build_target;
+
+  GArray *fd_mapping;
+
+  gchar *cwd;
+
+  IdeSubprocess *subprocess;
+
+  GQueue argv;
+
+  GSubprocessFlags flags;
+
+  int tty_fd;
+
+  guint clear_env : 1;
+  guint failed : 1;
+  guint run_on_host : 1;
+} IdeRunnerPrivate;
+
+typedef struct
+{
+  GSList *prehook_queue;
+  GSList *posthook_queue;
+} IdeRunnerRunState;
+
+typedef struct
+{
+  gint source_fd;
+  gint dest_fd;
+} FdMapping;
+
+enum {
+  PROP_0,
+  PROP_ARGV,
+  PROP_CLEAR_ENV,
+  PROP_CWD,
+  PROP_ENV,
+  PROP_FAILED,
+  PROP_RUN_ON_HOST,
+  PROP_BUILD_TARGET,
+  N_PROPS
+};
+
+enum {
+  EXITED,
+  SPAWNED,
+  N_SIGNALS
+};
+
+static void ide_runner_tick_posthook (IdeTask *task);
+static void ide_runner_tick_prehook  (IdeTask *task);
+static void ide_runner_tick_run      (IdeTask *task);
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeRunner, ide_runner, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static IdeRunnerAddin *
+pop_runner_addin (GSList **list)
+{
+  IdeRunnerAddin *ret;
+
+  g_assert (list != NULL);
+  g_assert (*list != NULL);
+
+  ret = (*list)->data;
+
+  *list = g_slist_delete_link (*list, *list);
+
+  return ret;
+}
+
+static void
+ide_runner_run_state_free (gpointer data)
+{
+  IdeRunnerRunState *state = data;
+
+  g_slist_foreach (state->prehook_queue, (GFunc)g_object_unref, NULL);
+  g_slist_free (state->prehook_queue);
+
+  g_slist_foreach (state->posthook_queue, (GFunc)g_object_unref, NULL);
+  g_slist_free (state->posthook_queue);
+
+  g_slice_free (IdeRunnerRunState, state);
+}
+
+static void
+ide_runner_run_wait_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  IdeRunnerPrivate *priv;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeRunner *self;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SUBPROCESS (subprocess));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  priv = ide_runner_get_instance_private (self);
+
+  g_assert (IDE_IS_RUNNER (self));
+
+  g_clear_object (&priv->subprocess);
+
+  g_signal_emit (self, signals [EXITED], 0);
+
+  if (!ide_subprocess_wait_finish (subprocess, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (ide_subprocess_get_if_exited (subprocess))
+    {
+      gint exit_code;
+
+      exit_code = ide_subprocess_get_exit_status (subprocess);
+
+      if (exit_code == EXIT_SUCCESS)
+        {
+          ide_task_return_boolean (task, TRUE);
+          IDE_EXIT;
+        }
+    }
+
+  ide_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_FAILED,
+                             "%s",
+                             _("Process quit unexpectedly"));
+
+  IDE_EXIT;
+}
+
+static IdeSubprocessLauncher *
+ide_runner_real_create_launcher (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+  IdeConfigurationManager *config_manager;
+  IdeSubprocessLauncher *ret;
+  IdeConfiguration *config;
+  IdeContext *context;
+  IdeRuntime *runtime;
+
+  g_assert (IDE_IS_RUNNER (self));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  config_manager = ide_configuration_manager_from_context (context);
+  config = ide_configuration_manager_get_current (config_manager);
+  runtime = ide_configuration_get_runtime (config);
+
+  ret = ide_runtime_create_launcher (runtime, NULL);
+
+  if (ret != NULL && priv->cwd != NULL)
+    ide_subprocess_launcher_set_cwd (ret, priv->cwd);
+
+  return ret;
+}
+
+static void
+ide_runner_real_run_async (IdeRunner           *self,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  IdeConfigurationManager *config_manager;
+  IdeConfiguration *config;
+  const gchar *identifier;
+  IdeContext *context;
+  IdeRuntime *runtime;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUNNER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_runner_real_run_async);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  config_manager = ide_configuration_manager_from_context (context);
+  config = ide_configuration_manager_get_current (config_manager);
+  runtime = ide_configuration_get_runtime (config);
+
+  if (runtime != NULL)
+    launcher = IDE_RUNNER_GET_CLASS (self)->create_launcher (self);
+
+  if (launcher == NULL)
+    launcher = ide_subprocess_launcher_new (0);
+
+  ide_subprocess_launcher_set_flags (launcher, priv->flags);
+
+  /*
+   * If we have a tty_fd set, then we want to override our stdin,
+   * stdout, and stderr fds with our TTY.
+   */
+  if (priv->tty_fd != -1)
+    {
+      IDE_TRACE_MSG ("Setting TTY fd to %d", priv->tty_fd);
+      ide_subprocess_launcher_take_stdin_fd (launcher, dup (priv->tty_fd));
+      ide_subprocess_launcher_take_stdout_fd (launcher, dup (priv->tty_fd));
+      ide_subprocess_launcher_take_stderr_fd (launcher, dup (priv->tty_fd));
+    }
+
+  /*
+   * Now map in any additionally requested FDs.
+   */
+  if (priv->fd_mapping != NULL)
+    {
+      g_autoptr(GArray) ar = g_steal_pointer (&priv->fd_mapping);
+
+      for (guint i = 0; i < ar->len; i++)
+        {
+          FdMapping *map = &g_array_index (ar, FdMapping, i);
+
+          ide_subprocess_launcher_take_fd (launcher, map->source_fd, map->dest_fd);
+        }
+    }
+
+  /*
+   * We want the runners to run on the host so that we aren't captive to
+   * our containing system (flatpak, jhbuild, etc).
+   */
+  ide_subprocess_launcher_set_run_on_host (launcher, priv->run_on_host);
+
+  /*
+   * We don't want the environment cleared because we need access to
+   * things like DISPLAY, WAYLAND_DISPLAY, and DBUS_SESSION_BUS_ADDRESS.
+   */
+  ide_subprocess_launcher_set_clear_env (launcher, priv->clear_env);
+
+  /*
+   * Overlay the environment provided.
+   */
+  ide_subprocess_launcher_overlay_environment (launcher, priv->env);
+
+  /*
+   * Push all of our configured arguments in order.
+   */
+  for (const GList *iter = priv->argv.head; iter != NULL; iter = iter->next)
+    ide_subprocess_launcher_push_argv (launcher, iter->data);
+
+  /* Give the runner a final chance to mutate the launcher */
+  if (IDE_RUNNER_GET_CLASS (self)->fixup_launcher)
+    IDE_RUNNER_GET_CLASS (self)->fixup_launcher (self, launcher);
+
+  subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error);
+
+  g_assert (subprocess == NULL || IDE_IS_SUBPROCESS (subprocess));
+
+  if (subprocess == NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_GOTO (failure);
+    }
+
+  priv->subprocess = g_object_ref (subprocess);
+
+  identifier = ide_subprocess_get_identifier (subprocess);
+  g_signal_emit (self, signals [SPAWNED], 0, identifier);
+
+  ide_subprocess_wait_async (subprocess,
+                             cancellable,
+                             ide_runner_run_wait_cb,
+                             g_steal_pointer (&task));
+
+failure:
+  IDE_EXIT;
+}
+
+static gboolean
+ide_runner_real_run_finish (IdeRunner     *self,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  g_assert (IDE_IS_RUNNER (self));
+  g_assert (IDE_IS_TASK (result));
+  g_assert (ide_task_is_valid (IDE_TASK (result), self));
+  g_assert (ide_task_get_source_tag (IDE_TASK (result)) == ide_runner_real_run_async);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static GOutputStream *
+ide_runner_real_get_stdin (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  if (priv->subprocess)
+    return g_object_ref (ide_subprocess_get_stdin_pipe (priv->subprocess));
+  return NULL;
+}
+
+static GInputStream *
+ide_runner_real_get_stdout (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  if (priv->subprocess)
+    return g_object_ref (ide_subprocess_get_stdout_pipe (priv->subprocess));
+  return NULL;
+}
+
+static GInputStream *
+ide_runner_real_get_stderr (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  if (priv->subprocess)
+    return g_object_ref (ide_subprocess_get_stderr_pipe (priv->subprocess));
+  return NULL;
+}
+
+gint
+ide_runner_steal_tty (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+  gint fd;
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), -1);
+
+  fd = priv->tty_fd;
+  priv->tty_fd = -1;
+
+  return fd;
+}
+
+static void
+ide_runner_real_set_tty (IdeRunner *self,
+                         int        tty_fd)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_assert (IDE_IS_RUNNER (self));
+  g_assert (tty_fd >= -1);
+
+  if (tty_fd != priv->tty_fd)
+    {
+      if (priv->tty_fd != -1)
+        {
+          close (priv->tty_fd);
+          priv->tty_fd = -1;
+        }
+
+      if (tty_fd != -1)
+        {
+          priv->tty_fd = dup (tty_fd);
+          if (priv->tty_fd == -1)
+            g_warning ("Failed to dup() tty_fd: %s", g_strerror (errno));
+        }
+    }
+}
+
+static void
+ide_runner_real_force_quit (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUNNER (self));
+
+  if (priv->subprocess != NULL)
+    ide_subprocess_force_exit (priv->subprocess);
+
+  IDE_EXIT;
+}
+
+static void
+ide_runner_extension_added (PeasExtensionSet *set,
+                            PeasPluginInfo   *plugin_info,
+                            PeasExtension    *exten,
+                            gpointer          user_data)
+{
+  IdeRunnerAddin *addin = (IdeRunnerAddin *)exten;
+  IdeRunner *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_RUNNER_ADDIN (addin));
+  g_assert (IDE_IS_RUNNER (self));
+
+  ide_runner_addin_load (addin, self);
+}
+
+static void
+ide_runner_extension_removed (PeasExtensionSet *set,
+                              PeasPluginInfo   *plugin_info,
+                              PeasExtension    *exten,
+                              gpointer          user_data)
+{
+  IdeRunnerAddin *addin = (IdeRunnerAddin *)exten;
+  IdeRunner *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_RUNNER_ADDIN (addin));
+  g_assert (IDE_IS_RUNNER (self));
+
+  ide_runner_addin_unload (addin, self);
+}
+
+static void
+ide_runner_constructed (GObject *object)
+{
+  IdeRunner *self = (IdeRunner *)object;
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  G_OBJECT_CLASS (ide_runner_parent_class)->constructed (object);
+
+  priv->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_RUNNER_ADDIN,
+                                         NULL);
+
+  g_signal_connect (priv->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_runner_extension_added),
+                    self);
+
+  g_signal_connect (priv->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_runner_extension_removed),
+                    self);
+
+  peas_extension_set_foreach (priv->addins,
+                              ide_runner_extension_added,
+                              self);
+}
+
+static void
+ide_runner_finalize (GObject *object)
+{
+  IdeRunner *self = (IdeRunner *)object;
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_queue_foreach (&priv->argv, (GFunc)g_free, NULL);
+  g_queue_clear (&priv->argv);
+  g_clear_object (&priv->env);
+  g_clear_object (&priv->subprocess);
+  g_clear_object (&priv->build_target);
+
+  if (priv->fd_mapping != NULL)
+    {
+      for (guint i = 0; i < priv->fd_mapping->len; i++)
+        {
+          FdMapping *map = &g_array_index (priv->fd_mapping, FdMapping, i);
+
+          if (map->source_fd != -1)
+            {
+              close (map->source_fd);
+              map->source_fd = -1;
+            }
+        }
+    }
+
+  g_clear_pointer (&priv->fd_mapping, g_array_unref);
+
+  if (priv->tty_fd != -1)
+    {
+      close (priv->tty_fd);
+      priv->tty_fd = -1;
+    }
+
+  G_OBJECT_CLASS (ide_runner_parent_class)->finalize (object);
+}
+
+static void
+ide_runner_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+  IdeRunner *self = IDE_RUNNER (object);
+
+  switch (prop_id)
+    {
+    case PROP_ARGV:
+      g_value_take_boxed (value, ide_runner_get_argv (self));
+      break;
+
+    case PROP_CLEAR_ENV:
+      g_value_set_boolean (value, ide_runner_get_clear_env (self));
+      break;
+
+    case PROP_CWD:
+      g_value_set_string (value, ide_runner_get_cwd (self));
+      break;
+
+    case PROP_ENV:
+      g_value_set_object (value, ide_runner_get_environment (self));
+      break;
+
+    case PROP_FAILED:
+      g_value_set_boolean (value, ide_runner_get_failed (self));
+      break;
+
+    case PROP_RUN_ON_HOST:
+      g_value_set_boolean (value, ide_runner_get_run_on_host (self));
+      break;
+
+    case PROP_BUILD_TARGET:
+      g_value_set_object (value, ide_runner_get_build_target (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_runner_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  IdeRunner *self = IDE_RUNNER (object);
+
+  switch (prop_id)
+    {
+    case PROP_ARGV:
+      ide_runner_set_argv (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_CLEAR_ENV:
+      ide_runner_set_clear_env (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_CWD:
+      ide_runner_set_cwd (self, g_value_get_string (value));
+      break;
+
+    case PROP_FAILED:
+      ide_runner_set_failed (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_RUN_ON_HOST:
+      ide_runner_set_run_on_host (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_BUILD_TARGET:
+      ide_runner_set_build_target (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_runner_class_init (IdeRunnerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_runner_constructed;
+  object_class->finalize = ide_runner_finalize;
+  object_class->get_property = ide_runner_get_property;
+  object_class->set_property = ide_runner_set_property;
+
+  klass->run_async = ide_runner_real_run_async;
+  klass->run_finish = ide_runner_real_run_finish;
+  klass->set_tty = ide_runner_real_set_tty;
+  klass->create_launcher = ide_runner_real_create_launcher;
+  klass->get_stdin = ide_runner_real_get_stdin;
+  klass->get_stdout = ide_runner_real_get_stdout;
+  klass->get_stderr = ide_runner_real_get_stderr;
+  klass->force_quit = ide_runner_real_force_quit;
+
+  properties [PROP_ARGV] =
+    g_param_spec_boxed ("argv",
+                        "Argv",
+                        "The argument list for the command",
+                        G_TYPE_STRV,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CLEAR_ENV] =
+    g_param_spec_boolean ("clear-env",
+                          "Clear Env",
+                          "If the environment should be cleared before applying overrides",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CWD] =
+    g_param_spec_string ("cwd",
+                         "Current Working Directory",
+                         "The directory to use as the working directory for the process",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ENV] =
+    g_param_spec_object ("environment",
+                         "Environment",
+                         "The environment variables for the command",
+                         IDE_TYPE_ENVIRONMENT,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeRunner:failed:
+   *
+   * If the runner has "failed". This should be set if a plugin can determine
+   * that the runner cannot be executed due to an external issue. One such
+   * example might be a debugger plugin that cannot locate a suitable debugger
+   * to run the program.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_FAILED] =
+    g_param_spec_boolean ("failed",
+                          "Failed",
+                          "If the runner has failed",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeRunner:run-on-host:
+   *
+   * The "run-on-host" property indicates the program should be run on the
+   * host machine rather than inside the application sandbox.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_RUN_ON_HOST] =
+    g_param_spec_boolean ("run-on-host",
+                          "Run on Host",
+                          "Run on Host",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeRunner:build-target:
+   *
+   * The %IdeBuildTarget from which this %IdeRunner was constructed.
+   *
+   * This is useful to retrieve various properties related to the program
+   * that will be launched, such as what programming language it uses,
+   * or whether it's a graphical application, a command line tool or a test
+   * program.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_BUILD_TARGET] =
+    g_param_spec_object ("build-target",
+                         "Build Target",
+                         "Build Target",
+                         IDE_TYPE_BUILD_TARGET,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [EXITED] =
+    g_signal_new ("exited",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL,
+                  NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  0);
+
+  signals [SPAWNED] =
+    g_signal_new ("spawned",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL,
+                  g_cclosure_marshal_VOID__STRING,
+                  G_TYPE_NONE, 1, G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static void
+ide_runner_init (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_queue_init (&priv->argv);
+
+  priv->env = ide_environment_new ();
+
+  priv->flags = 0;
+  priv->tty_fd = -1;
+}
+
+/**
+ * ide_runner_get_stdin:
+ *
+ * Returns: (nullable) (transfer full): An #GOutputStream or %NULL.
+ *
+ * Since: 3.32
+ */
+GOutputStream *
+ide_runner_get_stdin (IdeRunner *self)
+{
+  g_return_val_if_fail (IDE_IS_RUNNER (self), NULL);
+
+  return IDE_RUNNER_GET_CLASS (self)->get_stdin (self);
+}
+
+/**
+ * ide_runner_get_stdout:
+ *
+ * Returns: (nullable) (transfer full): An #GOutputStream or %NULL.
+ *
+ * Since: 3.32
+ */
+GInputStream *
+ide_runner_get_stdout (IdeRunner *self)
+{
+  g_return_val_if_fail (IDE_IS_RUNNER (self), NULL);
+
+  return IDE_RUNNER_GET_CLASS (self)->get_stdout (self);
+}
+
+/**
+ * ide_runner_get_stderr:
+ *
+ * Returns: (nullable) (transfer full): An #GOutputStream or %NULL.
+ *
+ * Since: 3.32
+ */
+GInputStream *
+ide_runner_get_stderr (IdeRunner *self)
+{
+  g_return_val_if_fail (IDE_IS_RUNNER (self), NULL);
+
+  return IDE_RUNNER_GET_CLASS (self)->get_stderr (self);
+}
+
+void
+ide_runner_force_quit (IdeRunner *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+
+  if (IDE_RUNNER_GET_CLASS (self)->force_quit)
+    IDE_RUNNER_GET_CLASS (self)->force_quit (self);
+
+  IDE_EXIT;
+}
+
+void
+ide_runner_set_argv (IdeRunner           *self,
+                     const gchar * const *argv)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+  guint i;
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+
+  g_queue_foreach (&priv->argv, (GFunc)g_free, NULL);
+  g_queue_clear (&priv->argv);
+
+  if (argv != NULL)
+    {
+      for (i = 0; argv [i]; i++)
+        g_queue_push_tail (&priv->argv, g_strdup (argv [i]));
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ARGV]);
+}
+
+/**
+ * ide_runner_get_environment:
+ *
+ * Returns: (transfer none): The #IdeEnvironment the process launched uses.
+ *
+ * Since: 3.32
+ */
+IdeEnvironment *
+ide_runner_get_environment (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), NULL);
+
+  return priv->env;
+}
+
+/**
+ * ide_runner_get_argv:
+ *
+ * Gets the argument list as a newly allocated string array.
+ *
+ * Returns: (transfer full): A newly allocated string array that should
+ *   be freed with g_strfreev().
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_runner_get_argv (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+  GPtrArray *ar;
+  GList *iter;
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), NULL);
+
+  ar = g_ptr_array_new ();
+
+  for (iter = priv->argv.head; iter != NULL; iter = iter->next)
+    {
+      const gchar *param = iter->data;
+
+      g_ptr_array_add (ar, g_strdup (param));
+    }
+
+  g_ptr_array_add (ar, NULL);
+
+  return (gchar **)g_ptr_array_free (ar, FALSE);
+}
+
+static void
+ide_runner_collect_addins_cb (PeasExtensionSet *set,
+                              PeasPluginInfo   *plugin_info,
+                              PeasExtension    *exten,
+                              gpointer          user_data)
+{
+  GSList **list = user_data;
+
+  *list = g_slist_prepend (*list, exten);
+}
+
+static void
+ide_runner_collect_addins (IdeRunner  *self,
+                           GSList    **list)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_assert (IDE_IS_RUNNER (self));
+  g_assert (list != NULL);
+
+  peas_extension_set_foreach (priv->addins,
+                              ide_runner_collect_addins_cb,
+                              list);
+}
+
+static void
+ide_runner_posthook_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  IdeRunnerAddin *addin = (IdeRunnerAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_RUNNER_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (!ide_runner_addin_posthook_finish (addin, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_runner_tick_posthook (task);
+}
+
+static void
+ide_runner_tick_posthook (IdeTask *task)
+{
+  IdeRunnerRunState *state;
+
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+
+  if (state->posthook_queue != NULL)
+    {
+      g_autoptr(IdeRunnerAddin) addin = NULL;
+
+      addin = pop_runner_addin (&state->posthook_queue);
+      ide_runner_addin_posthook_async (addin,
+                                       ide_task_get_cancellable (task),
+                                       ide_runner_posthook_cb,
+                                       g_object_ref (task));
+      return;
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_runner_run_cb (GObject      *object,
+                   GAsyncResult *result,
+                   gpointer      user_data)
+{
+  IdeRunner *self = (IdeRunner *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUNNER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!IDE_RUNNER_GET_CLASS (self)->run_finish (self, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_runner_tick_posthook (task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_runner_tick_run (IdeTask *task)
+{
+  IdeRunner *self;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TASK (task));
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_RUNNER (self));
+
+  IDE_RUNNER_GET_CLASS (self)->run_async (self,
+                                          ide_task_get_cancellable (task),
+                                          ide_runner_run_cb,
+                                          g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+static void
+ide_runner_prehook_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  IdeRunnerAddin *addin = (IdeRunnerAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUNNER_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_runner_addin_prehook_finish (addin, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_runner_tick_prehook (task);
+
+  IDE_EXIT;
+}
+
+static void
+ide_runner_tick_prehook (IdeTask *task)
+{
+  IdeRunnerRunState *state;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+
+  if (state->prehook_queue != NULL)
+    {
+      g_autoptr(IdeRunnerAddin) addin = NULL;
+
+      addin = pop_runner_addin (&state->prehook_queue);
+      ide_runner_addin_prehook_async (addin,
+                                      ide_task_get_cancellable (task),
+                                      ide_runner_prehook_cb,
+                                      g_object_ref (task));
+      IDE_EXIT;
+    }
+
+  ide_runner_tick_run (task);
+
+  IDE_EXIT;
+}
+
+void
+ide_runner_run_async (IdeRunner           *self,
+                      GCancellable        *cancellable,
+                      GAsyncReadyCallback  callback,
+                      gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  IdeRunnerRunState *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_runner_run_async);
+  ide_task_set_check_cancellable (task, FALSE);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  /*
+   * We need to run the prehook functions for each addin first before we
+   * can call our IdeRunnerClass.run vfunc.  Since these are async, we
+   * have to bring some state along with us.
+   */
+  state = g_slice_new0 (IdeRunnerRunState);
+  ide_runner_collect_addins (self, &state->prehook_queue);
+  ide_runner_collect_addins (self, &state->posthook_queue);
+  ide_task_set_task_data (task, state, ide_runner_run_state_free);
+
+  ide_runner_tick_prehook (task);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_runner_run_finish (IdeRunner     *self,
+                       GAsyncResult  *result,
+                       GError       **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+void
+ide_runner_append_argv (IdeRunner   *self,
+                        const gchar *param)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+  g_return_if_fail (param != NULL);
+
+  g_queue_push_tail (&priv->argv, g_strdup (param));
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ARGV]);
+}
+
+void
+ide_runner_prepend_argv (IdeRunner   *self,
+                         const gchar *param)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+  g_return_if_fail (param != NULL);
+
+  g_queue_push_head (&priv->argv, g_strdup (param));
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ARGV]);
+}
+
+IdeRunner *
+ide_runner_new (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return g_object_new (IDE_TYPE_RUNNER,
+                       NULL);
+}
+
+gboolean
+ide_runner_get_run_on_host (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), FALSE);
+
+  return priv->run_on_host;
+}
+
+void
+ide_runner_set_run_on_host (IdeRunner *self,
+                            gboolean   run_on_host)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  run_on_host = !!run_on_host;
+
+  if (run_on_host != priv->run_on_host)
+    {
+      priv->run_on_host = run_on_host;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUN_ON_HOST]);
+    }
+}
+
+GSubprocessFlags
+ide_runner_get_flags (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), 0);
+
+  return priv->flags;
+}
+
+void
+ide_runner_set_flags (IdeRunner        *self,
+                      GSubprocessFlags  flags)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+
+  priv->flags = flags;
+}
+
+gboolean
+ide_runner_get_clear_env (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), FALSE);
+
+  return priv->clear_env;
+}
+
+void
+ide_runner_set_clear_env (IdeRunner *self,
+                          gboolean   clear_env)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+
+  clear_env = !!clear_env;
+
+  if (clear_env != priv->clear_env)
+    {
+      priv->clear_env = clear_env;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLEAR_ENV]);
+    }
+}
+
+void
+ide_runner_set_tty (IdeRunner *self,
+                    int        tty_fd)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+  g_return_if_fail (tty_fd >= -1);
+
+  if (IDE_RUNNER_GET_CLASS (self)->set_tty)
+    {
+      IDE_RUNNER_GET_CLASS (self)->set_tty (self, tty_fd);
+      return;
+    }
+
+  g_warning ("%s does not support setting a TTY fd",
+             G_OBJECT_TYPE_NAME (self));
+
+  IDE_EXIT;
+}
+
+static gint
+sort_fd_mapping (gconstpointer a,
+                 gconstpointer b)
+{
+  const FdMapping *map_a = a;
+  const FdMapping *map_b = b;
+
+  return map_a->dest_fd - map_b->dest_fd;
+}
+
+/**
+ * ide_runner_take_fd:
+ * @self: An #IdeRunner
+ * @source_fd: the fd to map, this will be closed by #IdeRunner
+ * @dest_fd: the target FD in the spawned process, or -1 for next available
+ *
+ * This will ensure that @source_fd is mapped into the new process as @dest_fd.
+ * If @dest_fd is -1, then the next fd will be used and that value will be
+ * returned. Note that this is not a valid fd in the calling process, only
+ * within the destination process.
+ *
+ * Returns: @dest_fd or the FD or the next available dest_fd.
+ *
+ * Since: 3.32
+ */
+gint
+ide_runner_take_fd (IdeRunner *self,
+                    gint       source_fd,
+                    gint       dest_fd)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+  FdMapping map = { -1, -1 };
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), -1);
+  g_return_val_if_fail (source_fd > -1, -1);
+
+  if (priv->fd_mapping == NULL)
+    priv->fd_mapping = g_array_new (FALSE, FALSE, sizeof (FdMapping));
+
+  /*
+   * Quick and dirty hack to take the next FD, won't deal with people mapping
+   * to 1024 well, but we can fix that when we come across it.
+   */
+  if (dest_fd < 0)
+    {
+      gint max_fd = 2;
+
+      for (guint i = 0; i < priv->fd_mapping->len; i++)
+        {
+          FdMapping *entry = &g_array_index (priv->fd_mapping, FdMapping, i);
+
+          if (entry->dest_fd > max_fd)
+            max_fd = entry->dest_fd;
+        }
+
+      dest_fd = max_fd + 1;
+    }
+
+  map.source_fd = source_fd;
+  map.dest_fd = dest_fd;
+
+  g_array_append_val (priv->fd_mapping, map);
+  g_array_sort (priv->fd_mapping, sort_fd_mapping);
+
+  return dest_fd;
+}
+
+/**
+ * ide_runner_get_runtime:
+ * @self: An #IdeRuntime
+ *
+ * This function will get the #IdeRuntime that will be used to execute the
+ * application. Consumers may want to use this to determine if a particular
+ * program is available (such as gdb, perf, strace, etc).
+ *
+ * Returns: (nullable) (transfer full): An #IdeRuntime or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeRuntime *
+ide_runner_get_runtime (IdeRunner *self)
+{
+  IdeConfigurationManager *config_manager;
+  IdeConfiguration *config;
+  IdeContext *context;
+  IdeRuntime *runtime;
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), NULL);
+
+  if (IDE_RUNNER_GET_CLASS (self)->get_runtime)
+    return IDE_RUNNER_GET_CLASS (self)->get_runtime (self);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  config_manager = ide_configuration_manager_from_context (context);
+  config = ide_configuration_manager_get_current (config_manager);
+  runtime = ide_configuration_get_runtime (config);
+
+  return runtime != NULL ? g_object_ref (runtime) : NULL;
+}
+
+gboolean
+ide_runner_get_failed (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), FALSE);
+
+  return priv->failed;
+}
+
+void
+ide_runner_set_failed (IdeRunner *self,
+                       gboolean   failed)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+
+  failed = !!failed;
+
+  if (failed != priv->failed)
+    {
+      priv->failed = failed;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FAILED]);
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_runner_get_cwd:
+ * @self: a #IdeRunner
+ *
+ * Returns: (nullable): The current working directory, or %NULL.
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_runner_get_cwd (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), NULL);
+
+  return priv->cwd;
+}
+
+/**
+ * ide_runner_set_cwd:
+ * @self: a #IdeRunner
+ * @cwd: (nullable): The working directory or %NULL
+ *
+ * Sets the directory to use when spawning the runner.
+ *
+ * Since: 3.32
+ */
+void
+ide_runner_set_cwd (IdeRunner   *self,
+                    const gchar *cwd)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+
+  if (!ide_str_equal0 (priv->cwd, cwd))
+    {
+      g_free (priv->cwd);
+      priv->cwd = g_strdup (cwd);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CWD]);
+    }
+}
+
+/**
+ * ide_runner_push_args:
+ * @self: a #IdeRunner
+ * @args: (array zero-terminated=1) (element-type utf8) (nullable): the arguments
+ *
+ * Helper to call ide_runner_append_argv() for every argument
+ * contained in @args.
+ *
+ * Since: 3.32
+ */
+void
+ide_runner_push_args (IdeRunner           *self,
+                      const gchar * const *args)
+{
+  g_return_if_fail (IDE_IS_RUNNER (self));
+
+  if (args == NULL)
+    return;
+
+  for (guint i = 0; args[i] != NULL; i++)
+    ide_runner_append_argv (self, args[i]);
+}
+
+/**
+ * ide_runner_get_build_target:
+ * @self: a #IdeRunner
+ *
+ * Returns: (nullable) (transfer none): The %IdeBuildTarget associated with this %IdeRunner, or %NULL.
+ *   See #IdeRunner:build-target for details.
+ *
+ * Since: 3.32
+ */
+IdeBuildTarget *
+ide_runner_get_build_target (IdeRunner *self)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUNNER (self), NULL);
+
+  return priv->build_target;
+}
+
+/**
+ * ide_runner_set_build_target:
+ * @self: a #IdeRunner
+ * @build_target: (nullable): The build target, or %NULL
+ *
+ * Sets the build target associated with this runner.
+ *
+ * Since: 3.32
+ */
+void
+ide_runner_set_build_target (IdeRunner      *self,
+                             IdeBuildTarget *build_target)
+{
+  IdeRunnerPrivate *priv = ide_runner_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUNNER (self));
+
+  if (g_set_object (&priv->build_target, build_target))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUILD_TARGET]);
+}
diff --git a/src/libide/foundry/ide-runner.h b/src/libide/foundry/ide-runner.h
new file mode 100644
index 000000000..838f8521a
--- /dev/null
+++ b/src/libide/foundry/ide-runner.h
@@ -0,0 +1,143 @@
+/* ide-runner.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RUNNER (ide_runner_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeRunner, ide_runner, IDE, RUNNER, IdeObject)
+
+struct _IdeRunnerClass
+{
+  IdeObjectClass parent;
+
+  void                   (*force_quit)      (IdeRunner             *self);
+  GOutputStream         *(*get_stdin)       (IdeRunner             *self);
+  GInputStream          *(*get_stdout)      (IdeRunner             *self);
+  GInputStream          *(*get_stderr)      (IdeRunner             *self);
+  void                   (*run_async)       (IdeRunner             *self,
+                                             GCancellable          *cancellable,
+                                             GAsyncReadyCallback    callback,
+                                             gpointer               user_data);
+  gboolean               (*run_finish)      (IdeRunner             *self,
+                                             GAsyncResult          *result,
+                                             GError               **error);
+  void                   (*set_tty)         (IdeRunner             *self,
+                                             int                    tty_fd);
+  IdeSubprocessLauncher *(*create_launcher) (IdeRunner             *self);
+  void                   (*fixup_launcher)  (IdeRunner             *self,
+                                             IdeSubprocessLauncher *launcher);
+  IdeRuntime            *(*get_runtime)     (IdeRunner             *self);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeRunner         *ide_runner_new             (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+gboolean           ide_runner_get_failed      (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_set_failed      (IdeRunner            *self,
+                                               gboolean              failed);
+IDE_AVAILABLE_IN_3_32
+IdeRuntime        *ide_runner_get_runtime     (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_force_quit      (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+IdeEnvironment    *ide_runner_get_environment (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_run_async       (IdeRunner            *self,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean           ide_runner_run_finish      (IdeRunner            *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+IDE_AVAILABLE_IN_3_32
+GSubprocessFlags   ide_runner_get_flags       (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_set_flags       (IdeRunner            *self,
+                                               GSubprocessFlags      flags);
+IDE_AVAILABLE_IN_3_32
+const gchar       *ide_runner_get_cwd         (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_set_cwd         (IdeRunner            *self,
+                                               const gchar          *cwd);
+IDE_AVAILABLE_IN_3_32
+gboolean           ide_runner_get_clear_env   (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_set_clear_env   (IdeRunner            *self,
+                                               gboolean              clear_env);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_prepend_argv    (IdeRunner            *self,
+                                               const gchar          *param);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_append_argv     (IdeRunner            *self,
+                                               const gchar          *param);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_push_args       (IdeRunner            *self,
+                                               const gchar * const  *args);
+IDE_AVAILABLE_IN_3_32
+gchar            **ide_runner_get_argv        (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_set_argv        (IdeRunner            *self,
+                                               const gchar * const  *argv);
+IDE_AVAILABLE_IN_3_32
+gint               ide_runner_take_fd         (IdeRunner            *self,
+                                               gint                  source_fd,
+                                               gint                  dest_fd);
+IDE_AVAILABLE_IN_3_32
+GOutputStream     *ide_runner_get_stdin       (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+GInputStream      *ide_runner_get_stdout      (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+GInputStream      *ide_runner_get_stderr      (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+gboolean           ide_runner_get_run_on_host (IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_set_run_on_host (IdeRunner            *self,
+                                               gboolean              run_on_host);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_set_tty         (IdeRunner            *self,
+                                               int                   tty_fd);
+IDE_AVAILABLE_IN_3_32
+gint               ide_runner_steal_tty       (IdeRunner            *self);
+
+IDE_AVAILABLE_IN_3_32
+IdeBuildTarget    *ide_runner_get_build_target(IdeRunner            *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_runner_set_build_target(IdeRunner            *self,
+                                               IdeBuildTarget       *build_target);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-runtime-manager.c b/src/libide/foundry/ide-runtime-manager.c
new file mode 100644
index 000000000..6fbc115e3
--- /dev/null
+++ b/src/libide/foundry/ide-runtime-manager.c
@@ -0,0 +1,442 @@
+/* ide-runtime-manager.c
+ *
+ * Copyright 2016-2019 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 "ide-runtime-manager"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+
+#include "ide-build-pipeline.h"
+#include "ide-build-private.h"
+#include "ide-configuration.h"
+#include "ide-device.h"
+#include "ide-runtime.h"
+#include "ide-runtime-manager.h"
+#include "ide-runtime-private.h"
+#include "ide-runtime-provider.h"
+
+struct _IdeRuntimeManager
+{
+  IdeObject               parent_instance;
+  IdeExtensionSetAdapter *extensions;
+  GPtrArray              *runtimes;
+  guint                   unloading : 1;
+};
+
+typedef struct
+{
+  const gchar        *runtime_id;
+  IdeRuntimeProvider *provider;
+} InstallLookup;
+
+typedef struct
+{
+  IdeBuildPipeline *pipeline;
+  gchar            *runtime_id;
+} PrepareState;
+
+static void list_model_iface_init (GListModelInterface *iface);
+static void initable_iface_init   (GInitableIface      *iface);
+
+G_DEFINE_TYPE_EXTENDED (IdeRuntimeManager, ide_runtime_manager, IDE_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)
+                        G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))
+
+static void
+prepare_state_free (PrepareState *state)
+{
+  g_clear_object (&state->pipeline);
+  g_clear_pointer (&state->runtime_id, g_free);
+  g_slice_free (PrepareState, state);
+}
+
+static void
+ide_runtime_manager_extension_added (IdeExtensionSetAdapter *set,
+                                     PeasPluginInfo         *plugin_info,
+                                     PeasExtension          *exten,
+                                     gpointer                user_data)
+{
+  IdeRuntimeManager *self = user_data;
+  IdeRuntimeProvider *provider = (IdeRuntimeProvider *)exten;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_RUNTIME_PROVIDER (provider));
+
+  ide_runtime_provider_load (provider, self);
+}
+
+static void
+ide_runtime_manager_extension_removed (IdeExtensionSetAdapter *set,
+                                       PeasPluginInfo         *plugin_info,
+                                       PeasExtension          *exten,
+                                       gpointer                user_data)
+{
+  IdeRuntimeManager *self = user_data;
+  IdeRuntimeProvider *provider = (IdeRuntimeProvider *)exten;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_RUNTIME_PROVIDER (provider));
+
+  ide_runtime_provider_unload (provider, self);
+}
+
+static gboolean
+ide_runtime_manager_initable_init (GInitable     *initable,
+                                   GCancellable  *cancellable,
+                                   GError       **error)
+{
+  IdeRuntimeManager *self = (IdeRuntimeManager *)initable;
+  g_autoptr(IdeRuntime) host = NULL;
+  IdeContext *context;
+
+  g_assert (IDE_IS_RUNTIME_MANAGER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  self->extensions = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                    peas_engine_get_default (),
+                                                    IDE_TYPE_RUNTIME_PROVIDER,
+                                                    NULL, NULL);
+
+  g_signal_connect (self->extensions,
+                    "extension-added",
+                    G_CALLBACK (ide_runtime_manager_extension_added),
+                    self);
+
+  g_signal_connect (self->extensions,
+                    "extension-removed",
+                    G_CALLBACK (ide_runtime_manager_extension_removed),
+                    self);
+
+  ide_extension_set_adapter_foreach (self->extensions,
+                                     ide_runtime_manager_extension_added,
+                                     self);
+
+  host = ide_runtime_new ("host", _("Use host operating system"));
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (host));
+
+  ide_runtime_manager_add (self, host);
+
+  return TRUE;
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+  iface->init = ide_runtime_manager_initable_init;
+}
+
+static void
+ide_runtime_manager_destroy (IdeObject *object)
+{
+  IdeRuntimeManager *self = (IdeRuntimeManager *)object;
+
+  self->unloading = TRUE;
+
+  ide_clear_and_destroy_object (&self->extensions);
+  g_clear_pointer (&self->runtimes, g_ptr_array_unref);
+
+  IDE_OBJECT_CLASS (ide_runtime_manager_parent_class)->destroy (object);
+}
+
+static void
+ide_runtime_manager_class_init (IdeRuntimeManagerClass *klass)
+{
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  i_object_class->destroy = ide_runtime_manager_destroy;
+}
+
+static void
+ide_runtime_manager_init (IdeRuntimeManager *self)
+{
+  self->runtimes = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+static GType
+ide_runtime_manager_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_RUNTIME;
+}
+
+static guint
+ide_runtime_manager_get_n_items (GListModel *model)
+{
+  IdeRuntimeManager *self = (IdeRuntimeManager *)model;
+
+  g_return_val_if_fail (IDE_IS_RUNTIME_MANAGER (self), 0);
+
+  return self->runtimes->len;
+}
+
+static gpointer
+ide_runtime_manager_get_item (GListModel *model,
+                              guint       position)
+{
+  IdeRuntimeManager *self = (IdeRuntimeManager *)model;
+
+  g_return_val_if_fail (IDE_IS_RUNTIME_MANAGER (self), NULL);
+  g_return_val_if_fail (position < self->runtimes->len, NULL);
+
+  return g_object_ref (g_ptr_array_index (self->runtimes, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_runtime_manager_get_item_type;
+  iface->get_n_items = ide_runtime_manager_get_n_items;
+  iface->get_item = ide_runtime_manager_get_item;
+}
+
+void
+ide_runtime_manager_add (IdeRuntimeManager *self,
+                         IdeRuntime        *runtime)
+{
+  guint idx;
+
+  g_return_if_fail (IDE_IS_RUNTIME_MANAGER (self));
+  g_return_if_fail (IDE_IS_RUNTIME (runtime));
+
+  idx = self->runtimes->len;
+  g_ptr_array_add (self->runtimes, g_object_ref (runtime));
+  g_list_model_items_changed (G_LIST_MODEL (self), idx, 0, 1);
+}
+
+void
+ide_runtime_manager_remove (IdeRuntimeManager *self,
+                            IdeRuntime        *runtime)
+{
+  g_return_if_fail (IDE_IS_RUNTIME_MANAGER (self));
+  g_return_if_fail (IDE_IS_RUNTIME (runtime));
+
+  for (guint i = 0; i < self->runtimes->len; i++)
+    {
+      IdeRuntime *item = g_ptr_array_index (self->runtimes, i);
+
+      if (runtime == item)
+        {
+          g_ptr_array_remove_index (self->runtimes, i);
+          if (!ide_object_in_destruction (IDE_OBJECT (self)))
+            g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+          break;
+        }
+    }
+}
+
+/**
+ * ide_runtime_manager_get_runtime:
+ * @self: An #IdeRuntimeManager
+ * @id: the identifier of the runtime
+ *
+ * Gets the runtime by its internal identifier.
+ *
+ * Returns: (transfer none): An #IdeRuntime.
+ *
+ * Since: 3.32
+ */
+IdeRuntime *
+ide_runtime_manager_get_runtime (IdeRuntimeManager *self,
+                                 const gchar       *id)
+{
+  guint i;
+
+  g_return_val_if_fail (IDE_IS_RUNTIME_MANAGER (self), NULL);
+  g_return_val_if_fail (id != NULL, NULL);
+
+  for (i = 0; i < self->runtimes->len; i++)
+    {
+      IdeRuntime *runtime = g_ptr_array_index (self->runtimes, i);
+      const gchar *runtime_id = ide_runtime_get_id (runtime);
+
+      if (g_strcmp0 (runtime_id, id) == 0)
+        return runtime;
+    }
+
+  return NULL;
+}
+
+static void
+install_lookup_cb (IdeExtensionSetAdapter *set,
+                   PeasPluginInfo         *plugin,
+                   IdeRuntimeProvider     *provider,
+                   InstallLookup          *lookup)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin != NULL);
+  g_assert (IDE_IS_RUNTIME_PROVIDER (provider));
+  g_assert (lookup != NULL);
+  g_assert (lookup->runtime_id != NULL);
+  g_assert (lookup->provider == NULL || IDE_IS_RUNTIME_PROVIDER (lookup->provider));
+
+  if (lookup->provider == NULL)
+    {
+      if (ide_runtime_provider_can_install (provider, lookup->runtime_id))
+        lookup->provider = provider;
+    }
+}
+
+static void
+ide_runtime_manager_prepare_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeRuntimeProvider *provider = (IdeRuntimeProvider *)object;
+  g_autoptr(IdeRuntime) runtime = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUNTIME_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  runtime = ide_runtime_provider_bootstrap_finish (provider, result, &error);
+
+  g_assert (!runtime ||IDE_IS_RUNTIME (runtime));
+
+  if (runtime == NULL)
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task, g_steal_pointer (&runtime), g_object_unref);
+
+  IDE_EXIT;
+}
+
+void
+_ide_runtime_manager_prepare_async (IdeRuntimeManager   *self,
+                                    IdeBuildPipeline    *pipeline,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  IdeConfiguration *config;
+  PrepareState *state;
+  const gchar *runtime_id;
+  InstallLookup lookup = { 0 };
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_RUNTIME_MANAGER (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+  runtime_id = ide_configuration_get_runtime_id (config);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, _ide_runtime_manager_prepare_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_release_on_propagate (task, FALSE);
+
+  state = g_slice_new0 (PrepareState);
+  state->runtime_id = g_strdup (runtime_id);
+  state->pipeline = g_object_ref (pipeline);
+  ide_task_set_task_data (task, state, prepare_state_free);
+
+  if (runtime_id == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "Configuration lacks runtime specification");
+      IDE_EXIT;
+    }
+
+  /*
+   * It would be tempting to just return early here if we could locate
+   * the runtime as already registered. But that isn't enough since we
+   * might need to also install an SDK.
+   */
+
+  lookup.runtime_id = runtime_id;
+  ide_extension_set_adapter_foreach (self->extensions,
+                                     (IdeExtensionSetAdapterForeachFunc) install_lookup_cb,
+                                     &lookup);
+
+  if (lookup.provider == NULL)
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_SUPPORTED,
+                               "Failed to locate provider for runtime: %s",
+                               runtime_id);
+  else
+    ide_runtime_provider_bootstrap_async (lookup.provider,
+                                          pipeline,
+                                          cancellable,
+                                          ide_runtime_manager_prepare_cb,
+                                          g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+gboolean
+_ide_runtime_manager_prepare_finish (IdeRuntimeManager  *self,
+                                     GAsyncResult       *result,
+                                     GError            **error)
+{
+  g_autoptr(IdeRuntime) ret = NULL;
+  g_autoptr(GError) local_error = NULL;
+  PrepareState *state;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_RUNTIME_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  state = ide_task_get_task_data (IDE_TASK (result));
+  ret = ide_task_propagate_pointer (IDE_TASK (result), &local_error);
+
+  /*
+   * If we got NOT_SUPPORTED error, and the runtime already exists,
+   * then we can synthesize a successful result to the caller.
+   */
+  if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+    {
+      if ((ret = ide_runtime_manager_get_runtime (self, state->runtime_id)))
+        {
+          g_object_ref (ret);
+          g_clear_error (&local_error);
+        }
+    }
+
+  if (error != NULL)
+    *error = g_steal_pointer (&local_error);
+
+  g_return_val_if_fail (!ret || IDE_IS_RUNTIME (ret), FALSE);
+
+  if (IDE_IS_RUNTIME (ret))
+    _ide_build_pipeline_set_runtime (state->pipeline, ret);
+
+  IDE_RETURN (ret != NULL);
+}
diff --git a/src/libide/foundry/ide-runtime-manager.h b/src/libide/foundry/ide-runtime-manager.h
new file mode 100644
index 000000000..e63b011b6
--- /dev/null
+++ b/src/libide/foundry/ide-runtime-manager.h
@@ -0,0 +1,50 @@
+/* ide-runtime-manager.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RUNTIME_MANAGER (ide_runtime_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeRuntimeManager, ide_runtime_manager, IDE, RUNTIME_MANAGER, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeRuntimeManager       *ide_runtime_manager_from_context       (IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+IdeRuntime *ide_runtime_manager_get_runtime (IdeRuntimeManager    *self,
+                                             const gchar          *id);
+IDE_AVAILABLE_IN_3_32
+void        ide_runtime_manager_add         (IdeRuntimeManager    *self,
+                                             IdeRuntime           *runtime);
+IDE_AVAILABLE_IN_3_32
+void        ide_runtime_manager_remove      (IdeRuntimeManager    *self,
+                                             IdeRuntime           *runtime);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-runtime-private.h b/src/libide/foundry/ide-runtime-private.h
new file mode 100644
index 000000000..3809c5bad
--- /dev/null
+++ b/src/libide/foundry/ide-runtime-private.h
@@ -0,0 +1,37 @@
+/* ide-runtime-private.h
+ *
+ * Copyright 2018-2019 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 "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+void     _ide_runtime_manager_unload         (IdeRuntimeManager    *self);
+void     _ide_runtime_manager_prepare_async  (IdeRuntimeManager    *self,
+                                              IdeBuildPipeline     *pipeline,
+                                              GCancellable         *cancellable,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+gboolean _ide_runtime_manager_prepare_finish (IdeRuntimeManager    *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-runtime-provider.c b/src/libide/foundry/ide-runtime-provider.c
new file mode 100644
index 000000000..60568ee5b
--- /dev/null
+++ b/src/libide/foundry/ide-runtime-provider.c
@@ -0,0 +1,299 @@
+/* ide-runtime-provider.c
+ *
+ * Copyright 2016-2019 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 "ide-runtime-provider"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-build-pipeline.h"
+#include "ide-configuration.h"
+#include "ide-foundry-compat.h"
+#include "ide-runtime.h"
+#include "ide-runtime-manager.h"
+#include "ide-runtime-provider.h"
+
+G_DEFINE_INTERFACE (IdeRuntimeProvider, ide_runtime_provider, IDE_TYPE_OBJECT)
+
+static void
+ide_runtime_provider_real_load (IdeRuntimeProvider *self,
+                                IdeRuntimeManager  *manager)
+{
+}
+
+static void
+ide_runtime_provider_real_unload (IdeRuntimeProvider *self,
+                                  IdeRuntimeManager  *manager)
+{
+}
+
+static gboolean
+ide_runtime_provider_real_can_install (IdeRuntimeProvider *self,
+                                       const gchar        *runtime_id)
+{
+  return FALSE;
+}
+
+static void
+ide_runtime_provider_real_install_async (IdeRuntimeProvider  *self,
+                                         const gchar         *runtime_id,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  ide_task_report_new_error (self, callback, user_data,
+                             ide_runtime_provider_real_install_async,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "%s does not support installing runtimes",
+                             G_OBJECT_TYPE_NAME (self));
+}
+
+static gboolean
+ide_runtime_provider_real_install_finish (IdeRuntimeProvider  *self,
+                                          GAsyncResult        *result,
+                                          GError             **error)
+{
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_runtime_provider_real_bootstrap_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  IdeRuntimeProvider *self = (IdeRuntimeProvider *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  IdeRuntimeManager *runtime_manager;
+  const gchar *runtime_id;
+  IdeContext *context;
+  IdeRuntime *runtime;
+
+  g_assert (IDE_IS_RUNTIME_PROVIDER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_runtime_provider_install_finish (self, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  runtime_id = ide_task_get_task_data (task);
+  context = ide_object_get_context (IDE_OBJECT (self));
+  runtime_manager = ide_runtime_manager_from_context (context);
+  runtime = ide_runtime_manager_get_runtime (runtime_manager, runtime_id);
+
+  if (runtime == NULL)
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_FOUND,
+                               "No such runtime \"%s\"",
+                               runtime_id);
+  else
+    ide_task_return_pointer (task, g_object_ref (runtime), g_object_unref);
+}
+
+static void
+ide_runtime_provider_real_bootstrap_async (IdeRuntimeProvider  *self,
+                                           IdeBuildPipeline    *pipeline,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  IdeConfiguration *config;
+  const gchar *runtime_id;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUNTIME_PROVIDER (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_runtime_provider_real_bootstrap_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+  runtime_id = ide_configuration_get_runtime_id (config);
+  ide_task_set_task_data (task, g_strdup (runtime_id), g_free);
+
+  if (runtime_id == NULL)
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "No runtime provided to install");
+  else
+    ide_runtime_provider_install_async (self,
+                                        runtime_id,
+                                        cancellable,
+                                        ide_runtime_provider_real_bootstrap_cb,
+                                        g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static IdeRuntime *
+ide_runtime_provider_real_bootstrap_finish (IdeRuntimeProvider  *self,
+                                            GAsyncResult        *result,
+                                            GError             **error)
+{
+  IdeRuntime *ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+  g_return_val_if_fail (!ret || IDE_IS_RUNTIME (ret), NULL);
+  return ret;
+}
+
+static void
+ide_runtime_provider_default_init (IdeRuntimeProviderInterface *iface)
+{
+  iface->load = ide_runtime_provider_real_load;
+  iface->unload = ide_runtime_provider_real_unload;
+  iface->can_install = ide_runtime_provider_real_can_install;
+  iface->install_async = ide_runtime_provider_real_install_async;
+  iface->install_finish = ide_runtime_provider_real_install_finish;
+  iface->bootstrap_async = ide_runtime_provider_real_bootstrap_async;
+  iface->bootstrap_finish = ide_runtime_provider_real_bootstrap_finish;
+}
+
+void
+ide_runtime_provider_load (IdeRuntimeProvider *self,
+                           IdeRuntimeManager  *manager)
+{
+  g_return_if_fail (IDE_IS_RUNTIME_PROVIDER (self));
+  g_return_if_fail (IDE_IS_RUNTIME_MANAGER (manager));
+
+  IDE_RUNTIME_PROVIDER_GET_IFACE (self)->load (self, manager);
+}
+
+void
+ide_runtime_provider_unload (IdeRuntimeProvider *self,
+                             IdeRuntimeManager  *manager)
+{
+  g_return_if_fail (IDE_IS_RUNTIME_PROVIDER (self));
+  g_return_if_fail (IDE_IS_RUNTIME_MANAGER (manager));
+
+  IDE_RUNTIME_PROVIDER_GET_IFACE (self)->unload (self, manager);
+}
+
+gboolean
+ide_runtime_provider_can_install (IdeRuntimeProvider *self,
+                                  const gchar        *runtime_id)
+{
+  g_return_val_if_fail (IDE_IS_RUNTIME_PROVIDER (self), FALSE);
+  g_return_val_if_fail (runtime_id != NULL, FALSE);
+
+  return IDE_RUNTIME_PROVIDER_GET_IFACE (self)->can_install (self, runtime_id);
+}
+
+void
+ide_runtime_provider_install_async (IdeRuntimeProvider  *self,
+                                    const gchar         *runtime_id,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_RUNTIME_PROVIDER (self));
+  g_return_if_fail (runtime_id != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_RUNTIME_PROVIDER_GET_IFACE (self)->install_async (self, runtime_id, cancellable, callback, user_data);
+}
+
+gboolean
+ide_runtime_provider_install_finish (IdeRuntimeProvider  *self,
+                                     GAsyncResult        *result,
+                                     GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_RUNTIME_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_RUNTIME_PROVIDER_GET_IFACE (self)->install_finish (self, result, error);
+}
+
+/**
+ * ide_runtime_provider_bootstrap_async:
+ * @self: a #IdeRuntimeProvider
+ * @pipeline: an #IdeBuildPipeline
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a #GAsyncReadyCallback or %NULL
+ * @user_data: closure data for @callback
+ *
+ * This function allows to the runtime provider to install dependent runtimes
+ * similar to ide_runtime_provider_install_async(), but with the added benefit
+ * that it can access the pipeline for more information. For example, it may
+ * want to check the architecture of the pipeline, or the connected device for
+ * tweaks as to what runtime to use.
+ *
+ * Some runtime providers like Flatpak might use this to locate SDK extensions
+ * and install those too.
+ *
+ * This function should be used instead of ide_runtime_provider_install_async().
+ *
+ * Since: 3.32
+ */
+void
+ide_runtime_provider_bootstrap_async (IdeRuntimeProvider  *self,
+                                      IdeBuildPipeline    *pipeline,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_RUNTIME_PROVIDER (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_RUNTIME_PROVIDER_GET_IFACE (self)->bootstrap_async (self, pipeline, cancellable, callback, user_data);
+}
+
+/**
+ * ide_runtime_provider_bootstrap_finish:
+ * @self: a #IdeRuntimeProvider
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes the asynchronous request to bootstrap.
+ *
+ * The resulting runtime will be set as the runtime to use for the build
+ * pipeline.
+ *
+ * Returns: (transfer full): an #IdeRuntime if successful; otherwise %NULL
+ *   and @error is set.
+ *
+ * Since: 3.32
+ */
+IdeRuntime *
+ide_runtime_provider_bootstrap_finish (IdeRuntimeProvider  *self,
+                                       GAsyncResult        *result,
+                                       GError             **error)
+{
+  IdeRuntime *ret;
+
+  g_return_val_if_fail (IDE_IS_RUNTIME_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  ret = IDE_RUNTIME_PROVIDER_GET_IFACE (self)->bootstrap_finish (self, result, error);
+
+  g_return_val_if_fail (!ret || IDE_IS_RUNTIME (ret), NULL);
+
+  return ret;
+}
diff --git a/src/libide/foundry/ide-runtime-provider.h b/src/libide/foundry/ide-runtime-provider.h
new file mode 100644
index 000000000..a249d4280
--- /dev/null
+++ b/src/libide/foundry/ide-runtime-provider.h
@@ -0,0 +1,96 @@
+/* ide-runtime-provider.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RUNTIME_PROVIDER (ide_runtime_provider_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeRuntimeProvider, ide_runtime_provider, IDE, RUNTIME_PROVIDER, IdeObject)
+
+struct _IdeRuntimeProviderInterface
+{
+  GTypeInterface parent;
+
+  void        (*load)             (IdeRuntimeProvider   *self,
+                                   IdeRuntimeManager    *manager);
+  void        (*unload)           (IdeRuntimeProvider   *self,
+                                   IdeRuntimeManager    *manager);
+  gboolean    (*can_install)      (IdeRuntimeProvider   *self,
+                                   const gchar          *runtime_id);
+  void        (*install_async)    (IdeRuntimeProvider   *self,
+                                   const gchar          *runtime_id,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data);
+  gboolean    (*install_finish)   (IdeRuntimeProvider   *self,
+                                   GAsyncResult         *result,
+                                   GError              **error);
+  void        (*bootstrap_async)  (IdeRuntimeProvider   *self,
+                                   IdeBuildPipeline     *pipeline,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data);
+  IdeRuntime *(*bootstrap_finish) (IdeRuntimeProvider   *self,
+                                   GAsyncResult         *result,
+                                   GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void        ide_runtime_provider_load             (IdeRuntimeProvider   *self,
+                                                   IdeRuntimeManager    *manager);
+IDE_AVAILABLE_IN_3_32
+void        ide_runtime_provider_unload           (IdeRuntimeProvider   *self,
+                                                   IdeRuntimeManager    *manager);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_runtime_provider_can_install      (IdeRuntimeProvider   *self,
+                                                   const gchar          *runtime_id);
+IDE_AVAILABLE_IN_3_32
+void        ide_runtime_provider_install_async    (IdeRuntimeProvider   *self,
+                                                   const gchar          *runtime_id,
+                                                   GCancellable         *cancellable,
+                                                   GAsyncReadyCallback   callback,
+                                                   gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_runtime_provider_install_finish   (IdeRuntimeProvider   *self,
+                                                   GAsyncResult         *result,
+                                                   GError              **error);
+IDE_AVAILABLE_IN_3_32
+void        ide_runtime_provider_bootstrap_async  (IdeRuntimeProvider   *self,
+                                                   IdeBuildPipeline     *pipeline,
+                                                   GCancellable         *cancellable,
+                                                   GAsyncReadyCallback   callback,
+                                                   gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+IdeRuntime *ide_runtime_provider_bootstrap_finish (IdeRuntimeProvider   *self,
+                                                   GAsyncResult         *result,
+                                                   GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-runtime.c b/src/libide/foundry/ide-runtime.c
new file mode 100644
index 000000000..a46b5961e
--- /dev/null
+++ b/src/libide/foundry/ide-runtime.c
@@ -0,0 +1,712 @@
+/* ide-runtime.c
+ *
+ * Copyright 2016-2019 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 "ide-runtime"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-build-target.h"
+#include "ide-configuration.h"
+#include "ide-runtime.h"
+#include "ide-runner.h"
+#include "ide-toolchain.h"
+#include "ide-triplet.h"
+
+typedef struct
+{
+  gchar *id;
+  gchar *category;
+  gchar *display_name;
+} IdeRuntimePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeRuntime, ide_runtime, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_ID,
+  PROP_CATEGORY,
+  PROP_DISPLAY_NAME,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static IdeSubprocessLauncher *
+ide_runtime_real_create_launcher (IdeRuntime  *self,
+                                  GError     **error)
+{
+  IdeSubprocessLauncher *ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUNTIME (self));
+
+  ret = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE);
+
+  if (ret != NULL)
+    {
+      ide_subprocess_launcher_set_run_on_host (ret, TRUE);
+      ide_subprocess_launcher_set_clear_env (ret, FALSE);
+    }
+  else
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_FAILED,
+                   "An unknown error ocurred");
+    }
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+ide_runtime_real_contains_program_in_path (IdeRuntime   *self,
+                                           const gchar  *program,
+                                           GCancellable *cancellable)
+{
+  g_assert (IDE_IS_RUNTIME (self));
+  g_assert (program != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!ide_is_flatpak ())
+    {
+      g_autofree gchar *path = NULL;
+      path = g_find_program_in_path (program);
+      return path != NULL;
+    }
+  else
+    {
+      g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+
+      /*
+       * If we are in flatpak, we have to execute a program on the host to
+       * determine if there is a program available, as we cannot resolve
+       * file paths from inside the mount namespace.
+       */
+
+      if (NULL != (launcher = ide_runtime_create_launcher (self, NULL)))
+        {
+          g_autoptr(IdeSubprocess) subprocess = NULL;
+
+          ide_subprocess_launcher_set_run_on_host (launcher, TRUE);
+          ide_subprocess_launcher_push_argv (launcher, "which");
+          ide_subprocess_launcher_push_argv (launcher, program);
+
+          if (NULL != (subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, NULL)))
+            return ide_subprocess_wait_check (subprocess, NULL, NULL);
+        }
+
+      return FALSE;
+    }
+
+  g_assert_not_reached ();
+}
+
+gboolean
+ide_runtime_contains_program_in_path (IdeRuntime   *self,
+                                      const gchar  *program,
+                                      GCancellable *cancellable)
+{
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), FALSE);
+  g_return_val_if_fail (program != NULL, FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  return IDE_RUNTIME_GET_CLASS (self)->contains_program_in_path (self, program, cancellable);
+}
+
+static void
+ide_runtime_real_prepare_configuration (IdeRuntime       *self,
+                                        IdeConfiguration *configuration)
+{
+  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
+
+  g_assert (IDE_IS_RUNTIME (self));
+  g_assert (IDE_IS_CONFIGURATION (configuration));
+
+  if (NULL == ide_configuration_get_prefix (configuration))
+    {
+      g_autofree gchar *install_path = NULL;
+      g_autofree gchar *project_id = NULL;
+      IdeContext *context;
+
+      context = ide_object_get_context (IDE_OBJECT (self));
+      project_id = ide_context_dup_project_id (context);
+
+      install_path = g_build_filename (g_get_user_cache_dir (),
+                                       "gnome-builder",
+                                       "install",
+                                       project_id,
+                                       priv->id,
+                                       NULL);
+
+      ide_configuration_set_prefix (configuration, install_path);
+    }
+}
+
+static IdeRunner *
+ide_runtime_real_create_runner (IdeRuntime     *self,
+                                IdeBuildTarget *build_target)
+{
+  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
+  g_autoptr(GFile) installdir = NULL;
+  g_auto(GStrv) argv = NULL;
+  g_autofree gchar *cwd = NULL;
+  IdeContext *context;
+  IdeRunner *runner;
+
+  g_assert (IDE_IS_RUNTIME (self));
+  g_assert (!build_target || IDE_IS_BUILD_TARGET (build_target));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  runner = ide_runner_new (context);
+  g_assert (IDE_IS_RUNNER (runner));
+
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (runner));
+
+  if (ide_str_equal0 (priv->id, "host"))
+    ide_runner_set_run_on_host (runner, TRUE);
+
+  if (build_target != NULL)
+    {
+      ide_runner_set_build_target (runner, build_target);
+
+      installdir = ide_build_target_get_install_directory (build_target);
+      argv = ide_build_target_get_argv (build_target);
+      cwd = ide_build_target_get_cwd (build_target);
+    }
+
+  /* Possibly translate relative paths for the binary */
+  if (argv && argv[0] && !g_path_is_absolute (argv[0]))
+    {
+      const gchar *slash = strchr (argv[0], '/');
+      g_autofree gchar *copy = g_strdup (slash ? (slash + 1) : argv[0]);
+
+      g_free (argv[0]);
+
+      if (installdir != NULL)
+        {
+          g_autoptr(GFile) dest = g_file_get_child (installdir, copy);
+          argv[0] = g_file_get_path (dest);
+        }
+      else
+        argv[0] = g_steal_pointer (&copy);
+    }
+
+  if (installdir != NULL)
+    {
+      g_autoptr(GFile) parentdir = NULL;
+      g_autofree gchar *schemadir = NULL;
+      g_autofree gchar *parentpath = NULL;
+
+      /* GSettings requires an env var for non-standard dirs */
+      if (NULL != (parentdir = g_file_get_parent (installdir)))
+        {
+          IdeEnvironment *env = ide_runner_get_environment (runner);
+
+          parentpath = g_file_get_path (parentdir);
+          schemadir = g_build_filename (parentpath, "share", "glib-2.0", "schemas", NULL);
+          ide_environment_setenv (env, "GSETTINGS_SCHEMA_DIR", schemadir);
+        }
+    }
+
+  if (argv != NULL)
+    ide_runner_push_args (runner, (const gchar * const *)argv);
+
+  if (cwd != NULL)
+    ide_runner_set_cwd (runner, cwd);
+
+  return runner;
+}
+
+static GFile *
+ide_runtime_real_translate_file (IdeRuntime *self,
+                                 GFile      *file)
+{
+  g_autofree gchar *path = NULL;
+
+  g_assert (IDE_IS_RUNTIME (self));
+  g_assert (G_IS_FILE (file));
+
+  /* We only need to translate when running as flatpak */
+  if (!ide_is_flatpak ())
+    return NULL;
+
+  /* Only deal with native files */
+  if (!g_file_is_native (file) || NULL == (path = g_file_get_path (file)))
+    return NULL;
+
+  /* If this is /usr or /etc, then translate to /run/host/$dir,
+   * as that is where flatpak 0.10.1 and greater will mount them
+   * when --filesystem=host.
+   */
+  if (g_str_has_prefix (path, "/usr/") || g_str_has_prefix (path, "/etc/"))
+    return g_file_new_build_filename ("/run/host/", path, NULL);
+
+  return NULL;
+}
+
+static gchar *
+ide_runtime_repr (IdeObject *object)
+{
+  IdeRuntime *self = (IdeRuntime *)object;
+  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_RUNTIME (self));
+
+  return g_strdup_printf ("%s id=\"%s\" display-name=\"%s\"",
+                          G_OBJECT_TYPE_NAME (self),
+                          priv->id ?: "",
+                          priv->display_name ?: "");
+}
+
+static void
+ide_runtime_finalize (GObject *object)
+{
+  IdeRuntime *self = (IdeRuntime *)object;
+  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
+
+  g_clear_pointer (&priv->id, g_free);
+  g_clear_pointer (&priv->display_name, g_free);
+
+  G_OBJECT_CLASS (ide_runtime_parent_class)->finalize (object);
+}
+
+static void
+ide_runtime_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  IdeRuntime *self = IDE_RUNTIME (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      g_value_set_string (value, ide_runtime_get_id (self));
+      break;
+
+    case PROP_CATEGORY:
+      g_value_set_string (value, ide_runtime_get_category (self));
+      break;
+
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, ide_runtime_get_display_name (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_runtime_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  IdeRuntime *self = IDE_RUNTIME (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      ide_runtime_set_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_CATEGORY:
+      ide_runtime_set_category (self, g_value_get_string (value));
+      break;
+
+    case PROP_DISPLAY_NAME:
+      ide_runtime_set_display_name (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_runtime_class_init (IdeRuntimeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_runtime_finalize;
+  object_class->get_property = ide_runtime_get_property;
+  object_class->set_property = ide_runtime_set_property;
+
+  i_object_class->repr = ide_runtime_repr;
+
+  klass->create_launcher = ide_runtime_real_create_launcher;
+  klass->create_runner = ide_runtime_real_create_runner;
+  klass->contains_program_in_path = ide_runtime_real_contains_program_in_path;
+  klass->prepare_configuration = ide_runtime_real_prepare_configuration;
+  klass->translate_file = ide_runtime_real_translate_file;
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "The runtime identifier",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CATEGORY] =
+    g_param_spec_string ("category",
+                         "Category",
+                         "The runtime's category",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DISPLAY_NAME] =
+    g_param_spec_string ("display-name",
+                         "Display Name",
+                         "Display Name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_runtime_init (IdeRuntime *self)
+{
+}
+
+const gchar *
+ide_runtime_get_id (IdeRuntime  *self)
+{
+  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
+
+  return priv->id;
+}
+
+void
+ide_runtime_set_id (IdeRuntime  *self,
+                    const gchar *id)
+{
+  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUNTIME (self));
+  g_return_if_fail (id != NULL);
+
+  if (!ide_str_equal0 (id, priv->id))
+    {
+      g_free (priv->id);
+      priv->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+    }
+}
+
+const gchar *
+ide_runtime_get_category (IdeRuntime  *self)
+{
+  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
+  g_return_val_if_fail (priv->category != NULL, "Host System");
+
+  return priv->category;
+}
+
+void
+ide_runtime_set_category (IdeRuntime  *self,
+                          const gchar *category)
+{
+  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUNTIME (self));
+
+  if (category == NULL)
+    category = _("Host System");
+
+  if (!ide_str_equal0 (category, priv->category))
+    {
+      g_free (priv->category);
+      priv->category = g_strdup (category);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CATEGORY]);
+    }
+}
+
+const gchar *
+ide_runtime_get_display_name (IdeRuntime *self)
+{
+  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
+
+  return priv->display_name;
+}
+
+void
+ide_runtime_set_display_name (IdeRuntime  *self,
+                              const gchar *display_name)
+{
+  IdeRuntimePrivate *priv = ide_runtime_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_RUNTIME (self));
+  g_return_if_fail (display_name != NULL);
+
+  if (g_strcmp0 (display_name, priv->display_name) != 0)
+    {
+      g_free (priv->display_name);
+      priv->display_name = g_strdup (display_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
+    }
+}
+
+IdeRuntime *
+ide_runtime_new (const gchar *id,
+                 const gchar *display_name)
+{
+  g_return_val_if_fail (id != NULL, NULL);
+  g_return_val_if_fail (display_name != NULL, NULL);
+
+  return g_object_new (IDE_TYPE_RUNTIME,
+                       "id", id,
+                       "display-name", display_name,
+                       NULL);
+}
+
+/**
+ * ide_runtime_create_launcher:
+ *
+ * Creates a launcher for the runtime.
+ *
+ * This can be used to execute a command within a runtime.
+ *
+ * It is important that this function can be run from a thread without
+ * side effects.
+ *
+ * Returns: (transfer full): An #IdeSubprocessLauncher or %NULL upon failure.
+ *
+ * Since: 3.32
+ */
+IdeSubprocessLauncher *
+ide_runtime_create_launcher (IdeRuntime  *self,
+                             GError     **error)
+{
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
+
+  return IDE_RUNTIME_GET_CLASS (self)->create_launcher (self, error);
+}
+
+void
+ide_runtime_prepare_configuration (IdeRuntime       *self,
+                                   IdeConfiguration *configuration)
+{
+  g_return_if_fail (IDE_IS_RUNTIME (self));
+  g_return_if_fail (IDE_IS_CONFIGURATION (configuration));
+
+  IDE_RUNTIME_GET_CLASS (self)->prepare_configuration (self, configuration);
+}
+
+/**
+ * ide_runtime_create_runner:
+ * @self: An #IdeRuntime
+ * @build_target: (nullable): An #IdeBuildTarget or %NULL
+ *
+ * Creates a new runner that can be used to execute the build target within
+ * the runtime. This should be used to implement such features as "run target"
+ * or "run unit test" inside the target runtime.
+ *
+ * If @build_target is %NULL, the runtime should create a runner that allows
+ * the caller to specify the binary using the #IdeRunner API.
+ *
+ * Returns: (transfer full) (nullable): An #IdeRunner if successful, otherwise
+ *   %NULL and @error is set.
+ *
+ * Since: 3.32
+ */
+IdeRunner *
+ide_runtime_create_runner (IdeRuntime     *self,
+                           IdeBuildTarget *build_target)
+{
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
+  g_return_val_if_fail (!build_target || IDE_IS_BUILD_TARGET (build_target), NULL);
+
+  return IDE_RUNTIME_GET_CLASS (self)->create_runner (self, build_target);
+}
+
+GQuark
+ide_runtime_error_quark (void)
+{
+  static GQuark quark = 0;
+
+  if G_UNLIKELY (quark == 0)
+    quark = g_quark_from_static_string ("ide_runtime_error_quark");
+
+  return quark;
+}
+
+/**
+ * ide_runtime_translate_file:
+ * @self: An #IdeRuntime
+ * @file: a #GFile
+ *
+ * Translates the file from a path within the runtime to a path that can
+ * be accessed from the host system.
+ *
+ * Returns: (transfer full) (not nullable): a #GFile.
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_runtime_translate_file (IdeRuntime *self,
+                            GFile      *file)
+{
+  GFile *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  if (IDE_RUNTIME_GET_CLASS (self)->translate_file)
+    ret = IDE_RUNTIME_GET_CLASS (self)->translate_file (self, file);
+
+  if (ret == NULL)
+    ret = g_object_ref (file);
+
+  return ret;
+}
+
+/**
+ * ide_runtime_get_system_include_dirs:
+ * @self: a #IdeRuntime
+ *
+ * Gets the system include dirs for the runtime. Usually, this is just
+ * "/usr/include", but more complex runtimes may include additional.
+ *
+ * Returns: (transfer full) (array zero-terminated=1): A newly allocated
+ *   string containing the include dirs.
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_runtime_get_system_include_dirs (IdeRuntime *self)
+{
+  static const gchar *basic[] = { "/usr/include", NULL };
+
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
+
+  if (IDE_RUNTIME_GET_CLASS (self)->get_system_include_dirs)
+    return IDE_RUNTIME_GET_CLASS (self)->get_system_include_dirs (self);
+
+  return g_strdupv ((gchar **)basic);
+}
+
+/**
+ * ide_runtime_get_triplet:
+ * @self: a #IdeRuntime
+ *
+ * Gets the architecture triplet of the runtime.
+ *
+ * This can be used to ensure we're compiling for the right architecture
+ * given the current device.
+ *
+ * Returns: (transfer full) (not nullable): the architecture triplet the runtime
+ * will build for.
+ *
+ * Since: 3.32
+ */
+IdeTriplet *
+ide_runtime_get_triplet (IdeRuntime *self)
+{
+  IdeTriplet *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
+
+  if (IDE_RUNTIME_GET_CLASS (self)->get_triplet)
+    ret = IDE_RUNTIME_GET_CLASS (self)->get_triplet (self);
+
+  if (ret == NULL)
+    ret = ide_triplet_new_from_system ();
+
+  return ret;
+}
+
+/**
+ * ide_runtime_get_arch:
+ * @self: a #IdeRuntime
+ *
+ * Gets the architecture of the runtime.
+ *
+ * This can be used to ensure we're compiling for the right architecture
+ * given the current device.
+ *
+ * This is strictly equivalent to calling #ide_triplet_get_arch on the result
+ * of #ide_runtime_get_triplet.
+ *
+ * Returns: (transfer full) (not nullable): the name of the architecture
+ * the runtime will build for.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_runtime_get_arch (IdeRuntime *self)
+{
+  gchar *ret = NULL;
+  g_autoptr(IdeTriplet) triplet = NULL;
+
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), NULL);
+
+  triplet = ide_runtime_get_triplet (self);
+  ret = g_strdup (ide_triplet_get_arch (triplet));
+
+  return ret;
+}
+
+/**
+ * ide_runtime_supports_toolchain:
+ * @self: a #IdeRuntime
+ * @toolchain: the #IdeToolchain to check
+ *
+ * Informs wether a toolchain is supported by this.
+ *
+ * Returns: %TRUE if the toolchain is supported
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_runtime_supports_toolchain (IdeRuntime   *self,
+                                IdeToolchain *toolchain)
+{
+  const gchar *toolchain_id;
+
+  g_return_val_if_fail (IDE_IS_RUNTIME (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN (toolchain), FALSE);
+
+  toolchain_id = ide_toolchain_get_id (toolchain);
+  if (g_strcmp0 (toolchain_id, "default") == 0)
+    return TRUE;
+
+  if (IDE_RUNTIME_GET_CLASS (self)->supports_toolchain)
+    return IDE_RUNTIME_GET_CLASS (self)->supports_toolchain (self, toolchain);
+
+  return TRUE;
+}
diff --git a/src/libide/foundry/ide-runtime.h b/src/libide/foundry/ide-runtime.h
new file mode 100644
index 000000000..289753f23
--- /dev/null
+++ b/src/libide/foundry/ide-runtime.h
@@ -0,0 +1,118 @@
+/* ide-runtime.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_RUNTIME_ERROR_UNKNOWN = 0,
+  IDE_RUNTIME_ERROR_NO_SUCH_RUNTIME,
+  IDE_RUNTIME_ERROR_BUILD_FAILED,
+  IDE_RUNTIME_ERROR_TARGET_NOT_FOUND,
+  IDE_RUNTIME_ERROR_SPAWN_FAILED,
+} IdeRuntimeError;
+
+#define IDE_TYPE_RUNTIME (ide_runtime_get_type())
+#define IDE_RUNTIME_ERROR (ide_runtime_error_quark())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeRuntime, ide_runtime, IDE, RUNTIME, IdeObject)
+
+struct _IdeRuntimeClass
+{
+  IdeObjectClass parent;
+
+  gboolean                (*contains_program_in_path) (IdeRuntime           *self,
+                                                       const gchar          *program,
+                                                       GCancellable         *cancellable);
+  IdeSubprocessLauncher  *(*create_launcher)          (IdeRuntime           *self,
+                                                       GError              **error);
+  void                    (*prepare_configuration)    (IdeRuntime           *self,
+                                                       IdeConfiguration     *configuration);
+  IdeRunner              *(*create_runner)            (IdeRuntime           *self,
+                                                       IdeBuildTarget       *build_target);
+  GFile                  *(*translate_file)           (IdeRuntime           *self,
+                                                       GFile                *file);
+  gchar                 **(*get_system_include_dirs)  (IdeRuntime           *self);
+  IdeTriplet             *(*get_triplet)              (IdeRuntime           *self);
+  gboolean                (*supports_toolchain)       (IdeRuntime           *self,
+                                                       IdeToolchain         *toolchain);
+
+  /*< private >*/
+  gpointer _reserved[12];
+};
+
+IDE_AVAILABLE_IN_3_32
+GQuark                 ide_runtime_error_quark              (void) G_GNUC_CONST;
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_runtime_contains_program_in_path (IdeRuntime           *self,
+                                                             const gchar          *program,
+                                                             GCancellable         *cancellable);
+IDE_AVAILABLE_IN_3_32
+IdeSubprocessLauncher *ide_runtime_create_launcher          (IdeRuntime           *self,
+                                                             GError              **error);
+IDE_AVAILABLE_IN_3_32
+IdeRunner             *ide_runtime_create_runner            (IdeRuntime           *self,
+                                                             IdeBuildTarget       *build_target);
+IDE_AVAILABLE_IN_3_32
+void                   ide_runtime_prepare_configuration    (IdeRuntime           *self,
+                                                             IdeConfiguration     *configuration);
+IDE_AVAILABLE_IN_3_32
+IdeRuntime            *ide_runtime_new                      (const gchar          *id,
+                                                             const gchar          *title);
+IDE_AVAILABLE_IN_3_32
+const gchar           *ide_runtime_get_id                   (IdeRuntime           *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_runtime_set_id                   (IdeRuntime           *self,
+                                                             const gchar          *id);
+IDE_AVAILABLE_IN_3_32
+const gchar           *ide_runtime_get_category             (IdeRuntime           *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_runtime_set_category             (IdeRuntime           *self,
+                                                             const gchar          *category);
+IDE_AVAILABLE_IN_3_32
+const gchar           *ide_runtime_get_display_name         (IdeRuntime           *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_runtime_set_display_name         (IdeRuntime           *self,
+                                                             const gchar          *display_name);
+IDE_AVAILABLE_IN_3_32
+GFile                 *ide_runtime_translate_file           (IdeRuntime           *self,
+                                                             GFile                *file);
+IDE_AVAILABLE_IN_3_32
+gchar                **ide_runtime_get_system_include_dirs  (IdeRuntime           *self);
+IDE_AVAILABLE_IN_3_32
+gchar                 *ide_runtime_get_arch                 (IdeRuntime           *self);
+IDE_AVAILABLE_IN_3_32
+IdeTriplet            *ide_runtime_get_triplet              (IdeRuntime           *self);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_runtime_supports_toolchain       (IdeRuntime           *self,
+                                                             IdeToolchain         *toolchain);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-simple-build-system-discovery.c 
b/src/libide/foundry/ide-simple-build-system-discovery.c
new file mode 100644
index 000000000..36ed5e74e
--- /dev/null
+++ b/src/libide/foundry/ide-simple-build-system-discovery.c
@@ -0,0 +1,374 @@
+/* ide-simple-build-system-discovery.c
+ *
+ * Copyright 2018-2019 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 "ide-simple-build-system-discovery"
+
+#include "config.h"
+
+#include <fnmatch.h>
+
+#include "ide-simple-build-system-discovery.h"
+
+typedef struct
+{
+  gchar *glob;
+  gchar *hint;
+  guint  is_exact : 1;
+  gint   priority;
+} IdeSimpleBuildSystemDiscoveryPrivate;
+
+enum {
+  PROP_0,
+  PROP_GLOB,
+  PROP_HINT,
+  PROP_PRIORITY,
+  N_PROPS
+};
+
+static gchar *
+ide_simple_build_system_discovery_discover (IdeBuildSystemDiscovery  *discovery,
+                                            GFile                    *file,
+                                            GCancellable             *cancellable,
+                                            gint                     *priority,
+                                            GError                  **error);
+
+static void
+build_system_discovery_iface_init (IdeBuildSystemDiscoveryInterface *iface)
+{
+  iface->discover = ide_simple_build_system_discovery_discover;
+}
+
+G_DEFINE_TYPE_WITH_CODE (IdeSimpleBuildSystemDiscovery, ide_simple_build_system_discovery, IDE_TYPE_OBJECT,
+                         G_ADD_PRIVATE (IdeSimpleBuildSystemDiscovery)
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_SYSTEM_DISCOVERY,
+                                                build_system_discovery_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_simple_build_system_discovery_finalize (GObject *object)
+{
+  IdeSimpleBuildSystemDiscovery *self = (IdeSimpleBuildSystemDiscovery *)object;
+  IdeSimpleBuildSystemDiscoveryPrivate *priv = ide_simple_build_system_discovery_get_instance_private (self);
+
+  g_clear_pointer (&priv->glob, g_free);
+  g_clear_pointer (&priv->hint, g_free);
+
+  G_OBJECT_CLASS (ide_simple_build_system_discovery_parent_class)->finalize (object);
+}
+
+static void
+ide_simple_build_system_discovery_get_property (GObject    *object,
+                                                guint       prop_id,
+                                                GValue     *value,
+                                                GParamSpec *pspec)
+{
+  IdeSimpleBuildSystemDiscovery *self = IDE_SIMPLE_BUILD_SYSTEM_DISCOVERY (object);
+
+  switch (prop_id)
+    {
+    case PROP_GLOB:
+      g_value_set_string (value, ide_simple_build_system_discovery_get_glob (self));
+      break;
+
+    case PROP_HINT:
+      g_value_set_string (value, ide_simple_build_system_discovery_get_hint (self));
+      break;
+
+    case PROP_PRIORITY:
+      g_value_set_int (value, ide_simple_build_system_discovery_get_priority (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_simple_build_system_discovery_set_property (GObject      *object,
+                                                guint         prop_id,
+                                                const GValue *value,
+                                                GParamSpec   *pspec)
+{
+  IdeSimpleBuildSystemDiscovery *self = IDE_SIMPLE_BUILD_SYSTEM_DISCOVERY (object);
+
+  switch (prop_id)
+    {
+    case PROP_GLOB:
+      ide_simple_build_system_discovery_set_glob (self, g_value_get_string (value));
+      break;
+
+    case PROP_HINT:
+      ide_simple_build_system_discovery_set_hint (self, g_value_get_string (value));
+      break;
+
+    case PROP_PRIORITY:
+      ide_simple_build_system_discovery_set_priority (self, g_value_get_int (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_simple_build_system_discovery_class_init (IdeSimpleBuildSystemDiscoveryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_simple_build_system_discovery_finalize;
+  object_class->get_property = ide_simple_build_system_discovery_get_property;
+  object_class->set_property = ide_simple_build_system_discovery_set_property;
+
+  /**
+   * IdeSimpleBuildSystemDiscovery:glob:
+   *
+   * The "glob" property is a glob to match for files within the project
+   * directory. This can be used to quickly match the project file, such as
+   * "configure.*".
+   *
+   * Since: 3.32
+   */
+  properties [PROP_GLOB] =
+    g_param_spec_string ("glob",
+                         "Glob",
+                         "The glob to match project filenames",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeSimpleBuildSystemDiscovery:hint:
+   *
+   * The "hint" property is used from ide_build_system_discovery_discover()
+   * if the build file was discovered.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_HINT] =
+    g_param_spec_string ("hint",
+                         "Hint",
+                         "The hint of the plugin supporting the build system",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeSimpleBuildSystemDiscovery:priority:
+   *
+   * The "priority" property is the priority of any match.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority",
+                      "Priority",
+                      "The priority of the discovery",
+                      G_MININT, G_MAXINT, 0,
+                      (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_simple_build_system_discovery_init (IdeSimpleBuildSystemDiscovery *self)
+{
+}
+
+const gchar *
+ide_simple_build_system_discovery_get_glob (IdeSimpleBuildSystemDiscovery *self)
+{
+  IdeSimpleBuildSystemDiscoveryPrivate *priv = ide_simple_build_system_discovery_get_instance_private (self);
+  g_return_val_if_fail (IDE_IS_SIMPLE_BUILD_SYSTEM_DISCOVERY (self), NULL);
+  return priv->glob;
+}
+
+const gchar *
+ide_simple_build_system_discovery_get_hint (IdeSimpleBuildSystemDiscovery *self)
+{
+  IdeSimpleBuildSystemDiscoveryPrivate *priv = ide_simple_build_system_discovery_get_instance_private (self);
+  g_return_val_if_fail (IDE_IS_SIMPLE_BUILD_SYSTEM_DISCOVERY (self), NULL);
+  return priv->hint;
+}
+
+gint
+ide_simple_build_system_discovery_get_priority (IdeSimpleBuildSystemDiscovery *self)
+{
+  IdeSimpleBuildSystemDiscoveryPrivate *priv = ide_simple_build_system_discovery_get_instance_private (self);
+  g_return_val_if_fail (IDE_IS_SIMPLE_BUILD_SYSTEM_DISCOVERY (self), 0);
+  return priv->priority;
+}
+
+void
+ide_simple_build_system_discovery_set_glob (IdeSimpleBuildSystemDiscovery *self,
+                                            const gchar                   *glob)
+{
+  IdeSimpleBuildSystemDiscoveryPrivate *priv = ide_simple_build_system_discovery_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SIMPLE_BUILD_SYSTEM_DISCOVERY (self));
+  g_return_if_fail (glob != NULL);
+
+  if (!ide_str_equal0 (glob, priv->glob))
+    {
+      g_free (priv->glob);
+      priv->glob = g_strdup (glob);
+      priv->is_exact = TRUE;
+      for (; !priv->is_exact && *glob; glob = g_utf8_next_char (glob))
+        {
+          gunichar ch = g_utf8_get_char (glob);
+
+          switch (ch)
+            {
+            case '(': case '!': case '*': case '[': case '{': case '|':
+              priv->is_exact = FALSE;
+              break;
+
+            default:
+              break;
+            }
+        }
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_GLOB]);
+    }
+}
+
+void
+ide_simple_build_system_discovery_set_hint (IdeSimpleBuildSystemDiscovery *self,
+                                            const gchar                   *hint)
+{
+  IdeSimpleBuildSystemDiscoveryPrivate *priv = ide_simple_build_system_discovery_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SIMPLE_BUILD_SYSTEM_DISCOVERY (self));
+
+  if (!ide_str_equal0 (hint, priv->hint))
+    {
+      g_free (priv->hint);
+      priv->hint = g_strdup (hint);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HINT]);
+    }
+}
+
+void
+ide_simple_build_system_discovery_set_priority (IdeSimpleBuildSystemDiscovery *self,
+                                                gint                           priority)
+{
+  IdeSimpleBuildSystemDiscoveryPrivate *priv = ide_simple_build_system_discovery_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SIMPLE_BUILD_SYSTEM_DISCOVERY (self));
+
+  if (priority != priv->priority)
+    {
+      priv->priority = priority;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIORITY]);
+    }
+}
+
+static gboolean
+ide_simple_build_system_discovery_match (IdeSimpleBuildSystemDiscovery *self,
+                                         const gchar                   *name)
+{
+  IdeSimpleBuildSystemDiscoveryPrivate *priv = ide_simple_build_system_discovery_get_instance_private (self);
+
+  g_assert (IDE_IS_SIMPLE_BUILD_SYSTEM_DISCOVERY (self));
+  g_assert (name != NULL);
+
+  return fnmatch (priv->glob, name, 0) == 0;
+}
+
+static gboolean
+ide_simple_build_system_discovery_check_dir (IdeSimpleBuildSystemDiscovery *self,
+                                             GFile                         *directory,
+                                             GCancellable                  *cancellable)
+{
+  IdeSimpleBuildSystemDiscoveryPrivate *priv = ide_simple_build_system_discovery_get_instance_private (self);
+  g_autoptr(GFileEnumerator) enumerator = NULL;
+  gpointer infoptr;
+
+  g_assert (IDE_IS_SIMPLE_BUILD_SYSTEM_DISCOVERY (self));
+  g_assert (G_IS_FILE (directory));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (priv->is_exact)
+    {
+      g_autoptr(GFile) child = g_file_get_child (directory, priv->glob);
+      return g_file_query_exists (child, cancellable);
+    }
+
+  enumerator = g_file_enumerate_children (directory,
+                                          G_FILE_ATTRIBUTE_STANDARD_NAME,
+                                          G_FILE_QUERY_INFO_NONE,
+                                          cancellable,
+                                          NULL);
+
+  while ((infoptr = g_file_enumerator_next_file (enumerator, cancellable, NULL)))
+    {
+      g_autoptr(GFileInfo) info = infoptr;
+      const gchar *name = g_file_info_get_name (info);
+
+      if (fnmatch (priv->glob, name, 0) == 0)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gchar *
+ide_simple_build_system_discovery_discover (IdeBuildSystemDiscovery  *discovery,
+                                            GFile                    *file,
+                                            GCancellable             *cancellable,
+                                            gint                     *priority,
+                                            GError                  **error)
+{
+  IdeSimpleBuildSystemDiscovery *self = (IdeSimpleBuildSystemDiscovery *)discovery;
+  IdeSimpleBuildSystemDiscoveryPrivate *priv = ide_simple_build_system_discovery_get_instance_private (self);
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GFile) directory = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_autofree gchar *name = NULL;
+
+  g_assert (!IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SIMPLE_BUILD_SYSTEM_DISCOVERY (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (priority != NULL);
+
+  *priority = priv->priority;
+
+  if (priv->glob == NULL || priv->hint == NULL)
+    goto failure;
+
+  name = g_file_get_basename (file);
+  if (ide_simple_build_system_discovery_match (self, name))
+    return g_strdup (priv->hint);
+
+  context = ide_object_ref_context (IDE_OBJECT (self));
+  workdir = ide_context_ref_workdir (context);
+
+  if (g_file_query_file_type (file, G_FILE_QUERY_INFO_NONE, cancellable) != G_FILE_TYPE_DIRECTORY)
+    file = directory = g_file_get_parent (file);
+
+  if (ide_simple_build_system_discovery_check_dir (self, file, cancellable) ||
+      ide_simple_build_system_discovery_check_dir (self, workdir, cancellable))
+    return g_strdup (priv->hint);
+
+failure:
+  g_set_error (error,
+               G_IO_ERROR,
+               G_IO_ERROR_NOT_SUPPORTED,
+               "No match was discovered");
+  return NULL;
+}
diff --git a/src/libide/foundry/ide-simple-build-system-discovery.h 
b/src/libide/foundry/ide-simple-build-system-discovery.h
new file mode 100644
index 000000000..93e953942
--- /dev/null
+++ b/src/libide/foundry/ide-simple-build-system-discovery.h
@@ -0,0 +1,62 @@
+/* ide-simple-build-system-discovery.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+#include "ide-build-system-discovery.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SIMPLE_BUILD_SYSTEM_DISCOVERY (ide_simple_build_system_discovery_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeSimpleBuildSystemDiscovery, ide_simple_build_system_discovery, IDE, 
SIMPLE_BUILD_SYSTEM_DISCOVERY, IdeObject)
+
+struct _IdeSimpleBuildSystemDiscoveryClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_simple_build_system_discovery_get_glob     (IdeSimpleBuildSystemDiscovery *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_simple_build_system_discovery_set_glob     (IdeSimpleBuildSystemDiscovery *self,
+                                                             const gchar                   *glob);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_simple_build_system_discovery_get_hint     (IdeSimpleBuildSystemDiscovery *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_simple_build_system_discovery_set_hint     (IdeSimpleBuildSystemDiscovery *self,
+                                                             const gchar                   *hint);
+IDE_AVAILABLE_IN_3_32
+gint         ide_simple_build_system_discovery_get_priority (IdeSimpleBuildSystemDiscovery *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_simple_build_system_discovery_set_priority (IdeSimpleBuildSystemDiscovery *self,
+                                                             gint                           priority);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-simple-build-target.c b/src/libide/foundry/ide-simple-build-target.c
new file mode 100644
index 000000000..31909a45b
--- /dev/null
+++ b/src/libide/foundry/ide-simple-build-target.c
@@ -0,0 +1,220 @@
+/* ide-simple-build-target.c
+ *
+ * Copyright 2018-2019 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 "ide-simple-build-target"
+
+#include "config.h"
+
+#include "ide-build-target.h"
+#include "ide-simple-build-target.h"
+
+typedef struct
+{
+  GFile *install_directory;
+  gchar *name;
+  gchar **argv;
+  gchar *cwd;
+  gchar *language;
+  gint priority;
+} IdeSimpleBuildTargetPrivate;
+
+static void build_target_iface_init (IdeBuildTargetInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeSimpleBuildTarget, ide_simple_build_target, IDE_TYPE_OBJECT,
+                         G_ADD_PRIVATE (IdeSimpleBuildTarget)
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_TARGET,
+                                                build_target_iface_init))
+
+static void
+ide_simple_build_target_finalize (GObject *object)
+{
+  IdeSimpleBuildTarget *self = (IdeSimpleBuildTarget *)object;
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+
+  g_clear_object (&priv->install_directory);
+  g_clear_pointer (&priv->name, g_free);
+  g_clear_pointer (&priv->argv, g_strfreev);
+  g_clear_pointer (&priv->cwd, g_free);
+  g_clear_pointer (&priv->language, g_free);
+
+  G_OBJECT_CLASS (ide_simple_build_target_parent_class)->finalize (object);
+}
+
+static void
+ide_simple_build_target_class_init (IdeSimpleBuildTargetClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_simple_build_target_finalize;
+}
+
+static void
+ide_simple_build_target_init (IdeSimpleBuildTarget *self)
+{
+}
+
+IdeSimpleBuildTarget *
+ide_simple_build_target_new (IdeContext *context)
+{
+  return g_object_new (IDE_TYPE_SIMPLE_BUILD_TARGET,
+                       NULL);
+}
+
+void
+ide_simple_build_target_set_install_directory (IdeSimpleBuildTarget *self,
+                                               GFile                *install_directory)
+{
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SIMPLE_BUILD_TARGET (self));
+  g_return_if_fail (!install_directory || G_IS_FILE (install_directory));
+
+  g_set_object (&priv->install_directory, install_directory);
+}
+
+void
+ide_simple_build_target_set_name (IdeSimpleBuildTarget *self,
+                                  const gchar          *name)
+{
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SIMPLE_BUILD_TARGET (self));
+
+  if (g_strcmp0 (priv->name, name) != 0)
+    {
+      g_free (priv->name);
+      priv->name = g_strdup (name);
+    }
+}
+
+void
+ide_simple_build_target_set_priority (IdeSimpleBuildTarget *self,
+                                      gint                  priority)
+{
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SIMPLE_BUILD_TARGET (self));
+
+  priv->priority = priority;
+}
+
+void
+ide_simple_build_target_set_argv (IdeSimpleBuildTarget *self,
+                                  const gchar * const  *argv)
+{
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SIMPLE_BUILD_TARGET (self));
+
+  if (priv->argv != (gchar **)argv)
+    {
+      g_strfreev (priv->argv);
+      priv->argv = g_strdupv ((gchar **)argv);
+    }
+}
+
+void
+ide_simple_build_target_set_cwd (IdeSimpleBuildTarget *self,
+                                 const gchar          *cwd)
+{
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SIMPLE_BUILD_TARGET (self));
+
+  if (g_strcmp0 (priv->cwd, cwd) != 0)
+    {
+      g_free (priv->cwd);
+      priv->cwd = g_strdup (cwd);
+    }
+}
+
+void
+ide_simple_build_target_set_language (IdeSimpleBuildTarget *self,
+                                      const gchar          *language)
+{
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SIMPLE_BUILD_TARGET (self));
+
+  if (g_strcmp0 (priv->language, language) != 0)
+    {
+      g_free (priv->language);
+      priv->language = g_strdup (language);
+    }
+}
+
+static GFile *
+get_install_directory (IdeBuildTarget *target)
+{
+  IdeSimpleBuildTarget *self = IDE_SIMPLE_BUILD_TARGET (target);
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+  return priv->install_directory ? g_object_ref (priv->install_directory) : NULL;
+}
+
+static gchar *
+get_name (IdeBuildTarget *target)
+{
+  IdeSimpleBuildTarget *self = IDE_SIMPLE_BUILD_TARGET (target);
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+  return g_strdup (priv->name);
+}
+
+static gchar **
+get_argv (IdeBuildTarget *target)
+{
+  IdeSimpleBuildTarget *self = IDE_SIMPLE_BUILD_TARGET (target);
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+  return g_strdupv (priv->argv);
+}
+
+static gchar *
+get_cwd (IdeBuildTarget *target)
+{
+  IdeSimpleBuildTarget *self = IDE_SIMPLE_BUILD_TARGET (target);
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+  return g_strdup (priv->cwd);
+}
+
+static gchar *
+get_language (IdeBuildTarget *target)
+{
+  IdeSimpleBuildTarget *self = IDE_SIMPLE_BUILD_TARGET (target);
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+  return g_strdup (priv->language);
+}
+
+static gint
+get_priority (IdeBuildTarget *target)
+{
+  IdeSimpleBuildTarget *self = IDE_SIMPLE_BUILD_TARGET (target);
+  IdeSimpleBuildTargetPrivate *priv = ide_simple_build_target_get_instance_private (self);
+  return priv->priority;
+}
+
+static void
+build_target_iface_init (IdeBuildTargetInterface *iface)
+{
+  iface->get_install_directory = get_install_directory;
+  iface->get_name = get_name;
+  iface->get_priority = get_priority;
+  iface->get_argv = get_argv;
+  iface->get_cwd = get_cwd;
+  iface->get_language = get_language;
+}
diff --git a/src/libide/foundry/ide-simple-build-target.h b/src/libide/foundry/ide-simple-build-target.h
new file mode 100644
index 000000000..67a65727d
--- /dev/null
+++ b/src/libide/foundry/ide-simple-build-target.h
@@ -0,0 +1,67 @@
+/* ide-simple-build-target.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SIMPLE_BUILD_TARGET (ide_simple_build_target_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeSimpleBuildTarget, ide_simple_build_target, IDE, SIMPLE_BUILD_TARGET, IdeObject)
+
+struct _IdeSimpleBuildTargetClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeSimpleBuildTarget *ide_simple_build_target_new                   (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void                  ide_simple_build_target_set_install_directory (IdeSimpleBuildTarget *self,
+                                                                     GFile                
*install_directory);
+IDE_AVAILABLE_IN_3_32
+void                  ide_simple_build_target_set_name              (IdeSimpleBuildTarget *self,
+                                                                     const gchar          *name);
+IDE_AVAILABLE_IN_3_32
+void                  ide_simple_build_target_set_priority          (IdeSimpleBuildTarget *self,
+                                                                     gint                  priority);
+IDE_AVAILABLE_IN_3_32
+void                  ide_simple_build_target_set_argv              (IdeSimpleBuildTarget *self,
+                                                                     const gchar * const  *argv);
+IDE_AVAILABLE_IN_3_32
+void                  ide_simple_build_target_set_cwd               (IdeSimpleBuildTarget *self,
+                                                                     const gchar          *cwd);
+IDE_AVAILABLE_IN_3_32
+void                  ide_simple_build_target_set_language          (IdeSimpleBuildTarget *self,
+                                                                     const gchar          *language);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-simple-toolchain.c b/src/libide/foundry/ide-simple-toolchain.c
new file mode 100644
index 000000000..b68389840
--- /dev/null
+++ b/src/libide/foundry/ide-simple-toolchain.c
@@ -0,0 +1,168 @@
+/* ide-simple-toolchain.c
+ *
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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/>.
+ *
+ * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-simple-toolchain"
+
+#include "config.h"
+
+#include "ide-simple-toolchain.h"
+
+typedef struct
+{
+  GHashTable *tools;
+} IdeSimpleToolchainPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeSimpleToolchain, ide_simple_toolchain, IDE_TYPE_TOOLCHAIN)
+
+IdeSimpleToolchain *
+ide_simple_toolchain_new (const gchar  *id,
+                          const gchar  *display_name)
+{
+  g_return_val_if_fail (id != NULL, NULL);
+
+  return g_object_new (IDE_TYPE_SIMPLE_TOOLCHAIN,
+                       "id", id,
+                       "display-name", display_name,
+                       NULL);
+}
+
+typedef struct
+{
+  GHashTable  *found_tools;
+  const gchar *tool_id;
+} SimpleToolchainToolFind;
+
+static void
+tools_find_all_id (gpointer key,
+                   gpointer value,
+                   gpointer user_data)
+{
+  const gchar *tool_key = key;
+  const gchar *tool_path = value;
+  SimpleToolchainToolFind *tool_find = user_data;
+  g_auto(GStrv) tool_parts = NULL;
+
+  g_assert (tool_key != NULL);
+  g_assert (tool_find != NULL);
+
+  tool_parts = g_strsplit (tool_key, ":", 2);
+
+  g_return_if_fail (tool_parts != NULL);
+
+  if (g_strcmp0 (tool_parts[0], tool_find->tool_id) == 0)
+    g_hash_table_insert (tool_find->found_tools, g_strdup (tool_parts[1]), g_strdup (tool_path));
+}
+
+static GHashTable *
+ide_simple_toolchain_get_tools_for_id (IdeToolchain  *toolchain,
+                                       const gchar   *tool_id)
+{
+  IdeSimpleToolchain *self = (IdeSimpleToolchain *)toolchain;
+  IdeSimpleToolchainPrivate *priv;
+  SimpleToolchainToolFind tool_find;
+  g_autoptr(GHashTable) found_tools = NULL;
+
+  g_return_val_if_fail (IDE_IS_SIMPLE_TOOLCHAIN (self), NULL);
+  g_return_val_if_fail (tool_id != NULL, NULL);
+
+  priv = ide_simple_toolchain_get_instance_private (self);
+  found_tools = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+  tool_find.found_tools = found_tools;
+  tool_find.tool_id = tool_id;
+
+  g_hash_table_foreach (priv->tools, tools_find_all_id, &tool_find);
+
+  return g_steal_pointer (&found_tools);
+}
+
+static const gchar *
+ide_simple_toolchain_get_tool_for_language (IdeToolchain  *toolchain,
+                                            const gchar   *language,
+                                            const gchar   *tool_id)
+{
+  IdeSimpleToolchain *self = (IdeSimpleToolchain *)toolchain;
+  IdeSimpleToolchainPrivate *priv = ide_simple_toolchain_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SIMPLE_TOOLCHAIN (self), NULL);
+  g_return_val_if_fail (tool_id != NULL, NULL);
+
+  return g_hash_table_lookup (priv->tools, g_strconcat (tool_id, ":", language, NULL));
+}
+
+/**
+ * ide_simple_toolchain_set_tool_for_language:
+ * @self: an #IdeSimpleToolchain
+ * @tool_id: the identifier of the tool like %IDE_TOOLCHAIN_TOOL_CC
+ * @language: the language of the tool like %IDE_TOOLCHAIN_LANGUAGE_C.
+ * @tool_path: The path of
+ *
+ * Gets the path of the compiler executable
+ *
+ * Since: 3.32
+ */
+void
+ide_simple_toolchain_set_tool_for_language  (IdeSimpleToolchain  *self,
+                                             const gchar         *language,
+                                             const gchar         *tool_id,
+                                             const gchar         *tool_path)
+{
+  IdeSimpleToolchainPrivate *priv = ide_simple_toolchain_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SIMPLE_TOOLCHAIN (self));
+  g_return_if_fail (tool_id != NULL);
+
+  g_hash_table_insert (priv->tools,
+                       g_strconcat (tool_id, ":", language, NULL),
+                       g_strdup (tool_path));
+}
+
+static void
+ide_simple_toolchain_finalize (GObject *object)
+{
+  IdeSimpleToolchain *self = (IdeSimpleToolchain *)object;
+  IdeSimpleToolchainPrivate *priv = ide_simple_toolchain_get_instance_private (self);
+
+  g_clear_pointer (&priv->tools, g_hash_table_unref);
+
+  G_OBJECT_CLASS (ide_simple_toolchain_parent_class)->finalize (object);
+}
+
+static void
+ide_simple_toolchain_class_init (IdeSimpleToolchainClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeToolchainClass *toolchain_class = IDE_TOOLCHAIN_CLASS (klass);
+
+  object_class->finalize = ide_simple_toolchain_finalize;
+
+  toolchain_class->get_tool_for_language = ide_simple_toolchain_get_tool_for_language;
+  toolchain_class->get_tools_for_id = ide_simple_toolchain_get_tools_for_id;
+}
+
+static void
+ide_simple_toolchain_init (IdeSimpleToolchain *self)
+{
+  IdeSimpleToolchainPrivate *priv = ide_simple_toolchain_get_instance_private (self);
+
+  priv->tools = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+}
diff --git a/src/libide/foundry/ide-simple-toolchain.h b/src/libide/foundry/ide-simple-toolchain.h
new file mode 100644
index 000000000..8d0eac82f
--- /dev/null
+++ b/src/libide/foundry/ide-simple-toolchain.h
@@ -0,0 +1,57 @@
+/* ide-simple-toolchain.h
+ *
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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/>.
+ *
+ * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-toolchain.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SIMPLE_TOOLCHAIN (ide_simple_toolchain_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeSimpleToolchain, ide_simple_toolchain, IDE, SIMPLE_TOOLCHAIN, IdeToolchain)
+
+struct _IdeSimpleToolchainClass
+{
+  IdeToolchainClass parent;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeSimpleToolchain  *ide_simple_toolchain_new                    (const gchar         *id,
+                                                                  const gchar         *display_name);
+IDE_AVAILABLE_IN_3_32
+void                 ide_simple_toolchain_set_tool_for_language  (IdeSimpleToolchain  *self,
+                                                                  const gchar         *language,
+                                                                  const gchar         *tool_id,
+                                                                  const gchar         *tool_path);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-test-manager.c b/src/libide/foundry/ide-test-manager.c
new file mode 100644
index 000000000..acd79b155
--- /dev/null
+++ b/src/libide/foundry/ide-test-manager.c
@@ -0,0 +1,1021 @@
+/* ide-test-manager.c
+ *
+ * Copyright 2017-2019 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 "ide-test-manager"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+
+#include "ide-build-manager.h"
+#include "ide-build-pipeline.h"
+#include "ide-foundry-compat.h"
+#include "ide-test-manager.h"
+#include "ide-test-private.h"
+#include "ide-test-provider.h"
+
+#define MAX_UNIT_TESTS 4
+
+/**
+ * SECTION:ide-test-manager
+ * @title: IdeTestManager
+ * @short_description: Unit test discover and execution manager
+ *
+ * The #IdeTestManager is responsible for loading unit test provider
+ * plugins (via the #IdeTestProvider interface) and running those unit
+ * tests on behalf of the user.
+ *
+ * You can access the test manager using ide_context_get_text_manager()
+ * using the #IdeContext for the loaded project.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeTestManager
+{
+  IdeObject         parent_instance;
+
+  PeasExtensionSet *providers;
+  GPtrArray        *tests_by_provider;
+  GtkTreeStore     *tests_store;
+};
+
+typedef struct
+{
+  IdeTestProvider *provider;
+  GPtrArray       *tests;
+} TestsByProvider;
+
+typedef struct
+{
+  GQueue queue;
+  guint  n_active;
+} RunAllTaskData;
+
+enum {
+  PROP_0,
+  PROP_LOADING,
+  N_PROPS
+};
+
+static void initable_iface_init              (GInitableIface *iface);
+static void ide_test_manager_actions_run_all (IdeTestManager *self,
+                                              GVariant       *param);
+static void ide_test_manager_actions_reload  (IdeTestManager *self,
+                                              GVariant       *param);
+
+DZL_DEFINE_ACTION_GROUP (IdeTestManager, ide_test_manager, {
+  { "run-all", ide_test_manager_actions_run_all },
+  { "reload-tests", ide_test_manager_actions_reload },
+})
+
+G_DEFINE_TYPE_WITH_CODE (IdeTestManager, ide_test_manager, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
+                                                ide_test_manager_init_action_group))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+tests_by_provider_free (gpointer data)
+{
+  TestsByProvider *info = data;
+
+  g_clear_pointer (&info->tests, g_ptr_array_unref);
+  g_clear_object (&info->provider);
+  g_slice_free (TestsByProvider, info);
+}
+
+static void
+ide_test_manager_dispose (GObject *object)
+{
+  IdeTestManager *self = (IdeTestManager *)object;
+
+  if (self->tests_store != NULL)
+    {
+      gtk_tree_store_clear (self->tests_store);
+      g_clear_object (&self->tests_store);
+    }
+
+  g_clear_object (&self->providers);
+  g_clear_pointer (&self->tests_by_provider, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (ide_test_manager_parent_class)->dispose (object);
+}
+
+static void
+ide_test_manager_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  IdeTestManager *self = IDE_TEST_MANAGER (object);
+
+  switch (prop_id)
+    {
+    case PROP_LOADING:
+      g_value_set_boolean (value, ide_test_manager_get_loading (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_test_manager_class_init (IdeTestManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_test_manager_dispose;
+  object_class->get_property = ide_test_manager_get_property;
+
+  /**
+   * IdeTestManager:loading:
+   *
+   * The "loading" property denotes if a test provider is busy loading
+   * tests in the background.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_LOADING] =
+    g_param_spec_boolean ("loading",
+                          "Loading",
+                          "If a test provider is loading tests",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_test_manager_init (IdeTestManager *self)
+{
+  self->tests_by_provider = g_ptr_array_new_with_free_func (tests_by_provider_free);
+  self->tests_store = gtk_tree_store_new (2, G_TYPE_STRING, IDE_TYPE_TEST);
+}
+
+static void
+ide_test_manager_locate_group (IdeTestManager *self,
+                               GtkTreeIter    *iter,
+                               const gchar    *group)
+{
+  g_assert (IDE_IS_TEST_MANAGER (self));
+  g_assert (iter != NULL);
+
+  if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->tests_store), iter))
+    {
+      do
+        {
+          g_autofree gchar *row_group = NULL;
+
+          gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), iter,
+                              IDE_TEST_COLUMN_GROUP, &row_group,
+                              -1);
+
+          if (ide_str_equal0 (row_group, group))
+            return;
+        }
+      while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), iter));
+    }
+
+  /* TODO: Sort groups by name? */
+
+  gtk_tree_store_append (self->tests_store, iter, NULL);
+  gtk_tree_store_set (self->tests_store, iter,
+                      IDE_TEST_COLUMN_GROUP, group,
+                      -1);
+}
+
+static void
+ide_test_manager_test_notify_status (IdeTestManager *self,
+                                     GParamSpec     *pspec,
+                                     IdeTest        *test)
+{
+  const gchar *group;
+  GtkTreeIter parent;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_TEST_MANAGER (self));
+  g_assert (IDE_IS_TEST (test));
+
+  group = ide_test_get_group (test);
+
+  ide_test_manager_locate_group (self, &parent, group);
+
+  if (gtk_tree_model_iter_children (GTK_TREE_MODEL (self->tests_store), &iter, &parent))
+    {
+      do
+        {
+          g_autoptr(IdeTest) row_test = NULL;
+
+          gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), &iter,
+                              IDE_TEST_COLUMN_TEST, &row_test,
+                              -1);
+
+          if (row_test == test)
+            {
+              GtkTreePath *path;
+
+              path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->tests_store), &iter);
+              gtk_tree_model_row_changed (GTK_TREE_MODEL (self->tests_store), path, &iter);
+              gtk_tree_path_free (path);
+
+              break;
+            }
+        }
+      while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), &iter));
+    }
+}
+
+static void
+ide_test_manager_add_test (IdeTestManager        *self,
+                           const TestsByProvider *info,
+                           guint                  position,
+                           IdeTest               *test)
+{
+  const gchar *group;
+  GtkTreeIter iter;
+  GtkTreeIter parent;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TEST_MANAGER (self));
+  g_assert (info != NULL);
+  g_assert (IDE_IS_TEST (test));
+
+  g_ptr_array_insert (info->tests, position, g_object_ref (test));
+
+  group = ide_test_get_group (test);
+
+  ide_test_manager_locate_group (self, &parent, group);
+  gtk_tree_store_append (self->tests_store, &iter, &parent);
+  gtk_tree_store_set (self->tests_store, &iter,
+                      IDE_TEST_COLUMN_GROUP, NULL,
+                      IDE_TEST_COLUMN_TEST, test,
+                      -1);
+
+  g_signal_connect_object (test,
+                           "notify::status",
+                           G_CALLBACK (ide_test_manager_test_notify_status),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  IDE_EXIT;
+}
+
+static void
+ide_test_manager_remove_test (IdeTestManager        *self,
+                              const TestsByProvider *info,
+                              IdeTest               *test)
+{
+  const gchar *group;
+  GtkTreeIter iter;
+  GtkTreeIter parent;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TEST_MANAGER (self));
+  g_assert (info != NULL);
+  g_assert (IDE_IS_TEST (test));
+
+  group = ide_test_get_group (test);
+
+  ide_test_manager_locate_group (self, &parent, group);
+
+  if (gtk_tree_model_iter_children (GTK_TREE_MODEL (self->tests_store), &iter, &parent))
+    {
+      do
+        {
+          g_autoptr(IdeTest) row = NULL;
+
+          gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), &iter,
+                              IDE_TEST_COLUMN_TEST, &row,
+                              -1);
+
+          if (row == test)
+            {
+              g_signal_handlers_disconnect_by_func (test,
+                                                    G_CALLBACK (ide_test_manager_test_notify_status),
+                                                    self);
+              gtk_tree_store_remove (self->tests_store, &iter);
+              break;
+            }
+        }
+      while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), &iter));
+    }
+
+  g_ptr_array_remove (info->tests, test);
+
+  IDE_EXIT;
+}
+
+static void
+ide_test_manager_provider_items_changed (IdeTestManager  *self,
+                                         guint            position,
+                                         guint            removed,
+                                         guint            added,
+                                         IdeTestProvider *provider)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TEST_MANAGER (self));
+  g_assert (IDE_IS_TEST_PROVIDER (provider));
+
+  for (guint i = 0; i < self->tests_by_provider->len; i++)
+    {
+      const TestsByProvider *info = g_ptr_array_index (self->tests_by_provider, i);
+
+      if (info->provider == provider)
+        {
+          /* Remove tests from cache that were deleted */
+          for (guint j = 0; j < removed; j++)
+            {
+              IdeTest *test = g_ptr_array_index (info->tests, position);
+
+              g_assert (IDE_IS_TEST (test));
+              ide_test_manager_remove_test (self, info, test);
+            }
+
+          /* Add tests to cache that were added */
+          for (guint j = 0; j < added; j++)
+            {
+              g_autoptr(IdeTest) test = NULL;
+
+              test = g_list_model_get_item (G_LIST_MODEL (provider), position + j);
+              g_assert (IDE_IS_TEST (test));
+              ide_test_manager_add_test (self, info, position + j, test);
+            }
+        }
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_test_manager_provider_notify_loading (IdeTestManager  *self,
+                                          GParamSpec      *pspec,
+                                          IdeTestProvider *provider)
+{
+  g_assert (IDE_IS_TEST_MANAGER (self));
+  g_assert (IDE_IS_TEST_PROVIDER (provider));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LOADING]);
+}
+
+static void
+ide_test_manager_provider_added (PeasExtensionSet *set,
+                                 PeasPluginInfo   *plugin_info,
+                                 PeasExtension    *exten,
+                                 gpointer          user_data)
+{
+  IdeTestManager *self = user_data;
+  IdeTestProvider *provider = (IdeTestProvider *)exten;
+  TestsByProvider *tests;
+  guint len;
+
+  IDE_ENTRY;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TEST_PROVIDER (provider));
+  g_assert (G_IS_LIST_MODEL (provider));
+  g_assert (IDE_IS_TEST_MANAGER (self));
+
+  tests = g_slice_new0 (TestsByProvider);
+  tests->provider = g_object_ref (provider);
+  tests->tests = g_ptr_array_new_with_free_func (g_object_unref);
+  g_ptr_array_add (self->tests_by_provider, tests);
+
+  g_signal_connect_swapped (provider,
+                            "items-changed",
+                            G_CALLBACK (ide_test_manager_provider_items_changed),
+                            self);
+  g_signal_connect_swapped (provider,
+                            "notify::loading",
+                            G_CALLBACK (ide_test_manager_provider_notify_loading),
+                            self);
+
+  len = g_list_model_get_n_items (G_LIST_MODEL (provider));
+  ide_test_manager_provider_items_changed (self, 0, 0, len, provider);
+
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (provider));
+
+  IDE_EXIT;
+}
+
+static void
+ide_test_manager_provider_removed (PeasExtensionSet *set,
+                                   PeasPluginInfo   *plugin_info,
+                                   PeasExtension    *exten,
+                                   gpointer          user_data)
+{
+  IdeTestManager *self = user_data;
+  IdeTestProvider *provider = (IdeTestProvider *)exten;
+
+  IDE_ENTRY;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TEST_PROVIDER (provider));
+  g_assert (IDE_IS_TEST_MANAGER (self));
+
+  for (guint i = 0; i < self->tests_by_provider->len; i++)
+    {
+      const TestsByProvider *info = g_ptr_array_index (self->tests_by_provider, i);
+
+      if (info->provider == provider)
+        {
+          g_ptr_array_remove_index (self->tests_by_provider, i);
+          break;
+        }
+    }
+
+  g_signal_handlers_disconnect_by_func (provider,
+                                        G_CALLBACK (ide_test_manager_provider_items_changed),
+                                        self);
+  g_signal_handlers_disconnect_by_func (provider,
+                                        G_CALLBACK (ide_test_manager_provider_notify_loading),
+                                        self);
+
+  ide_object_destroy (IDE_OBJECT (provider));
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_test_manager_initiable_init (GInitable     *initable,
+                                 GCancellable  *cancellable,
+                                 GError       **error)
+{
+  IdeTestManager *self = (IdeTestManager *)initable;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TEST_MANAGER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  self->providers = peas_extension_set_new (peas_engine_get_default (),
+                                            IDE_TYPE_TEST_PROVIDER,
+                                            NULL);
+
+  g_signal_connect (self->providers,
+                    "extension-added",
+                    G_CALLBACK (ide_test_manager_provider_added),
+                    self);
+
+  g_signal_connect (self->providers,
+                    "extension-removed",
+                    G_CALLBACK (ide_test_manager_provider_removed),
+                    self);
+
+  peas_extension_set_foreach (self->providers,
+                              ide_test_manager_provider_added,
+                              self);
+
+  IDE_RETURN (TRUE);
+}
+
+static void
+initable_iface_init (GInitableIface *iface)
+{
+  iface->init = ide_test_manager_initiable_init;
+}
+
+static void
+ide_test_manager_run_all_cb (GObject      *object,
+                             GAsyncResult *result,
+                             gpointer      user_data)
+{
+  IdeTestManager *self = (IdeTestManager *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTest) test = NULL;
+  RunAllTaskData *task_data;
+  GCancellable *cancellable;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TEST_MANAGER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  cancellable = g_task_get_cancellable (task);
+  task_data = g_task_get_task_data (task);
+  g_assert (task_data != NULL);
+  g_assert (task_data->n_active > 0);
+
+  if (!ide_test_manager_run_finish (self, result, &error))
+    g_message ("%s", error->message);
+
+  test = g_queue_pop_head (&task_data->queue);
+
+  if (test != NULL)
+    {
+      task_data->n_active++;
+      ide_test_manager_run_async (self,
+                                  test,
+                                  cancellable,
+                                  ide_test_manager_run_all_cb,
+                                  g_object_ref (task));
+    }
+
+  task_data->n_active--;
+
+  if (task_data->n_active == 0)
+    g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_test_manager_run_all_async:
+ * @self: An #IdeTestManager
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * Executes all tests in an undefined order.
+ *
+ * Upon completion, @callback will be executed which must call
+ * ide_test_manager_run_all_finish() to get the result.
+ *
+ * Note that the individual test result information will be attached
+ * to the specific #IdeTest instances.
+ *
+ * Since: 3.32
+ */
+void
+ide_test_manager_run_all_async (IdeTestManager      *self,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  RunAllTaskData *task_data;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_TEST_MANAGER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_priority (task, G_PRIORITY_LOW);
+  g_task_set_source_tag (task, ide_test_manager_run_all_async);
+
+  task_data = g_new0 (RunAllTaskData, 1);
+  g_task_set_task_data (task, task_data, g_free);
+
+  for (guint i = 0; i < self->tests_by_provider->len; i++)
+    {
+      TestsByProvider *info = g_ptr_array_index (self->tests_by_provider, i);
+
+      for (guint j = 0; j < info->tests->len; j++)
+        {
+          IdeTest *test = g_ptr_array_index (info->tests, j);
+
+          g_queue_push_tail (&task_data->queue, g_object_ref (test));
+        }
+    }
+
+  task_data->n_active = MIN (MAX_UNIT_TESTS, task_data->queue.length);
+
+  if (task_data->n_active == 0)
+    {
+      g_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  for (guint i = 0; i < MAX_UNIT_TESTS; i++)
+    {
+      g_autoptr(IdeTest) test = g_queue_pop_head (&task_data->queue);
+
+      if (test == NULL)
+        break;
+
+      ide_test_manager_run_async (self,
+                                  test,
+                                  cancellable,
+                                  ide_test_manager_run_all_cb,
+                                  g_object_ref (task));
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_test_manager_run_all_finish:
+ * @self: An #IdeTestManager
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to execute all unit tests.
+ *
+ * A return value of %TRUE does not indicate that all tests succeeded,
+ * only that all tests were executed. Individual test failures will be
+ * attached to the #IdeTest instances.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_test_manager_run_all_finish (IdeTestManager  *self,
+                                 GAsyncResult    *result,
+                                 GError         **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_test_manager_run_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  IdeTestProvider *provider = (IdeTestProvider *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TEST_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_TASK (task));
+
+  if (!ide_test_provider_run_finish (provider, result, &error))
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_test_manager_run_async:
+ * @self: An #IdeTestManager
+ * @test: An #IdeTest
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * Executes a single unit test, asynchronously.
+ *
+ * The caller can access the result of the operation from @callback
+ * by calling ide_test_manager_run_finish() with the provided result.
+ *
+ * Since: 3.32
+ */
+void
+ide_test_manager_run_async (IdeTestManager      *self,
+                            IdeTest             *test,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  IdeBuildPipeline *pipeline;
+  IdeTestProvider *provider;
+  IdeBuildManager *build_manager;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_TEST_MANAGER (self));
+  g_return_if_fail (IDE_IS_TEST (test));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_priority (task, G_PRIORITY_LOW);
+  g_task_set_source_tag (task, ide_test_manager_run_async);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  build_manager = ide_build_manager_from_context (context);
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+
+  if (pipeline == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "Pipeline is not ready, cannot run test");
+      IDE_EXIT;
+    }
+
+  provider = _ide_test_get_provider (test);
+
+  ide_test_provider_run_async (provider,
+                               test,
+                               pipeline,
+                               cancellable,
+                               ide_test_manager_run_cb,
+                               g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_test_manager_run_finish:
+ * @self: An #IdeTestManager
+ * @result: The #GAsyncResult provided to callback
+ * @error: A location for a #GError, or %NULL
+ *
+ * Completes a request to ide_test_manager_run_finish().
+ *
+ * When this function returns %TRUE, it does not indicate that the test
+ * succeeded; only that the test was executed. Thest #IdeTest instance
+ * itself will contain information about the success of the test.
+ *
+ * Returns: %TRUE if the test was executed; otherwise %FALSE
+ *   and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_test_manager_run_finish (IdeTestManager  *self,
+                             GAsyncResult    *result,
+                             GError         **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_test_manager_actions_run_all (IdeTestManager *self,
+                                  GVariant       *param)
+{
+  g_assert (IDE_IS_TEST_MANAGER (self));
+
+  ide_test_manager_run_all_async (self, NULL, NULL, NULL);
+}
+
+static void
+ide_test_manager_actions_reload (IdeTestManager *self,
+                                 GVariant       *param)
+{
+  g_assert (IDE_IS_TEST_MANAGER (self));
+
+  gtk_tree_store_clear (self->tests_store);
+
+  for (guint i = 0; i < self->tests_by_provider->len; i++)
+    {
+      const TestsByProvider *info = g_ptr_array_index (self->tests_by_provider, i);
+
+      ide_test_provider_reload (info->provider);
+    }
+}
+
+GtkTreeModel *
+_ide_test_manager_get_model (IdeTestManager *self)
+{
+  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), NULL);
+
+  return GTK_TREE_MODEL (self->tests_store);
+}
+
+static void
+ide_test_manager_get_loading_cb (PeasExtensionSet *set,
+                                 PeasPluginInfo   *plugin_info,
+                                 PeasExtension    *exten,
+                                 gpointer          user_data)
+{
+  IdeTestProvider *provider = (IdeTestProvider *)exten;
+  gboolean *loading = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TEST_PROVIDER (provider));
+  g_assert (loading != NULL);
+
+  *loading |= ide_test_provider_get_loading (provider);
+}
+
+gboolean
+ide_test_manager_get_loading (IdeTestManager *self)
+{
+  gboolean loading = FALSE;
+
+  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), FALSE);
+
+  peas_extension_set_foreach (self->providers,
+                              ide_test_manager_get_loading_cb,
+                              &loading);
+
+  return loading;
+}
+
+/**
+ * ide_test_manager_get_tests:
+ * @self: a #IdeTestManager
+ * @path: (nullable): the path to the test or %NULL for the root path
+ *
+ * Locates and returns any #IdeTest that is found as a direct child
+ * of @path.
+ *
+ * Returns: (transfer full) (element-type IdeTest): an array of #IdeTest
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_test_manager_get_tests (IdeTestManager *self,
+                            const gchar    *path)
+{
+  GPtrArray *ret;
+  GtkTreeIter iter;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), NULL);
+
+  ret = g_ptr_array_new ();
+
+  if (path == NULL)
+    {
+      if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->tests_store), &iter))
+        goto failure;
+    }
+  else
+    {
+      GtkTreeIter parent;
+
+      ide_test_manager_locate_group (self, &parent, path);
+
+      if (!gtk_tree_model_iter_children (GTK_TREE_MODEL (self->tests_store), &iter, &parent))
+        goto failure;
+    }
+
+  do
+    {
+      IdeTest *test = NULL;
+
+      gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), &iter,
+                          IDE_TEST_COLUMN_TEST, &test,
+                          -1);
+      if (test != NULL)
+        g_ptr_array_add (ret, g_steal_pointer (&test));
+    }
+  while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), &iter));
+
+failure:
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_test_manager_get_folders:
+ * @self: a #IdeTestManager
+ * @path: (nullable): the path to the test or %NULL for the root path
+ *
+ * Gets the sub-paths of @path that are not individual tests.
+ *
+ * Returns: (transfer full) (array zero-terminated=1): an array of strings
+ *   describing available sub-paths to @path.
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_test_manager_get_folders (IdeTestManager *self,
+                              const gchar    *path)
+{
+  static const gchar *empty[] = { NULL };
+  GPtrArray *ret;
+  GtkTreeIter iter;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), NULL);
+
+  ret = g_ptr_array_new ();
+
+  if (path == NULL)
+    {
+      if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->tests_store), &iter))
+        return g_strdupv ((gchar **)empty);
+    }
+  else
+    {
+      GtkTreeIter parent;
+
+      ide_test_manager_locate_group (self, &parent, path);
+
+      if (!gtk_tree_model_iter_children (GTK_TREE_MODEL (self->tests_store), &iter, &parent))
+        return g_strdupv ((gchar **)empty);
+    }
+
+  do
+    {
+      gchar *group = NULL;
+
+      gtk_tree_model_get (GTK_TREE_MODEL (self->tests_store), &iter,
+                          IDE_TEST_COLUMN_GROUP, &group,
+                          -1);
+      if (group != NULL)
+        g_ptr_array_add (ret, g_steal_pointer (&group));
+    }
+  while (gtk_tree_model_iter_next (GTK_TREE_MODEL (self->tests_store), &iter));
+
+  g_ptr_array_add (ret, NULL);
+
+  return (gchar **)g_ptr_array_free (ret, FALSE);
+}
+
+static void
+ide_test_manager_ensure_loaded_cb (IdeTestManager *self,
+                                   GParamSpec     *pspec,
+                                   IdeTask        *task)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TEST_MANAGER (self));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_test_manager_get_loading (self))
+    {
+      g_signal_handlers_disconnect_by_func (self,
+                                            G_CALLBACK (ide_test_manager_ensure_loaded_cb),
+                                            task);
+      ide_task_return_boolean (task, TRUE);
+    }
+}
+
+/**
+ * ide_test_manager_ensure_loaded_async:
+ * @self: a #IdeTestManager
+ *
+ * Calls @callback after the test manager has loaded tests.
+ *
+ * If the test manager has already loaded tests, then @callback will
+ * be called after returning to the main loop.
+ *
+ * Since: 3.32
+ */
+void
+ide_test_manager_ensure_loaded_async (IdeTestManager      *self,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_TEST_MANAGER (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_test_manager_ensure_loaded_async);
+
+  if (ide_test_manager_get_loading (self))
+    {
+      g_signal_connect_data (self,
+                             "notify::loading",
+                             G_CALLBACK (ide_test_manager_ensure_loaded_cb),
+                             g_steal_pointer (&task),
+                             (GClosureNotify)g_object_unref,
+                             0);
+      return;
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+gboolean
+ide_test_manager_ensure_loaded_finish (IdeTestManager  *self,
+                                       GAsyncResult    *result,
+                                       GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TEST_MANAGER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
diff --git a/src/libide/foundry/ide-test-manager.h b/src/libide/foundry/ide-test-manager.h
new file mode 100644
index 000000000..5336cc758
--- /dev/null
+++ b/src/libide/foundry/ide-test-manager.h
@@ -0,0 +1,77 @@
+/* ide-test-manager.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEST_MANAGER (ide_test_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTestManager, ide_test_manager, IDE, TEST_MANAGER, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeTestManager          *ide_test_manager_from_context          (IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_test_manager_get_loading          (IdeTestManager       *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_test_manager_run_async            (IdeTestManager       *self,
+                                                   IdeTest              *test,
+                                                   GCancellable         *cancellable,
+                                                   GAsyncReadyCallback   callback,
+                                                   gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_test_manager_run_finish           (IdeTestManager       *self,
+                                                   GAsyncResult         *result,
+                                                   GError              **error);
+IDE_AVAILABLE_IN_3_32
+void        ide_test_manager_run_all_async        (IdeTestManager       *self,
+                                                   GCancellable         *cancellable,
+                                                   GAsyncReadyCallback   callback,
+                                                   gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_test_manager_run_all_finish       (IdeTestManager       *self,
+                                                   GAsyncResult         *result,
+                                                   GError              **error);
+IDE_AVAILABLE_IN_3_32
+GPtrArray  *ide_test_manager_get_tests            (IdeTestManager       *self,
+                                                   const gchar          *path);
+IDE_AVAILABLE_IN_3_32
+gchar     **ide_test_manager_get_folders          (IdeTestManager       *self,
+                                                   const gchar          *path);
+IDE_AVAILABLE_IN_3_32
+void        ide_test_manager_ensure_loaded_async  (IdeTestManager       *self,
+                                                   GCancellable         *cancellable,
+                                                   GAsyncReadyCallback   callback,
+                                                   gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_test_manager_ensure_loaded_finish (IdeTestManager       *self,
+                                                   GAsyncResult         *result,
+                                                   GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-test-private.h b/src/libide/foundry/ide-test-private.h
new file mode 100644
index 000000000..744cc2742
--- /dev/null
+++ b/src/libide/foundry/ide-test-private.h
@@ -0,0 +1,43 @@
+/* ide-test-private.h
+ *
+ * Copyright 2017-2019 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 <gtk/gtk.h>
+
+#include "ide-test.h"
+#include "ide-test-manager.h"
+#include "ide-test-provider.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+  IDE_TEST_COLUMN_GROUP,
+  IDE_TEST_COLUMN_TEST,
+} IdeTestColumn;
+
+GtkTreeModel    *_ide_test_manager_get_model (IdeTestManager  *self);
+void             _ide_test_set_provider      (IdeTest         *self,
+                                                               IdeTestProvider *provider);
+IdeTestProvider *_ide_test_get_provider      (IdeTest         *self);
+
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-test-provider.c b/src/libide/foundry/ide-test-provider.c
new file mode 100644
index 000000000..0fe462c00
--- /dev/null
+++ b/src/libide/foundry/ide-test-provider.c
@@ -0,0 +1,340 @@
+/* ide-test-provider.c
+ *
+ * Copyright 2017-2019 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 "ide-test-provider"
+
+#include "config.h"
+
+#include "ide-build-pipeline.h"
+#include "ide-test-provider.h"
+#include "ide-test-private.h"
+
+typedef struct
+{
+  GPtrArray *items;
+  guint loading : 1;
+} IdeTestProviderPrivate;
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdeTestProvider, ide_test_provider, IDE_TYPE_OBJECT,
+                                  G_ADD_PRIVATE (IdeTestProvider)
+                                  G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_LOADING,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_test_provider_real_run_async (IdeTestProvider     *self,
+                                  IdeTest             *test,
+                                  IdeBuildPipeline    *pipeline,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_test_provider_real_run_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "%s is missing test runner implementation",
+                           G_OBJECT_TYPE_NAME (self));
+}
+
+static gboolean
+ide_test_provider_real_run_finish (IdeTestProvider  *self,
+                                   GAsyncResult     *result,
+                                   GError          **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_test_provider_dispose (GObject *object)
+{
+  IdeTestProvider *self = (IdeTestProvider *)object;
+  IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+  if (priv->items != NULL && priv->items->len > 0)
+    {
+      guint len = priv->items->len;
+
+      g_ptr_array_remove_range (priv->items, 0, len);
+      g_list_model_items_changed (G_LIST_MODEL (self), 0, len, 0);
+    }
+
+  g_clear_pointer (&priv->items, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (ide_test_provider_parent_class)->dispose (object);
+}
+
+static void
+ide_test_provider_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeTestProvider *self = IDE_TEST_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    case PROP_LOADING:
+      g_value_set_boolean (value, ide_test_provider_get_loading (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_test_provider_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeTestProvider *self = IDE_TEST_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    case PROP_LOADING:
+      ide_test_provider_set_loading (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_test_provider_class_init (IdeTestProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_test_provider_dispose;
+  object_class->get_property = ide_test_provider_get_property;
+  object_class->set_property = ide_test_provider_set_property;
+
+  klass->run_async = ide_test_provider_real_run_async;
+  klass->run_finish = ide_test_provider_real_run_finish;
+
+  properties [PROP_LOADING] =
+    g_param_spec_boolean ("loading",
+                          "Loading",
+                          "If the provider is loading tests",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_test_provider_init (IdeTestProvider *self)
+{
+  IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+  priv->items = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+static GType
+ide_test_provider_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_TEST;
+}
+
+static guint
+ide_test_provider_get_n_items (GListModel *model)
+{
+  IdeTestProvider *self = (IdeTestProvider *)model;
+  IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+  g_assert (IDE_IS_TEST_PROVIDER (self));
+
+  return priv->items ? priv->items->len : 0;
+}
+
+static gpointer
+ide_test_provider_get_item (GListModel *model,
+                            guint       position)
+{
+  IdeTestProvider *self = (IdeTestProvider *)model;
+  IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+  g_assert (IDE_IS_TEST_PROVIDER (self));
+
+  if (priv->items != NULL)
+    {
+      if (position < priv->items->len)
+        return g_object_ref (g_ptr_array_index (priv->items, position));
+    }
+
+  return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item = ide_test_provider_get_item;
+  iface->get_n_items = ide_test_provider_get_n_items;
+  iface->get_item_type = ide_test_provider_get_item_type;
+}
+
+void
+ide_test_provider_add (IdeTestProvider *self,
+                       IdeTest         *test)
+{
+  IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEST_PROVIDER (self));
+  g_return_if_fail (IDE_IS_TEST (test));
+
+  if (priv->items != NULL)
+    {
+      g_ptr_array_add (priv->items, g_object_ref (test));
+      _ide_test_set_provider (test, self);
+      g_list_model_items_changed (G_LIST_MODEL (self), priv->items->len - 1, 0, 1);
+    }
+}
+
+void
+ide_test_provider_remove (IdeTestProvider *self,
+                          IdeTest         *test)
+{
+  IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEST_PROVIDER (self));
+  g_return_if_fail (IDE_IS_TEST (test));
+
+  if (priv->items != NULL)
+    {
+      for (guint i = 0; i < priv->items->len; i++)
+        {
+          IdeTest *element = g_ptr_array_index (priv->items, i);
+
+          if (element == test)
+            {
+              _ide_test_set_provider (test, NULL);
+              g_ptr_array_remove_index (priv->items, i);
+              g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+              break;
+            }
+        }
+    }
+}
+
+void
+ide_test_provider_clear (IdeTestProvider *self)
+{
+  IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+  g_autoptr(GPtrArray) ar = NULL;
+
+  g_return_if_fail (IDE_IS_TEST_PROVIDER (self));
+
+  ar = priv->items;
+  priv->items = g_ptr_array_new_with_free_func (g_object_unref);
+
+  for (guint i = 0; i < ar->len; i++)
+    {
+      IdeTest *test = g_ptr_array_index (ar, i);
+      _ide_test_set_provider (test, NULL);
+    }
+
+  g_list_model_items_changed (G_LIST_MODEL (self), 0, ar->len, 0);
+}
+
+void
+ide_test_provider_run_async (IdeTestProvider     *self,
+                             IdeTest             *test,
+                             IdeBuildPipeline    *pipeline,
+                             GCancellable        *cancellable,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_TEST_PROVIDER (self));
+  g_return_if_fail (IDE_IS_TEST (test));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_TEST_PROVIDER_GET_CLASS (self)->run_async (self,
+                                                 test,
+                                                 pipeline,
+                                                 cancellable,
+                                                 callback,
+                                                 user_data);
+}
+
+gboolean
+ide_test_provider_run_finish (IdeTestProvider  *self,
+                              GAsyncResult     *result,
+                              GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_TEST_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_TEST_PROVIDER_GET_CLASS (self)->run_finish (self, result, error);
+}
+
+gboolean
+ide_test_provider_get_loading (IdeTestProvider *self)
+{
+  IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEST_PROVIDER (self), FALSE);
+
+  return priv->loading;
+}
+
+void
+ide_test_provider_set_loading (IdeTestProvider *self,
+                               gboolean         loading)
+{
+  IdeTestProviderPrivate *priv = ide_test_provider_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEST_PROVIDER (self));
+
+  loading = !!loading;
+
+  if (priv->loading != loading)
+    {
+      priv->loading = loading;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LOADING]);
+    }
+}
+
+/**
+ * ide_test_provider_reload:
+ * @self: a #IdeTestProvider
+ *
+ * Requests the test provider reloads the tests.
+ *
+ * Since: 3.32
+ */
+void
+ide_test_provider_reload (IdeTestProvider *self)
+{
+  g_return_if_fail (IDE_IS_TEST_PROVIDER (self));
+
+  if (IDE_TEST_PROVIDER_GET_CLASS (self)->reload)
+    IDE_TEST_PROVIDER_GET_CLASS (self)->reload (self);
+}
diff --git a/src/libide/foundry/ide-test-provider.h b/src/libide/foundry/ide-test-provider.h
new file mode 100644
index 000000000..cec3eba1f
--- /dev/null
+++ b/src/libide/foundry/ide-test-provider.h
@@ -0,0 +1,84 @@
+/* ide-test-provider.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEST_PROVIDER (ide_test_provider_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTestProvider, ide_test_provider, IDE, TEST_PROVIDER, IdeObject)
+
+struct _IdeTestProviderClass
+{
+  IdeObjectClass parent_class;
+
+  void     (*run_async)  (IdeTestProvider      *self,
+                          IdeTest              *test,
+                          IdeBuildPipeline     *pipeline,
+                          GCancellable         *cancellable,
+                          GAsyncReadyCallback   callback,
+                          gpointer              user_data);
+  gboolean (*run_finish) (IdeTestProvider      *self,
+                          GAsyncResult         *result,
+                          GError              **error);
+  void     (*reload)     (IdeTestProvider      *self);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+gboolean ide_test_provider_get_loading (IdeTestProvider      *self);
+IDE_AVAILABLE_IN_3_32
+void     ide_test_provider_set_loading (IdeTestProvider      *self,
+                                        gboolean              loading);
+IDE_AVAILABLE_IN_3_32
+void     ide_test_provider_clear       (IdeTestProvider      *self);
+IDE_AVAILABLE_IN_3_32
+void     ide_test_provider_add         (IdeTestProvider      *self,
+                                        IdeTest              *test);
+IDE_AVAILABLE_IN_3_32
+void     ide_test_provider_remove      (IdeTestProvider      *self,
+                                        IdeTest              *test);
+IDE_AVAILABLE_IN_3_32
+void     ide_test_provider_run_async   (IdeTestProvider      *self,
+                                        IdeTest              *test,
+                                        IdeBuildPipeline     *pipeline,
+                                        GCancellable         *cancellable,
+                                        GAsyncReadyCallback   callback,
+                                        gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_test_provider_run_finish  (IdeTestProvider      *self,
+                                        GAsyncResult         *result,
+                                        GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_test_provider_reload      (IdeTestProvider      *self);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-test.c b/src/libide/foundry/ide-test.c
new file mode 100644
index 000000000..f2980bbe4
--- /dev/null
+++ b/src/libide/foundry/ide-test.c
@@ -0,0 +1,429 @@
+/* ide-test.c
+ *
+ * Copyright 2017-2019 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 "ide-test"
+
+#include "config.h"
+
+#include "ide-foundry-enums.h"
+
+#include "ide-test.h"
+#include "ide-test-private.h"
+#include "ide-test-provider.h"
+
+typedef struct
+{
+  /* Unowned references */
+  IdeTestProvider *provider;
+
+  /* Owned references */
+  gchar *display_name;
+  gchar *group;
+  gchar *id;
+
+  IdeTestStatus status;
+} IdeTestPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTest, ide_test, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_DISPLAY_NAME,
+  PROP_GROUP,
+  PROP_ID,
+  PROP_STATUS,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+IdeTest *
+ide_test_new (void)
+{
+  return g_object_new (IDE_TYPE_TEST, NULL);
+}
+
+static void
+ide_test_finalize (GObject *object)
+{
+  IdeTest *self = (IdeTest *)object;
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  priv->provider = NULL;
+
+  g_clear_pointer (&priv->group, g_free);
+  g_clear_pointer (&priv->id, g_free);
+  g_clear_pointer (&priv->display_name, g_free);
+
+  G_OBJECT_CLASS (ide_test_parent_class)->finalize (object);
+}
+
+static void
+ide_test_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
+{
+  IdeTest *self = IDE_TEST (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      g_value_set_string (value, ide_test_get_id (self));
+      break;
+
+    case PROP_GROUP:
+      g_value_set_string (value, ide_test_get_group (self));
+      break;
+
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, ide_test_get_display_name (self));
+      break;
+
+    case PROP_STATUS:
+      g_value_set_enum (value, ide_test_get_status (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_test_set_property (GObject      *object,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
+{
+  IdeTest *self = IDE_TEST (object);
+
+  switch (prop_id)
+    {
+    case PROP_GROUP:
+      ide_test_set_group (self, g_value_get_string (value));
+      break;
+
+    case PROP_ID:
+      ide_test_set_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_DISPLAY_NAME:
+      ide_test_set_display_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_STATUS:
+      ide_test_set_status (self, g_value_get_enum (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_test_class_init (IdeTestClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_test_finalize;
+  object_class->get_property = ide_test_get_property;
+  object_class->set_property = ide_test_set_property;
+
+  /**
+   * IdeTest:display_name:
+   *
+   * The "display-name" property contains the display name of the test as
+   * the user is expected to read in UI elements.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_DISPLAY_NAME] =
+    g_param_spec_string ("display-name",
+                         "Name",
+                         "The display_name of the unit test",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTest:id:
+   *
+   * The "id" property contains the unique identifier of the test.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "The unique identifier of the test",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTest:group:
+   *
+   * The "group" property contains the name of the gruop the test belongs
+   * to, if any.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_GROUP] =
+    g_param_spec_string ("group",
+                         "Group",
+                         "The name of the group the test belongs to, if any",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTest::status:
+   *
+   * The "status" property contains the status of the test, updated by
+   * providers when they have run the test.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_STATUS] =
+    g_param_spec_enum ("status",
+                       "Status",
+                       "The status of the test",
+                       IDE_TYPE_TEST_STATUS,
+                       IDE_TEST_STATUS_NONE,
+                       (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_test_init (IdeTest *self)
+{
+}
+
+IdeTestProvider *
+_ide_test_get_provider (IdeTest *self)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+
+  return priv->provider;
+}
+
+void
+_ide_test_set_provider (IdeTest         *self,
+                        IdeTestProvider *provider)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEST (self));
+  g_return_if_fail (!provider || IDE_IS_TEST_PROVIDER (provider));
+
+  priv->provider = provider;
+}
+
+/**
+ * ide_test_get_display_name:
+ * @self: An #IdeTest
+ *
+ * Gets the "display-name" property of the test.
+ *
+ * Returns: (nullable): The display_name of the test or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_test_get_display_name (IdeTest *self)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+
+  return priv->display_name;
+}
+
+/**
+ * ide_test_set_display_name:
+ * @self: An #IdeTest
+ * @display_name: (nullable): The display_name of the test, or %NULL to unset
+ *
+ * Sets the "display-name" property of the unit test.
+ *
+ * Since: 3.32
+ */
+void
+ide_test_set_display_name (IdeTest     *self,
+                           const gchar *display_name)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEST (self));
+
+  if (g_strcmp0 (display_name, priv->display_name) != 0)
+    {
+      g_free (priv->display_name);
+      priv->display_name = g_strdup (display_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
+    }
+}
+
+/**
+ * ide_test_get_group:
+ * @self: a #IdeTest
+ *
+ * Gets the "group" property.
+ *
+ * The group name is used to group tests together.
+ *
+ * Returns: (nullable): The group name or %NULL.
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_test_get_group (IdeTest *self)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+
+  return priv->group;
+}
+
+/**
+ * ide_test_set_group:
+ * @self: a #IdeTest
+ * @group: (nullable): the name of the group or %NULL
+ *
+ * Sets the #IdeTest:group property.
+ *
+ * The group property is used to group related tests together.
+ *
+ * Since: 3.32
+ */
+void
+ide_test_set_group (IdeTest     *self,
+                    const gchar *group)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEST (self));
+
+  if (g_strcmp0 (group, priv->group) != 0)
+    {
+      g_free (priv->group);
+      priv->group = g_strdup (group);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_GROUP]);
+    }
+}
+
+/**
+ * ide_test_get_id:
+ * @self: a #IdeTest
+ *
+ * Gets the #IdeTest:id property.
+ *
+ * Returns: (nullable): The id of the test, or %NULL if it has not been set.
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_test_get_id (IdeTest *self)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+
+  return priv->id;
+}
+
+/**
+ * ide_test_set_id:
+ * @self: a #IdeTest
+ * @id: (nullable): the id of the test or %NULL
+ *
+ * Sets the #IdeTest:id property.
+ *
+ * The id property is used to uniquely identify the test.
+ *
+ * Since: 3.32
+ */
+void
+ide_test_set_id (IdeTest     *self,
+                 const gchar *id)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEST (self));
+
+  if (g_strcmp0 (id, priv->id) != 0)
+    {
+      g_free (priv->id);
+      priv->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+    }
+}
+
+IdeTestStatus
+ide_test_get_status (IdeTest *self)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEST (self), 0);
+
+  return priv->status;
+}
+
+void
+ide_test_set_status (IdeTest       *self,
+                     IdeTestStatus  status)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEST (self));
+
+  if (priv->status != status)
+    {
+      priv->status = status;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_STATUS]);
+    }
+}
+
+const gchar *
+ide_test_get_icon_name (IdeTest *self)
+{
+  IdeTestPrivate *priv = ide_test_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEST (self), NULL);
+
+  switch (priv->status)
+    {
+    case IDE_TEST_STATUS_NONE:
+      return "builder-unit-tests-symbolic";
+
+    case IDE_TEST_STATUS_RUNNING:
+      return "builder-unit-tests-running-symbolic";
+
+    case IDE_TEST_STATUS_FAILED:
+      return "builder-unit-tests-fail-symbolic";
+
+    case IDE_TEST_STATUS_SUCCESS:
+      return "builder-unit-tests-pass-symbolic";
+
+    default:
+      g_return_val_if_reached (NULL);
+    }
+}
diff --git a/src/libide/foundry/ide-test.h b/src/libide/foundry/ide-test.h
new file mode 100644
index 000000000..6cdffe3af
--- /dev/null
+++ b/src/libide/foundry/ide-test.h
@@ -0,0 +1,77 @@
+/* ide-test.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEST (ide_test_get_type())
+
+typedef enum
+{
+  IDE_TEST_STATUS_NONE,
+  IDE_TEST_STATUS_RUNNING,
+  IDE_TEST_STATUS_SUCCESS,
+  IDE_TEST_STATUS_FAILED,
+} IdeTestStatus;
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTest, ide_test, IDE, TEST, GObject)
+
+struct _IdeTestClass
+{
+  GObjectClass parent;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeTest       *ide_test_new               (void);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_test_get_display_name  (IdeTest       *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_test_set_display_name  (IdeTest       *self,
+                                           const gchar   *display_name);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_test_get_group         (IdeTest       *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_test_set_group         (IdeTest       *self,
+                                           const gchar   *group);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_test_get_icon_name     (IdeTest       *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_test_get_id            (IdeTest       *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_test_set_id            (IdeTest       *self,
+                                           const gchar   *id);
+IDE_AVAILABLE_IN_3_32
+IdeTestStatus  ide_test_get_status        (IdeTest       *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_test_set_status        (IdeTest       *self,
+                                           IdeTestStatus  status);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-toolchain-manager.c b/src/libide/foundry/ide-toolchain-manager.c
new file mode 100644
index 000000000..37b66e3b1
--- /dev/null
+++ b/src/libide/foundry/ide-toolchain-manager.c
@@ -0,0 +1,590 @@
+/* ide-toolchain-manager.c
+ *
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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/>.
+ *
+ * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-toolchain-manager"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+
+#include "ide-build-private.h"
+#include "ide-build-pipeline.h"
+#include "ide-configuration.h"
+#include "ide-device.h"
+#include "ide-simple-toolchain.h"
+#include "ide-toolchain.h"
+#include "ide-toolchain-manager.h"
+#include "ide-toolchain-private.h"
+#include "ide-toolchain-provider.h"
+
+struct _IdeToolchainManager
+{
+  IdeObject         parent_instance;
+
+  GCancellable     *cancellable;
+  PeasExtensionSet *extensions;
+  GPtrArray        *toolchains;
+  guint             loaded : 1;
+};
+
+typedef struct
+{
+  IdeBuildPipeline *pipeline;
+  gchar            *toolchain_id;
+} PrepareState;
+
+static void list_model_iface_init     (GListModelInterface *iface);
+static void async_initable_iface_init (GAsyncInitableIface *iface);
+
+G_DEFINE_TYPE_EXTENDED (IdeToolchainManager, ide_toolchain_manager, IDE_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)
+                        G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init))
+
+static void
+prepare_state_free (PrepareState *state)
+{
+  g_clear_object (&state->pipeline);
+  g_clear_pointer (&state->toolchain_id, g_free);
+  g_slice_free (PrepareState, state);
+}
+
+static void
+ide_toolchain_manager_toolchain_added (IdeToolchainManager  *self,
+                                       IdeToolchain         *toolchain,
+                                       IdeToolchainProvider *provider)
+{
+  guint idx;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (self));
+  g_assert (IDE_IS_TOOLCHAIN (toolchain));
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (provider));
+
+  idx = self->toolchains->len;
+  g_ptr_array_add (self->toolchains, g_object_ref (toolchain));
+  g_list_model_items_changed (G_LIST_MODEL (self), idx, 0, 1);
+
+  IDE_EXIT;
+}
+
+static void
+ide_toolchain_manager_toolchain_removed (IdeToolchainManager  *self,
+                                         IdeToolchain         *toolchain,
+                                         IdeToolchainProvider *provider)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (self));
+  g_assert (IDE_IS_TOOLCHAIN (toolchain));
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (provider));
+
+  for (guint i = 0; i < self->toolchains->len; i++)
+    {
+      IdeToolchain *item = g_ptr_array_index (self->toolchains, i);
+
+      if (toolchain == item)
+        {
+          g_ptr_array_remove_index (self->toolchains, i);
+          g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+          break;
+        }
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_toolchain_manager_toolchain_load_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  IdeToolchainProvider *provider = (IdeToolchainProvider *)object;
+  IdeContext *context;
+  g_autoptr(IdeToolchainManager) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (provider));
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  if (!ide_toolchain_provider_load_finish (provider, result, &error))
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
+          !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+        ide_context_warning (context,
+                             "Failed to initialize toolchain provider: %s: %s",
+                             G_OBJECT_TYPE_NAME (provider), error->message);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+provider_connect (IdeToolchainManager  *self,
+                  IdeToolchainProvider *provider)
+{
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (self));
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (provider));
+
+  g_signal_connect_object (provider,
+                           "added",
+                           G_CALLBACK (ide_toolchain_manager_toolchain_added),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (provider,
+                           "removed",
+                           G_CALLBACK (ide_toolchain_manager_toolchain_removed),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+provider_disconnect (IdeToolchainManager  *self,
+                     IdeToolchainProvider *provider)
+{
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (self));
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (provider));
+
+  g_signal_handlers_disconnect_by_func (provider,
+                                        G_CALLBACK (ide_toolchain_manager_toolchain_added),
+                                        self);
+  g_signal_handlers_disconnect_by_func (provider,
+                                        G_CALLBACK (ide_toolchain_manager_toolchain_removed),
+                                        self);
+}
+
+static void
+ide_toolchain_manager_extension_added (PeasExtensionSet *set,
+                                       PeasPluginInfo   *plugin_info,
+                                       PeasExtension    *exten,
+                                       gpointer          user_data)
+{
+  IdeToolchainManager *self = user_data;
+  IdeToolchainProvider *provider = (IdeToolchainProvider *)exten;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (provider));
+
+  provider_connect (self, provider);
+
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (provider));
+
+  ide_toolchain_provider_load_async (provider,
+                                     self->cancellable,
+                                     ide_toolchain_manager_toolchain_load_cb,
+                                     g_object_ref (self));
+}
+
+static void
+ide_toolchain_manager_extension_removed (PeasExtensionSet *set,
+                                         PeasPluginInfo   *plugin_info,
+                                         PeasExtension    *exten,
+                                         gpointer          user_data)
+{
+  IdeToolchainManager *self = user_data;
+  IdeToolchainProvider *provider = (IdeToolchainProvider *)exten;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (provider));
+
+  provider_disconnect (self, provider);
+
+  ide_toolchain_provider_unload (provider, self);
+
+  ide_object_destroy (IDE_OBJECT (self));
+}
+
+static void
+ide_toolchain_manager_init_load_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  IdeToolchainProvider *provider = (IdeToolchainProvider *)object;
+  IdeToolchainManager *self;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  GPtrArray *providers;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (self));
+
+  if (!ide_toolchain_provider_load_finish (provider, result, &error))
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
+          !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+        g_warning ("Failed to initialize toolchain provider: %s: %s",
+                   G_OBJECT_TYPE_NAME (provider), error->message);
+    }
+
+  providers = ide_task_get_task_data (task);
+  g_assert (providers != NULL);
+  g_assert (providers->len > 0);
+
+  if (!g_ptr_array_remove (providers, provider))
+    g_critical ("Failed to locate provider in active set");
+
+  if (providers->len == 0)
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_toolchain_manager_collect_providers (PeasExtensionSet *set,
+                                         PeasPluginInfo   *plugin_info,
+                                         PeasExtension    *exten,
+                                         gpointer          user_data)
+{
+  IdeToolchainProvider *provider = (IdeToolchainProvider *)exten;
+  GPtrArray *providers = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (provider));
+  g_assert (providers != NULL);
+
+  g_ptr_array_add (providers, g_object_ref (provider));
+}
+
+static void
+ide_toolchain_manager_init_async (GAsyncInitable      *initable,
+                                  gint                 priority,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  IdeToolchainManager *self = (IdeToolchainManager *)initable;
+  g_autoptr(IdeSimpleToolchain) default_toolchain = NULL;
+  g_autoptr(GPtrArray) providers = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  IdeContext *context;
+  guint idx;
+
+  g_assert (G_IS_ASYNC_INITABLE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_toolchain_manager_init_async);
+  ide_task_set_priority (task, priority);
+
+#if 0
+  g_signal_connect_swapped (task,
+                            "notify::completed",
+                            G_CALLBACK (notify_providers_loaded),
+                            self);
+#endif
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  self->extensions = peas_extension_set_new (peas_engine_get_default (),
+                                             IDE_TYPE_TOOLCHAIN_PROVIDER,
+                                             NULL);
+
+  g_signal_connect (self->extensions,
+                    "extension-added",
+                    G_CALLBACK (ide_toolchain_manager_extension_added),
+                    self);
+
+  g_signal_connect (self->extensions,
+                    "extension-removed",
+                    G_CALLBACK (ide_toolchain_manager_extension_removed),
+                    self);
+
+  providers = g_ptr_array_new_with_free_func (g_object_unref);
+  peas_extension_set_foreach (self->extensions,
+                              ide_toolchain_manager_collect_providers,
+                              providers);
+  ide_task_set_task_data (task,
+                          g_ptr_array_ref (providers),
+                          g_ptr_array_unref);
+
+  default_toolchain = ide_simple_toolchain_new ("default", _("Default (Host operating system)"));
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (default_toolchain));
+
+  idx = self->toolchains->len;
+  g_ptr_array_add (self->toolchains, g_steal_pointer (&default_toolchain));
+  g_list_model_items_changed (G_LIST_MODEL (self), idx, 0, 1);
+
+  for (guint i = 0; i < providers->len; i++)
+    {
+      IdeToolchainProvider *provider = g_ptr_array_index (providers, i);
+
+      g_assert (IDE_IS_TOOLCHAIN_PROVIDER (provider));
+
+      provider_connect (self, provider);
+
+      ide_object_append (IDE_OBJECT (self), IDE_OBJECT (provider));
+
+      ide_toolchain_provider_load_async (provider,
+                                         cancellable,
+                                         ide_toolchain_manager_init_load_cb,
+                                         g_object_ref (task));
+    }
+
+  if (providers->len == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_toolchain_manager_init_finish (GAsyncInitable  *initable,
+                                   GAsyncResult    *result,
+                                   GError         **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (initable));
+  g_assert (IDE_IS_TASK (result));
+
+  IDE_TOOLCHAIN_MANAGER (initable)->loaded = TRUE;
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+async_initable_iface_init (GAsyncInitableIface *iface)
+{
+  iface->init_async = ide_toolchain_manager_init_async;
+  iface->init_finish = ide_toolchain_manager_init_finish;
+}
+
+static void
+ide_toolchain_manager_dispose (GObject *object)
+{
+  IdeToolchainManager *self = (IdeToolchainManager *)object;
+
+  g_clear_object (&self->extensions);
+  g_clear_pointer (&self->toolchains, g_ptr_array_unref);
+  g_clear_object (&self->cancellable);
+
+  G_OBJECT_CLASS (ide_toolchain_manager_parent_class)->dispose (object);
+}
+
+static void
+ide_toolchain_manager_class_init (IdeToolchainManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_toolchain_manager_dispose;
+}
+
+static void
+ide_toolchain_manager_init (IdeToolchainManager *self)
+{
+  self->loaded = FALSE;
+  self->cancellable = g_cancellable_new ();
+  self->toolchains = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+static GType
+ide_toolchain_manager_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_TOOLCHAIN;
+}
+
+static guint
+ide_toolchain_manager_get_n_items (GListModel *model)
+{
+  IdeToolchainManager *self = (IdeToolchainManager *)model;
+
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN_MANAGER (self), 0);
+
+  return self->toolchains->len;
+}
+
+static gpointer
+ide_toolchain_manager_get_item (GListModel *model,
+                                guint       position)
+{
+  IdeToolchainManager *self = (IdeToolchainManager *)model;
+
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN_MANAGER (self), NULL);
+  g_return_val_if_fail (position < self->toolchains->len, NULL);
+
+  return g_object_ref (g_ptr_array_index (self->toolchains, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_toolchain_manager_get_item_type;
+  iface->get_n_items = ide_toolchain_manager_get_n_items;
+  iface->get_item = ide_toolchain_manager_get_item;
+}
+
+/**
+ * ide_toolchain_manager_get_toolchain:
+ * @self: An #IdeToolchainManager
+ * @id: the identifier of the toolchain
+ *
+ * Gets the toolchain by its internal identifier.
+ *
+ * Returns: (transfer full): An #IdeToolchain.
+ *
+ * Since: 3.32
+ */
+IdeToolchain *
+ide_toolchain_manager_get_toolchain (IdeToolchainManager *self,
+                                     const gchar         *id)
+{
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN_MANAGER (self), NULL);
+  g_return_val_if_fail (id != NULL, NULL);
+
+  for (guint i = 0; i < self->toolchains->len; i++)
+    {
+      IdeToolchain *toolchain = g_ptr_array_index (self->toolchains, i);
+      const gchar *toolchain_id = ide_toolchain_get_id (toolchain);
+
+      if (g_strcmp0 (toolchain_id, id) == 0)
+        return g_object_ref (toolchain);
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_toolchain_manager_is_loaded:
+ * @self: An #IdeToolchainManager
+ *
+ * Gets whether all the #IdeToolchainProvider implementations are loaded
+ * and have registered all their #IdeToolchain.
+ *
+ * Returns: %TRUE if all the toolchains are loaded
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_toolchain_manager_is_loaded (IdeToolchainManager  *self)
+{
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN_MANAGER (self), FALSE);
+
+  return self->loaded;
+}
+
+void
+_ide_toolchain_manager_prepare_async (IdeToolchainManager  *self,
+                                      IdeBuildPipeline     *pipeline,
+                                      GCancellable         *cancellable,
+                                      GAsyncReadyCallback   callback,
+                                      gpointer              user_data)
+{
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(IdeToolchain) toolchain = NULL;
+  IdeConfiguration *config;
+  PrepareState *state;
+  const gchar *toolchain_id;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_TOOLCHAIN_MANAGER (self));
+  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+  toolchain_id = ide_configuration_get_toolchain_id (config);
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, _ide_toolchain_manager_prepare_async);
+  g_task_set_priority (task, G_PRIORITY_LOW);
+
+  state = g_slice_new0 (PrepareState);
+  state->toolchain_id = g_strdup (toolchain_id);
+  state->pipeline = g_object_ref (pipeline);
+  g_task_set_task_data (task, state, (GDestroyNotify)prepare_state_free);
+
+  if (toolchain_id == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "Configuration lacks toolchain specification");
+      IDE_EXIT;
+    }
+
+  toolchain = ide_toolchain_manager_get_toolchain (self, toolchain_id);
+
+  if (toolchain == NULL)
+    {
+      g_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "Configuration toolchain specification does not exist");
+      IDE_EXIT;
+    }
+
+  g_task_return_pointer (task, g_object_ref (toolchain), g_object_unref);
+
+  IDE_EXIT;
+}
+
+gboolean
+_ide_toolchain_manager_prepare_finish (IdeToolchainManager  *self,
+                                       GAsyncResult         *result,
+                                       GError              **error)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autoptr(IdeToolchain) ret = NULL;
+  PrepareState *state;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN_MANAGER (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  state = g_task_get_task_data (G_TASK (result));
+  ret = g_task_propagate_pointer (G_TASK (result), &local_error);
+
+  /*
+   * If we got NOT_SUPPORTED error, and the toolchain already exists,
+   * then we can synthesize a successful result to the caller.
+   */
+  if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+    {
+      if ((ret = ide_toolchain_manager_get_toolchain (self, state->toolchain_id)))
+        g_clear_error (&local_error);
+    }
+
+  if (error != NULL)
+    *error = g_steal_pointer (&local_error);
+
+  g_return_val_if_fail (!ret || IDE_IS_TOOLCHAIN (ret), FALSE);
+
+  if (IDE_IS_TOOLCHAIN (ret))
+    _ide_build_pipeline_set_toolchain (state->pipeline, ret);
+
+  IDE_RETURN (ret != NULL);
+}
diff --git a/src/libide/foundry/ide-toolchain-manager.h b/src/libide/foundry/ide-toolchain-manager.h
new file mode 100644
index 000000000..ad2b1c210
--- /dev/null
+++ b/src/libide/foundry/ide-toolchain-manager.h
@@ -0,0 +1,46 @@
+/* ide-toolchain-manager.h
+ *
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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/>.
+ *
+ * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TOOLCHAIN_MANAGER (ide_toolchain_manager_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeToolchainManager, ide_toolchain_manager, IDE, TOOLCHAIN_MANAGER, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeToolchain *ide_toolchain_manager_get_toolchain (IdeToolchainManager  *self,
+                                                   const gchar          *id);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_toolchain_manager_is_loaded     (IdeToolchainManager  *self);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-toolchain-private.h b/src/libide/foundry/ide-toolchain-private.h
new file mode 100644
index 000000000..be819f89e
--- /dev/null
+++ b/src/libide/foundry/ide-toolchain-private.h
@@ -0,0 +1,38 @@
+/* ide-toolchain-private.h
+ *
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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/>.
+ *
+ * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+void     _ide_toolchain_manager_prepare_async  (IdeToolchainManager  *self,
+                                                IdeBuildPipeline     *pipeline,
+                                                GCancellable         *cancellable,
+                                                GAsyncReadyCallback   callback,
+                                                gpointer              user_data);
+gboolean _ide_toolchain_manager_prepare_finish (IdeToolchainManager  *self,
+                                                GAsyncResult         *result,
+                                                GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-toolchain-provider.c b/src/libide/foundry/ide-toolchain-provider.c
new file mode 100644
index 000000000..b7b5d2b54
--- /dev/null
+++ b/src/libide/foundry/ide-toolchain-provider.c
@@ -0,0 +1,233 @@
+/* ide-toolchain-provider.c
+ *
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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/>.
+ *
+ * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-toolchain-provider"
+
+#include "config.h"
+
+#include "ide-toolchain.h"
+#include "ide-toolchain-manager.h"
+#include "ide-toolchain-provider.h"
+
+G_DEFINE_INTERFACE (IdeToolchainProvider, ide_toolchain_provider, IDE_TYPE_OBJECT)
+
+enum {
+  ADDED,
+  REMOVED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+ide_toolchain_provider_real_load_async (IdeToolchainProvider *self,
+                                        GCancellable         *cancellable,
+                                        GAsyncReadyCallback   callback,
+                                        gpointer              user_data)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_task_report_new_error (self, callback, user_data,
+                           ide_toolchain_provider_real_load_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "%s does not implement load_async",
+                           G_OBJECT_TYPE_NAME (self));
+}
+
+static gboolean
+ide_toolchain_provider_real_load_finish (IdeToolchainProvider  *self,
+                                         GAsyncResult          *result,
+                                         GError               **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (self));
+  g_assert (G_IS_TASK (result));
+  g_assert (g_task_is_valid (G_TASK (result), self));
+
+  return g_task_propagate_boolean (G_TASK (self), error);
+}
+
+void
+ide_toolchain_provider_unload (IdeToolchainProvider *self,
+                               IdeToolchainManager  *manager)
+{
+  g_return_if_fail (IDE_IS_TOOLCHAIN_PROVIDER (self));
+  g_return_if_fail (IDE_IS_TOOLCHAIN_MANAGER (manager));
+
+  IDE_TOOLCHAIN_PROVIDER_GET_IFACE (self)->unload (self, manager);
+}
+
+static void
+ide_toolchain_provider_real_unload (IdeToolchainProvider *self,
+                                    IdeToolchainManager  *manager)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TOOLCHAIN_PROVIDER (self));
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (manager));
+
+}
+
+static void
+ide_toolchain_provider_default_init (IdeToolchainProviderInterface *iface)
+{
+  iface->load_async = ide_toolchain_provider_real_load_async;
+  iface->load_finish = ide_toolchain_provider_real_load_finish;
+  iface->unload = ide_toolchain_provider_real_unload;
+
+  /**
+   * IdeToolchainProvider:added:
+   * @self: an #IdeToolchainProvider
+   * @toolchain: an #IdeToolchain
+   *
+   * The "added" signal is emitted when a toolchain
+   * has been added to a toolchain provider.
+   *
+   * Since: 3.32
+   */
+  signals [ADDED] =
+    g_signal_new ("added",
+                  G_TYPE_FROM_INTERFACE (iface),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeToolchainProviderInterface, added),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_TOOLCHAIN);
+  g_signal_set_va_marshaller (signals [ADDED],
+                              G_TYPE_FROM_INTERFACE (iface),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  /**
+   * IdeToolchainProvider:removed:
+   * @self: an #IdeToolchainProvider
+   * @toolchain: an #IdeToolchain
+   *
+   * The "removed" signal is emitted when a toolchain
+   * has been removed from a toolchain provider.
+   *
+   * Since: 3.32
+   */
+  signals [REMOVED] =
+    g_signal_new ("removed",
+                  G_TYPE_FROM_INTERFACE (iface),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeToolchainProviderInterface, removed),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_TOOLCHAIN);
+  g_signal_set_va_marshaller (signals [REMOVED],
+                              G_TYPE_FROM_INTERFACE (iface),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+}
+
+/**
+ * ide_toolchain_provider_load_async:
+ * @self: a #IdeToolchainProvider
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * This function is called to initialize the toolchain provider after
+ * the plugin instance has been created. The provider should locate any
+ * toolchain within the project and call ide_toolchain_provider_emit_added()
+ * before completing the asynchronous function so that the toolchain
+ * manager may be made aware of the toolchains.
+ *
+ * Since: 3.32
+ */
+void
+ide_toolchain_provider_load_async (IdeToolchainProvider *self,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data)
+{
+  g_return_if_fail (IDE_IS_TOOLCHAIN_PROVIDER (self));
+
+  IDE_TOOLCHAIN_PROVIDER_GET_IFACE (self)->load_async (self, cancellable, callback, user_data);
+}
+
+/**
+ * ide_toolchain_provider_load_finish:
+ * @self: a #IdeToolchainProvider
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_toolchain_provider_load_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_toolchain_provider_load_finish (IdeToolchainProvider  *self,
+                                    GAsyncResult          *result,
+                                    GError               **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_TOOLCHAIN_PROVIDER_GET_IFACE (self)->load_finish (self, result, error);
+}
+
+/**
+ * ide_toolchain_provider_emit_added:
+ * @self: an #IdeToolchainProvider
+ * @toolchain: an #IdeToolchain
+ *
+ * #IdeToolchainProvider implementations should call this function with
+ * a @toolchain when it has discovered a new toolchain.
+ *
+ * Since: 3.32
+ */
+void
+ide_toolchain_provider_emit_added (IdeToolchainProvider *self,
+                                   IdeToolchain         *toolchain)
+{
+  g_return_if_fail (IDE_IS_TOOLCHAIN_PROVIDER (self));
+  g_return_if_fail (IDE_IS_TOOLCHAIN (toolchain));
+
+  g_signal_emit (self, signals [ADDED], 0, toolchain);
+}
+
+/**
+ * ide_toolchain_provider_emit_removed:
+ * @self: an #IdeToolchainProvider
+ * @toolchain: an #IdeToolchain
+ *
+ * #IdeToolchainProvider implementations should call this function with
+ * a @toolchain when the toolchain was removed.
+ *
+ * Since: 3.32
+ */
+void
+ide_toolchain_provider_emit_removed (IdeToolchainProvider *self,
+                                     IdeToolchain         *toolchain)
+{
+  g_return_if_fail (IDE_IS_TOOLCHAIN_PROVIDER (self));
+  g_return_if_fail (IDE_IS_TOOLCHAIN (toolchain));
+
+  g_signal_emit (self, signals [REMOVED], 0, toolchain);
+}
diff --git a/src/libide/foundry/ide-toolchain-provider.h b/src/libide/foundry/ide-toolchain-provider.h
new file mode 100644
index 000000000..222a5e9b0
--- /dev/null
+++ b/src/libide/foundry/ide-toolchain-provider.h
@@ -0,0 +1,78 @@
+/* ide-toolchain-provider.h
+ *
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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/>.
+ *
+ * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TOOLCHAIN_PROVIDER (ide_toolchain_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeToolchainProvider, ide_toolchain_provider, IDE, TOOLCHAIN_PROVIDER, IdeObject)
+
+struct _IdeToolchainProviderInterface
+{
+  GTypeInterface parent_iface;
+
+  void        (*load_async)       (IdeToolchainProvider  *self,
+                                   GCancellable          *cancellable,
+                                   GAsyncReadyCallback    callback,
+                                   gpointer               user_data);
+  gboolean    (*load_finish)      (IdeToolchainProvider  *self,
+                                   GAsyncResult          *result,
+                                   GError               **error);
+  void        (*unload)           (IdeToolchainProvider  *self,
+                                   IdeToolchainManager   *manager);
+  void        (*added)            (IdeToolchainProvider  *self,
+                                   IdeToolchain          *toolchain);
+  void        (*removed)          (IdeToolchainProvider  *self,
+                                   IdeToolchain          *toolchain);
+};
+
+IDE_AVAILABLE_IN_3_32
+void        ide_toolchain_provider_load_async   (IdeToolchainProvider  *self,
+                                                 GCancellable          *cancellable,
+                                                 GAsyncReadyCallback    callback,
+                                                 gpointer               user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_toolchain_provider_load_finish  (IdeToolchainProvider  *self,
+                                                 GAsyncResult          *result,
+                                                 GError               **error);
+IDE_AVAILABLE_IN_3_32
+void        ide_toolchain_provider_unload       (IdeToolchainProvider  *self,
+                                                 IdeToolchainManager   *manager);
+IDE_AVAILABLE_IN_3_32
+void        ide_toolchain_provider_emit_added   (IdeToolchainProvider  *self,
+                                                 IdeToolchain          *toolchain);
+IDE_AVAILABLE_IN_3_32
+void        ide_toolchain_provider_emit_removed (IdeToolchainProvider  *self,
+                                                 IdeToolchain          *toolchain);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-toolchain.c b/src/libide/foundry/ide-toolchain.c
new file mode 100644
index 000000000..d8d8b2f23
--- /dev/null
+++ b/src/libide/foundry/ide-toolchain.c
@@ -0,0 +1,354 @@
+/* ide-toolchain.c
+ *
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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/>.
+ *
+ * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-toolchain"
+
+#include "config.h"
+
+#include "ide-toolchain.h"
+#include "ide-triplet.h"
+
+typedef struct
+{
+  gchar *id;
+  gchar *display_name;
+  IdeTriplet *host_triplet;
+} IdeToolchainPrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (IdeToolchain, ide_toolchain, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_ID,
+  PROP_DISPLAY_NAME,
+  PROP_HOST_TRIPLET,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * ide_toolchain_get_id:
+ * @self: an #IdeToolchain
+ *
+ * Gets the internal identifier of the toolchain
+ *
+ * Returns: (transfer none): the unique identifier.
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_toolchain_get_id (IdeToolchain  *self)
+{
+  IdeToolchainPrivate *priv = ide_toolchain_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN (self), NULL);
+
+  return priv->id;
+}
+
+
+/**
+ * ide_toolchain_set_id:
+ * @self: an #IdeToolchain
+ * @id: the unique identifier
+ *
+ * Sets the internal identifier of the toolchain
+ *
+ * Since: 3.32
+ */
+void
+ide_toolchain_set_id (IdeToolchain  *self,
+                      const gchar   *id)
+{
+  IdeToolchainPrivate *priv = ide_toolchain_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TOOLCHAIN (self));
+  g_return_if_fail (id != NULL);
+
+  if (g_strcmp0 (id, priv->id) != 0)
+    {
+      g_clear_pointer (&priv->id, g_free);
+      priv->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+    }
+}
+
+const gchar *
+ide_toolchain_get_display_name (IdeToolchain  *self)
+{
+  IdeToolchainPrivate *priv = ide_toolchain_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN (self), NULL);
+
+  return priv->display_name;
+}
+
+void
+ide_toolchain_set_display_name (IdeToolchain  *self,
+                                const gchar   *display_name)
+{
+  IdeToolchainPrivate *priv = ide_toolchain_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TOOLCHAIN (self));
+  g_return_if_fail (display_name != NULL);
+
+  if (g_strcmp0 (display_name, priv->display_name) != 0)
+    {
+      g_clear_pointer (&priv->display_name, g_free);
+      priv->display_name = g_strdup (display_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
+    }
+}
+
+/**
+ * ide_toolchain_set_host_triplet:
+ * @self: an #IdeToolchain
+ * @host_triplet: an #IdeTriplet representing the host architecture of the toolchain
+ *
+ * Sets the host system of the toolchain
+ *
+ * Since: 3.32
+ */
+void
+ide_toolchain_set_host_triplet (IdeToolchain *self,
+                                IdeTriplet   *host_triplet)
+{
+  IdeToolchainPrivate *priv = ide_toolchain_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TOOLCHAIN (self));
+
+  if (host_triplet != priv->host_triplet)
+    {
+      g_clear_pointer (&priv->host_triplet, ide_triplet_unref);
+      priv->host_triplet = ide_triplet_ref (host_triplet);
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HOST_TRIPLET]);
+    }
+}
+
+/**
+ * ide_toolchain_get_host_triplet:
+ * @self: an #IdeToolchain
+ *
+ * Gets the combination of arch-kernel-system, sometimes referred to as
+ * the "host triplet".
+ *
+ * For Linux based devices, this will generally be something like
+ * "x86_64-linux-gnu".
+ *
+ * Returns: (transfer full): The host system.type of the toolchain
+ *
+ * Since: 3.32
+ */
+IdeTriplet *
+ide_toolchain_get_host_triplet (IdeToolchain *self)
+{
+  IdeToolchainPrivate *priv = ide_toolchain_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN (self), NULL);
+
+  return ide_triplet_ref (priv->host_triplet);
+}
+
+static const gchar *
+ide_toolchain_real_get_tool_for_language (IdeToolchain  *self,
+                                          const gchar   *language,
+                                          const gchar   *tool_id)
+{
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN (self), NULL);
+
+  g_critical ("%s has not implemented get_tool_for_language()", G_OBJECT_TYPE_NAME (self));
+
+  return NULL;
+}
+
+static GHashTable *
+ide_toolchain_real_get_tools_for_id (IdeToolchain  *self,
+                                     const gchar   *tool_id)
+{
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN (self), NULL);
+
+  g_critical ("%s has not implemented get_tools_for_id()", G_OBJECT_TYPE_NAME (self));
+
+  return NULL;
+}
+
+/**
+ * ide_toolchain_get_tool_for_language:
+ * @self: an #IdeToolchain
+ * @language: the language of the tool like %IDE_TOOLCHAIN_LANGUAGE_C.
+ * @tool_id: the identifier of the tool like %IDE_TOOLCHAIN_TOOL_CC
+ *
+ * Gets the path of the specified tool for the requested language.
+ * If %IDE_TOOLCHAIN_LANGUAGE_ANY is used in the @language field, the first tool matching @tool_id
+ * will be returned.
+ *
+ * Returns: (transfer none): A string containing the path of the tool for the given language, or
+ * %NULL is no tool has been found.
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_toolchain_get_tool_for_language (IdeToolchain *self,
+                                     const gchar  *language,
+                                     const gchar  *tool_id)
+{
+  const gchar *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN (self), NULL);
+
+  ret = IDE_TOOLCHAIN_GET_CLASS (self)->get_tool_for_language (self, language, tool_id);
+
+  IDE_RETURN (ret);
+}
+
+/**
+ * ide_toolchain_get_tools_for_id:
+ * @self: an #IdeToolchain
+ * @tool_id: the identifier of the tool like %IDE_TOOLCHAIN_TOOL_CC
+ *
+ * Gets the list of all the paths to the specified tool id.
+ *
+ * Returns: (transfer full) (element-type utf8 utf8): A table of language names and paths.
+ *
+ * Since: 3.32
+ */
+GHashTable *
+ide_toolchain_get_tools_for_id (IdeToolchain  *self,
+                                const gchar   *tool_id)
+{
+  GHashTable *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TOOLCHAIN (self), NULL);
+
+  ret = IDE_TOOLCHAIN_GET_CLASS (self)->get_tools_for_id (self, tool_id);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_toolchain_finalize (GObject *object)
+{
+  IdeToolchain *self = (IdeToolchain *)object;
+  IdeToolchainPrivate *priv = ide_toolchain_get_instance_private (self);
+
+  g_clear_pointer (&priv->id, g_free);
+  g_clear_pointer (&priv->display_name, g_free);
+  g_clear_pointer (&priv->host_triplet, ide_triplet_unref);
+
+  G_OBJECT_CLASS (ide_toolchain_parent_class)->finalize (object);
+}
+
+static void
+ide_toolchain_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  IdeToolchain *self = IDE_TOOLCHAIN (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      g_value_set_string (value, ide_toolchain_get_id (self));
+      break;
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, ide_toolchain_get_display_name (self));
+      break;
+    case PROP_HOST_TRIPLET:
+      g_value_set_boxed (value, ide_toolchain_get_host_triplet (self));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_toolchain_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeToolchain *self = IDE_TOOLCHAIN (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      ide_toolchain_set_id (self, g_value_get_string (value));
+      break;
+    case PROP_DISPLAY_NAME:
+      ide_toolchain_set_display_name (self, g_value_get_string (value));
+      break;
+    case PROP_HOST_TRIPLET:
+      ide_toolchain_set_host_triplet (self, g_value_get_boxed (value));
+      break;
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_toolchain_class_init (IdeToolchainClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_toolchain_finalize;
+  object_class->get_property = ide_toolchain_get_property;
+  object_class->set_property = ide_toolchain_set_property;
+
+  klass->get_tool_for_language = ide_toolchain_real_get_tool_for_language;
+  klass->get_tools_for_id = ide_toolchain_real_get_tools_for_id;
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "The toolchain identifier",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DISPLAY_NAME] =
+    g_param_spec_string ("display-name",
+                         "Display Name",
+                         "The displayable name of the toolchain",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HOST_TRIPLET] =
+    g_param_spec_boxed ("host-triplet",
+                         "Host Triplet",
+                         "The #IdeTriplet object containing the architecture of the machine on which the 
compiled binary will run",
+                         IDE_TYPE_TRIPLET,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_toolchain_init (IdeToolchain *self)
+{
+  IdeToolchainPrivate *priv = ide_toolchain_get_instance_private (self);
+  priv->host_triplet = ide_triplet_new_from_system ();
+}
diff --git a/src/libide/foundry/ide-toolchain.h b/src/libide/foundry/ide-toolchain.h
new file mode 100644
index 000000000..2d4cca3fe
--- /dev/null
+++ b/src/libide/foundry/ide-toolchain.h
@@ -0,0 +1,94 @@
+/* ide-toolchain.h
+ *
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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/>.
+ *
+ * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TOOLCHAIN (ide_toolchain_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeToolchain, ide_toolchain, IDE, TOOLCHAIN, IdeObject)
+
+struct _IdeToolchainClass
+{
+  IdeObjectClass parent;
+
+  const gchar *(*get_tool_for_language) (IdeToolchain  *self,
+                                         const gchar   *language,
+                                         const gchar   *tool_id);
+
+  GHashTable  *(*get_tools_for_id)      (IdeToolchain  *self,
+                                         const gchar   *tool_id);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+#define IDE_TOOLCHAIN_TOOL_CC          "cc"
+#define IDE_TOOLCHAIN_TOOL_CPP         "cpp"
+#define IDE_TOOLCHAIN_TOOL_AR          "ar"
+#define IDE_TOOLCHAIN_TOOL_LD          "ld"
+#define IDE_TOOLCHAIN_TOOL_STRIP       "strip"
+#define IDE_TOOLCHAIN_TOOL_EXEC        "exec"
+#define IDE_TOOLCHAIN_TOOL_PKG_CONFIG  "pkg-config"
+
+#define IDE_TOOLCHAIN_LANGUAGE_ANY       "*"
+#define IDE_TOOLCHAIN_LANGUAGE_C         "c"
+#define IDE_TOOLCHAIN_LANGUAGE_CPLUSPLUS "c++"
+#define IDE_TOOLCHAIN_LANGUAGE_PYTHON    "python"
+#define IDE_TOOLCHAIN_LANGUAGE_VALA      "vala"
+#define IDE_TOOLCHAIN_LANGUAGE_FORTRAN   "fortran"
+#define IDE_TOOLCHAIN_LANGUAGE_D         "d"
+
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_toolchain_get_id                (IdeToolchain  *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_toolchain_set_id                (IdeToolchain  *self,
+                                                    const gchar   *id);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_toolchain_get_display_name      (IdeToolchain  *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_toolchain_set_display_name      (IdeToolchain  *self,
+                                                    const gchar   *display_name);
+IDE_AVAILABLE_IN_3_32
+IdeTriplet    *ide_toolchain_get_host_triplet      (IdeToolchain  *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_toolchain_set_host_triplet      (IdeToolchain  *self,
+                                                    IdeTriplet    *host_triplet);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_toolchain_get_tool_for_language (IdeToolchain  *self,
+                                                    const gchar   *language,
+                                                    const gchar   *tool_id);
+IDE_AVAILABLE_IN_3_32
+GHashTable    *ide_toolchain_get_tools_for_id      (IdeToolchain  *self,
+                                                    const gchar   *tool_id);
+
+G_END_DECLS
diff --git a/src/libide/foundry/ide-triplet.c b/src/libide/foundry/ide-triplet.c
new file mode 100644
index 000000000..389fb001e
--- /dev/null
+++ b/src/libide/foundry/ide-triplet.c
@@ -0,0 +1,389 @@
+/* ide-triplet.c
+ *
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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 "ide-triplet"
+
+#include "config.h"
+
+#include "ide-triplet.h"
+
+G_DEFINE_BOXED_TYPE (IdeTriplet, ide_triplet, ide_triplet_ref, ide_triplet_unref)
+
+struct _IdeTriplet
+{
+  volatile gint ref_count;
+
+  gchar *full_name;
+  gchar *arch;
+  gchar *vendor;
+  gchar *kernel;
+  gchar *operating_system;
+};
+
+static IdeTriplet *
+_ide_triplet_construct (void)
+{
+  IdeTriplet *self;
+
+  self = g_slice_new0 (IdeTriplet);
+  self->ref_count = 1;
+  self->full_name = NULL;
+  self->arch = NULL;
+  self->vendor = NULL;
+  self->kernel = NULL;
+  self->operating_system = NULL;
+
+  return self;
+}
+
+/**
+ * ide_triplet_new:
+ * @full_name: The complete identifier of the machine
+ *
+ * Creates a new #IdeTriplet from a given identifier. This identifier
+ * can be a simple architecture name, a duet of "arch-kernel" (like "m68k-coff"), a triplet
+ * of "arch-kernel-os" (like "x86_64-linux-gnu") or a quadriplet of "arch-vendor-kernel-os"
+ * (like "i686-pc-linux-gnu")
+ *
+ * Returns: (transfer full): An #IdeTriplet.
+ *
+ * Since: 3.32
+ */
+IdeTriplet *
+ide_triplet_new (const gchar *full_name)
+{
+  IdeTriplet *self;
+  g_auto (GStrv) parts = NULL;
+  guint parts_length = 0;
+
+  g_return_val_if_fail (full_name != NULL, NULL);
+
+  self = _ide_triplet_construct ();
+  self->full_name = g_strdup (full_name);
+
+  parts = g_strsplit (full_name, "-", 4);
+  parts_length = g_strv_length (parts);
+  /* Currently they can't have more than 4 parts */
+  if (parts_length >= 4)
+    {
+      self->arch = g_strdup (parts[0]);
+      self->vendor = g_strdup (parts[1]);
+      self->kernel = g_strdup (parts[2]);
+      self->operating_system = g_strdup (parts[3]);
+    }
+  else if (parts_length == 3)
+    {
+      self->arch = g_strdup (parts[0]);
+      self->kernel = g_strdup (parts[1]);
+      self->operating_system = g_strdup (parts[2]);
+    }
+  else if (parts_length == 2)
+    {
+      self->arch = g_strdup (parts[0]);
+      self->kernel = g_strdup (parts[1]);
+    }
+  else if (parts_length == 1)
+    self->arch = g_strdup (parts[0]);
+
+  return self;
+}
+
+/**
+ * ide_triplet_new_from_system:
+ *
+ * Creates a new #IdeTriplet from a the current system information
+ *
+ * Returns: (transfer full): An #IdeTriplet.
+ *
+ * Since: 3.32
+ */
+IdeTriplet *
+ide_triplet_new_from_system (void)
+{
+  static IdeTriplet *system_triplet;
+
+  if (g_once_init_enter (&system_triplet))
+    g_once_init_leave (&system_triplet, ide_triplet_new (ide_get_system_type ()));
+
+  return ide_triplet_ref (system_triplet);
+}
+
+/**
+ * ide_triplet_new_with_triplet:
+ * @arch: The name of the architecture of the machine (like "x86_64")
+ * @kernel: (nullable): The name of the kernel of the machine (like "linux")
+ * @operating_system: (nullable): The name of the os of the machine
+ * (like "gnuabi64")
+ *
+ * Creates a new #IdeTriplet from a given triplet of "arch-kernel-os"
+ * (like "x86_64-linux-gnu")
+ *
+ * Returns: (transfer full): An #IdeTriplet.
+ *
+ * Since: 3.32
+ */
+IdeTriplet *
+ide_triplet_new_with_triplet (const gchar *arch,
+                              const gchar *kernel,
+                              const gchar *operating_system)
+{
+  IdeTriplet *self;
+  g_autofree gchar *full_name = NULL;
+
+  g_return_val_if_fail (arch != NULL, NULL);
+
+  self = _ide_triplet_construct ();
+  self->arch = g_strdup (arch);
+  self->kernel = g_strdup (kernel);
+  self->operating_system = g_strdup (operating_system);
+
+  full_name = g_strdup (arch);
+  if (kernel != NULL)
+    {
+      g_autofree gchar *start_full_name = full_name;
+      full_name = g_strdup_printf ("%s-%s", start_full_name, kernel);
+    }
+
+  if (operating_system != NULL)
+    {
+      g_autofree gchar *start_full_name = full_name;
+      full_name = g_strdup_printf ("%s-%s", start_full_name, operating_system);
+    }
+
+  self->full_name = g_steal_pointer (&full_name);
+
+  return self;
+}
+
+/**
+ * ide_triplet_new_with_quadruplet:
+ * @arch: The name of the architecture of the machine (like "x86_64")
+ * @vendor: (nullable): The name of the vendor of the machine (like "pc")
+ * @kernel: (nullable): The name of the kernel of the machine (like "linux")
+ * @operating_system: (nullable): The name of the os of the machine (like "gnuabi64")
+ *
+ * Creates a new #IdeTriplet from a given quadruplet of
+ * "arch-vendor-kernel-os" (like "i686-pc-linux-gnu")
+ *
+ * Returns: (transfer full): An #IdeTriplet.
+ *
+ * Since: 3.32
+ */
+IdeTriplet *
+ide_triplet_new_with_quadruplet (const gchar *arch,
+                                 const gchar *vendor,
+                                 const gchar *kernel,
+                                 const gchar *operating_system)
+{
+  IdeTriplet *self;
+  g_autofree gchar *full_name = NULL;
+
+  g_return_val_if_fail (arch != NULL, NULL);
+
+  if (vendor == NULL)
+    return ide_triplet_new_with_triplet (arch, kernel, operating_system);
+
+  self = _ide_triplet_construct ();
+  self->arch = g_strdup (arch);
+  self->vendor = g_strdup (vendor);
+  self->kernel = g_strdup (kernel);
+  self->operating_system = g_strdup (operating_system);
+
+  full_name = g_strdup_printf ("%s-%s", arch, vendor);
+  if (kernel != NULL)
+    {
+      g_autofree gchar *start_full_name = full_name;
+      full_name = g_strdup_printf ("%s-%s", start_full_name, kernel);
+    }
+
+  if (operating_system != NULL)
+    {
+      g_autofree gchar *start_full_name = full_name;
+      full_name = g_strdup_printf ("%s-%s", start_full_name, operating_system);
+    }
+
+  self->full_name = g_steal_pointer (&full_name);
+
+  return self;
+}
+
+static void
+ide_triplet_finalize (IdeTriplet *self)
+{
+  g_free (self->full_name);
+  g_free (self->arch);
+  g_free (self->vendor);
+  g_free (self->kernel);
+  g_free (self->operating_system);
+  g_slice_free (IdeTriplet, self);
+}
+
+/**
+ * ide_triplet_ref:
+ * @self: An #IdeTriplet
+ *
+ * Increases the reference count of @self
+ *
+ * Returns: (transfer none): An #IdeTriplet.
+ *
+ * Since: 3.32
+ */
+IdeTriplet *
+ide_triplet_ref (IdeTriplet *self)
+{
+  g_return_val_if_fail (self, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  g_atomic_int_inc (&self->ref_count);
+
+  return self;
+}
+
+/**
+ * ide_triplet_unref:
+ * @self: An #IdeTriplet
+ *
+ * Decreases the reference count of @self
+ * Once the reference count reaches 0, the object is freed.
+ *
+ * Since: 3.32
+ */
+void
+ide_triplet_unref (IdeTriplet *self)
+{
+  g_return_if_fail (self);
+  g_return_if_fail (self->ref_count > 0);
+
+  if (g_atomic_int_dec_and_test (&self->ref_count))
+    ide_triplet_finalize (self);
+}
+
+/**
+ * ide_triplet_get_full_name:
+ * @self: An #IdeTriplet
+ *
+ * Gets the full name of the machine configuration name (can be an architecture name,
+ * a duet, a triplet or a quadruplet).
+ *
+ * Returns: (transfer none): The full name of the machine configuration name
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_triplet_get_full_name (IdeTriplet *self)
+{
+  g_return_val_if_fail (self, NULL);
+
+  return self->full_name;
+}
+
+/**
+ * ide_triplet_get_arch:
+ * @self: An #IdeTriplet
+ *
+ * Gets the architecture name of the machine
+ *
+ * Returns: (transfer none): The architecture name of the machine
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_triplet_get_arch (IdeTriplet *self)
+{
+  g_return_val_if_fail (self, NULL);
+
+  return self->arch;
+}
+
+/**
+ * ide_triplet_get_vendor:
+ * @self: An #IdeTriplet
+ *
+ * Gets the vendor name of the machine
+ *
+ * Returns: (transfer none) (nullable): The vendor name of the machine
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_triplet_get_vendor (IdeTriplet *self)
+{
+  g_return_val_if_fail (self, NULL);
+
+  return self->vendor;
+}
+
+/**
+ * ide_triplet_get_kernel:
+ * @self: An #IdeTriplet
+ *
+ * Gets name of the kernel of the machine
+ *
+ * Returns: (transfer none) (nullable): The name of the kernel of the machine
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_triplet_get_kernel (IdeTriplet *self)
+{
+  g_return_val_if_fail (self, NULL);
+
+  return self->kernel;
+}
+
+/**
+ * ide_triplet_get_operating_system:
+ * @self: An #IdeTriplet
+ *
+ * Gets name of the operating system of the machine
+ *
+ * Returns: (transfer none) (nullable): The name of the operating system of the machine
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_triplet_get_operating_system (IdeTriplet *self)
+{
+  g_return_val_if_fail (self, NULL);
+
+  return self->operating_system;
+}
+
+
+/**
+ * ide_triplet_is_system:
+ * @self: An #IdeTriplet
+ *
+ * Gets whether this is the same architecture as the system
+ *
+ * Returns: %TRUE if this is the same architecture as the system, %FALSE otherwise
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_triplet_is_system (IdeTriplet *self)
+{
+  g_autofree gchar *system_arch = ide_get_system_arch ();
+
+  g_return_val_if_fail (self, FALSE);
+
+  return g_strcmp0 (self->arch, system_arch) == 0;
+}
diff --git a/src/libide/foundry/ide-triplet.h b/src/libide/foundry/ide-triplet.h
new file mode 100644
index 000000000..54c70fb7a
--- /dev/null
+++ b/src/libide/foundry/ide-triplet.h
@@ -0,0 +1,70 @@
+/* ide-triplet.c
+ *
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
+ *
+ * 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
+
+#if !defined (IDE_FOUNDRY_INSIDE) && !defined (IDE_FOUNDRY_COMPILATION)
+# error "Only <libide-foundry.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-foundry-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TRIPLET (ide_triplet_get_type())
+
+IDE_AVAILABLE_IN_3_32
+GType         ide_triplet_get_type             (void);
+IDE_AVAILABLE_IN_3_32
+IdeTriplet   *ide_triplet_new                  (const gchar  *full_name);
+IDE_AVAILABLE_IN_3_32
+IdeTriplet   *ide_triplet_new_from_system      (void);
+IDE_AVAILABLE_IN_3_32
+IdeTriplet   *ide_triplet_new_with_triplet     (const gchar  *arch,
+                                                const gchar  *kernel,
+                                                const gchar  *operating_system);
+IDE_AVAILABLE_IN_3_32
+IdeTriplet   *ide_triplet_new_with_quadruplet  (const gchar  *arch,
+                                                const gchar  *vendor,
+                                                const gchar  *kernel,
+                                                const gchar  *operating_system);
+IDE_AVAILABLE_IN_3_32
+IdeTriplet   *ide_triplet_ref                  (IdeTriplet   *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_triplet_unref                (IdeTriplet   *self);
+IDE_AVAILABLE_IN_3_32
+const gchar  *ide_triplet_get_full_name        (IdeTriplet   *self);
+IDE_AVAILABLE_IN_3_32
+const gchar  *ide_triplet_get_arch             (IdeTriplet   *self);
+IDE_AVAILABLE_IN_3_32
+const gchar  *ide_triplet_get_vendor           (IdeTriplet   *self);
+IDE_AVAILABLE_IN_3_32
+const gchar  *ide_triplet_get_kernel           (IdeTriplet   *self);
+IDE_AVAILABLE_IN_3_32
+const gchar  *ide_triplet_get_operating_system (IdeTriplet   *self);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_triplet_is_system            (IdeTriplet   *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeTriplet, ide_triplet_unref)
+
+G_END_DECLS
diff --git a/src/libide/foundry/libide-foundry.h b/src/libide/foundry/libide-foundry.h
new file mode 100644
index 000000000..066082321
--- /dev/null
+++ b/src/libide/foundry/libide-foundry.h
@@ -0,0 +1,75 @@
+/* libide-foundry.h"
+ *
+ * Copyright 2018-2019 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-core.h>
+#include <libide-io.h>
+#include <libide-threading.h>
+
+G_BEGIN_DECLS
+
+#define IDE_FOUNDRY_INSIDE
+
+#include "ide-build-log.h"
+#include "ide-build-manager.h"
+#include "ide-build-pipeline-addin.h"
+#include "ide-build-pipeline.h"
+#include "ide-build-stage-launcher.h"
+#include "ide-build-stage-mkdirs.h"
+#include "ide-build-stage-transfer.h"
+#include "ide-build-stage.h"
+#include "ide-build-system-discovery.h"
+#include "ide-build-system.h"
+#include "ide-build-target-provider.h"
+#include "ide-build-target.h"
+#include "ide-configuration.h"
+#include "ide-configuration-manager.h"
+#include "ide-configuration-provider.h"
+#include "ide-compile-commands.h"
+#include "ide-dependency-updater.h"
+#include "ide-deploy-strategy.h"
+#include "ide-device-info.h"
+#include "ide-device-manager.h"
+#include "ide-device-provider.h"
+#include "ide-device.h"
+#include "ide-fallback-build-system.h"
+#include "ide-foundry-compat.h"
+#include "ide-local-device.h"
+#include "ide-run-manager.h"
+#include "ide-runner-addin.h"
+#include "ide-runner.h"
+#include "ide-runtime-manager.h"
+#include "ide-runtime-provider.h"
+#include "ide-runtime.h"
+#include "ide-simple-build-system-discovery.h"
+#include "ide-simple-build-target.h"
+#include "ide-simple-toolchain.h"
+#include "ide-test.h"
+#include "ide-test-manager.h"
+#include "ide-test-provider.h"
+#include "ide-toolchain-manager.h"
+#include "ide-toolchain-provider.h"
+#include "ide-toolchain.h"
+#include "ide-triplet.h"
+
+#undef IDE_FOUNDRY_INSIDE
+
+G_END_DECLS
diff --git a/src/libide/foundry/meson.build b/src/libide/foundry/meson.build
new file mode 100644
index 000000000..173036f03
--- /dev/null
+++ b/src/libide/foundry/meson.build
@@ -0,0 +1,192 @@
+libide_foundry_header_dir = join_paths(libide_header_dir, 'foundry')
+libide_foundry_header_subdir = join_paths(libide_header_subdir, 'foundry')
+libide_include_directories += include_directories('.')
+
+libide_foundry_sources = []
+libide_foundry_public_headers = []
+libide_foundry_generated_headers = []
+
+#
+# Public API Headers
+#
+
+libide_foundry_public_headers = [
+  'ide-build-log.h',
+  'ide-build-manager.h',
+  'ide-build-pipeline-addin.h',
+  'ide-build-pipeline.h',
+  'ide-build-stage-launcher.h',
+  'ide-build-stage-mkdirs.h',
+  'ide-build-stage-transfer.h',
+  'ide-build-stage.h',
+  'ide-build-system-discovery.h',
+  'ide-build-system.h',
+  'ide-build-target-provider.h',
+  'ide-build-target.h',
+  'ide-compile-commands.h',
+  'ide-configuration-manager.h',
+  'ide-configuration-provider.h',
+  'ide-configuration.h',
+  'ide-dependency-updater.h',
+  'ide-deploy-strategy.h',
+  'ide-device-info.h',
+  'ide-device-manager.h',
+  'ide-device-provider.h',
+  'ide-device.h',
+  'ide-fallback-build-system.h',
+  'ide-foundry-types.h',
+  'ide-local-device.h',
+  'ide-run-manager.h',
+  'ide-runner-addin.h',
+  'ide-runner.h',
+  'ide-runtime-manager.h',
+  'ide-runtime-provider.h',
+  'ide-runtime.h',
+  'ide-simple-build-system-discovery.h',
+  'ide-simple-build-target.h',
+  'ide-simple-toolchain.h',
+  'ide-toolchain-manager.h',
+  'ide-toolchain-provider.h',
+  'ide-toolchain.h',
+  'ide-triplet.h',
+  'libide-foundry.h',
+]
+
+libide_foundry_private_headers = [
+  'ide-build-log-private.h',
+  'ide-build-private.h',
+  'ide-build-stage-private.h',
+  'ide-configuration-private.h',
+  'ide-device-private.h',
+  'ide-foundry-init.h',
+  'ide-run-manager-private.h',
+  'ide-runtime-private.h',
+  'ide-toolchain-private.h',
+]
+
+libide_foundry_enum_headers = [
+  'ide-build-log.h',
+  'ide-build-pipeline.h',
+  'ide-configuration.h',
+  'ide-device.h',
+  'ide-device-info.h',
+  'ide-runtime.h',
+  'ide-test.h',
+]
+
+install_headers(libide_foundry_public_headers, subdir: libide_foundry_header_subdir)
+
+#
+# Sources
+#
+
+libide_foundry_public_sources = [
+  'ide-build-manager.c',
+  'ide-build-pipeline-addin.c',
+  'ide-build-pipeline.c',
+  'ide-build-stage-launcher.c',
+  'ide-build-stage-mkdirs.c',
+  'ide-build-stage-transfer.c',
+  'ide-build-stage.c',
+  'ide-build-system-discovery.c',
+  'ide-build-system.c',
+  'ide-build-target-provider.c',
+  'ide-build-target.c',
+  'ide-compile-commands.c',
+  'ide-configuration-manager.c',
+  'ide-configuration-provider.c',
+  'ide-configuration.c',
+  'ide-dependency-updater.c',
+  'ide-deploy-strategy.c',
+  'ide-device-info.c',
+  'ide-device-manager.c',
+  'ide-device-provider.c',
+  'ide-device.c',
+  'ide-fallback-build-system.c',
+  'ide-foundry-compat.c',
+  'ide-local-device.c',
+  'ide-run-manager.c',
+  'ide-runner-addin.c',
+  'ide-runner.c',
+  'ide-runtime-manager.c',
+  'ide-runtime-provider.c',
+  'ide-runtime.c',
+  'ide-simple-build-system-discovery.c',
+  'ide-simple-build-target.c',
+  'ide-simple-toolchain.c',
+  'ide-test.c',
+  'ide-test-manager.c',
+  'ide-test-provider.c',
+  'ide-toolchain-manager.c',
+  'ide-toolchain-provider.c',
+  'ide-toolchain.c',
+  'ide-triplet.c',
+]
+
+
+libide_foundry_private_sources = [
+  'ide-build-log.c',
+  'ide-build-utils.c',
+  'ide-foundry-init.c',
+]
+
+libide_foundry_sources += libide_foundry_public_sources
+libide_foundry_sources += libide_foundry_private_sources
+
+#
+# Enum generation
+#
+
+libide_foundry_enums = gnome.mkenums_simple('ide-foundry-enums',
+     body_prefix: '#include "config.h"',
+   header_prefix: '#include <libide-core.h>',
+       decorator: '_IDE_EXTERN',
+         sources: libide_foundry_enum_headers,
+  install_header: true,
+     install_dir: libide_foundry_header_dir,
+)
+libide_foundry_sources += [libide_foundry_enums[0]]
+libide_foundry_generated_headers += [libide_foundry_enums[1]]
+
+#
+# Dependencies
+#
+
+libide_foundry_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+  libpeas_dep,
+  libvte_dep,
+  libjson_glib_dep,
+
+  libide_core_dep,
+  libide_io_dep,
+  libide_projects_dep,
+  libide_threading_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_foundry = static_library('ide-foundry-' + libide_api_version,
+  libide_foundry_sources, libide_foundry_generated_headers,
+   dependencies: libide_foundry_deps,
+         c_args: libide_args + release_args + ['-DIDE_FOUNDRY_COMPILATION'],
+)
+
+libide_foundry_dep = declare_dependency(
+         dependencies: libide_foundry_deps,
+           link_whole: libide_foundry,
+  include_directories: include_directories('.'),
+              sources: libide_foundry_generated_headers,
+)
+
+gnome_builder_public_sources += files(libide_foundry_public_sources)
+gnome_builder_public_headers += files(libide_foundry_public_headers)
+gnome_builder_private_sources += files(libide_foundry_private_sources)
+gnome_builder_private_headers += files(libide_foundry_private_headers)
+gnome_builder_generated_headers += libide_foundry_generated_headers
+gnome_builder_include_subdirs += libide_foundry_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-foundry.h', '-DIDE_FOUNDRY_COMPILATION']
diff --git a/src/libide/greeter/ide-clone-surface.c b/src/libide/greeter/ide-clone-surface.c
new file mode 100644
index 000000000..aa038842c
--- /dev/null
+++ b/src/libide/greeter/ide-clone-surface.c
@@ -0,0 +1,564 @@
+/* ide-clone-surface.c
+ *
+ * Copyright 2018-2019 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 "ide-clone-surface"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+#include <libide-vcs.h>
+
+#include "ide-clone-surface.h"
+#include "ide-greeter-private.h"
+#include "ide-greeter-workspace.h"
+
+struct _IdeCloneSurface
+{
+  IdeSurface           parent_instance;
+
+  /* This extension set contains IdeVcsCloner implementations which we
+   * use to validate URIs, as well as provide some toggles for how the
+   * user wants to perform the clone operation. Currently, we have a
+   * very limited set of cloning (basically just git), but that could be
+   * expanded in the future based on demand.
+   */
+  PeasExtensionSet    *addins;
+  guint                n_addins;
+
+  /* We calculate the file to the target folder based on the vcs uri and
+   * the destination file chooser. It's cached here so that we don't have
+   * to recaclulate it in multiple code paths.
+   */
+  GFile               *destination;
+
+  /* Template Widgets */
+  DzlFileChooserEntry *destination_chooser;
+  GtkLabel            *destination_label;
+  DzlRadioBox         *kind_radio;
+  GtkLabel            *kind_label;
+  GtkLabel            *status_message;
+  GtkEntry            *uri_entry;
+  GtkEntry            *author_entry;
+  GtkEntry            *email_entry;
+  GtkEntry            *branch_entry;
+  GtkButton           *clone_button;
+  GtkButton           *cancel_button;
+  GtkStack            *button_stack;
+};
+
+G_DEFINE_TYPE (IdeCloneSurface, ide_clone_surface, IDE_TYPE_SURFACE)
+
+enum {
+  PROP_0,
+  PROP_URI,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * ide_clone_surface_new:
+ *
+ * Create a new #IdeCloneSurface.
+ *
+ * Returns: (transfer full): a newly created #IdeCloneSurface
+ *
+ * Since: 3.32
+ */
+IdeCloneSurface *
+ide_clone_surface_new (void)
+{
+  return g_object_new (IDE_TYPE_CLONE_SURFACE, NULL);
+}
+
+static void
+ide_clone_surface_addin_added_cb (PeasExtensionSet *set,
+                                  PeasPluginInfo   *plugin_info,
+                                  PeasExtension    *exten,
+                                  gpointer          user_data)
+{
+  IdeVcsCloner *cloner = (IdeVcsCloner *)exten;
+  IdeCloneSurface *self = user_data;
+  g_autofree gchar *title = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_VCS_CLONER (cloner));
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  self->n_addins++;
+
+  title = ide_vcs_cloner_get_title (cloner);
+
+  dzl_radio_box_add_item (self->kind_radio,
+                          peas_plugin_info_get_module_name (plugin_info),
+                          title);
+
+  if (self->n_addins > 1)
+    {
+      gtk_widget_show (GTK_WIDGET (self->kind_label));
+      gtk_widget_show (GTK_WIDGET (self->kind_radio));
+    }
+}
+
+static void
+ide_clone_surface_addin_removed_cb (PeasExtensionSet *set,
+                                    PeasPluginInfo   *plugin_info,
+                                    PeasExtension    *exten,
+                                    gpointer          user_data)
+{
+  IdeVcsCloner *cloner = (IdeVcsCloner *)exten;
+  IdeCloneSurface *self = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_VCS_CLONER (cloner));
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  self->n_addins--;
+
+  dzl_radio_box_remove_item (self->kind_radio,
+                             peas_plugin_info_get_module_name (plugin_info));
+
+  if (self->n_addins < 2)
+    {
+      gtk_widget_hide (GTK_WIDGET (self->kind_label));
+      gtk_widget_hide (GTK_WIDGET (self->kind_radio));
+    }
+}
+
+static void
+ide_clone_surface_validate_cb (PeasExtensionSet *set,
+                               PeasPluginInfo   *plugin_info,
+                               PeasExtension    *exten,
+                               gpointer          user_data)
+{
+  IdeVcsCloner *cloner = (IdeVcsCloner *)exten;
+  struct {
+    const gchar *text;
+    gchar       *errmsg;
+    gboolean     valid;
+  } *validate = user_data;
+  g_autofree gchar *errmsg = NULL;
+
+  g_assert (IDE_IS_VCS_CLONER (cloner));
+
+  if (validate->valid)
+    return;
+
+  validate->valid = ide_vcs_cloner_validate_uri (cloner, validate->text, &errmsg);
+
+  if (!validate->errmsg)
+    validate->errmsg = g_steal_pointer (&errmsg);
+}
+
+static void
+ide_clone_surface_validate (IdeCloneSurface *self)
+{
+  struct {
+    const gchar *text;
+    gchar       *errmsg;
+    gboolean     valid;
+  } validate;
+
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  validate.text = gtk_entry_get_text (self->uri_entry);
+  validate.errmsg = NULL;
+  validate.valid = FALSE;
+
+  if (self->addins != NULL)
+    peas_extension_set_foreach (self->addins,
+                                ide_clone_surface_validate_cb,
+                                &validate);
+
+  if (validate.valid)
+    dzl_gtk_widget_remove_style_class (GTK_WIDGET (self->uri_entry), "error");
+  else
+    dzl_gtk_widget_add_style_class (GTK_WIDGET (self->uri_entry), "error");
+
+  if (validate.errmsg)
+    gtk_widget_set_tooltip_text (GTK_WIDGET (self->uri_entry), validate.errmsg);
+  else
+    gtk_widget_set_tooltip_text (GTK_WIDGET (self->uri_entry), NULL);
+
+  g_free (validate.errmsg);
+}
+
+static void
+ide_clone_surface_update (IdeCloneSurface *self)
+{
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(GFile) child_file = NULL;
+  g_autoptr(IdeVcsUri) uri = NULL;
+  g_autofree gchar *child = NULL;
+  g_autofree gchar *collapsed = NULL;
+  g_autofree gchar *formatted = NULL;
+  const gchar *text;
+  GtkEntry *entry;
+
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  ide_clone_surface_validate (self);
+
+  file = dzl_file_chooser_entry_get_file (self->destination_chooser);
+  text = gtk_entry_get_text (self->uri_entry);
+  uri = ide_vcs_uri_new (text);
+
+  if (uri != NULL)
+    child = ide_vcs_uri_get_clone_name (uri);
+
+  if (child)
+    child_file = g_file_get_child (file, child);
+  else
+    child_file = g_object_ref (file);
+
+  g_set_object (&self->destination, child_file);
+
+  collapsed = ide_path_collapse (g_file_peek_path (child_file));
+
+  entry = dzl_file_chooser_entry_get_entry (self->destination_chooser);
+
+  if (g_file_query_exists (child_file, NULL))
+    {
+      /* translators: %s is replaced with the path to the project */
+      formatted = g_strdup_printf (_("The directory “%s” already exists. Please choose another directory."),
+                                   collapsed);
+      dzl_gtk_widget_add_style_class (GTK_WIDGET (entry), "error");
+    }
+  else
+    {
+      /* translators: %s is replaced with the path to the project */
+      formatted = g_strdup_printf (_("Your project will be created at %s"), collapsed);
+      dzl_gtk_widget_remove_style_class (GTK_WIDGET (entry), "error");
+    }
+
+  gtk_label_set_label (self->destination_label, formatted);
+}
+
+static void
+ide_clone_surface_uri_entry_changed (IdeCloneSurface *self,
+                                     GtkEntry        *entry)
+{
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  ide_clone_surface_update (self);
+}
+
+static void
+ide_clone_surface_destination_changed (IdeCloneSurface     *self,
+                                       GParamSpec          *pspec,
+                                       DzlFileChooserEntry *chooser)
+{
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+  g_assert (DZL_IS_FILE_CHOOSER_ENTRY (chooser));
+
+  ide_clone_surface_update (self);
+}
+
+static void
+ide_clone_surface_grab_focus (GtkWidget *widget)
+{
+  gtk_widget_grab_focus (GTK_WIDGET (IDE_CLONE_SURFACE (widget)->uri_entry));
+}
+
+static void
+ide_clone_surface_destroy (GtkWidget *widget)
+{
+  IdeCloneSurface *self = (IdeCloneSurface *)widget;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  g_clear_object (&self->addins);
+  g_clear_object (&self->destination);
+
+  GTK_WIDGET_CLASS (ide_clone_surface_parent_class)->destroy (widget);
+}
+
+static void
+ide_clone_surface_constructed (GObject *object)
+{
+  IdeCloneSurface *self = (IdeCloneSurface *)object;
+  g_autoptr(GFile) file = NULL;
+
+  G_OBJECT_CLASS (ide_clone_surface_parent_class)->constructed (object);
+
+  gtk_entry_set_text (self->author_entry, g_get_real_name ());
+
+  file = g_file_new_for_path (ide_get_projects_dir ());
+  dzl_file_chooser_entry_set_file (self->destination_chooser, file);
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_VCS_CLONER,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_clone_surface_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_clone_surface_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_clone_surface_addin_added_cb,
+                              self);
+
+  ide_clone_surface_update (self);
+}
+
+static void
+ide_clone_surface_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeCloneSurface *self = IDE_CLONE_SURFACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_URI:
+      g_value_set_string (value, ide_clone_surface_get_uri (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_clone_surface_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeCloneSurface *self = IDE_CLONE_SURFACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_URI:
+      ide_clone_surface_set_uri (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_clone_surface_class_init (IdeCloneSurfaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = ide_clone_surface_constructed;
+  object_class->get_property = ide_clone_surface_get_property;
+  object_class->set_property = ide_clone_surface_set_property;
+
+  widget_class->destroy = ide_clone_surface_destroy;
+  widget_class->grab_focus = ide_clone_surface_grab_focus;
+
+  /**
+   * IdeCloneSurface:uri:
+   *
+   * The "uri" property is the URI of the version control repository to
+   * be cloned. Usually, this is something like
+   *
+   *   "https://gitlab.gnome.org/GNOME/gnome-builder.git";
+   *
+   * Since: 3.32
+   */
+  properties [PROP_URI] =
+    g_param_spec_string ("uri",
+                         "Uri",
+                         "The URI of the repository to clone.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-clone-surface.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, author_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, branch_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, button_stack);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, cancel_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, clone_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, destination_chooser);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, destination_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, email_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, kind_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, kind_radio);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, status_message);
+  gtk_widget_class_bind_template_child (widget_class, IdeCloneSurface, uri_entry);
+  gtk_widget_class_bind_template_callback (widget_class, ide_clone_surface_clone);
+  gtk_widget_class_bind_template_callback (widget_class, ide_clone_surface_destination_changed);
+  gtk_widget_class_bind_template_callback (widget_class, ide_clone_surface_uri_entry_changed);
+}
+
+static void
+ide_clone_surface_init (IdeCloneSurface *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+const gchar *
+ide_clone_surface_get_uri (IdeCloneSurface *self)
+{
+  g_return_val_if_fail (IDE_IS_CLONE_SURFACE (self), NULL);
+
+  return gtk_entry_get_text (self->uri_entry);
+}
+
+void
+ide_clone_surface_set_uri (IdeCloneSurface *self,
+                           const gchar     *uri)
+{
+  g_return_if_fail (IDE_IS_CLONE_SURFACE (self));
+
+  gtk_entry_set_text (self->uri_entry, uri);
+}
+
+static void
+ide_clone_surface_clone_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeVcsCloner *cloner = (IdeVcsCloner *)object;
+  g_autoptr(IdeCloneSurface) self = user_data;
+  g_autoptr(IdeProjectInfo) project_info = NULL;
+  g_autoptr(GError) error = NULL;
+  GtkWidget *workspace;
+
+  g_assert (IDE_IS_VCS_CLONER (cloner));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_CLONE_SURFACE (self));
+
+  workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GREETER_WORKSPACE);
+  ide_greeter_workspace_end (IDE_GREETER_WORKSPACE (workspace));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->uri_entry), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->destination_chooser), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->clone_button), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->author_entry), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->email_entry), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->branch_entry), TRUE);
+  gtk_stack_set_visible_child (self->button_stack, GTK_WIDGET (self->clone_button));
+  gtk_label_set_label (self->status_message, "");
+
+  if (!ide_vcs_cloner_clone_finish (cloner, result, &error))
+    {
+      g_warning ("Failed to clone repository: %s", error->message);
+      gtk_label_set_label (self->status_message, error->message);
+      gtk_entry_set_progress_fraction (self->uri_entry, 0);
+      return;
+    }
+
+  project_info = ide_project_info_new ();
+  ide_project_info_set_vcs_uri (project_info, gtk_entry_get_text (self->uri_entry));
+  ide_project_info_set_file (project_info, self->destination);
+  ide_project_info_set_directory (project_info, self->destination);
+
+  ide_greeter_workspace_open_project (IDE_GREETER_WORKSPACE (workspace), project_info);
+}
+
+void
+ide_clone_surface_clone (IdeCloneSurface *self)
+{
+  PeasEngine *engine = peas_engine_get_default ();
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(GCancellable) cancellable = NULL;
+  PeasPluginInfo *plugin_info;
+  IdeVcsCloner *addin;
+  GVariantDict dict;
+  const gchar *uri;
+  const gchar *path;
+  const gchar *module_name;
+  const gchar *author;
+  const gchar *email;
+  GtkWidget *workspace;
+
+  g_return_if_fail (IDE_IS_CLONE_SURFACE (self));
+
+  if (!(module_name = dzl_radio_box_get_active_id (self->kind_radio)) ||
+      !(plugin_info = peas_engine_get_plugin_info (engine, module_name)) ||
+      !(addin = (IdeVcsCloner *)peas_extension_set_get_extension (self->addins, plugin_info)))
+    {
+      g_warning ("Failed to locate module to use for cloning");
+      return;
+    }
+
+  g_variant_dict_init (&dict, NULL);
+
+  uri = gtk_entry_get_text (self->uri_entry);
+  author = gtk_entry_get_text (self->author_entry);
+  email = gtk_entry_get_text (self->email_entry);
+  path = g_file_peek_path (self->destination);
+
+  if (!ide_str_empty0 (author) && !g_str_equal (g_get_real_name (), author))
+    g_variant_dict_insert (&dict, "author-name", "s", author);
+
+  if (!ide_str_empty0 (email))
+    g_variant_dict_insert (&dict, "author-email", "s", email);
+
+  g_debug ("Cloning repository using addin: %s", module_name);
+
+  workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GREETER_WORKSPACE);
+  ide_greeter_workspace_begin (IDE_GREETER_WORKSPACE (workspace));
+
+  cancellable = g_cancellable_new ();
+
+  g_signal_connect_object (self->cancel_button,
+                           "clicked",
+                           G_CALLBACK (g_cancellable_cancel),
+                           cancellable,
+                           G_CONNECT_SWAPPED);
+
+  ide_vcs_cloner_clone_async (addin,
+                              uri,
+                              path,
+                              &dict,
+                              cancellable,
+                              &notif,
+                              ide_clone_surface_clone_cb,
+                              g_object_ref (self));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->uri_entry), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->destination_chooser), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->clone_button), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->author_entry), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->email_entry), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->branch_entry), FALSE);
+  gtk_stack_set_visible_child (self->button_stack, GTK_WIDGET (self->cancel_button));
+
+  if (notif != NULL)
+    {
+      g_object_bind_property (notif, "progress", self->uri_entry, "progress-fraction", 
G_BINDING_SYNC_CREATE);
+      g_object_bind_property (notif, "body", self->status_message, "label", G_BINDING_SYNC_CREATE);
+    }
+
+  g_variant_dict_clear (&dict);
+}
diff --git a/src/libide/greeter/ide-clone-surface.h b/src/libide/greeter/ide-clone-surface.h
new file mode 100644
index 000000000..c9d48c87e
--- /dev/null
+++ b/src/libide/greeter/ide-clone-surface.h
@@ -0,0 +1,42 @@
+/* ide-clone-surface.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CLONE_SURFACE (ide_clone_surface_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeCloneSurface, ide_clone_surface, IDE, CLONE_SURFACE, IdeSurface)
+
+IDE_AVAILABLE_IN_3_32
+IdeCloneSurface *ide_clone_surface_new     (void);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_clone_surface_get_uri (IdeCloneSurface *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_clone_surface_set_uri (IdeCloneSurface *self,
+                                            const gchar     *uri);
+IDE_AVAILABLE_IN_3_32
+void             ide_clone_surface_clone   (IdeCloneSurface *self);
+
+G_END_DECLS
diff --git a/src/libide/greeter/ide-clone-surface.ui b/src/libide/greeter/ide-clone-surface.ui
new file mode 100644
index 000000000..716a2f752
--- /dev/null
+++ b/src/libide/greeter/ide-clone-surface.ui
@@ -0,0 +1,383 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.24 -->
+  <template class="IdeCloneSurface" parent="IdeSurface">
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="propagate-natural-width">true</property>
+        <property name="propagate-natural-height">true</property>
+        <property name="hscrollbar-policy">never</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkViewport">
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkBox">
+                <property name="margin">32</property>
+                <property name="orientation">vertical</property>
+                <property name="valign">start</property>
+                <property name="vexpand">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="DzlThreeGrid" id="grid">
+                    <property name="column-spacing">12</property>
+                    <!-- can't use row-spacing because we have to animate in
+                         the revealer which messes up the margins.  -->
+                    <property name="row-spacing">0</property>
+                    <property name="expand">true</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkImage" id="splash">
+                        <property name="valign">end</property>
+                        <property name="vexpand">true</property>
+                        <property name="icon-name">document-save-symbolic</property>
+                        <property name="pixel-size">128</property>
+                        <property name="visible">true</property>
+                        <property name="margin">24</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="row">0</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="repo_label">
+                        <property name="label" translatable="yes">Repository URL</property>
+                        <property name="valign">center</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">1.0</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="row">1</property>
+                        <property name="column">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="uri_entry_help">
+                        <property name="label" translatable="yes">Enter the repository of the project you 
would like to clone. The URL should look similar to 
"https://gitlab.gnome.org/GNOME/gnome-builder.git";.</property>
+                        <property name="margin-top">3</property>
+                        <property name="width-chars">40</property>
+                        <property name="max-width-chars">60</property>
+                        <property name="visible">true</property>
+                        <property name="wrap">true</property>
+                        <property name="xalign">0.0</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                        <attributes>
+                          <attribute name="scale" value="0.833333"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="row">2</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkEntry" id="uri_entry">
+                        <property name="placeholder-text" 
translatable="yes">user@host:repository.git</property>
+                        <property name="width-chars">40</property>
+                        <property name="max-width-chars">50</property>
+                        <property name="valign">center</property>
+                        <property name="visible">true</property>
+                        <signal name="changed" handler="ide_clone_surface_uri_entry_changed" swapped="true" 
object="IdeCloneSurface"/>
+                      </object>
+                      <packing>
+                        <property name="row">1</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkToggleButton" id="more_button">
+                        <property name="halign">start</property>
+                        <property name="hexpand">false</property>
+                        <property name="visible">true</property>
+                        <property name="has-tooltip">true</property>
+                        <property name="tooltip-text" translatable="yes">Select branch and other 
options.</property>
+                        <style>
+                          <class name="flat"/>
+                        </style>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="visible">true</property>
+                            <property name="icon-name">view-more-symbolic</property>
+                            <style>
+                              <class name="image-button"/>
+                            </style>
+                          </object>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="row">1</property>
+                        <property name="column">2</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkRevealer" id="more_revealer">
+                    <property name="reveal-child" bind-source="more_button" bind-property="active"/>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="margin-top">12</property>
+                        <property name="orientation">vertical</property>
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="DzlThreeGrid">
+                            <property name="column-spacing">12</property>
+                            <property name="row-spacing">12</property>
+                            <property name="visible">true</property>
+                            <child>
+                              <object class="GtkLabel" id="kind_label">
+                                <property name="label" translatable="yes">Repository Kind</property>
+                                <property name="visible">false</property>
+                                <property name="xalign">1.0</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                              <packing>
+                                <property name="row">0</property>
+                                <property name="column">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="DzlRadioBox" id="kind_radio">
+                                <property name="visible">false</property>
+                                <property name="valign">center</property>
+                              </object>
+                              <packing>
+                                <property name="row">0</property>
+                                <property name="column">1</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="branch_label">
+                                <property name="label" translatable="yes">Branch</property>
+                                <property name="visible">true</property>
+                                <property name="xalign">1.0</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                              <packing>
+                                <property name="row">1</property>
+                                <property name="column">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkEntry" id="branch_entry">
+                                <property name="visible">true</property>
+                                <property name="valign">center</property>
+                                <property name="text">master</property>
+                              </object>
+                              <packing>
+                                <property name="row">1</property>
+                                <property name="column">1</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="author_label">
+                                <property name="label" translatable="yes">Author Name</property>
+                                <property name="visible">true</property>
+                                <property name="xalign">1.0</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                              <packing>
+                                <property name="row">2</property>
+                                <property name="column">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkEntry" id="author_entry">
+                                <property name="visible">true</property>
+                                <property name="valign">center</property>
+                              </object>
+                              <packing>
+                                <property name="row">2</property>
+                                <property name="column">1</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="email_label">
+                                <property name="label" translatable="yes">Author Email</property>
+                                <property name="visible">true</property>
+                                <property name="xalign">1.0</property>
+                                <property name="valign">center</property>
+                                <style>
+                                  <class name="dim-label"/>
+                                </style>
+                              </object>
+                              <packing>
+                                <property name="row">3</property>
+                                <property name="column">0</property>
+                              </packing>
+                            </child>
+                            <child>
+                              <object class="GtkEntry" id="email_entry">
+                                <property name="visible">true</property>
+                                <property name="valign">center</property>
+                              </object>
+                              <packing>
+                                <property name="row">3</property>
+                                <property name="column">1</property>
+                              </packing>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="DzlThreeGrid">
+                    <property name="margin-top">12</property>
+                    <property name="column-spacing">12</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkLabel" id="dest_label">
+                        <property name="label" translatable="yes">Project Destination</property>
+                        <property name="visible">true</property>
+                        <property name="xalign">1.0</property>
+                        <property name="valign">center</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="row">0</property>
+                        <property name="column">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="DzlFileChooserEntry" id="destination_chooser">
+                        <property name="valign">center</property>
+                        <property name="visible">true</property>
+                        <signal name="notify::file" handler="ide_clone_surface_destination_changed" 
object="IdeCloneSurface" swapped="true"/>
+                      </object>
+                      <packing>
+                        <property name="row">0</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="destination_label">
+                        <property name="visible">true</property>
+                        <property name="xalign">0.0</property>
+                        <property name="valign">center</property>
+                        <property name="margin-top">3</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                        <attributes>
+                          <attribute name="scale" value="0.83333"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="row">2</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="margin-top">32</property>
+                        <property name="visible">true</property>
+                        <property name="orientation">horizontal</property>
+                        <child>
+                          <object class="GtkStack" id="button_stack">
+                            <property name="visible">true</property>
+                            <property name="homogeneous">true</property>
+                            <property name="halign">end</property>
+                            <child>
+                              <object class="GtkButton" id="clone_button">
+                                <property name="label" translatable="yes">Clone Project</property>
+                                <property name="visible">true</property>
+                                <signal name="clicked" handler="ide_clone_surface_clone" 
object="IdeCloneSurface" swapped="true"/>
+                                <style>
+                                  <class name="suggested-action"/>
+                                </style>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkButton" id="cancel_button">
+                                <property name="label" translatable="yes">Cancel</property>
+                                <property name="visible">true</property>
+                                <style>
+                                  <class name="destructive-action"/>
+                                </style>
+                              </object>
+                            </child>
+                          </object>
+                          <packing>
+                            <property name="pack-type">end</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="row">3</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="status_message">
+                        <property name="margin-top">24</property>
+                        <property name="valign">center</property>
+                        <property name="visible">true</property>
+                        <property name="width-chars">50</property>
+                        <property name="max-width-chars">50</property>
+                        <property name="wrap">true</property>
+                        <property name="xalign">0.0</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="row">4</property>
+                        <property name="column">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkSizeGroup">
+    <property name="mode">horizontal</property>
+    <widgets>
+      <widget name="splash"/>
+      <widget name="branch_entry"/>
+      <widget name="author_entry"/>
+      <widget name="email_entry"/>
+      <widget name="uri_entry"/>
+      <widget name="uri_entry_help"/>
+      <widget name="destination_chooser"/>
+      <widget name="kind_radio"/>
+    </widgets>
+  </object>
+  <object class="GtkSizeGroup">
+    <property name="mode">horizontal</property>
+    <widgets>
+      <widget name="repo_label"/>
+      <widget name="dest_label"/>
+      <widget name="author_label"/>
+      <widget name="email_label"/>
+      <widget name="branch_label"/>
+    </widgets>
+  </object>
+</interface>
diff --git a/src/libide/greeter/ide-greeter-private.h b/src/libide/greeter/ide-greeter-private.h
new file mode 100644
index 000000000..1b41ee4d6
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-private.h
@@ -0,0 +1,32 @@
+/* ide-greeter-private.h
+ *
+ * Copyright 2018-2019 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-projects.h>
+
+#include "ide-greeter-workspace.h"
+
+G_BEGIN_DECLS
+
+void _ide_greeter_workspace_init_actions   (IdeGreeterWorkspace *self);
+void _ide_greeter_workspace_init_shortcuts (IdeGreeterWorkspace *self);
+
+G_END_DECLS
diff --git a/src/libide/greeter/ide-greeter-section.c b/src/libide/greeter/ide-greeter-section.c
index 79fcbc7da..6794135d8 100644
--- a/src/libide/greeter/ide-greeter-section.c
+++ b/src/libide/greeter/ide-greeter-section.c
@@ -1,6 +1,6 @@
 /* ide-greeter-section.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-greeter-section"
 
 #include "config.h"
 
-#include "greeter/ide-greeter-section.h"
+#include "ide-greeter-section.h"
 
 G_DEFINE_INTERFACE (IdeGreeterSection, ide_greeter_section, GTK_TYPE_WIDGET)
 
@@ -52,7 +54,7 @@ ide_greeter_section_default_init (IdeGreeterSectionInterface *iface)
    * Use ide_greeter_section_emit_project_activated() to activate
    * this signal.
    *
-   * Since: 3.28
+   * Since: 3.32
    */
   signals [PROJECT_ACTIVATED] =
     g_signal_new ("project-activated",
@@ -72,7 +74,7 @@ ide_greeter_section_default_init (IdeGreeterSectionInterface *iface)
  *
  * Returns: the priority for the section
  *
- * Since: 3.28
+ * Since: 3.32
  */
 gint
 ide_greeter_section_get_priority (IdeGreeterSection *self)
@@ -94,7 +96,7 @@ ide_greeter_section_get_priority (IdeGreeterSection *self)
  *
  * Returns: %TRUE if at least one element matched.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 gboolean
 ide_greeter_section_filter (IdeGreeterSection *self,
@@ -132,7 +134,7 @@ ide_greeter_section_emit_project_activated (IdeGreeterSection *self,
  *
  * Returns: %TRUE if an item was activated
  *
- * Since: 3.28
+ * Since: 3.32
  */
 gboolean
 ide_greeter_section_activate_first (IdeGreeterSection *self)
diff --git a/src/libide/greeter/ide-greeter-section.h b/src/libide/greeter/ide-greeter-section.h
index 8950a3c93..8dc0f939a 100644
--- a/src/libide/greeter/ide-greeter-section.h
+++ b/src/libide/greeter/ide-greeter-section.h
@@ -1,6 +1,6 @@
 /* ide-greeter-section.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,21 +14,21 @@
  *
  * 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 <dazzle.h>
-
-#include "ide-version-macros.h"
-
-#include "projects/ide-project-info.h"
+#include <libide-core.h>
+#include <libide-projects.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_GREETER_SECTION (ide_greeter_section_get_type ())
 
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_INTERFACE (IdeGreeterSection, ide_greeter_section, IDE, GREETER_SECTION, GtkWidget)
 
 struct _IdeGreeterSectionInterface
@@ -47,22 +47,22 @@ struct _IdeGreeterSectionInterface
   void     (*purge_selected)     (IdeGreeterSection *self);
 };
 
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gint     ide_greeter_section_get_priority           (IdeGreeterSection *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean ide_greeter_section_filter                 (IdeGreeterSection *self,
                                                      DzlPatternSpec    *spec);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void     ide_greeter_section_emit_project_activated (IdeGreeterSection *self,
                                                      IdeProjectInfo    *project_info);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean ide_greeter_section_activate_first         (IdeGreeterSection *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void     ide_greeter_section_set_selection_mode     (IdeGreeterSection *self,
                                                      gboolean           selection_mode);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void     ide_greeter_section_delete_selected        (IdeGreeterSection *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void     ide_greeter_section_purge_selected         (IdeGreeterSection *self);
 
 G_END_DECLS
diff --git a/src/libide/greeter/ide-greeter-workspace-actions.c 
b/src/libide/greeter/ide-greeter-workspace-actions.c
new file mode 100644
index 000000000..344a8ca10
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-workspace-actions.c
@@ -0,0 +1,223 @@
+/* ide-greeter-workspace-actions.c
+ *
+ * Copyright 2018-2019 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 "ide-greeter-workspace-actions"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
+#include "ide-greeter-private.h"
+#include "ide-greeter-workspace.h"
+
+static void
+ide_greeter_workspace_dialog_response (IdeGreeterWorkspace  *self,
+                                       gint                  response_id,
+                                       GtkFileChooserDialog *dialog)
+{
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (GTK_IS_FILE_CHOOSER_DIALOG (dialog));
+
+  if (response_id == GTK_RESPONSE_OK)
+    {
+      g_autoptr(IdeProjectInfo) project_info = NULL;
+      g_autoptr(GFile) project_file = NULL;
+
+      project_file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (dialog));
+
+      project_info = ide_project_info_new ();
+      ide_project_info_set_file (project_info, project_file);
+
+      ide_greeter_workspace_open_project (self, project_info);
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+ide_greeter_workspace_dialog_notify_filter (IdeGreeterWorkspace  *self,
+                                            GParamSpec           *pspec,
+                                            GtkFileChooserDialog *dialog)
+{
+  GtkFileFilter *filter;
+  GtkFileChooserAction action;
+
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (pspec != NULL);
+  g_assert (GTK_IS_FILE_CHOOSER_DIALOG (dialog));
+
+  filter = gtk_file_chooser_get_filter (GTK_FILE_CHOOSER (dialog));
+
+  if (filter && g_object_get_data (G_OBJECT (filter), "IS_DIRECTORY"))
+    action = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
+  else
+    action = GTK_FILE_CHOOSER_ACTION_OPEN;
+
+  gtk_file_chooser_set_action (GTK_FILE_CHOOSER (dialog), action);
+}
+
+static void
+ide_greeter_workspace_actions_open (GSimpleAction *action,
+                                    GVariant      *param,
+                                    gpointer       user_data)
+{
+  IdeGreeterWorkspace *self = user_data;
+  GtkFileChooserDialog *dialog;
+  GtkFileFilter *all_filter;
+  const GList *list;
+  gint64 last_priority = G_MAXINT64;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param == NULL);
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  list = peas_engine_get_plugin_list (peas_engine_get_default ());
+
+  dialog = g_object_new (GTK_TYPE_FILE_CHOOSER_DIALOG,
+                         "action", GTK_FILE_CHOOSER_ACTION_OPEN,
+                         "transient-for", self,
+                         "modal", TRUE,
+                         "title", _("Open Project"),
+                         "visible", TRUE,
+                         NULL);
+  gtk_dialog_add_buttons (GTK_DIALOG (dialog),
+                          _("Cancel"), GTK_RESPONSE_CANCEL,
+                          _("Open"), GTK_RESPONSE_OK,
+                          NULL);
+  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK);
+
+  g_signal_connect_object (dialog,
+                           "notify::filter",
+                           G_CALLBACK (ide_greeter_workspace_dialog_notify_filter),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  all_filter = gtk_file_filter_new ();
+  gtk_file_filter_set_name (all_filter, _("All Project Types"));
+  gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), all_filter);
+
+  /* For testing with no plugins */
+  if (list == NULL)
+    gtk_file_filter_add_pattern (all_filter, "*");
+
+  for (; list != NULL; list = list->next)
+    {
+      PeasPluginInfo *plugin_info = list->data;
+      GtkFileFilter *filter;
+      const gchar *pattern;
+      const gchar *content_type;
+      const gchar *name;
+      const gchar *priority;
+      gchar **patterns;
+      gchar **content_types;
+      gint i;
+
+      if (!peas_plugin_info_is_loaded (plugin_info))
+        continue;
+
+      name = peas_plugin_info_get_external_data (plugin_info, "X-Project-File-Filter-Name");
+      if (name == NULL)
+        continue;
+
+      pattern = peas_plugin_info_get_external_data (plugin_info, "X-Project-File-Filter-Pattern");
+      content_type = peas_plugin_info_get_external_data (plugin_info, "X-Project-File-Filter-Content-Type");
+      priority = peas_plugin_info_get_external_data (plugin_info, "X-Project-File-Filter-Priority");
+
+      if (pattern == NULL && content_type == NULL)
+        continue;
+
+      patterns = g_strsplit (pattern ?: "", ",", 0);
+      content_types = g_strsplit (content_type ?: "", ",", 0);
+
+      filter = gtk_file_filter_new ();
+
+      gtk_file_filter_set_name (filter, name);
+
+      for (i = 0; patterns [i] != NULL; i++)
+        {
+          if (*patterns [i])
+            {
+              gtk_file_filter_add_pattern (filter, patterns [i]);
+              gtk_file_filter_add_pattern (all_filter, patterns [i]);
+            }
+        }
+
+      for (i = 0; content_types [i] != NULL; i++)
+        {
+          if (*content_types [i])
+            {
+              gtk_file_filter_add_mime_type (filter, content_types [i]);
+              gtk_file_filter_add_mime_type (all_filter, content_types [i]);
+
+              /* Helper so we can change the file chooser action to OPEN_DIRECTORY,
+               * otherwise the user won't be able to choose a directory, it will
+               * instead dive into the directory.
+               */
+              if (g_strcmp0 (content_types [i], "inode/directory") == 0)
+                g_object_set_data (G_OBJECT (filter), "IS_DIRECTORY", GINT_TO_POINTER (1));
+            }
+        }
+
+      gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
+
+      /* Look at the priority to set the default file filter. */
+      if (priority != NULL)
+        {
+          gint64 pval = g_ascii_strtoll (priority, NULL, 10);
+
+          if (pval < last_priority)
+            {
+              gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), filter);
+              last_priority = pval;
+            }
+        }
+
+      g_strfreev (patterns);
+      g_strfreev (content_types);
+    }
+
+  g_signal_connect_object (dialog,
+                           "response",
+                           G_CALLBACK (ide_greeter_workspace_dialog_response),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* If unset, set the default filter */
+  if (last_priority == G_MAXINT64)
+    gtk_file_chooser_set_filter (GTK_FILE_CHOOSER (dialog), all_filter);
+
+  gtk_file_chooser_set_current_folder (GTK_FILE_CHOOSER (dialog),
+                                       ide_get_projects_dir ());
+
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static const GActionEntry actions[] = {
+  { "open", ide_greeter_workspace_actions_open },
+};
+
+void
+_ide_greeter_workspace_init_actions (IdeGreeterWorkspace *self)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (self), actions, G_N_ELEMENTS (actions), self);
+}
diff --git a/src/libide/greeter/ide-greeter-workspace-shortcuts.c 
b/src/libide/greeter/ide-greeter-workspace-shortcuts.c
new file mode 100644
index 000000000..18a1a9472
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-workspace-shortcuts.c
@@ -0,0 +1,44 @@
+/* ide-greeter-workspace-shortcuts.c
+ *
+ * Copyright 2018-2019 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 "ide-greeter-workspace-shortcuts"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-greeter-private.h"
+#include "ide-greeter-workspace.h"
+
+#define I_(s) (g_intern_static_string(s))
+
+void
+_ide_greeter_workspace_init_shortcuts (IdeGreeterWorkspace *self)
+{
+  DzlShortcutController *controller;
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.greeter.close"),
+                                              "<Primary>w",
+                                              DZL_SHORTCUT_PHASE_DISPATCH,
+                                              I_("win.close"));
+}
diff --git a/src/libide/greeter/ide-greeter-workspace.c b/src/libide/greeter/ide-greeter-workspace.c
new file mode 100644
index 000000000..2a31ce4a7
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-workspace.c
@@ -0,0 +1,808 @@
+/* ide-greeter-workspace.c
+ *
+ * Copyright 2018-2019 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 "ide-greeter-workspace"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
+#include "ide-clone-surface.h"
+#include "ide-greeter-private.h"
+#include "ide-greeter-workspace.h"
+
+/**
+ * SECTION:ide-greeter-workspace
+ * @title: IdeGreeterWorkspace
+ * @short_description: The greeter upon starting Builder
+ *
+ * Use the #IdeWorkspace APIs to add surfaces for user guides such
+ * as the git workflow or project creation wizard.
+ *
+ * You can add buttons to the headerbar and use actions to change
+ * surfaces such as "win.surface::'surface-name'".
+ *
+ * Since: 3.32
+ */
+
+struct _IdeGreeterWorkspace
+{
+  IdeWorkspace       parent_instance;
+
+  PeasExtensionSet  *addins;
+  DzlPatternSpec    *pattern_spec;
+  GSimpleAction     *delete_action;
+  GSimpleAction     *purge_action;
+
+  /* Template Widgets */
+  IdeCloneSurface   *clone_surface;
+  IdeHeaderBar      *header_bar;
+  DzlPriorityBox    *sections;
+  DzlPriorityBox    *left_box;
+  GtkStack          *surfaces;
+  IdeSurface        *sections_surface;
+  GtkSearchEntry    *search_entry;
+  GtkButton         *back_button;
+  GtkButton         *select_button;
+  GtkActionBar      *action_bar;
+
+  guint              selection_mode : 1;
+};
+
+G_DEFINE_TYPE (IdeGreeterWorkspace, ide_greeter_workspace, IDE_TYPE_WORKSPACE)
+
+enum {
+  PROP_0,
+  PROP_SELECTION_MODE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_greeter_workspace_filter_sections (PeasExtensionSet *set,
+                                       PeasPluginInfo   *plugin_info,
+                                       PeasExtension    *exten,
+                                       gpointer          user_data)
+{
+  IdeGreeterWorkspace *self = user_data;
+  IdeGreeterSection *section = (IdeGreeterSection *)exten;
+  gboolean has_child;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_GREETER_SECTION (section));
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  has_child = ide_greeter_section_filter (section, self->pattern_spec);
+
+  gtk_widget_set_visible (GTK_WIDGET (section), has_child);
+}
+
+static void
+ide_greeter_workspace_apply_filter_all (IdeGreeterWorkspace *self)
+{
+  const gchar *text;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  g_clear_pointer (&self->pattern_spec, dzl_pattern_spec_unref);
+
+  if (NULL != (text = gtk_entry_get_text (GTK_ENTRY (self->search_entry))))
+    self->pattern_spec = dzl_pattern_spec_new (text);
+
+  if (self->addins != NULL)
+    peas_extension_set_foreach (self->addins,
+                                ide_greeter_workspace_filter_sections,
+                                self);
+}
+
+static void
+ide_greeter_workspace_activate_cb (GtkWidget *widget,
+                                   gpointer   user_data)
+{
+  gboolean *handled = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (handled != NULL);
+
+  if (!IDE_IS_GREETER_SECTION (widget))
+    return;
+
+  if (!*handled)
+    *handled = ide_greeter_section_activate_first (IDE_GREETER_SECTION (widget));
+}
+
+static void
+ide_greeter_workspace_search_entry_activate (IdeGreeterWorkspace *self,
+                                             GtkSearchEntry      *search_entry)
+{
+  gboolean handled = FALSE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
+
+  gtk_container_foreach (GTK_CONTAINER (self->sections),
+                         ide_greeter_workspace_activate_cb,
+                         &handled);
+
+  if (!handled)
+    gdk_window_beep (gtk_widget_get_window (GTK_WIDGET (search_entry)));
+}
+
+static void
+ide_greeter_workspace_search_entry_changed (IdeGreeterWorkspace *self,
+                                            GtkSearchEntry      *search_entry)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (GTK_IS_SEARCH_ENTRY (search_entry));
+
+  ide_greeter_workspace_apply_filter_all (self);
+}
+
+static void
+stack_notify_visible_child_cb (IdeGreeterWorkspace *self,
+                               GParamSpec          *pspec,
+                               GtkStack            *stack)
+{
+  g_autofree gchar *title = NULL;
+  GtkWidget *visible_child;
+  gboolean sections;
+
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (GTK_IS_STACK (stack));
+
+  visible_child = gtk_stack_get_visible_child (stack);
+
+  if (DZL_IS_DOCK_ITEM (visible_child))
+    title = dzl_dock_item_get_title (DZL_DOCK_ITEM (visible_child));
+
+  gtk_header_bar_set_title (GTK_HEADER_BAR (self->header_bar), title);
+
+  sections = ide_str_equal0 ("sections", gtk_stack_get_visible_child_name (stack));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->left_box), sections);
+  gtk_widget_set_visible (GTK_WIDGET (self->back_button), !sections);
+  gtk_widget_set_visible (GTK_WIDGET (self->select_button), sections);
+}
+
+static void
+ide_greeter_workspace_addin_added_cb (PeasExtensionSet *set,
+                                      PeasPluginInfo   *plugin_info,
+                                      PeasExtension    *exten,
+                                      gpointer          user_data)
+{
+  IdeGreeterSection *section = (IdeGreeterSection *)exten;
+  IdeGreeterWorkspace *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_GREETER_SECTION (section));
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  /* Don't allow floating, to work with extension set*/
+  if (g_object_is_floating (G_OBJECT (section)))
+    g_object_ref_sink (section);
+
+  gtk_widget_show (GTK_WIDGET (section));
+
+  ide_greeter_workspace_add_section (self, section);
+}
+
+static void
+ide_greeter_workspace_addin_removed_cb (PeasExtensionSet *set,
+                                        PeasPluginInfo   *plugin_info,
+                                        PeasExtension    *exten,
+                                        gpointer          user_data)
+{
+  IdeGreeterSection *section = (IdeGreeterSection *)exten;
+  IdeGreeterWorkspace *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_GREETER_SECTION (section));
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  gtk_widget_destroy (GTK_WIDGET (section));
+}
+
+static void
+ide_greeter_workspace_constructed (GObject *object)
+{
+  IdeGreeterWorkspace *self = (IdeGreeterWorkspace *)object;
+
+  G_OBJECT_CLASS (ide_greeter_workspace_parent_class)->constructed (object);
+
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "greeter");
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_GREETER_SECTION,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_greeter_workspace_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_greeter_workspace_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_greeter_workspace_addin_added_cb,
+                              self);
+
+  /* Ensure that no plugin changed our page */
+  ide_workspace_set_visible_surface_name (IDE_WORKSPACE (self), "sections");
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->search_entry));
+}
+
+static void
+ide_greeter_workspace_open_project_cb (GObject      *object,
+                                       GAsyncResult *result,
+                                       gpointer      user_data)
+{
+  IdeWorkbench *workbench = (IdeWorkbench *)object;
+  g_autoptr(IdeGreeterWorkspace) self = (IdeGreeterWorkspace *)user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  if (!ide_workbench_load_project_finish (workbench, result, &error))
+    {
+      GtkWidget *dialog;
+
+      dialog = gtk_message_dialog_new (GTK_WINDOW (workbench),
+                                       GTK_DIALOG_USE_HEADER_BAR,
+                                       GTK_MESSAGE_ERROR,
+                                       GTK_BUTTONS_CLOSE,
+                                       _("Failed to load the project"));
+
+      g_object_set (dialog,
+                    "modal", TRUE,
+                    "secondary-text", error->message,
+                    NULL);
+
+      g_signal_connect (dialog,
+                        "response",
+                        G_CALLBACK (gtk_widget_destroy),
+                        NULL);
+      g_signal_connect_swapped (dialog,
+                                "response",
+                                G_CALLBACK (gtk_widget_destroy),
+                                workbench);
+
+      gtk_window_present (GTK_WINDOW (dialog));
+
+      ide_greeter_workspace_end (self);
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (self));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_greeter_workspace_open_project:
+ * @self: an #IdeGreeterWorkspace
+ * @project_info: an #IdeProjectInfo
+ *
+ * Opens the project described by @project_info.
+ *
+ * This is useful by greeter workspace extensions that add new surfaces
+ * which may not have other means to activate a project.
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_open_project (IdeGreeterWorkspace *self,
+                                    IdeProjectInfo      *project_info)
+{
+  IdeWorkbench *workbench;
+  const gchar *vcs_uri = NULL;
+  GFile *file;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+
+  /* If there is a VCS Uri and no project file/directory, then we want
+   * to switch to the clone dialog. However, we can use the VCS Uri to
+   * determine what the check-out directory would be, and if so, we can
+   * just open that directory.
+   */
+  if (!ide_project_info_get_file (project_info) &&
+      !ide_project_info_get_directory (project_info) &&
+      (vcs_uri = ide_project_info_get_vcs_uri (project_info)))
+    {
+      g_autoptr(IdeVcsUri) uri = ide_vcs_uri_new (vcs_uri);
+      g_autofree gchar *suggested = NULL;
+      g_autofree gchar *checkout = NULL;
+
+      if (uri != NULL &&
+          (suggested = ide_vcs_uri_get_clone_name (uri)) &&
+          (checkout = g_build_filename (ide_get_projects_dir (), suggested, NULL)) &&
+          g_file_test (checkout, G_FILE_TEST_IS_DIR))
+        {
+          g_autoptr(GFile) directory = g_file_new_for_path (checkout);
+          ide_project_info_set_directory (project_info, directory);
+        }
+      else
+        {
+          ide_clone_surface_set_uri (self->clone_surface, vcs_uri);
+          ide_workspace_set_visible_surface_name (IDE_WORKSPACE (self), "clone");
+          return;
+        }
+    }
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+
+  ide_greeter_workspace_begin (self);
+
+  if (ide_project_info_get_directory (project_info) == NULL)
+    {
+      if ((file = ide_project_info_get_file (project_info)))
+        {
+          g_autoptr(GFile) parent = g_file_get_parent (file);
+
+          /* If it's a directory, set that too, otherwise use the parent */
+          if (g_file_query_file_type (file, 0, NULL) == G_FILE_TYPE_DIRECTORY)
+            ide_project_info_set_directory (project_info, file);
+          else
+            ide_project_info_set_directory (project_info, parent);
+        }
+    }
+
+  ide_workbench_load_project_async (workbench,
+                                    project_info,
+                                    IDE_TYPE_PRIMARY_WORKSPACE,
+                                    ide_workspace_get_cancellable (IDE_WORKSPACE (self)),
+                                    ide_greeter_workspace_open_project_cb,
+                                    g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+ide_greeter_workspace_project_activated_cb (IdeGreeterWorkspace *self,
+                                            IdeProjectInfo      *project_info,
+                                            IdeGreeterSection   *section)
+{
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+  g_assert (IDE_IS_GREETER_SECTION (section));
+
+  ide_greeter_workspace_open_project (self, project_info);
+}
+
+static void
+ide_greeter_workspace_delete_selected_rows_cb (GtkWidget *widget,
+                                               gpointer   user_data)
+{
+  if (IDE_IS_GREETER_SECTION (widget))
+    ide_greeter_section_delete_selected (IDE_GREETER_SECTION (widget));
+}
+
+static void
+ide_greeter_workspace_delete_selected_rows (GSimpleAction *action,
+                                            GVariant      *param,
+                                            gpointer       user_data)
+{
+  IdeGreeterWorkspace *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param == NULL);
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  gtk_container_foreach (GTK_CONTAINER (self->sections),
+                         ide_greeter_workspace_delete_selected_rows_cb,
+                         NULL);
+  ide_greeter_workspace_apply_filter_all (self);
+  ide_greeter_workspace_set_selection_mode (self, FALSE);
+}
+
+static void
+ide_greeter_workspace_purge_selected_rows_cb (GtkWidget *widget,
+                                              gpointer   user_data)
+{
+  if (IDE_IS_GREETER_SECTION (widget))
+    ide_greeter_section_purge_selected (IDE_GREETER_SECTION (widget));
+}
+
+static void
+purge_selected_rows_response (IdeGreeterWorkspace *self,
+                              gint                 response,
+                              GtkDialog           *dialog)
+{
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+  g_assert (GTK_IS_DIALOG (dialog));
+
+  if (response == GTK_RESPONSE_OK)
+    {
+      gtk_container_foreach (GTK_CONTAINER (self->sections),
+                             ide_greeter_workspace_purge_selected_rows_cb,
+                             NULL);
+      ide_greeter_workspace_apply_filter_all (self);
+      ide_greeter_workspace_set_selection_mode (self, FALSE);
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (dialog));
+}
+
+static void
+ide_greeter_workspace_purge_selected_rows (GSimpleAction *action,
+                                           GVariant      *param,
+                                           gpointer       user_data)
+{
+  IdeGreeterWorkspace *self = user_data;
+  GtkWidget *parent;
+  GtkWidget *button;
+  GtkDialog *dialog;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param == NULL);
+  g_assert (IDE_IS_GREETER_WORKSPACE (self));
+
+  parent = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);
+  dialog = g_object_new (GTK_TYPE_MESSAGE_DIALOG,
+                         "modal", TRUE,
+                         "transient-for", parent,
+                         "attached-to", parent,
+                         "text", _("Removing project sources will delete them from your computer and cannot 
be undone."),
+                         NULL);
+  gtk_dialog_add_buttons (dialog,
+                          _("Cancel"), GTK_RESPONSE_CANCEL,
+                          _("Delete Project Sources"), GTK_RESPONSE_OK,
+                          NULL);
+  button = gtk_dialog_get_widget_for_response (dialog, GTK_RESPONSE_OK);
+  dzl_gtk_widget_add_style_class (button, "destructive-action");
+  g_signal_connect_data (dialog,
+                         "response",
+                         G_CALLBACK (purge_selected_rows_response),
+                         g_object_ref (self),
+                         (GClosureNotify)g_object_unref,
+                         G_CONNECT_SWAPPED);
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+ide_greeter_workspace_destroy (GtkWidget *widget)
+{
+  IdeGreeterWorkspace *self = (IdeGreeterWorkspace *)widget;
+
+  g_clear_object (&self->addins);
+  g_clear_object (&self->delete_action);
+  g_clear_object (&self->purge_action);
+  g_clear_pointer (&self->pattern_spec, dzl_pattern_spec_unref);
+
+  GTK_WIDGET_CLASS (ide_greeter_workspace_parent_class)->destroy (widget);
+}
+
+static void
+ide_greeter_workspace_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  IdeGreeterWorkspace *self = IDE_GREETER_WORKSPACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_SELECTION_MODE:
+      g_value_set_boolean (value, ide_greeter_workspace_get_selection_mode (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_greeter_workspace_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  IdeGreeterWorkspace *self = IDE_GREETER_WORKSPACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_SELECTION_MODE:
+      ide_greeter_workspace_set_selection_mode (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_greeter_workspace_class_init (IdeGreeterWorkspaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdeWorkspaceClass *workspace_class = IDE_WORKSPACE_CLASS (klass);
+
+  object_class->constructed = ide_greeter_workspace_constructed;
+  object_class->get_property = ide_greeter_workspace_get_property;
+  object_class->set_property = ide_greeter_workspace_set_property;
+
+  widget_class->destroy = ide_greeter_workspace_destroy;
+
+  /**
+   * IdeGreeterWorkspace:selection-mode:
+   *
+   * The "selection-mode" property indicates if the workspace allows
+   * selecting existing projects and removing them, including source files
+   * and cached data.
+   *
+   * This is usually used by the checkmark button to toggle selections.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_SELECTION_MODE] =
+    g_param_spec_boolean ("selection-mode",
+                          "Selection Mode",
+                          "If the workspace is in selection mode",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  ide_workspace_class_set_kind (workspace_class, "greeter");
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/ui/ide-greeter-workspace.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, action_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, back_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, clone_surface);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, header_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, left_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, search_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, select_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, surfaces);
+  gtk_widget_class_bind_template_child (widget_class, IdeGreeterWorkspace, sections);
+  gtk_widget_class_bind_template_callback (widget_class, stack_notify_visible_child_cb);
+
+  g_type_ensure (IDE_TYPE_CLONE_SURFACE);
+}
+
+static void
+ide_greeter_workspace_init (IdeGreeterWorkspace *self)
+{
+  g_autoptr(GPropertyAction) selection_action = NULL;
+  static const GActionEntry actions[] = {
+    { "purge-selected-rows", ide_greeter_workspace_purge_selected_rows },
+    { "delete-selected-rows", ide_greeter_workspace_delete_selected_rows },
+  };
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  selection_action = g_property_action_new ("selection-mode", G_OBJECT (self), "selection-mode");
+  g_action_map_add_action (G_ACTION_MAP (self), G_ACTION (selection_action));
+  g_action_map_add_action_entries (G_ACTION_MAP (self), actions, G_N_ELEMENTS (actions), self);
+
+  g_signal_connect_object (self->search_entry,
+                           "activate",
+                           G_CALLBACK (ide_greeter_workspace_search_entry_activate),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->search_entry,
+                           "changed",
+                           G_CALLBACK (ide_greeter_workspace_search_entry_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  stack_notify_visible_child_cb (self, NULL, self->surfaces);
+
+  _ide_greeter_workspace_init_actions (self);
+  _ide_greeter_workspace_init_shortcuts (self);
+}
+
+IdeGreeterWorkspace *
+ide_greeter_workspace_new (IdeApplication *app)
+{
+  return g_object_new (IDE_TYPE_GREETER_WORKSPACE,
+                       "application", app,
+                       "default-width", 1000,
+                       "default-height", 600,
+                       NULL);
+}
+
+/**
+ * ide_greeter_workspace_add_section:
+ * @self: a #IdeGreeterWorkspace
+ * @section: an #IdeGreeterSection based #GtkWidget
+ *
+ * Adds the #IdeGreeterSection to the display.
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_add_section (IdeGreeterWorkspace *self,
+                                   IdeGreeterSection   *section)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_GREETER_SECTION (section));
+
+  g_signal_connect_object (section,
+                           "project-activated",
+                           G_CALLBACK (ide_greeter_workspace_project_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->sections), GTK_WIDGET (section),
+                                     "priority", ide_greeter_section_get_priority (section),
+                                     NULL);
+}
+
+/**
+ * ide_greeter_workspace_remove_section:
+ * @self: a #IdeGreeterWorkspace
+ * @section: an #IdeGreeterSection based #GtkWidget
+ *
+ * Remvoes the #IdeGreeterSection from the display. This should be a section
+ * that was previously added with ide_greeter_workspace_add_section().
+ *
+ * Plugins should clean up after themselves when they are unloaded, which may
+ * include calling this function.
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_remove_section (IdeGreeterWorkspace *self,
+                                      IdeGreeterSection   *section)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_GREETER_SECTION (section));
+
+  gtk_container_remove (GTK_CONTAINER (self->sections), GTK_WIDGET (section));
+}
+
+void
+ide_greeter_workspace_add_button (IdeGreeterWorkspace *self,
+                                  GtkWidget           *button,
+                                  gint                 priority)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+  g_return_if_fail (GTK_IS_WIDGET (button));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->left_box), button,
+                                     "priority", priority,
+                                     NULL);
+}
+
+/**
+ * ide_greeter_workspace_begin:
+ * @self: a #IdeGreeterWorkspace
+ *
+ * This function will disable various actions and should be called before
+ * an #IdeGreeterAddin begins doing work that cannot be undone except to
+ * cancel the operation.
+ *
+ * Actions such as switching guides will be disabled during this process.
+ *
+ * See ide_greeter_workspace_end() to restore actions.
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_begin (IdeGreeterWorkspace *self)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "win", "open",
+                             "enabled", FALSE,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "win", "surface",
+                             "enabled", FALSE,
+                             NULL);
+}
+
+/**
+ * ide_greeter_workspace_end:
+ * @self: a #IdeGreeterWorkspace
+ *
+ * Restores actions after a call to ide_greeter_workspace_begin().
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_end (IdeGreeterWorkspace *self)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "win", "open",
+                             "enabled", TRUE,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "win", "surface",
+                             "enabled", TRUE,
+                             NULL);
+}
+
+/**
+ * ide_greeter_workspace_get_selection_mode:
+ * @self: a #IdeGreeterWorkspace
+ *
+ * Gets if the greeter is in selection mode, which means that the workspace
+ * allows selecting projects for removal.
+ *
+ * Returns: %TRUE if in selection mode, otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_greeter_workspace_get_selection_mode (IdeGreeterWorkspace *self)
+{
+  g_return_val_if_fail (IDE_IS_GREETER_WORKSPACE (self), FALSE);
+
+  return self->selection_mode;
+}
+
+static void
+ide_greeter_workspace_set_selection_mode_cb (GtkWidget *widget,
+                                             gpointer   user_data)
+{
+  if (IDE_IS_GREETER_SECTION (widget))
+    ide_greeter_section_set_selection_mode (IDE_GREETER_SECTION (widget),
+                                            GPOINTER_TO_INT (user_data));
+}
+
+/**
+ * ide_greeter_workspace_set_selection_mode:
+ * @self: a #IdeGreeterWorkspace
+ * @selection_mode: if the workspace should be in selection mode
+ *
+ * Sets the workspace in selection mode.
+ *
+ * Since: 3.32
+ */
+void
+ide_greeter_workspace_set_selection_mode (IdeGreeterWorkspace *self,
+                                          gboolean             selection_mode)
+{
+  g_return_if_fail (IDE_IS_GREETER_WORKSPACE (self));
+
+  selection_mode = !!selection_mode;
+
+  if (selection_mode != self->selection_mode)
+    {
+      self->selection_mode = selection_mode;
+      gtk_container_foreach (GTK_CONTAINER (self->sections),
+                             ide_greeter_workspace_set_selection_mode_cb,
+                             GINT_TO_POINTER (selection_mode));
+      gtk_widget_set_visible (GTK_WIDGET (self->action_bar), selection_mode);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SELECTION_MODE]);
+    }
+}
diff --git a/src/libide/greeter/ide-greeter-workspace.h b/src/libide/greeter/ide-greeter-workspace.h
new file mode 100644
index 000000000..d3b0dd749
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-workspace.h
@@ -0,0 +1,61 @@
+/* ide-greeter-workspace.h
+ *
+ * Copyright 2018-2019 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-projects.h>
+#include <libide-gui.h>
+
+#include "ide-greeter-section.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_GREETER_WORKSPACE (ide_greeter_workspace_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeGreeterWorkspace, ide_greeter_workspace, IDE, GREETER_WORKSPACE, IdeWorkspace)
+
+IDE_AVAILABLE_IN_3_32
+IdeGreeterWorkspace *ide_greeter_workspace_new                (IdeApplication      *app);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_add_section        (IdeGreeterWorkspace *self,
+                                                               IdeGreeterSection   *section);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_remove_section     (IdeGreeterWorkspace *self,
+                                                               IdeGreeterSection   *section);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_add_button         (IdeGreeterWorkspace *self,
+                                                               GtkWidget           *button,
+                                                               gint                 priority);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_begin              (IdeGreeterWorkspace *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_end                (IdeGreeterWorkspace *self);
+IDE_AVAILABLE_IN_3_32
+gboolean             ide_greeter_workspace_get_selection_mode (IdeGreeterWorkspace *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_set_selection_mode (IdeGreeterWorkspace *self,
+                                                               gboolean             selection_mode);
+IDE_AVAILABLE_IN_3_32
+void                 ide_greeter_workspace_open_project       (IdeGreeterWorkspace *self,
+                                                               IdeProjectInfo      *project_info);
+
+
+G_END_DECLS
diff --git a/src/libide/greeter/ide-greeter-workspace.ui b/src/libide/greeter/ide-greeter-workspace.ui
new file mode 100644
index 000000000..905e14441
--- /dev/null
+++ b/src/libide/greeter/ide-greeter-workspace.ui
@@ -0,0 +1,177 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.24 -->
+  <template class="IdeGreeterWorkspace" parent="IdeWorkspace">
+    <child type="titlebar">
+      <object class="IdeHeaderBar" id="header_bar">
+        <property name="menu-id">ide-greeter-workspace-menu</property>
+        <property name="show-fullscreen-button">false</property>
+        <property name="show-close-button">true</property>
+        <property name="visible">true</property>
+        <child type="left">
+          <object class="GtkButton" id="back_button">
+            <property name="action-name">win.surface</property>
+            <property name="action-target">'sections'</property>
+            <property name="has-tooltip">true</property>
+            <property name="tooltip-text" translatable="yes">Go back</property>
+            <property name="margin-end">6</property>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">true</property>
+                <property name="icon-name">pan-start-symbolic</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="left">
+          <object class="DzlPriorityBox" id="left_box">
+            <property name="spacing">10</property>
+            <property name="hexpand">false</property>
+            <property name="homogeneous">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkButton">
+                <property name="action-name">win.open</property>
+                <property name="label" translatable="yes">_Open…</property>
+                <property name="use-underline">true</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="priority">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="action-name">win.surface</property>
+                <property name="action-target">'clone'</property>
+                <property name="label" translatable="yes">_Clone…</property>
+                <property name="use-underline">true</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="priority">10</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="pack-type">start</property>
+          </packing>
+        </child>
+        <child type="right">
+          <object class="GtkToggleButton" id="select_button">
+            <property name="action-name">win.selection-mode</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="image-button"/>
+            </style>
+            <child>
+              <object class="GtkImage">
+                <property name="icon-name">object-select-symbolic</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child internal-child="surfaces">
+      <object class="GtkStack" id="surfaces">
+        <property name="transition-type">crossfade</property>
+        <signal name="notify::visible-child" handler="stack_notify_visible_child_cb" 
object="IdeGreeterWorkspace" swapped="true"/>
+        <child>
+          <object class="IdeSurface" id="sections_surface">
+            <property name="title" translatable="yes">Select a Project</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="sectionssurface"/>
+            </style>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkScrolledWindow">
+                    <property name="expand">true</property>
+                    <property name="hscrollbar-policy">never</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkViewport">
+                        <property name="expand">true</property>
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="margin">32</property>
+                            <property name="orientation">vertical</property>
+                            <property name="spacing">24</property>
+                            <property name="visible">true</property>
+                            <child>
+                              <object class="GtkSearchEntry" id="search_entry">
+                                <property name="halign">center</property>
+                                <property name="visible">true</property>
+                                <property name="width-chars">45</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="DzlPriorityBox" id="sections">
+                                <property name="orientation">vertical</property>
+                                <property name="spacing">32</property>
+                                <property name="visible">true</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkActionBar" id="action_bar">
+                    <child>
+                      <object class="GtkButton" id="remove_button">
+                        <property name="action-name">win.delete-selected-rows</property>
+                        <property name="label" translatable="yes">_Remove Projects</property>
+                        <property name="use-underline">true</property>
+                        <property name="visible">true</property>
+                        <property name="sensitive">false</property>
+                        <style>
+                          <class name="destructive-action"/>
+                        </style>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="purge_button">
+                        <property name="action-name">win.purge-selected-rows</property>
+                        <property name="label" translatable="yes">Remove Projects and Sources…</property>
+                        <property name="use-underline">true</property>
+                        <property name="visible">true</property>
+                        <property name="sensitive">false</property>
+                        <style>
+                          <class name="destructive-action"/>
+                        </style>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="name">sections</property>
+          </packing>
+        </child>
+        <child>
+          <object class="IdeCloneSurface" id="clone_surface">
+            <property name="title" translatable="yes">Clone Project</property>
+            <property name="visible">true</property>
+          </object>
+          <packing>
+            <property name="name">clone</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/greeter/libide-greeter.gresource.xml b/src/libide/greeter/libide-greeter.gresource.xml
new file mode 100644
index 000000000..002f2abb2
--- /dev/null
+++ b/src/libide/greeter/libide-greeter.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/builder/ui">
+    <file preprocess="xml-stripblanks">ide-clone-surface.ui</file>
+    <file preprocess="xml-stripblanks">ide-greeter-workspace.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/greeter/libide-greeter.h b/src/libide/greeter/libide-greeter.h
new file mode 100644
index 000000000..bce96a180
--- /dev/null
+++ b/src/libide/greeter/libide-greeter.h
@@ -0,0 +1,34 @@
+/* ide-greeter.h
+ *
+ * Copyright 2018-2019 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-core.h>
+#include <libide-gui.h>
+#include <libide-projects.h>
+#include <libide-threading.h>
+
+#define IDE_GREETER_INSIDE
+
+#include "ide-clone-surface.h"
+#include "ide-greeter-section.h"
+#include "ide-greeter-workspace.h"
+
+#undef IDE_GREETER_INSIDE
diff --git a/src/libide/greeter/meson.build b/src/libide/greeter/meson.build
index d74c836fd..68e932c0d 100644
--- a/src/libide/greeter/meson.build
+++ b/src/libide/greeter/meson.build
@@ -1,18 +1,88 @@
-greeter_headers = [
+libide_greeter_header_subdir = join_paths(libide_header_subdir, 'greeter')
+libide_include_directories += include_directories('.')
+
+libide_greeter_generated_headers = []
+
+#
+# Public API Headers
+#
+
+libide_greeter_public_headers = [
+  'ide-clone-surface.h',
   'ide-greeter-section.h',
+  'ide-greeter-workspace.h',
+  'libide-greeter.h',
+]
+
+libide_greeter_private_headers = [
+  'ide-greeter-private.h',
 ]
 
-greeter_sources = [
+install_headers(libide_greeter_public_headers, subdir: libide_greeter_header_subdir)
+
+#
+# Sources
+#
+
+libide_greeter_public_sources = [
+  'ide-clone-surface.c',
   'ide-greeter-section.c',
+  'ide-greeter-workspace.c',
+]
+
+libide_greeter_private_sources = [
+  'ide-greeter-workspace-actions.c',
+  'ide-greeter-workspace-shortcuts.c',
 ]
 
-greeter_private_sources = [
-  'ide-greeter-perspective.c',
-  'ide-greeter-perspective.h',
+#
+# Generated Resource Files
+#
+
+libide_greeter_resources = gnome.compile_resources(
+  'ide-greeter-resources',
+  'libide-greeter.gresource.xml',
+  c_name: 'ide_greeter',
+)
+libide_greeter_generated_headers += [libide_greeter_resources[1]]
+libide_greeter_private_sources += libide_greeter_resources[0]
+
+
+#
+# Dependencies
+#
+
+libide_greeter_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+
+  libide_core_dep,
+  libide_gui_dep,
+  libide_io_dep,
+  libide_threading_dep,
+  libide_vcs_dep,
 ]
 
-libide_public_headers += files(greeter_headers)
-libide_public_sources += files(greeter_sources)
-libide_private_sources += files(greeter_private_sources)
+#
+# Library Definitions
+#
+
+libide_greeter = static_library('ide-greeter-' + libide_api_version,
+   libide_greeter_public_sources + libide_greeter_private_sources,
+   dependencies: libide_greeter_deps,
+         c_args: libide_args + release_args + ['-DIDE_GREETER_COMPILATION'],
+)
+
+libide_greeter_dep = declare_dependency(
+              sources: libide_greeter_private_headers + libide_greeter_generated_headers,
+         dependencies: libide_greeter_deps,
+           link_whole: libide_greeter,
+  include_directories: include_directories('.'),
+)
 
-install_headers(greeter_headers, subdir: join_paths(libide_header_subdir, 'greeter'))
+gnome_builder_public_sources += files(libide_greeter_public_sources)
+gnome_builder_public_headers += files(libide_greeter_public_headers)
+gnome_builder_generated_headers += libide_greeter_generated_headers
+gnome_builder_include_subdirs += libide_greeter_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-greeter.h', '-DIDE_GREETER_COMPILATION']
diff --git a/src/libide/gui/gs-markdown-private.h b/src/libide/gui/gs-markdown-private.h
new file mode 100644
index 000000000..aae18a6aa
--- /dev/null
+++ b/src/libide/gui/gs-markdown-private.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright 2008-2013 Richard Hughes <richard hughsie com>
+ * Copyright 2015 Kalev Lember <klember redhat com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef __GS_MARKDOWN_H
+#define __GS_MARKDOWN_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_MARKDOWN (gs_markdown_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsMarkdown, gs_markdown, GS, MARKDOWN, GObject)
+
+typedef enum {
+        GS_MARKDOWN_OUTPUT_TEXT,
+        GS_MARKDOWN_OUTPUT_PANGO,
+        GS_MARKDOWN_OUTPUT_HTML,
+        GS_MARKDOWN_OUTPUT_LAST
+} GsMarkdownOutputKind;
+
+GsMarkdown      *gs_markdown_new                        (GsMarkdownOutputKind    output);
+void             gs_markdown_set_max_lines              (GsMarkdown             *self,
+                                                         gint                    max_lines);
+void             gs_markdown_set_smart_quoting          (GsMarkdown             *self,
+                                                         gboolean                smart_quoting);
+void             gs_markdown_set_escape                 (GsMarkdown             *self,
+                                                         gboolean                escape);
+void             gs_markdown_set_autocode               (GsMarkdown             *self,
+                                                         gboolean                autocode);
+void             gs_markdown_set_autolinkify            (GsMarkdown             *self,
+                                                         gboolean                autolinkify);
+gchar           *gs_markdown_parse                      (GsMarkdown             *self,
+                                                         const gchar            *text);
+
+G_END_DECLS
+
+#endif /* __GS_MARKDOWN_H */
+
diff --git a/src/libide/gui/gs-markdown.c b/src/libide/gui/gs-markdown.c
new file mode 100644
index 000000000..ed8eb30d7
--- /dev/null
+++ b/src/libide/gui/gs-markdown.c
@@ -0,0 +1,872 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright 2008 Richard Hughes <richard hughsie com>
+ * Copyright 2015 Kalev Lember <klember redhat com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 2 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <glib.h>
+
+#include "gs-markdown-private.h"
+
+/*******************************************************************************
+ *
+ * This is a simple Markdown parser.
+ * It can output to Pango, HTML or plain text. The following limitations are
+ * already known, and properly deliberate:
+ *
+ * - No code section support
+ * - No ordered list support
+ * - No blockquote section support
+ * - No image support
+ * - No links or email support
+ * - No backslash escapes support
+ * - No HTML escaping support
+ * - Auto-escapes certain word patterns, like http://
+ *
+ * It does support the rest of the standard pretty well, although it's not
+ * been run against any conformance tests. The parsing is single pass, with
+ * a simple enumerated interpretor mode and a single line back-memory.
+ *
+ *
+ * Since: 3.32
+ ******************************************************************************/
+
+typedef enum {
+       GS_MARKDOWN_MODE_BLANK,
+       GS_MARKDOWN_MODE_RULE,
+       GS_MARKDOWN_MODE_BULLETT,
+       GS_MARKDOWN_MODE_PARA,
+       GS_MARKDOWN_MODE_H1,
+       GS_MARKDOWN_MODE_H2,
+       GS_MARKDOWN_MODE_UNKNOWN
+} GsMarkdownMode;
+
+typedef struct {
+       const gchar *em_start;
+       const gchar *em_end;
+       const gchar *strong_start;
+       const gchar *strong_end;
+       const gchar *code_start;
+       const gchar *code_end;
+       const gchar *h1_start;
+       const gchar *h1_end;
+       const gchar *h2_start;
+       const gchar *h2_end;
+       const gchar *bullet_start;
+       const gchar *bullet_end;
+       const gchar *rule;
+} GsMarkdownTags;
+
+struct _GsMarkdown {
+       GObject                  parent_instance;
+
+       GsMarkdownMode           mode;
+       GsMarkdownTags           tags;
+       GsMarkdownOutputKind     output;
+       gint                     max_lines;
+       gint                     line_count;
+       gboolean                 smart_quoting;
+       gboolean                 escape;
+       gboolean                 autocode;
+       gboolean                 autolinkify;
+       GString                 *pending;
+       GString                 *processed;
+};
+
+G_DEFINE_TYPE (GsMarkdown, gs_markdown, G_TYPE_OBJECT)
+
+/*
+ * gs_markdown_to_text_line_is_rule:
+ *
+ * Horizontal rules are created by placing three or more hyphens, asterisks,
+ * or underscores on a line by themselves.
+ * You may use spaces between the hyphens or asterisks.
+ **/
+static gboolean
+gs_markdown_to_text_line_is_rule (const gchar *line)
+{
+       guint i;
+       guint len;
+       guint count = 0;
+       g_autofree gchar *copy = NULL;
+
+       len = (guint) strlen (line);
+       if (len == 0)
+               return FALSE;
+
+       /* replace non-rule chars with ~ */
+       copy = g_strdup (line);
+       g_strcanon (copy, "-*_ ", '~');
+       for (i = 0; i < len; i++) {
+               if (copy[i] == '~')
+                       return FALSE;
+               if (copy[i] != ' ')
+                       count++;
+       }
+
+       /* if we matched, return true */
+       if (count >= 3)
+               return TRUE;
+       return FALSE;
+}
+
+static gboolean
+gs_markdown_to_text_line_is_bullet (const gchar *line)
+{
+       return (g_str_has_prefix (line, "- ") ||
+               g_str_has_prefix (line, "* ") ||
+               g_str_has_prefix (line, "+ ") ||
+               g_str_has_prefix (line, " - ") ||
+               g_str_has_prefix (line, " * ") ||
+               g_str_has_prefix (line, " + "));
+}
+
+static gboolean
+gs_markdown_to_text_line_is_header1 (const gchar *line)
+{
+       return g_str_has_prefix (line, "# ");
+}
+
+static gboolean
+gs_markdown_to_text_line_is_header2 (const gchar *line)
+{
+       return g_str_has_prefix (line, "## ");
+}
+
+static gboolean
+gs_markdown_to_text_line_is_header1_type2 (const gchar *line)
+{
+       return g_str_has_prefix (line, "===");
+}
+
+static gboolean
+gs_markdown_to_text_line_is_header2_type2 (const gchar *line)
+{
+       return g_str_has_prefix (line, "---");
+}
+
+#if 0
+static gboolean
+gs_markdown_to_text_line_is_code (const gchar *line)
+{
+       return (g_str_has_prefix (line, "    ") ||
+               g_str_has_prefix (line, "\t"));
+}
+
+static gboolean
+gs_markdown_to_text_line_is_blockquote (const gchar *line)
+{
+       return (g_str_has_prefix (line, "> "));
+}
+#endif
+
+static gboolean
+gs_markdown_to_text_line_is_blank (const gchar *line)
+{
+       guint i;
+       guint len;
+
+       /* a line with no characters is blank by definition */
+       len = (guint) strlen (line);
+       if (len == 0)
+               return TRUE;
+
+       /* find if there are only space chars */
+       for (i = 0; i < len; i++) {
+               if (line[i] != ' ' && line[i] != '\t')
+                       return FALSE;
+       }
+
+       /* if we matched, return true */
+       return TRUE;
+}
+
+static gchar *
+gs_markdown_replace (const gchar *haystack,
+                    const gchar *needle,
+                    const gchar *replace)
+{
+       g_auto(GStrv) split = NULL;
+       split = g_strsplit (haystack, needle, -1);
+       return g_strjoinv (replace, split);
+}
+
+static gchar *
+gs_markdown_strstr_spaces (const gchar *haystack, const gchar *needle)
+{
+       gchar *found;
+       const gchar *haystack_new = haystack;
+
+retry:
+       /* don't find if surrounded by spaces */
+       found = strstr (haystack_new, needle);
+       if (found == NULL)
+               return NULL;
+
+       /* start of the string, always valid */
+       if (found == haystack)
+               return found;
+
+       /* end of the string, always valid */
+       if (*(found-1) == ' ' && *(found+1) == ' ') {
+               haystack_new = found+1;
+               goto retry;
+       }
+       return found;
+}
+
+static gchar *
+gs_markdown_to_text_line_formatter (const gchar *line,
+                                   const gchar *formatter,
+                                   const gchar *left,
+                                   const gchar *right)
+{
+       guint len;
+       gchar *str1;
+       gchar *str2;
+       gchar *start = NULL;
+       gchar *middle = NULL;
+       gchar *end = NULL;
+       g_autofree gchar *copy = NULL;
+
+       /* needed to know for shifts */
+       len = (guint) strlen (formatter);
+       if (len == 0)
+               return NULL;
+
+       /* find sections */
+       copy = g_strdup (line);
+       str1 = gs_markdown_strstr_spaces (copy, formatter);
+       if (str1 != NULL) {
+               *str1 = '\0';
+               str2 = gs_markdown_strstr_spaces (str1+len, formatter);
+               if (str2 != NULL) {
+                       *str2 = '\0';
+                       middle = str1 + len;
+                       start = copy;
+                       end = str2 + len;
+               }
+       }
+
+       /* if we found, replace and keep looking for the same string */
+       if (start != NULL && middle != NULL && end != NULL) {
+               g_autofree gchar *temp = NULL;
+               temp = g_strdup_printf ("%s%s%s%s%s", start, left, middle, right, end);
+               /* recursive */
+               return gs_markdown_to_text_line_formatter (temp, formatter, left, right);
+       }
+
+       /* not found, keep return as-is */
+       return g_strdup (line);
+}
+
+static gchar *
+gs_markdown_to_text_line_format_sections (GsMarkdown *self, const gchar *line)
+{
+       gchar *data = g_strdup (line);
+       gchar *temp;
+
+       /* bold1 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "**",
+                                                  self->tags.strong_start,
+                                                  self->tags.strong_end);
+       g_free (temp);
+
+       /* bold2 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "__",
+                                                  self->tags.strong_start,
+                                                  self->tags.strong_end);
+       g_free (temp);
+
+       /* italic1 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "*",
+                                                  self->tags.em_start,
+                                                  self->tags.em_end);
+       g_free (temp);
+
+       /* italic2 */
+       temp = data;
+       data = gs_markdown_to_text_line_formatter (temp, "_",
+                                                  self->tags.em_start,
+                                                  self->tags.em_end);
+       g_free (temp);
+
+       /* em-dash */
+       temp = data;
+       data = gs_markdown_replace (temp, " -- ", " — ");
+       g_free (temp);
+
+       /* smart quoting */
+       if (self->smart_quoting) {
+               temp = data;
+               data = gs_markdown_to_text_line_formatter (temp, "\"", "“", "”");
+               g_free (temp);
+
+               temp = data;
+               data = gs_markdown_to_text_line_formatter (temp, "'", "‘", "’");
+               g_free (temp);
+       }
+
+       return data;
+}
+
+static gchar *
+gs_markdown_to_text_line_format (GsMarkdown *self, const gchar *line)
+{
+       GString *string;
+       gboolean mode = FALSE;
+       gchar *text;
+       guint i;
+       g_auto(GStrv) codes = NULL;
+
+       /* optimise the trivial case where we don't have any code tags */
+       text = strstr (line, "`");
+       if (text == NULL)
+               return gs_markdown_to_text_line_format_sections (self, line);
+
+       /* we want to parse the code sections without formatting */
+       codes = g_strsplit (line, "`", -1);
+       string = g_string_new ("");
+       for (i = 0; codes[i] != NULL; i++) {
+               if (!mode) {
+                       text = gs_markdown_to_text_line_format_sections (self, codes[i]);
+                       g_string_append (string, text);
+                       g_free (text);
+                       mode = TRUE;
+               } else {
+                       /* just append without formatting */
+                       g_string_append (string, self->tags.code_start);
+                       g_string_append (string, codes[i]);
+                       g_string_append (string, self->tags.code_end);
+                       mode = FALSE;
+               }
+       }
+       return g_string_free (string, FALSE);
+}
+
+static gboolean
+gs_markdown_add_pending (GsMarkdown *self, const gchar *line)
+{
+       g_autofree gchar *copy = NULL;
+
+       /* would put us over the limit */
+       if (self->max_lines > 0 && self->line_count >= self->max_lines)
+               return FALSE;
+
+       copy = g_strdup (line);
+
+       /* strip leading and trailing spaces */
+       g_strstrip (copy);
+
+       /* append */
+       g_string_append_printf (self->pending, "%s ", copy);
+       return TRUE;
+}
+
+static gboolean
+gs_markdown_add_pending_header (GsMarkdown *self, const gchar *line)
+{
+       g_autofree gchar *copy = NULL;
+
+       /* strip trailing # */
+       copy = g_strdup (line);
+       g_strdelimit (copy, "#", ' ');
+       return gs_markdown_add_pending (self, copy);
+}
+
+static guint
+gs_markdown_count_chars_in_word (const gchar *text, gchar find)
+{
+       guint i;
+       guint len;
+       guint count = 0;
+
+       /* get length */
+       len = (guint) strlen (text);
+       if (len == 0)
+               return 0;
+
+       /* find matching chars */
+       for (i = 0; i < len; i++) {
+               if (text[i] == find)
+                       count++;
+       }
+       return count;
+}
+
+static gboolean
+gs_markdown_word_is_code (const gchar *text)
+{
+       /* already code */
+       if (g_str_has_prefix (text, "`"))
+               return FALSE;
+       if (g_str_has_suffix (text, "`"))
+               return FALSE;
+
+       /* paths */
+       if (g_str_has_prefix (text, "/"))
+               return TRUE;
+
+       /* bugzillas */
+       if (g_str_has_prefix (text, "#"))
+               return TRUE;
+
+       /* patch files */
+       if (g_strrstr (text, ".patch") != NULL)
+               return TRUE;
+       if (g_strrstr (text, ".diff") != NULL)
+               return TRUE;
+
+       /* function names */
+       if (g_strrstr (text, "()") != NULL)
+               return TRUE;
+
+       /* email addresses */
+       if (g_strrstr (text, "@") != NULL)
+               return TRUE;
+
+       /* compiler defines */
+       if (text[0] != '_' &&
+           gs_markdown_count_chars_in_word (text, '_') > 1)
+               return TRUE;
+
+       /* nothing special */
+       return FALSE;
+}
+
+static gchar *
+gs_markdown_word_auto_format_code (const gchar *text)
+{
+       guint i;
+       gchar *temp;
+       gboolean ret = FALSE;
+       g_auto(GStrv) words = NULL;
+
+       /* split sentence up with space */
+       words = g_strsplit (text, " ", -1);
+
+       /* search each word */
+       for (i = 0; words[i] != NULL; i++) {
+               if (gs_markdown_word_is_code (words[i])) {
+                       temp = g_strdup_printf ("`%s`", words[i]);
+                       g_free (words[i]);
+                       words[i] = temp;
+                       ret = TRUE;
+               }
+       }
+
+       /* no replacements, so just return a copy */
+       if (!ret)
+               return g_strdup (text);
+
+       /* join the array back into a string */
+       return g_strjoinv (" ", words);
+}
+
+static gboolean
+gs_markdown_word_is_url (const gchar *text)
+{
+       if (g_str_has_prefix (text, "http://";))
+               return TRUE;
+       if (g_str_has_prefix (text, "https://";))
+               return TRUE;
+       if (g_str_has_prefix (text, "ftp://";))
+               return TRUE;
+       return FALSE;
+}
+
+static gchar *
+gs_markdown_word_auto_format_urls (const gchar *text)
+{
+       guint i;
+       gchar *temp;
+       gboolean ret = FALSE;
+       g_auto(GStrv) words = NULL;
+
+       /* split sentence up with space */
+       words = g_strsplit (text, " ", -1);
+
+       /* search each word */
+       for (i = 0; words[i] != NULL; i++) {
+               if (gs_markdown_word_is_url (words[i])) {
+                       temp = g_strdup_printf ("<a href=\"%s\">%s</a>",
+                                               words[i], words[i]);
+                       g_free (words[i]);
+                       words[i] = temp;
+                       ret = TRUE;
+               }
+       }
+
+       /* no replacements, so just return a copy */
+       if (!ret)
+               return g_strdup (text);
+
+       /* join the array back into a string */
+       return g_strjoinv (" ", words);
+}
+
+static void
+gs_markdown_flush_pending (GsMarkdown *self)
+{
+       g_autofree gchar *copy = NULL;
+       g_autofree gchar *temp = NULL;
+
+       /* no data yet */
+       if (self->mode == GS_MARKDOWN_MODE_UNKNOWN)
+               return;
+
+       /* remove trailing spaces */
+       while (g_str_has_suffix (self->pending->str, " "))
+               g_string_set_size (self->pending, self->pending->len - 1);
+
+       /* pango requires escaping */
+       copy = g_strdup (self->pending->str);
+       if (!self->escape && self->output == GS_MARKDOWN_OUTPUT_PANGO) {
+               g_strdelimit (copy, "<", '(');
+               g_strdelimit (copy, ">", ')');
+               g_strdelimit (copy, "&", '+');
+       }
+
+       /* check words for code */
+       if (self->autocode &&
+           (self->mode == GS_MARKDOWN_MODE_PARA ||
+            self->mode == GS_MARKDOWN_MODE_BULLETT)) {
+               temp = gs_markdown_word_auto_format_code (copy);
+               g_free (copy);
+               copy = temp;
+       }
+
+       /* escape */
+       if (self->escape) {
+               temp = g_markup_escape_text (copy, -1);
+               g_free (copy);
+               copy = temp;
+       }
+
+       /* check words for URLS */
+       if (self->autolinkify &&
+           self->output == GS_MARKDOWN_OUTPUT_PANGO &&
+           (self->mode == GS_MARKDOWN_MODE_PARA ||
+            self->mode == GS_MARKDOWN_MODE_BULLETT)) {
+               temp = gs_markdown_word_auto_format_urls (copy);
+               g_free (copy);
+               copy = temp;
+       }
+
+       /* do formatting */
+       temp = gs_markdown_to_text_line_format (self, copy);
+       if (self->mode == GS_MARKDOWN_MODE_BULLETT) {
+               g_string_append_printf (self->processed, "%s%s%s\n",
+                                       self->tags.bullet_start,
+                                       temp,
+                                       self->tags.bullet_end);
+               self->line_count++;
+       } else if (self->mode == GS_MARKDOWN_MODE_H1) {
+               g_string_append_printf (self->processed, "%s%s%s\n",
+                                       self->tags.h1_start,
+                                       temp,
+                                       self->tags.h1_end);
+       } else if (self->mode == GS_MARKDOWN_MODE_H2) {
+               g_string_append_printf (self->processed, "%s%s%s\n",
+                                       self->tags.h2_start,
+                                       temp,
+                                       self->tags.h2_end);
+       } else if (self->mode == GS_MARKDOWN_MODE_PARA ||
+                  self->mode == GS_MARKDOWN_MODE_RULE) {
+               g_string_append_printf (self->processed, "%s\n", temp);
+               self->line_count++;
+       }
+
+       /* clear */
+       g_string_truncate (self->pending, 0);
+}
+
+static gboolean
+gs_markdown_to_text_line_process (GsMarkdown *self, const gchar *line)
+{
+       gboolean ret;
+
+       /* blank */
+       ret = gs_markdown_to_text_line_is_blank (line);
+       if (ret) {
+               gs_markdown_flush_pending (self);
+               /* a new line after a list is the end of list, not a gap */
+               if (self->mode != GS_MARKDOWN_MODE_BULLETT)
+                       ret = gs_markdown_add_pending (self, "\n");
+               self->mode = GS_MARKDOWN_MODE_BLANK;
+               goto out;
+       }
+
+       /* header1_type2 */
+       ret = gs_markdown_to_text_line_is_header1_type2 (line);
+       if (ret) {
+               if (self->mode == GS_MARKDOWN_MODE_PARA)
+                       self->mode = GS_MARKDOWN_MODE_H1;
+               goto out;
+       }
+
+       /* header2_type2 */
+       ret = gs_markdown_to_text_line_is_header2_type2 (line);
+       if (ret) {
+               if (self->mode == GS_MARKDOWN_MODE_PARA)
+                       self->mode = GS_MARKDOWN_MODE_H2;
+               goto out;
+       }
+
+       /* rule */
+       ret = gs_markdown_to_text_line_is_rule (line);
+       if (ret) {
+               gs_markdown_flush_pending (self);
+               self->mode = GS_MARKDOWN_MODE_RULE;
+               ret = gs_markdown_add_pending (self, self->tags.rule);
+               goto out;
+       }
+
+       /* bullet */
+       ret = gs_markdown_to_text_line_is_bullet (line);
+       if (ret) {
+               gs_markdown_flush_pending (self);
+               self->mode = GS_MARKDOWN_MODE_BULLETT;
+               ret = gs_markdown_add_pending (self, &line[2]);
+               goto out;
+       }
+
+       /* header1 */
+       ret = gs_markdown_to_text_line_is_header1 (line);
+       if (ret) {
+               gs_markdown_flush_pending (self);
+               self->mode = GS_MARKDOWN_MODE_H1;
+               ret = gs_markdown_add_pending_header (self, &line[2]);
+               goto out;
+       }
+
+       /* header2 */
+       ret = gs_markdown_to_text_line_is_header2 (line);
+       if (ret) {
+               gs_markdown_flush_pending (self);
+               self->mode = GS_MARKDOWN_MODE_H2;
+               ret = gs_markdown_add_pending_header (self, &line[3]);
+               goto out;
+       }
+
+       /* paragraph */
+       if (self->mode == GS_MARKDOWN_MODE_BLANK ||
+           self->mode == GS_MARKDOWN_MODE_UNKNOWN) {
+               gs_markdown_flush_pending (self);
+               self->mode = GS_MARKDOWN_MODE_PARA;
+       }
+
+       /* add to pending */
+       ret = gs_markdown_add_pending (self, line);
+out:
+       /* if we failed to add, we don't know the mode */
+       if (!ret)
+               self->mode = GS_MARKDOWN_MODE_UNKNOWN;
+       return ret;
+}
+
+static void
+gs_markdown_set_output_kind (GsMarkdown *self, GsMarkdownOutputKind output)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+
+       self->output = output;
+       switch (output) {
+       case GS_MARKDOWN_OUTPUT_PANGO:
+               /* PangoMarkup */
+               self->tags.em_start = "<i>";
+               self->tags.em_end = "</i>";
+               self->tags.strong_start = "<b>";
+               self->tags.strong_end = "</b>";
+               self->tags.code_start = "<tt>";
+               self->tags.code_end = "</tt>";
+               self->tags.h1_start = "<big>";
+               self->tags.h1_end = "</big>";
+               self->tags.h2_start = "<b>";
+               self->tags.h2_end = "</b>";
+               self->tags.bullet_start = "• ";
+               self->tags.bullet_end = "";
+               self->tags.rule = "⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯\n";
+               self->escape = TRUE;
+               self->autolinkify = TRUE;
+               break;
+       case GS_MARKDOWN_OUTPUT_HTML:
+               /* XHTML */
+               self->tags.em_start = "<em>";
+               self->tags.em_end = "<em>";
+               self->tags.strong_start = "<strong>";
+               self->tags.strong_end = "</strong>";
+               self->tags.code_start = "<code>";
+               self->tags.code_end = "</code>";
+               self->tags.h1_start = "<h1>";
+               self->tags.h1_end = "</h1>";
+               self->tags.h2_start = "<h2>";
+               self->tags.h2_end = "</h2>";
+               self->tags.bullet_start = "<li>";
+               self->tags.bullet_end = "</li>";
+               self->tags.rule = "<hr>";
+               self->escape = TRUE;
+               self->autolinkify = TRUE;
+               break;
+       case GS_MARKDOWN_OUTPUT_TEXT:
+               /* plain text */
+               self->tags.em_start = "";
+               self->tags.em_end = "";
+               self->tags.strong_start = "";
+               self->tags.strong_end = "";
+               self->tags.code_start = "";
+               self->tags.code_end = "";
+               self->tags.h1_start = "[";
+               self->tags.h1_end = "]";
+               self->tags.h2_start = "-";
+               self->tags.h2_end = "-";
+               self->tags.bullet_start = "* ";
+               self->tags.bullet_end = "";
+               self->tags.rule = " ----- \n";
+               self->escape = FALSE;
+               self->autolinkify = FALSE;
+               break;
+  case GS_MARKDOWN_OUTPUT_LAST:
+       default:
+               g_warning ("unknown output enum");
+               break;
+       }
+}
+
+void
+gs_markdown_set_max_lines (GsMarkdown *self, gint max_lines)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+       self->max_lines = max_lines;
+}
+
+void
+gs_markdown_set_smart_quoting (GsMarkdown *self, gboolean smart_quoting)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+       self->smart_quoting = smart_quoting;
+}
+
+void
+gs_markdown_set_escape (GsMarkdown *self, gboolean escape)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+       self->escape = escape;
+}
+
+void
+gs_markdown_set_autocode (GsMarkdown *self, gboolean autocode)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+       self->autocode = autocode;
+}
+
+void
+gs_markdown_set_autolinkify (GsMarkdown *self, gboolean autolinkify)
+{
+       g_return_if_fail (GS_IS_MARKDOWN (self));
+       self->autolinkify = autolinkify;
+}
+
+gchar *
+gs_markdown_parse (GsMarkdown *self, const gchar *markdown)
+{
+       gboolean ret;
+       gchar *temp;
+       guint i;
+       guint len;
+       g_auto(GStrv) lines = NULL;
+
+       g_return_val_if_fail (GS_IS_MARKDOWN (self), NULL);
+
+       /* process */
+       self->mode = GS_MARKDOWN_MODE_UNKNOWN;
+       self->line_count = 0;
+       g_string_truncate (self->pending, 0);
+       g_string_truncate (self->processed, 0);
+       lines = g_strsplit (markdown, "\n", -1);
+       len = g_strv_length (lines);
+
+       /* process each line */
+       for (i = 0; i < len; i++) {
+               ret = gs_markdown_to_text_line_process (self, lines[i]);
+               if (!ret)
+                       break;
+       }
+       gs_markdown_flush_pending (self);
+
+       /* remove trailing \n */
+       while (g_str_has_suffix (self->processed->str, "\n"))
+               g_string_set_size (self->processed, self->processed->len - 1);
+
+       /* get a copy */
+       temp = g_strdup (self->processed->str);
+       g_string_truncate (self->pending, 0);
+       g_string_truncate (self->processed, 0);
+       return temp;
+}
+
+static void
+gs_markdown_finalize (GObject *object)
+{
+       GsMarkdown *self;
+
+       g_return_if_fail (GS_IS_MARKDOWN (object));
+
+       self = GS_MARKDOWN (object);
+
+       g_string_free (self->pending, TRUE);
+       g_string_free (self->processed, TRUE);
+
+       G_OBJECT_CLASS (gs_markdown_parent_class)->finalize (object);
+}
+
+static void
+gs_markdown_class_init (GsMarkdownClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = gs_markdown_finalize;
+}
+
+static void
+gs_markdown_init (GsMarkdown *self)
+{
+       self->mode = GS_MARKDOWN_MODE_UNKNOWN;
+       self->pending = g_string_new ("");
+       self->processed = g_string_new ("");
+       self->max_lines = -1;
+       self->smart_quoting = FALSE;
+       self->escape = FALSE;
+       self->autocode = FALSE;
+}
+
+GsMarkdown *
+gs_markdown_new (GsMarkdownOutputKind output)
+{
+       GsMarkdown *self;
+       self = g_object_new (GS_TYPE_MARKDOWN, NULL);
+       gs_markdown_set_output_kind (self, output);
+       return GS_MARKDOWN (self);
+}
diff --git a/src/libide/gui/gtk/menus.ui b/src/libide/gui/gtk/menus.ui
new file mode 100644
index 000000000..99e8390ef
--- /dev/null
+++ b/src/libide/gui/gtk/menus.ui
@@ -0,0 +1,86 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-primary-workspace-surfaces-menu">
+    <section id="ide-primary-workspace-surfaces-menu-section">
+      <attribute name="label" translatable="yes">Switch Surface</attribute>
+    </section>
+  </menu>
+  <menu id="ide-primary-workspace-menu">
+    <section id="ide-primary-workspace-menu-projects-section"/>
+    <section id="ide-primary-workspace-menu-placeholder1"/>
+    <section id="ide-primary-workspace-menu-open-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-open</attribute>
+        <attribute name="label" translatable="yes">Open File…</attribute>
+        <attribute name="action">workbench.open</attribute>
+        <attribute name="accel">&lt;primary&gt;o</attribute>
+      </item>
+    </section>
+    <section id="ide-primary-workspace-menu-placeholder2"/>
+    <section id="ide-primary-workspace-menu-close-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-close-project</attribute>
+        <attribute name="label" translatable="yes">Close Project</attribute>
+        <attribute name="action">workbench.close</attribute>
+      </item>
+    </section>
+    <section id="ide-primary-workspace-menu-placeholder3"/>
+    <section id="ide-primary-workspace-menu-app-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-preferences</attribute>
+        <attribute name="label" translatable="yes">Preferences</attribute>
+        <attribute name="action">app.preferences</attribute>
+        <attribute name="accel">&lt;primary&gt;comma</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-shortcuts</attribute>
+        <attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
+        <attribute name="action">app.shortcuts</attribute>
+        <attribute name="accel">&lt;primary&gt;question</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-help</attribute>
+        <attribute name="label" translatable="yes">Help</attribute>
+        <attribute name="action">app.help</attribute>
+        <attribute name="accel">F1</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-about</attribute>
+        <attribute name="label" translatable="yes">About Builder</attribute>
+        <attribute name="action">app.about</attribute>
+      </item>
+    </section>
+    <section id="ide-primary-workspace-menu-quit-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-quit</attribute>
+        <attribute name="label" translatable="yes">_Quit</attribute>
+        <attribute name="action">app.quit</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-primary-workspace-new-menu">
+    <section id="new-document-section">
+    </section>
+    <section id="open-document-section">
+      <item>
+        <attribute name="id">open-file</attribute>
+        <attribute name="label" translatable="yes">Open File…</attribute>
+        <attribute name="action">workbench.open</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="run-menu">
+    <section id="run-menu-section">
+      <attribute name="label" translatable="yes">Run Options</attribute>
+      <item>
+        <attribute name="id">default-run-handler</attribute>
+        <attribute name="action">run-manager.run-with-handler</attribute>
+        <attribute name="target">run</attribute>
+        <attribute name="label" translatable="yes">Run</attribute>
+        <attribute name="verb-icon-name">media-playback-start-symbolic</attribute>
+        <attribute name="accel">&lt;Control&gt;F5</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
+
diff --git a/src/libide/gui/ide-application-actions.c b/src/libide/gui/ide-application-actions.c
new file mode 100644
index 000000000..c819cef8c
--- /dev/null
+++ b/src/libide/gui/ide-application-actions.c
@@ -0,0 +1,441 @@
+/* ide-application-actions.c
+ *
+ * Copyright 2018-2019 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 "ide-application-addins"
+#define DOCS_URI "https://builder.readthedocs.io";
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-projects.h>
+
+#include "ide-application.h"
+#include "ide-application-credits.h"
+#include "ide-application-private.h"
+#include "ide-gui-global.h"
+#include "ide-preferences-window.h"
+#include "ide-shortcuts-window-private.h"
+
+static void
+ide_application_actions_preferences (GSimpleAction *action,
+                                     GVariant      *parameter,
+                                     gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+  GtkWindow *toplevel = NULL;
+  GtkWindow *window;
+  GList *windows;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_APPLICATION (self));
+
+  /* Locate a toplevel for a transient-for property, or a previous
+   * preferences window to display.
+   */
+  windows = gtk_application_get_windows (GTK_APPLICATION (self));
+  for (; windows != NULL; windows = windows->next)
+    {
+      GtkWindow *win = windows->data;
+
+      if (IDE_IS_PREFERENCES_WINDOW (win))
+        {
+          gtk_window_present (win);
+          return;
+        }
+
+      if (toplevel == NULL && IDE_IS_WORKBENCH (win))
+        toplevel = win;
+    }
+
+  /* Create a new window for preferences, with enough space for
+   * 2 columns of preferences. The window manager will automatically
+   * maximize the window if necessary.
+   */
+  window = g_object_new (IDE_TYPE_PREFERENCES_WINDOW,
+                         "transient-for", toplevel,
+                         "default-width", 1300,
+                         "default-height", 800,
+                         "window-position", GTK_WIN_POS_CENTER_ON_PARENT,
+                         NULL);
+  gtk_application_add_window (GTK_APPLICATION (self), window);
+  gtk_window_present (window);
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_actions_quit (GSimpleAction *action,
+                              GVariant      *param,
+                              gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  /* TODO: Ask all workbenches to cleanup */
+
+  g_application_quit (G_APPLICATION (self));
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_actions_about (GSimpleAction *action,
+                               GVariant      *param,
+                               gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+  g_autoptr(GString) version = NULL;
+  GtkDialog *dialog;
+  GtkWindow *parent = NULL;
+  GList *iter;
+  GList *windows;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  windows = gtk_application_get_windows (GTK_APPLICATION (self));
+
+  for (iter = windows; iter; iter = iter->next)
+    {
+      if (IDE_IS_WORKBENCH (iter->data))
+        {
+          parent = iter->data;
+          break;
+        }
+    }
+
+  version = g_string_new (NULL);
+
+  if (!g_str_equal (IDE_BUILD_TYPE, "release"))
+    g_string_append (version, IDE_BUILD_IDENTIFIER);
+  else
+    g_string_append (version, PACKAGE_VERSION);
+
+  if (g_strcmp0 (IDE_BUILD_CHANNEL, "other") != 0)
+    g_string_append (version, "\n" IDE_BUILD_CHANNEL);
+
+  dialog = g_object_new (GTK_TYPE_ABOUT_DIALOG,
+                         "artists", ide_application_credits_artists,
+                         "authors", ide_application_credits_authors,
+                         "comments", _("An IDE for GNOME"),
+                         "copyright", "© 2014–2018 Christian Hergert, et al.",
+                         "documenters", ide_application_credits_documenters,
+                         "license-type", GTK_LICENSE_GPL_3_0,
+                         "logo-icon-name", "org.gnome.Builder",
+                         "modal", TRUE,
+                         "program-name", _("GNOME Builder"),
+                         "transient-for", parent,
+                         "translator-credits", _("translator-credits"),
+                         "use-header-bar", TRUE,
+                         "version", version->str,
+                         "website", "https://wiki.gnome.org/Apps/Builder";,
+                         "website-label", _("Learn more about GNOME Builder"),
+                         NULL);
+  gtk_about_dialog_add_credit_section (GTK_ABOUT_DIALOG (dialog),
+                                       _("Funded By"),
+                                       ide_application_credits_funders);
+
+  g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+ide_application_actions_help_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  GNetworkMonitor *monitor = (GNetworkMonitor *)object;
+  g_autoptr(IdeApplication) self = user_data;
+  GtkWindow *focused_window;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  focused_window = gtk_application_get_active_window (GTK_APPLICATION (self));
+
+  /*
+   * If we can reach the documentation website, prefer showing up-to-date
+   * documentation from the website.
+   */
+  if (g_network_monitor_can_reach_finish (monitor, result, NULL))
+    {
+      g_debug ("Can reach documentation site, opening online");
+      if (gtk_show_uri_on_window (focused_window, DOCS_URI, gtk_get_current_event_time (), NULL))
+        IDE_EXIT;
+    }
+
+  g_debug ("Cannot reach online documentation, trying locally");
+
+  /*
+   * We failed to reach the online site for some reason (offline, transient error, etc),
+   * so instead try to load the local documentation.
+   */
+  if (g_file_test (PACKAGE_DOCDIR"/en/index.html", G_FILE_TEST_IS_REGULAR))
+    {
+      g_autofree gchar *file_base = NULL;
+      g_autofree gchar *uri = NULL;
+      g_autoptr(GError) error = NULL;
+
+      if (ide_is_flatpak ())
+        file_base = ide_get_relocatable_path ("/share/doc/gnome-builder");
+      else
+        file_base = g_strdup (PACKAGE_DOCDIR);
+
+      uri = g_strdup_printf ("file://%s/en/index.html", file_base);
+
+      g_debug ("Documentation URI: %s", uri);
+
+      if (!ide_gtk_show_uri_on_window (focused_window, uri, gtk_get_current_event_time (), &error))
+        g_warning ("Failed to load documentation: %s", error->message);
+
+      IDE_EXIT;
+    }
+
+  g_debug ("No locally installed documentation to display");
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_actions_help (GSimpleAction *action,
+                              GVariant      *param,
+                              gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+  g_autoptr(GSocketConnectable) network_address = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_APPLICATION (self));
+
+  /*
+   * Check for access to the internet. Sadly, we cannot use
+   * g_network_monitor_get_network_available() because that does not seem to
+   * act correctly on some systems (Ubuntu appears to be one example). So
+   * instead, we can asynchronously check if we can reach the peer first.
+   */
+  network_address = g_network_address_parse_uri (DOCS_URI, 443, NULL);
+  g_network_monitor_can_reach_async (g_network_monitor_get_default (),
+                                     network_address,
+                                     NULL,
+                                     ide_application_actions_help_cb,
+                                     g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_actions_shortcuts (GSimpleAction *action,
+                                   GVariant      *variant,
+                                   gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+  GtkWindow *window;
+  GtkWindow *parent = NULL;
+  GList *list;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  list = gtk_application_get_windows (GTK_APPLICATION (self));
+
+  for (; list; list = list->next)
+    {
+      window = list->data;
+
+      if (IDE_IS_SHORTCUTS_WINDOW (window))
+        {
+          gtk_window_present (window);
+          return;
+        }
+
+      if (IDE_IS_WORKBENCH (window))
+        {
+          parent = window;
+          break;
+        }
+    }
+
+  window = g_object_new (IDE_TYPE_SHORTCUTS_WINDOW,
+                         "application", self,
+                         "window-position", GTK_WIN_POS_CENTER,
+                         "transient-for", parent,
+                         NULL);
+
+  gtk_window_present (GTK_WINDOW (window));
+}
+
+static void
+ide_application_actions_nighthack (GSimpleAction *action,
+                                   GVariant      *variant,
+                                   gpointer       user_data)
+{
+  g_autoptr(GSettings) settings = NULL;
+
+  g_object_set (gtk_settings_get_default (),
+                "gtk-application-prefer-dark-theme", TRUE,
+                NULL);
+
+  settings = g_settings_new ("org.gnome.builder.editor");
+  g_settings_set_string (settings, "style-scheme-name", "builder-dark");
+}
+
+static void
+ide_application_actions_dayhack (GSimpleAction *action,
+                                 GVariant      *variant,
+                                 gpointer       user_data)
+{
+  g_autoptr(GSettings) settings = NULL;
+
+  g_object_set (gtk_settings_get_default (),
+                "gtk-application-prefer-dark-theme", FALSE,
+                NULL);
+
+  settings = g_settings_new ("org.gnome.builder.editor");
+  g_settings_set_string (settings, "style-scheme-name", "builder");
+}
+
+static void
+ide_application_actions_load_project (GSimpleAction *action,
+                                      GVariant      *args,
+                                      gpointer       user_data)
+{
+  IdeApplication *self = user_data;
+  g_autoptr(IdeProjectInfo) project_info = NULL;
+  g_autofree gchar *filename = NULL;
+  g_autoptr(GFile) file = NULL;
+  g_autofree gchar *scheme = NULL;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  g_variant_get (args, "s", &filename);
+
+  if ((scheme = g_uri_parse_scheme (filename)))
+    file = g_file_new_for_uri (filename);
+  else
+    file = g_file_new_for_path (filename);
+
+  project_info = ide_project_info_new ();
+  ide_project_info_set_file (project_info, file);
+
+  ide_application_open_project_async (self,
+                                      project_info,
+                                      G_TYPE_INVALID,
+                                      NULL, NULL, NULL);
+}
+
+static gint
+type_compare (gconstpointer a,
+              gconstpointer b)
+{
+  GType *ta = (GType *)a;
+  GType *tb = (GType *)b;
+
+  return g_type_get_instance_count (*ta) - g_type_get_instance_count (*tb);
+}
+
+static void
+ide_application_actions_stats (GSimpleAction *action,
+                               GVariant *args,
+                               gpointer user_data)
+{
+  guint n_types = 0;
+  g_autofree GType *types = g_type_children (G_TYPE_OBJECT, &n_types);
+  GtkScrolledWindow *scroller;
+  GtkTextBuffer *buffer;
+  GtkTextView *text_view;
+  GtkWindow *window;
+  gboolean found = FALSE;
+
+  window = g_object_new (GTK_TYPE_WINDOW,
+                         "default-width", 1000,
+                         "default-height", 600,
+                         "title", "about:types",
+                         NULL);
+  scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
+                           "visible", TRUE,
+                           NULL);
+  gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (scroller));
+  text_view = g_object_new (GTK_TYPE_TEXT_VIEW,
+                            "editable", FALSE,
+                            "monospace", TRUE,
+                            "visible", TRUE,
+                            NULL);
+  gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (text_view));
+  buffer = gtk_text_view_get_buffer (text_view);
+
+  gtk_text_buffer_insert_at_cursor (buffer, "Count | Type\n", -1);
+  gtk_text_buffer_insert_at_cursor (buffer, "======+======\n", -1);
+
+  qsort (types, n_types, sizeof (GType), type_compare);
+
+  for (guint i = 0; i < n_types; i++)
+    {
+      gint count = g_type_get_instance_count (types[i]);
+
+      if (count)
+        {
+          gchar str[12];
+
+          found = TRUE;
+
+          g_snprintf (str, sizeof str, "%6d", count);
+          gtk_text_buffer_insert_at_cursor (buffer, str, -1);
+          gtk_text_buffer_insert_at_cursor (buffer, " ", -1);
+          gtk_text_buffer_insert_at_cursor (buffer, g_type_name (types[i]), -1);
+          gtk_text_buffer_insert_at_cursor (buffer, "\n", -1);
+        }
+    }
+
+  if (!found)
+    gtk_text_buffer_insert_at_cursor (buffer, "No stats were found, was GOBJECT_DEBUG=instance-count set?", 
-1);
+
+  gtk_window_present (window);
+}
+
+static const GActionEntry IdeApplicationActions[] = {
+  { "about:types",  ide_application_actions_stats },
+  { "about",        ide_application_actions_about },
+  { "dayhack",      ide_application_actions_dayhack },
+  { "nighthack",    ide_application_actions_nighthack },
+  { "load-project", ide_application_actions_load_project, "s"},
+  { "preferences",  ide_application_actions_preferences },
+  { "quit",         ide_application_actions_quit },
+  { "shortcuts",    ide_application_actions_shortcuts },
+  { "help",         ide_application_actions_help },
+};
+
+void
+_ide_application_init_actions (IdeApplication *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (self),
+                                   IdeApplicationActions,
+                                   G_N_ELEMENTS (IdeApplicationActions),
+                                   self);
+}
diff --git a/src/libide/gui/ide-application-addin.c b/src/libide/gui/ide-application-addin.c
new file mode 100644
index 000000000..557832e0e
--- /dev/null
+++ b/src/libide/gui/ide-application-addin.c
@@ -0,0 +1,189 @@
+/* ide-application-addin.c
+ *
+ * Copyright 2015-2019 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 "ide-application-addin"
+
+#include "config.h"
+
+#include "ide-application-addin.h"
+
+/**
+ * SECTION:ide-application-addin
+ * @title: IdeApplicationAddin
+ * @short_description: extend functionality of #IdeApplication
+ *
+ * The #IdeApplicationAddin interface is used by plugins that want to extend
+ * the set of features provided by #IdeApplication. This is useful if you need
+ * utility code that is bound to the lifetime of the #IdeApplication.
+ *
+ * The #IdeApplicationAddin is created after the application has initialized
+ * and unloaded when Builder is shut down.
+ *
+ * Use this interface when you can share code between multiple projects that
+ * are open at the same time.
+ *
+ * Since: 3.32
+ */
+
+G_DEFINE_INTERFACE (IdeApplicationAddin, ide_application_addin, G_TYPE_OBJECT)
+
+static void
+ide_application_addin_real_load (IdeApplicationAddin *self,
+                                 IdeApplication      *application)
+{
+}
+
+static void
+ide_application_addin_real_unload (IdeApplicationAddin *self,
+                                   IdeApplication      *application)
+{
+}
+
+static void
+ide_application_addin_default_init (IdeApplicationAddinInterface *iface)
+{
+  iface->load = ide_application_addin_real_load;
+  iface->unload = ide_application_addin_real_unload;
+}
+
+/**
+ * ide_application_addin_load:
+ * @self: An #IdeApplicationAddin.
+ * @application: An #IdeApplication.
+ *
+ * This interface method is called when the application is started or the
+ * plugin has just been activated.
+ *
+ * Use this to setup code in your plugin that needs to be loaded once per
+ * application process.
+ *
+ * Since: 3.32
+ */
+void
+ide_application_addin_load (IdeApplicationAddin *self,
+                            IdeApplication      *application)
+{
+  g_return_if_fail (IDE_IS_APPLICATION_ADDIN (self));
+  g_return_if_fail (IDE_IS_APPLICATION (application));
+
+  IDE_APPLICATION_ADDIN_GET_IFACE (self)->load (self, application);
+}
+
+/**
+ * ide_application_addin_unload:
+ * @self: An #IdeApplicationAddin.
+ * @application: An #IdeApplication.
+ *
+ * This inteface method is called when the application is shutting down or the
+ * plugin has been unloaded.
+ *
+ * Use this function to cleanup after anything setup in
+ * ide_application_addin_load().
+ *
+ * Since: 3.32
+ */
+void
+ide_application_addin_unload (IdeApplicationAddin *self,
+                              IdeApplication      *application)
+{
+  g_return_if_fail (IDE_IS_APPLICATION_ADDIN (self));
+  g_return_if_fail (IDE_IS_APPLICATION (application));
+
+  IDE_APPLICATION_ADDIN_GET_IFACE (self)->unload (self, application);
+}
+
+/**
+ * ide_application_addin_add_option_entries:
+ * @self: a #IdeApplicationAddin
+ * @application: an #IdeApplication
+ *
+ * This function is called to allow the application a chance to add various
+ * command-line options to the #GOptionContext. See
+ * g_application_add_main_option_entries() for more information on how to
+ * add arguments.
+ *
+ * See ide_application_addin_handle_command_line() for how to handle arguments
+ * once command line argument processing begins.
+ *
+ * Make sure you set `X-At-Startup=true` in your `.plugin` file so that the
+ * plugin is loaded early during startup or this virtual function will not
+ * be called.
+ *
+ * Since: 3.32
+ */
+void
+ide_application_addin_add_option_entries (IdeApplicationAddin *self,
+                                          IdeApplication      *application)
+{
+  g_return_if_fail (IDE_IS_APPLICATION_ADDIN (self));
+  g_return_if_fail (IDE_IS_APPLICATION (application));
+
+  if (IDE_APPLICATION_ADDIN_GET_IFACE (self)->add_option_entries)
+    IDE_APPLICATION_ADDIN_GET_IFACE (self)->add_option_entries (self, application);
+}
+
+/**
+ * ide_application_addin_handle_command_line:
+ * @self: a #IdeApplicationAddin
+ * @application: an #IdeApplication
+ * @cmdline: a #GApplicationCommandLine
+ *
+ * This function is called to allow the addin to procses command line arguments
+ * that were parsed based on options added in
+ * ide_application_addin_add_option_entries().
+ *
+ * See g_application_command_line_get_option_dict() for more information.
+ *
+ * Since: 3.32
+ */
+void
+ide_application_addin_handle_command_line (IdeApplicationAddin     *self,
+                                           IdeApplication          *application,
+                                           GApplicationCommandLine *cmdline)
+{
+  g_return_if_fail (IDE_IS_APPLICATION_ADDIN (self));
+  g_return_if_fail (IDE_IS_APPLICATION (application));
+  g_return_if_fail (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  if (IDE_APPLICATION_ADDIN_GET_IFACE (self)->handle_command_line)
+    IDE_APPLICATION_ADDIN_GET_IFACE (self)->handle_command_line (self, application, cmdline);
+}
+
+void
+ide_application_addin_workbench_added (IdeApplicationAddin *self,
+                                       IdeWorkbench        *workbench)
+{
+  g_return_if_fail (IDE_IS_APPLICATION_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+
+  if (IDE_APPLICATION_ADDIN_GET_IFACE (self)->workbench_added)
+    IDE_APPLICATION_ADDIN_GET_IFACE (self)->workbench_added (self, workbench);
+}
+
+void
+ide_application_addin_workbench_removed (IdeApplicationAddin *self,
+                                         IdeWorkbench        *workbench)
+{
+  g_return_if_fail (IDE_IS_APPLICATION_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+
+  if (IDE_APPLICATION_ADDIN_GET_IFACE (self)->workbench_removed)
+    IDE_APPLICATION_ADDIN_GET_IFACE (self)->workbench_removed (self, workbench);
+}
diff --git a/src/libide/gui/ide-application-addin.h b/src/libide/gui/ide-application-addin.h
new file mode 100644
index 000000000..5551cd2e7
--- /dev/null
+++ b/src/libide/gui/ide-application-addin.h
@@ -0,0 +1,87 @@
+/* ide-application-addin.h
+ *
+ * Copyright 2015-2019 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-core.h>
+
+#include "ide-application.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_APPLICATION_ADDIN (ide_application_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeApplicationAddin, ide_application_addin, IDE, APPLICATION_ADDIN, GObject)
+
+/**
+ * IdeApplicationAddinInterface:
+ * @load: Set this virtual method to implement the ide_application_addin_load()
+ *   virtual method.
+ * @unload: Set this virtual method to implement the
+ *   ide_application_addin_unload() virtual method.
+ * @add_option_entries: Set this virtual method to add option entries to
+ *   the gnome-builder command-line argument parsing. See
+ *   g_application_add_main_option_entries().
+ * @handle_command_line: Set this virtual method to handle parsing command
+ *   line arguments.
+ *
+ * Since: 3.32
+ */
+struct _IdeApplicationAddinInterface
+{
+  GTypeInterface parent_interface;
+
+  void (*load)                (IdeApplicationAddin     *self,
+                               IdeApplication          *application);
+  void (*unload)              (IdeApplicationAddin     *self,
+                               IdeApplication          *application);
+  void (*add_option_entries)  (IdeApplicationAddin     *self,
+                               IdeApplication          *application);
+  void (*handle_command_line) (IdeApplicationAddin     *self,
+                               IdeApplication          *application,
+                               GApplicationCommandLine *cmdline);
+  void (*workbench_added)     (IdeApplicationAddin     *self,
+                               IdeWorkbench            *workbench);
+  void (*workbench_removed)   (IdeApplicationAddin     *self,
+                               IdeWorkbench            *workbench);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_application_addin_load                (IdeApplicationAddin     *self,
+                                                IdeApplication          *application);
+IDE_AVAILABLE_IN_3_32
+void ide_application_addin_unload              (IdeApplicationAddin     *self,
+                                                IdeApplication          *application);
+IDE_AVAILABLE_IN_3_32
+void ide_application_addin_add_option_entries  (IdeApplicationAddin     *self,
+                                                IdeApplication          *application);
+IDE_AVAILABLE_IN_3_32
+void ide_application_addin_handle_command_line (IdeApplicationAddin     *self,
+                                                IdeApplication          *application,
+                                                GApplicationCommandLine *cmdline);
+IDE_AVAILABLE_IN_3_32
+void ide_application_addin_workbench_added     (IdeApplicationAddin     *self,
+                                                IdeWorkbench            *workbench);
+IDE_AVAILABLE_IN_3_32
+void ide_application_addin_workbench_removed   (IdeApplicationAddin     *self,
+                                                IdeWorkbench            *workbench);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-application-color.c b/src/libide/gui/ide-application-color.c
new file mode 100644
index 000000000..de467ed77
--- /dev/null
+++ b/src/libide/gui/ide-application-color.c
@@ -0,0 +1,232 @@
+/* ide-application-color.c
+ *
+ * Copyright 2017-2019 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 "ide-application-color"
+
+#include "config.h"
+
+#include <gtksourceview/gtksource.h>
+
+#include "ide-application.h"
+#include "ide-application-private.h"
+
+static void
+add_style_name (GPtrArray   *ar,
+                const gchar *base,
+                gboolean     dark)
+{
+  g_ptr_array_add (ar, g_strdup_printf ("%s-%s", base, dark ? "dark" : "light"));
+}
+
+static gchar *
+find_similar_style_scheme (const gchar *name,
+                           gboolean     is_dark_mode)
+{
+  g_autoptr(GPtrArray) attempts = NULL;
+  GtkSourceStyleSchemeManager *mgr;
+  const gchar * const *scheme_ids;
+  const gchar *dash;
+
+  g_assert (name != NULL);
+
+  attempts = g_ptr_array_new_with_free_func (g_free);
+
+  mgr = gtk_source_style_scheme_manager_get_default ();
+  scheme_ids = gtk_source_style_scheme_manager_get_scheme_ids (mgr);
+
+  add_style_name (attempts, name, is_dark_mode);
+
+  if ((dash = strrchr (name, '-')))
+    {
+      if (g_str_equal (dash, "-light") ||
+          g_str_equal (dash, "-dark"))
+        {
+          g_autofree gchar *base = NULL;
+
+          base = g_strndup (name, dash - name);
+          add_style_name (attempts, base, is_dark_mode);
+
+          /* Add the base name last so light/dark matches first */
+          g_ptr_array_add (attempts, g_steal_pointer (&base));
+        }
+    }
+
+  /*
+   * Instead of using gtk_source_style_scheme_manager_get_scheme(), we get the
+   * IDs and look using case insensitive search so that its more likely we get
+   * a match when something is named Dark or Light in the id.
+   */
+
+  for (guint i = 0; i < attempts->len; i++)
+    {
+      const gchar *attempt = g_ptr_array_index (attempts, i);
+
+      for (guint j = 0; scheme_ids[j] != NULL; j++)
+        {
+          if (strcasecmp (attempt, scheme_ids[j]) == 0)
+            return g_strdup (scheme_ids[j]);
+        }
+    }
+
+  return NULL;
+}
+
+static void
+_ide_application_update_color (IdeApplication *self)
+{
+  static gboolean ignore_reentrant = FALSE;
+  GtkSettings *gtk_settings;
+  gboolean prefer_dark_theme;
+  gboolean follow;
+  gboolean night_mode;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  if (ignore_reentrant)
+    return;
+
+  if (self->color_proxy == NULL || self->settings == NULL)
+    return;
+
+  ignore_reentrant = TRUE;
+
+  g_assert (G_IS_SETTINGS (self->settings));
+  g_assert (G_IS_DBUS_PROXY (self->color_proxy));
+
+  follow = g_settings_get_boolean (self->settings, "follow-night-light");
+  night_mode = g_settings_get_boolean (self->settings, "night-mode");
+
+  /*
+   * If we are using the Follow Night Light feature, then we want to update
+   * the application color based on the DBus NightLightActive property from
+   * GNOME Shell.
+   */
+
+  if (follow)
+    {
+      g_autoptr(GVariant) activev = NULL;
+      g_autoptr(GSettings) editor_settings = NULL;
+      g_autofree gchar *old_name = NULL;
+      g_autofree gchar *new_name = NULL;
+      gboolean active;
+
+      /*
+       * Update our internal night-mode setting based on the GNOME Shell
+       * Night Light setting.
+       */
+
+      activev = g_dbus_proxy_get_cached_property (self->color_proxy, "NightLightActive");
+      active = g_variant_get_boolean (activev);
+
+      if (active != night_mode)
+        {
+          night_mode = active;
+          g_settings_set_boolean (self->settings, "night-mode", night_mode);
+        }
+
+      /*
+       * Now that we have our color up to date, we need to possibly update the
+       * color scheme to match the setting. We always do this (and not just when
+       * the night-mode changes) so that we pick up changes at startup.
+       *
+       * Try to locate a corresponding style-scheme for the light/dark switch
+       * based on some naming conventions. If found, switch the current style
+       * scheme to match.
+       */
+
+      editor_settings = g_settings_new ("org.gnome.builder.editor");
+      old_name = g_settings_get_string (editor_settings, "style-scheme-name");
+      new_name = find_similar_style_scheme (old_name, night_mode);
+
+      if (new_name != NULL)
+        g_settings_set_string (editor_settings, "style-scheme-name", new_name);
+    }
+
+  gtk_settings = gtk_settings_get_default ();
+
+  g_object_get (gtk_settings,
+                "gtk-application-prefer-dark-theme", &prefer_dark_theme,
+                NULL);
+
+  if (prefer_dark_theme != night_mode)
+    g_object_set (gtk_settings,
+                  "gtk-application-prefer-dark-theme", night_mode,
+                  NULL);
+
+  ignore_reentrant = FALSE;
+}
+
+static void
+ide_application_color_properties_changed (IdeApplication      *self,
+                                          GVariant            *properties,
+                                          const gchar * const *invalidated,
+                                          GDBusProxy          *proxy)
+{
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (G_IS_DBUS_PROXY (proxy));
+
+  _ide_application_update_color (self);
+}
+
+void
+_ide_application_init_color (IdeApplication *self)
+{
+  g_autoptr(GDBusConnection) conn = NULL;
+  g_autoptr(GDBusProxy) proxy = NULL;
+
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+  g_return_if_fail (G_IS_SETTINGS (self->settings));
+
+  if (g_getenv ("GTK_THEME") == NULL)
+    {
+      g_signal_connect_object (self->settings,
+                               "changed::follow-night-light",
+                               G_CALLBACK (_ide_application_update_color),
+                               self,
+                               G_CONNECT_SWAPPED);
+      g_signal_connect_object (self->settings,
+                               "changed::night-mode",
+                               G_CALLBACK (_ide_application_update_color),
+                               self,
+                               G_CONNECT_SWAPPED);
+    }
+
+  if (NULL == (conn = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL)))
+    return;
+
+  if (NULL == (proxy = g_dbus_proxy_new_sync (conn,
+                                              G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES,
+                                              NULL,
+                                              "org.gnome.SettingsDaemon.Color",
+                                              "/org/gnome/SettingsDaemon/Color",
+                                              "org.gnome.SettingsDaemon.Color",
+                                              NULL, NULL)))
+    return;
+
+  g_signal_connect_object (proxy,
+                           "g-properties-changed",
+                           G_CALLBACK (ide_application_color_properties_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  self->color_proxy = g_steal_pointer (&proxy);
+
+  _ide_application_update_color (self);
+}
diff --git a/src/libide/gui/ide-application-command-line.c b/src/libide/gui/ide-application-command-line.c
new file mode 100644
index 000000000..ebdfc88f0
--- /dev/null
+++ b/src/libide/gui/ide-application-command-line.c
@@ -0,0 +1,241 @@
+/* ide-application-command-line.c
+ *
+ * Copyright 2018-2019 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 "ide-application-command-line"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-core.h>
+#include <stdlib.h>
+
+#include "ide-application-addin.h"
+#include "ide-application-private.h"
+#include "ide-primary-workspace.h"
+
+static void
+add_option_entries_foreach_cb (PeasExtensionSet *set,
+                               PeasPluginInfo   *plugin_info,
+                               PeasExtension    *exten,
+                               gpointer          user_data)
+{
+  IdeApplicationAddin *addin = (IdeApplicationAddin *)exten;
+  IdeApplication *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (self));
+
+  ide_application_addin_add_option_entries (addin, self);
+}
+
+/**
+ * _ide_application_add_option_entries:
+ *
+ * Inflate all early stage plugins asking them to let us know about what
+ * command-line options they support.
+ *
+ * Since: 3.32
+ */
+void
+_ide_application_add_option_entries (IdeApplication *self)
+{
+  static const GOptionEntry main_entries[] = {
+    { "preferences", 0, 0, G_OPTION_ARG_NONE, NULL, N_("Show the application preferences") },
+    { "project", 'p', 0, G_OPTION_ARG_FILENAME, NULL, N_("Open project in new workbench"), N_("FILE")  },
+    { "version", 'V', 0, G_OPTION_ARG_NONE, NULL, N_("Print version information and exit") },
+    /* Verbose is handled in main(), but we need to add to --help here */
+    { "verbose", 'v', 0, G_OPTION_ARG_NONE, NULL, N_("Increase log verbosity") },
+    { NULL }
+  };
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  g_application_add_main_option_entries (G_APPLICATION (self), main_entries);
+  peas_extension_set_foreach (self->addins, add_option_entries_foreach_cb, self);
+}
+
+static void
+command_line_foreach_cb (PeasExtensionSet *set,
+                         PeasPluginInfo   *plugin_info,
+                         PeasExtension    *exten,
+                         gpointer          user_data)
+{
+  IdeApplicationAddin *addin = (IdeApplicationAddin *)exten;
+  GApplicationCommandLine *cmdline = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  ide_application_addin_handle_command_line (addin, IDE_APPLICATION_DEFAULT, cmdline);
+}
+
+static void
+ide_application_command_line_open_project_cb (GObject      *object,
+                                              GAsyncResult *result,
+                                              gpointer      user_data)
+{
+  IdeApplication *app = (IdeApplication *)object;
+  g_autoptr(GApplicationCommandLine) cmdline = user_data;
+  g_autoptr(IdeWorkbench) workbench = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_APPLICATION (app));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  g_application_release (G_APPLICATION (app));
+
+  if (!(workbench = ide_application_open_project_finish (app, result, &error)))
+    {
+      g_application_command_line_printerr (cmdline,
+                                           _("Failed to open project: %s"),
+                                           error->message);
+      return;
+    }
+
+  g_application_command_line_set_exit_status (cmdline, workbench ? EXIT_SUCCESS : EXIT_FAILURE);
+}
+
+/**
+ * _ide_application_command_line:
+ *
+ * This function will dispatch the command-line to the various
+ * plugins who have elected to handle command-line options. Some
+ * of them, like the greeter, may create an initial workbench
+ * and workspace window in response.
+ *
+ * Since: 3.32
+ */
+void
+_ide_application_command_line (IdeApplication          *self,
+                               GApplicationCommandLine *cmdline)
+{
+  g_autoptr(PeasExtensionSet) set = NULL;
+  g_autofree gchar *project = NULL;
+  GVariantDict *dict;
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  dict = g_application_command_line_get_options_dict (cmdline);
+
+  /* Short-circuit with version info if we can */
+  if (g_variant_dict_contains (dict, "version"))
+    {
+      g_application_command_line_print (cmdline, "GNOME Builder "PACKAGE_VERSION"\n");
+      g_application_command_line_set_exit_status (cmdline, 0);
+      return;
+    }
+
+  /* Short-circuit with --preferences if we can */
+  if (g_variant_dict_contains (dict, "preferences"))
+    {
+      g_action_group_activate_action (G_ACTION_GROUP (self), "preferences", NULL);
+      return;
+    }
+
+  /*
+   * Allow any plugin that has registered a command-line handler to
+   * handle the command-line options. They may return an exit status
+   * in the process of our iteration, at which point we shoudl bail
+   * any furter processings.
+   *
+   * This is done before -p/--project parsing so that options may be
+   * changed before loading a project.
+   */
+  peas_extension_set_foreach (self->addins, command_line_foreach_cb, cmdline);
+
+  /*
+   * Open the project if --project/-p was spefified by the invoking
+   * processes command-line.
+   */
+  if (g_variant_dict_lookup (dict, "project", "^ay", &project))
+    {
+      g_autoptr(IdeProjectInfo) project_info = NULL;
+      g_autoptr(GFile) project_file = NULL;
+      g_autoptr(GFile) parent = NULL;
+
+      project_file = g_application_command_line_create_file_for_arg (cmdline, project);
+      parent = g_file_get_parent (project_file);
+
+      project_info = ide_project_info_new ();
+      ide_project_info_set_file (project_info, project_file);
+
+      /* If it's a directory, set that too, otherwise use the parent */
+      if (g_file_query_file_type (project_file, 0, NULL) == G_FILE_TYPE_DIRECTORY)
+        ide_project_info_set_directory (project_info, project_file);
+      else
+        ide_project_info_set_directory (project_info, parent);
+
+      g_application_hold (G_APPLICATION (self));
+
+      ide_application_open_project_async (self,
+                                          project_info,
+                                          G_TYPE_INVALID,
+                                          NULL,
+                                          ide_application_command_line_open_project_cb,
+                                          g_object_ref (cmdline));
+
+      return;
+    }
+
+  g_application_activate (G_APPLICATION (self));
+}
+
+/**
+ * ide_application_get_argv:
+ * @self: an #IdeApplication
+ * @cmdline: a #GApplicationCommandLine
+ *
+ * Gets the commandline for @cmdline as it was before any processing.
+ * This is useful to handle both local and remote processing of argv
+ * when you need to know what the arguments were before further
+ * options parsing.
+ *
+ * Returns: (transfer full) (nullable) (array zero-terminated=1): an
+ *   array of strings or %NULL
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_application_get_argv (IdeApplication          *self,
+                          GApplicationCommandLine *cmdline)
+{
+  g_autoptr(GVariant) ret = NULL;
+  GVariant *platform_data;
+
+  g_return_val_if_fail (IDE_IS_APPLICATION (self), NULL);
+  g_return_val_if_fail (G_IS_APPLICATION_COMMAND_LINE (cmdline), NULL);
+
+  if (!g_application_command_line_get_is_remote (cmdline))
+    return g_strdupv (self->argv);
+
+  if (!(platform_data = g_application_command_line_get_platform_data (cmdline)))
+    return NULL;
+
+  if ((ret = g_variant_lookup_value (platform_data, "argv", G_VARIANT_TYPE_STRING_ARRAY)))
+    return g_variant_dup_strv (ret, NULL);
+
+  return NULL;
+}
diff --git a/src/libide/gui/ide-application-credits.h b/src/libide/gui/ide-application-credits.h
new file mode 100644
index 000000000..982cc6516
--- /dev/null
+++ b/src/libide/gui/ide-application-credits.h
@@ -0,0 +1,599 @@
+/* ide-application-credits.h
+ *
+ * Copyright 2015-2019 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.h>
+
+G_BEGIN_DECLS
+
+static const gchar *ide_application_credits_artists[] = {
+  "Allan Day",
+  "Hylke Bons",
+  "Jakub Steiner",
+  "Sam Hewitt",
+  NULL
+};
+
+static const gchar *ide_application_credits_authors[] = {
+  "Akshaya Kakkilaya",
+  "Alberto Fanjul",
+  "Alex285",
+  "Alexander Larsson",
+  "Alexandre Franke",
+  "Andika Triwidada",
+  "Andreas Brauchli",
+  "Andreas Henriksson",
+  "Anoop Chandu",
+  "Antoine Jacoutot",
+  "Anwar Sadath",
+  "Aurimas Černius",
+  "Baurzhan Muftakhidinov",
+  "Ben Iofel",
+  "Bernd Homuth",
+  "Boris Egorov",
+  "burningTyger",
+  "Carlos Garnacho",
+  "Carlos Soriano",
+  "chandu",
+  "Changwoo Ryu",
+  "Chao-Hsiung Liao",
+  "Cheng-Chia Tseng",
+  "Christian Hergert",
+  "Christian Kirbach",
+  "Cosimo Cecchi",
+  "Daiki Ueno",
+  "Damien Lespiau",
+  "Daniel Boles",
+  "Daniel Korostil",
+  "Daniel Mustieles",
+  "David King",
+  "Debarshi Dutta",
+  "Dimitrios Christidis",
+  "Dimitris Zenios",
+  "Dor Askayo",
+  "Dušan Kazik",
+  "Ekaterina Gerasimova",
+  "Elad Alfassa",
+  "Erick Pérez Castellanos",
+  "Evgeny Shulgin",
+  "Fabiano Fidêncio",
+  "Fangwen Yu",
+  "Felix Schwarz",
+  "Fernando Fernandez",
+  "Florian Bäuerle",
+  "Florian Müllner",
+  "Fran Dieguez",
+  "Gábor Kelemen",
+  "Garrett Regier",
+  "Gautier Pelloux-Prayer",
+  "Gennady Kovalev",
+  "Georg Vienna",
+  "Giovanni Campagna",
+  "Günther Wutz",
+  "Hashem Nasarat",
+  "heroin",
+  "Hylke Bons",
+  "Ian Hernandez",
+  "Ignacio Casal Quinteiro",
+  "Igor Gnatenko",
+  "Jakub Steiner",
+  "Jasper St. Pierre",
+  "Joaquim Rocha",
+  "Johan Svensson",
+  "Jonathon Jongsma",
+  "Jürg Billeter",
+  "Jordi Mas",
+  "Kalev Lember",
+  "Kjartan Maraas",
+  "Kris Thomsen",
+  "kritarth",
+  "Kristjan Schmidt",
+  "Lars Uebernickel",
+  "Lionel Landwerlin",
+  "Lucie Charvat",
+  "Marek Černocký",
+  "Marinus Schraal",
+  "Mario Sanchez Prada",
+  "Matej Urbančič",
+  "Mathieu Bridon",
+  "Mathieu Duponchelle",
+  "Matthew Leeds",
+  "Matthias Clasen",
+  "Megh Parikh",
+  "Michael Biebl",
+  "Michael Catanzaro",
+  "Mohan R",
+  "Muhammet Kara",
+  "Мирослав Николић",
+  "namanyadav12",
+  "Paolo Borelli",
+  "Patrick Griffis",
+  "Peter Sonntag",
+  "Pedro Albuquerque",
+  "Pete Travis",
+  "Philip Withnall",
+  "Pooja Dhannawat",
+  "Piotr Drąg",
+  "Ray Strode",
+  "Roberto Majadas",
+  "Samir Ribic",
+  "Sébastien Lafargue",
+  "Simon Schampijer",
+  "Sourav",
+  "Thibault Saunier",
+  "Timm Bäder",
+  "Ting-Wei Lan",
+  "Tobias Schönberg",
+  "Tom Tryfonidis",
+  "Trinh Anh Ngoc",
+  "Umang Jain",
+  "Wolf Vollprecht",
+  "Yannick Inizan",
+  "Yosef Or Boczko",
+  "zilla hmt im",
+  NULL
+};
+
+static const gchar *ide_application_credits_documenters[] = {
+  "Christian Hergert",
+  NULL
+};
+
+static const gchar *ide_application_credits_funders[] = {
+  "曾政嘉",
+  "Aaron Hergert",
+  "Abdul Kadri Gündoğdu",
+  "Abimael Martinez Carrete",
+  "Adam grunden",
+  "Adrian Bradshaw",
+  "Adrian Rocha",
+  "Adrià Arrufat",
+  "Alaska Subedi",
+  "Albert Murciego Rico",
+  "Alessandro Bono",
+  "Alexander B Libby",
+  "Alexander Gleason",
+  "Alexander Khatsayuk",
+  "Alexander Larsson",
+  "Alexander Murray",
+  "Alexandre Amoedo",
+  "Alexandre Franke",
+  "Alexandros Diavatis",
+  "Alfonso de Cala Bravo",
+  "Alfred Santacatalina Gea",
+  "Ambrose Andrews",
+  "Andreas Nilsson",
+  "Andrew Stiegmann",
+  "Andrew Walton",
+  "Anthony Taranto",
+  "Anton Shafarenko",
+  "Aram J Agajanian",
+  "Arne Hoch",
+  "Arturo Buentello G",
+  "Arun Raghavan",
+  "Ashley Sommer",
+  "Aurélien Naldi",
+  "B. Mille-Mathias",
+  "Baldessari Michele",
+  "Bastian Ilsø Hougaard",
+  "Bastien Nocera",
+  "Benjamin Grimm-Lebsanft",
+  "Bernd Homuth",
+  "Bill Roth",
+  "Bill Thornton",
+  "Brad Taylor",
+  "Brendan Long",
+  "Brijesh Kartha",
+  "Bruce Cowan",
+  "Bruce M Franklin",
+  "Bruno Windels",
+  "Búza Géza",
+  "Canek Pelaez Valdes",
+  "Carlos Soriano Sanchez",
+  "Casey E Megginson",
+  "Cedric Briner",
+  "Cees Meijer",
+  "Centricular Ltd",
+  "Chema Casanova",
+  "Cheng-Chia Tseng",
+  "Chris Bagwell",
+  "Chris Kühl",
+  "Chris Tonkinson",
+  "Christian Hergert",
+  "Christian Lange",
+  "Christopher Brian Sherlock",
+  "Christopher Horton",
+  "Christopher Kim",
+  "Christopher William Bell",
+  "Chun-Sheng Wu",
+  "Cody Russell",
+  "Cosimo Cecchi",
+  "Craig A Cabrey",
+  "Dag Robøle",
+  "Daiki Ueno",
+  "Damián Nohales",
+  "Dandauchy",
+  "Daniel Buch",
+  "Daniel Dui",
+  "Daniel Espinosa Ortiz",
+  "Daniel Menchaca",
+  "Daniel Nemec",
+  "Daniel Pfeifer",
+  "Daniel Vazquez Rivera",
+  "Danilo Gropelo",
+  "Danylo Korostil",
+  "Debarshi Ray",
+  "Demetris Lambrou",
+  "Dennis Schulmeister",
+  "Denver Gingerich",
+  "Derek 呆",
+  "Dolgoff ",
+  "Eduardo Silva",
+  "Eitan Isaacson",
+  "Elad Alfassa",
+  "Ellis Kenyo",
+  "Emanuele Aina",
+  "Emanuele Gissi",
+  "Emmanuele Bassi",
+  "Enrique Ocaña González",
+  "Eric Streit",
+  "Eric T Miller",
+  "Erik Helin",
+  "Ernest hershey",
+  "Erwan Bousse",
+  "Erwan Georget",
+  "F. Kooman",
+  "Fabian Alexander Wilms",
+  "Fabien Cortina",
+  "Fabio Valentini",
+  "Faizan Qazi",
+  "Faron S Anslow",
+  "Fasiello Nicomede",
+  "Federico Mena Quintero",
+  "Felix Schröter",
+  "Filipe Santos",
+  "Florian Bäuerle",
+  "Florian Over",
+  "Florian Schweikert",
+  "Florin Florica",
+  "Frank Dietrich",
+  "Frank Hansen",
+  "Fränz Ney",
+  "Fredrik Schaller",
+  "G A Foster",
+  "Gabriel Rauter",
+  "Garrett LeSage",
+  "Georges Seguin",
+  "Georges-Mickael Seguin",
+  "Gianluigi Calcaterra",
+  "Gil Forcada Codinachs",
+  "Giovanni Forte",
+  "Go Min YounG",
+  "Gonzalo Paniagua Javier",
+  "Gordon Martin",
+  "Guilherme Rodrigues",
+  "Guillaume Beaudin",
+  "Guillaume Hain",
+  "Guillaume Pasquet",
+  "Guillaume Quintard",
+  "Gustavo Arejano",
+  "Gustavo N Silva",
+  "Hain Guillaume",
+  "Hannes Ovrén",
+  "Harald Hoyer",
+  "Havoc Pennington",
+  "Hendrik Richter",
+  "Henrique Almeida",
+  "Henry Finucane",
+  "I Martin Rodriguez",
+  "Ian Bolf",
+  "Ian McKellar",
+  "Ignacio Casal Quinteiro",
+  "Igor Gnatenko",
+  "Ilya Novosyolov",
+  "Ioram Gordadze",
+  "Ivan Nezhdanov",
+  "J. Pereira Rocha",
+  "J.A.J. Vermeulen",
+  "Jack Jennings",
+  "Jakub Steiner",
+  "James M Cape",
+  "James Mason",
+  "James",
+  "Jan Dudulski",
+  "Jan-Christoph Borchardt",
+  "Jason Carey",
+  "Jason D Levine",
+  "Jason R Anderson",
+  "Jason Scurtu",
+  "Javier Jardón",
+  "Javier Monteagudo",
+  "Jean-François Fortin Tam",
+  "Jeff Waugh",
+  "Jeffrey Dorrycott",
+  "Jesse van den Kieboom",
+  "Jim Campbell",
+  "Jiri Eischmann",
+  "Joakim Söderlund",
+  "Joaquin Mendez",
+  "Johan Dahlin",
+  "John Gary Billings",
+  "John M Carr",
+  "John Palmieri",
+  "Jonathan Lane",
+  "Jonathan Lestrelin",
+  "Jonathan Zuñiga Juarez",
+  "Jonathon Jongsma",
+  "Jorge Rodriguez Flores Esp",
+  "Joseph Hain",
+  "Juan Jose Marin Martinez",
+  "Jugoslav Gacas",
+  "Julien Girardin",
+  "Jussi Henrik Kukkonen",
+  "Justin D Kruger",
+  "Justin Roth",
+  "Justyn Butler",
+  "Katrin Leinweber",
+  "Keith Tokash",
+  "Kenneth Nielsen",
+  "Khalid Eldehairy",
+  "Kris Thomsen",
+  "Lapo Calamandrei",
+  "Lars Uebernickel",
+  "Laurent Mouillart",
+  "Le Guevel Gwendal",
+  "Leif Gruenwoldt",
+  "Lionel Landwerlin",
+  "Logan VanCuren",
+  "Lucas Almeida Rocha",
+  "Lukasz Ochoda",
+  "Luke Gaudreau",
+  "M C V Crouch",
+  "M. Aurélien Couderc",
+  "Mac Baker",
+  "Maciej M Piechotka",
+  "Magnun Leno Silva",
+  "Marc Andre Lureau",
+  "Marc Bodmer",
+  "Marc Thomas",
+  "Marcio Sousa Rocha",
+  "Marco Barisione",
+  "Marcus Husar",
+  "Marcus Lundblad",
+  "Marek Suchánek",
+  "Marina Zhurakhinskaya",
+  "Mario Sanchez-Prada",
+  "Marius Heidenreich",
+  "Marius Mather",
+  "Markus Berg",
+  "Mart Roosmaa",
+  "Martin A Stembel",
+  "Martin Andersson",
+  "Martin Blanchard",
+  "Martin C Foster",
+  "Martin Unzner",
+  "Matej Smid",
+  "Mathieu Bridon",
+  "Matthew Nicholson",
+  "Mattias Bengtsson",
+  "Max Whittingham",
+  "Maxim Yaskevich",
+  "Michael Catanzaro",
+  "Michael Grundy",
+  "Michael Hill",
+  "Michael Ivanov",
+  "Michael Kuhn",
+  "Michael Mansell",
+  "Michael S DePaulo",
+  "Michael Scofield",
+  "Michel Alexandre Salim",
+  "Miguel e dos Santos",
+  "Mika",
+  "Mikel Olasagasti Uranga",
+  "Mikhail Feshchenko",
+  "Molly Shelestak",
+  "Nasser Alshammari",
+  "Nathan Samson",
+  "Neil Stalker",
+  "Neils Nesse",
+  "Nelson Jesus Benitez Leon",
+  "Nicholas E Richards",
+  "Nicholas George",
+  "Nick Melnick",
+  "Niclas Moeslund Overby",
+  "Nicola Mazbar",
+  "Nicolas Jeker",
+  "Niklas Rosenqvist",
+  "Nikola Trifunovic",
+  "Nil Gradisnik",
+  "Nirbheek Chauhan",
+  "Olav Vitters",
+  "Oliver Propst",
+  "Olivier Crete",
+  "Ondřej Holý",
+  "Ondřej Tůma",
+  "Owen Taylor",
+  "P Tunnell Wilson",
+  "P.F. Mulder",
+  "Pacaud Emmanuel",
+  "Pakkanen Jussi T",
+  "Paolo Borelli",
+  "Pascal Garber",
+  "Patrick Griffis",
+  "Patrick Wspanialy",
+  "Patrik Nilsson",
+  "Patrizio Bruno",
+  "Paul R Martin",
+  "Pedro J Ayala Gomariz",
+  "Perry L Peters",
+  "Peter Baumgarten",
+  "Peter Cornelis",
+  "Peter J Shinners",
+  "Peter Weber",
+  "Philip Corbett",
+  "Philip F Chimento",
+  "Philip J Freeman",
+  "Philip Whitfield",
+  "Piotr Zurek",
+  "R A McQueen",
+  "RM van Schouwen",
+  "Radosław Sierbiński",
+  "Ray Strode",
+  "Remco Kranenburg",
+  "Remi Grolleau",
+  "Rickard Johansson",
+  "Robert Carr",
+  "Robert Taylor",
+  "Roberto Clapis",
+  "Rocco Muscaritolo",
+  "Rodolphe PP",
+  "Rory MacQueen",
+  "Rosanna Blandford",
+  "Ross N Gardiner",
+  "Rouchon Jean-Noel",
+  "Rui Paulo Barreira",
+  "Russell Cox",
+  "Ryan Hartlage",
+  "Ryan Lerch",
+  "Rémi Lauzier",
+  "S Axon",
+  "Sajid Badi-uz-zaman",
+  "Samuel B Thursfield",
+  "Samuel Gyger",
+  "Sasan Namiranian",
+  "Saul Vargas Sandoval",
+  "Sebastian Droege",
+  "Shapor Naghibzadeh",
+  "Shawn Ferris",
+  "Shlomo Choina",
+  "Shuji Narazaki",
+  "Simon Roesch",
+  "Sriram Ramkrishna",
+  "Stefi T Petit",
+  "Stephany Wilkes",
+  "Stephen Genusa",
+  "Stephen Shaw",
+  "Steve Z McCauley",
+  "Steven J Herber",
+  "Steven W Brown",
+  "Steven Wills",
+  "Stewart Webb",
+  "Stuart Ellis",
+  "Stéphane Démurget",
+  "Stéphane Maniaci",
+  "Søren Hauberg",
+  "Tadej Janež",
+  "Ted Hennicke",
+  "Thibault Saunier",
+  "Thomas Andersen",
+  "Thomas Maffia",
+  "Thomas McDonald",
+  "Tom Erik Gundersen",
+  "Tom Pollok",
+  "Tomas Peterka",
+  "Tommi Tauriainen",
+  "Tomáš Krchňák",
+  "Tomáš Popela",
+  "Toni Willberg",
+  "Torsten Scholak",
+  "Travis Hartwell",
+  "Tyler J. Brock",
+  "Uwe Hametner",
+  "Vadzim Rutkouski",
+  "Valter Schütz",
+  "Vincent Bermel",
+  "WP Manley",
+  "Wee Weea",
+  "Wesley Wiser",
+  "Will Binns-Smith",
+  "William Hoffmann",
+  "William J Thompson",
+  "William Jon McCann",
+  "William R Lachance",
+  "Z Jedrzejewski-Szmek",
+  "adam820",
+  "arclnx",
+  "aurelien.busi",
+  "carwyn",
+  "d.westerik",
+  "daniel.fontaine",
+  "david odenwald",
+  "dchristidis",
+  "demirtas burakk",
+  "eik.w1911",
+  "eliasdorneles",
+  "elken.tdos",
+  "fafatheone",
+  "florian.muellner",
+  "gerald.b.nunn",
+  "gyger",
+  "ich",
+  "ideasman42",
+  "isak1096",
+  "jnoel",
+  "joannis.orlandos",
+  "joaquin8mendez",
+  "joel",
+  "juanjomarin96",
+  "kamilprusko",
+  "kenneth",
+  "kesmarag",
+  "kevin",
+  "kidoz",
+  "kuba",
+  "lovenemesis",
+  "luke.a.morton",
+  "madstitz",
+  "marc.chocolat",
+  "mariospr",
+  "markpariente",
+  "matthias.clasen",
+  "maxlupo",
+  "mcatanzaro",
+  "muflone",
+  "nils.werner",
+  "otaylor",
+  "pedroclg",
+  "peterldev94",
+  "pizzamartijn",
+  "pseus7+indiegogo",
+  "public228",
+  "ray strode",
+  "ross",
+  "sandquist",
+  "scottm2031",
+  "sebastien lafargue",
+  "sebastien.wilmet",
+  "sfs",
+  "slyon",
+  "swalf",
+  "sylvain.pasche",
+  "tglman",
+  "theo",
+  "tommaso.visconti",
+  "vamega",
+  "verduler",
+  "vperetokin",
+  "w.vollprecht",
+  NULL
+};
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-application-open.c b/src/libide/gui/ide-application-open.c
new file mode 100644
index 000000000..721bb0664
--- /dev/null
+++ b/src/libide/gui/ide-application-open.c
@@ -0,0 +1,169 @@
+/* ide-application-open.c
+ *
+ * Copyright 2018-2019 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 "ide-application-open"
+
+#include "config.h"
+
+#include "ide-application.h"
+#include "ide-application-private.h"
+#include "ide-primary-workspace.h"
+#include "ide-workbench.h"
+
+typedef struct
+{
+  IdeProjectInfo *project_info;
+  IdeWorkbench   *workbench;
+} LocateProjectByFile;
+
+static void
+locate_project_by_file (gpointer item,
+                        gpointer user_data)
+{
+  LocateProjectByFile *lookup = user_data;
+  IdeProjectInfo *project_info;
+  IdeWorkbench *workbench = item;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (lookup != NULL);
+
+  if (lookup->workbench != NULL)
+    return;
+
+  if (!(project_info = ide_workbench_get_project_info (workbench)))
+    return;
+
+  if (ide_project_info_equal (project_info, lookup->project_info))
+    lookup->workbench = workbench;
+}
+
+static void
+ide_application_open_project_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  IdeWorkbench *workbench = (IdeWorkbench *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_workbench_load_project_finish (workbench, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task, g_object_ref (workbench), g_object_unref);
+
+  IDE_EXIT;
+}
+
+void
+ide_application_open_project_async (IdeApplication      *self,
+                                    IdeProjectInfo      *project_info,
+                                    GType                workspace_type,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(IdeWorkbench) workbench = NULL;
+  LocateProjectByFile lookup = { project_info, NULL };
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+  g_return_if_fail (workspace_type == G_TYPE_INVALID ||
+                    g_type_is_a (workspace_type, IDE_TYPE_WORKSPACE));
+
+  if (workspace_type == G_TYPE_INVALID)
+    workspace_type = self->workspace_type;
+
+  self->workspace_type = IDE_TYPE_PRIMARY_WORKSPACE;
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_application_open_project_async);
+
+  /* Try to activate a previously opened workbench before creating
+   * and loading the project in a new one.
+   */
+  ide_application_foreach_workbench (self, locate_project_by_file, &lookup);
+
+  if (lookup.workbench != NULL)
+    {
+      ide_workbench_activate (lookup.workbench);
+      ide_task_return_pointer (task,
+                               g_object_ref (lookup.workbench),
+                               g_object_unref);
+      IDE_EXIT;
+    }
+
+  workbench = ide_workbench_new ();
+  ide_application_add_workbench (self, workbench);
+
+  ide_workbench_load_project_async (workbench,
+                                    project_info,
+                                    workspace_type,
+                                    cancellable,
+                                    ide_application_open_project_cb,
+                                    g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_application_open_project_finish:
+ * @self: a #IdeApplication
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError
+ *
+ * Completes a request to open a project.
+ *
+ * The workbench containing the project is returned, which may be an existing
+ * workbench if the project was already opened.
+ *
+ * Returns: (transfer full): an #IdeWorkbench or %NULL on failure and @error
+ *   is set.
+ *
+ * Since: 3.32
+ */
+IdeWorkbench *
+ide_application_open_project_finish (IdeApplication  *self,
+                                     GAsyncResult    *result,
+                                     GError         **error)
+{
+  IdeWorkbench *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_APPLICATION (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
diff --git a/src/libide/gui/ide-application-plugins.c b/src/libide/gui/ide-application-plugins.c
new file mode 100644
index 000000000..1f2a5e544
--- /dev/null
+++ b/src/libide/gui/ide-application-plugins.c
@@ -0,0 +1,471 @@
+/* ide-application-plugins.c
+ *
+ * Copyright 2018-2019 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 "ide-application-plugins"
+
+#include "config.h"
+
+#include <libide-plugins.h>
+
+#include "ide-application.h"
+#include "ide-application-addin.h"
+#include "ide-application-private.h"
+
+static GSettings *
+_ide_application_plugin_get_settings (IdeApplication *self,
+                                      const gchar    *module_name)
+{
+  GSettings *settings;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (module_name != NULL);
+
+  if G_UNLIKELY (self->plugin_settings == NULL)
+    self->plugin_settings =
+      g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+
+  if (!(settings = g_hash_table_lookup (self->plugin_settings, module_name)))
+    {
+      g_autofree gchar *path = NULL;
+
+      path = g_strdup_printf ("/org/gnome/builder/plugins/%s/", module_name);
+      settings = g_settings_new_with_path ("org.gnome.builder.plugin", path);
+      g_hash_table_insert (self->plugin_settings, g_strdup (module_name), settings);
+    }
+
+  return settings;
+}
+
+static gboolean
+ide_application_can_load_plugin (IdeApplication *self,
+                                 PeasPluginInfo *plugin_info,
+                                 GHashTable     *circular)
+{
+  PeasEngine *engine = peas_engine_get_default ();
+  const gchar *module_name;
+  const gchar *module_dir;
+  const gchar **deps;
+  GSettings *settings;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (circular != NULL);
+
+  if (plugin_info == NULL)
+    return FALSE;
+
+  module_dir = peas_plugin_info_get_module_dir (plugin_info);
+  module_name = peas_plugin_info_get_module_name (plugin_info);
+
+  /* Short-circuit for single-plugin mode */
+  if (self->plugin != NULL)
+    return ide_str_equal0 (module_name, self->plugin);
+
+  if (g_hash_table_contains (circular, module_name))
+    {
+      g_warning ("Circular dependency found in module %s", module_name);
+      return FALSE;
+    }
+
+  g_hash_table_add (circular, (gpointer)module_name);
+
+  /* Make sure the plugin has not been disabled in settings. */
+  settings = _ide_application_plugin_get_settings (self, module_name);
+  if (!g_settings_get_boolean (settings, "enabled"))
+    return FALSE;
+
+#if 0
+  if (self->mode == IDE_APPLICATION_MODE_WORKER)
+    {
+      if (self->worker != plugin_info)
+        return FALSE;
+    }
+#endif
+
+  /*
+   * If the plugin is not bundled within the Builder executable, then we
+   * require that an X-Builder-ABI=major.minor style extended data be
+   * provided to ensure we have proper ABI.
+   *
+   * You could get around this by loading a plugin that then loads resouces
+   * containing external data, but this is good enough for now.
+   */
+
+  if (!g_str_has_prefix (module_dir, "resource:///plugins/"))
+    {
+      const gchar *abi;
+
+      if (!(abi = peas_plugin_info_get_external_data (plugin_info, "Builder-ABI")))
+        {
+          g_critical ("Refusing to load plugin %s because X-Builder-ABI is missing",
+                      module_name);
+          return FALSE;
+        }
+
+      if (!g_str_has_prefix (IDE_VERSION_S, abi) ||
+          IDE_VERSION_S [strlen (abi)] != '.')
+        {
+          g_critical ("Refusing to load plugin %s, expected ABI %d.%d and got %s",
+                      module_name, IDE_MAJOR_VERSION, IDE_MINOR_VERSION, abi);
+          return FALSE;
+        }
+    }
+
+  /*
+   * If this plugin has dependencies, we need to check that the dependencies
+   * can also be loaded.
+   */
+  if ((deps = peas_plugin_info_get_dependencies (plugin_info)))
+    {
+      for (guint i = 0; deps[i]; i++)
+        {
+          PeasPluginInfo *dep = peas_engine_get_plugin_info (engine, deps[i]);
+
+          if (!ide_application_can_load_plugin (self, dep, circular))
+            return FALSE;
+        }
+    }
+
+  g_hash_table_remove (circular, (gpointer)module_name);
+
+  return TRUE;
+}
+
+static void
+ide_application_load_plugin_resources (IdeApplication *self,
+                                       PeasEngine     *engine,
+                                       PeasPluginInfo *plugin_info)
+{
+  g_autofree gchar *gresources_path = NULL;
+  g_autofree gchar *gresources_basename = NULL;
+  const gchar *module_dir;
+  const gchar *module_name;
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (plugin_info != NULL);
+  g_assert (PEAS_IS_ENGINE (engine));
+
+  module_dir = peas_plugin_info_get_module_dir (plugin_info);
+  module_name = peas_plugin_info_get_module_name (plugin_info);
+  gresources_basename = g_strdup_printf ("%s.gresource", module_name);
+  gresources_path = g_build_filename (module_dir, gresources_basename, NULL);
+
+  if (g_file_test (gresources_path, G_FILE_TEST_IS_REGULAR))
+    {
+      g_autofree gchar *resource_path = NULL;
+      g_autoptr(GError) error = NULL;
+      GResource *resource;
+
+      resource = g_resource_load (gresources_path, &error);
+
+      if (resource == NULL)
+        {
+          g_warning ("Failed to load gresources: %s", error->message);
+          return;
+        }
+
+      g_hash_table_insert (self->plugin_gresources, g_strdup (module_name), resource);
+      g_resources_register (resource);
+
+      resource_path = g_strdup_printf ("resource:///plugins/%s", module_name);
+      dzl_application_add_resources (DZL_APPLICATION (self), resource_path);
+    }
+}
+
+void
+_ide_application_load_plugin (IdeApplication *self,
+                              PeasPluginInfo *plugin_info)
+{
+  PeasEngine *engine = peas_engine_get_default ();
+  g_autoptr(GHashTable) circular = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (plugin_info != NULL);
+
+  circular = g_hash_table_new (g_str_hash, g_str_equal);
+
+  if (ide_application_can_load_plugin (self, plugin_info, circular))
+    peas_engine_load_plugin (engine, plugin_info);
+}
+
+static void
+ide_application_plugins_load_plugin_cb (IdeApplication *self,
+                                        PeasPluginInfo *plugin_info,
+                                        PeasEngine     *engine)
+{
+  const gchar *data_dir;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (plugin_info != NULL);
+  g_assert (PEAS_IS_ENGINE (engine));
+
+  if (peas_plugin_info_get_external_data (plugin_info, "Has-Resources"))
+    {
+      /* Possibly load bundled .gresource files if the plugin is not
+       * embedded into the application (such as python3 modules).
+       */
+      ide_application_load_plugin_resources (self, engine, plugin_info);
+    }
+
+  data_dir = peas_plugin_info_get_data_dir (plugin_info);
+
+  /*
+   * Only register resources if the path is to an embedded resource
+   * or if it's not builtin (and therefore maybe doesn't use .gresource
+   * files). That helps reduce the number IOPS we do.
+   */
+  if (g_str_has_prefix (data_dir, "resource://") ||
+      !peas_plugin_info_is_builtin (plugin_info))
+    dzl_application_add_resources (DZL_APPLICATION (self), data_dir);
+}
+
+static void
+ide_application_plugins_unload_plugin_cb (IdeApplication *self,
+                                          PeasPluginInfo *plugin_info,
+                                          PeasEngine     *engine)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (plugin_info != NULL);
+  g_assert (PEAS_IS_ENGINE (engine));
+
+}
+
+/**
+ * _ide_application_load_plugins_for_startup:
+ *
+ * This function will load all of the plugins that are candidates for
+ * early-stage initialization. Usually, that is any plugin that has a
+ * command-line handler and uses "X-At-Startup=true" in their .plugin
+ * manifest.
+ *
+ * Since: 3.32
+ */
+void
+_ide_application_load_plugins_for_startup (IdeApplication *self)
+{
+  PeasEngine *engine = peas_engine_get_default ();
+  const GList *plugins;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  g_signal_connect_object (engine,
+                           "load-plugin",
+                           G_CALLBACK (ide_application_plugins_load_plugin_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (engine,
+                           "unload-plugin",
+                           G_CALLBACK (ide_application_plugins_unload_plugin_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* Ensure that our embedded plugins are allowed early access to
+   * start loading (before we ever look at anything on disk). This
+   * ensures that only embedded plugins can be used at startup,
+   * saving us some precious disk I/O.
+   */
+  peas_engine_prepend_search_path (engine, "resource:///plugins", "resource:///plugins");
+
+  /* Our first step is to load our "At-Startup" plugins, which may
+   * contain things like command-line handlers. For example, the
+   * greeter may handle command-line options and then show the
+   * greeter workspace.
+   */
+  plugins = peas_engine_get_plugin_list (engine);
+  for (const GList *iter = plugins; iter; iter = iter->next)
+    {
+      PeasPluginInfo *plugin_info = iter->data;
+
+      if (!peas_plugin_info_is_loaded (plugin_info) &&
+          peas_plugin_info_get_external_data (plugin_info, "At-Startup"))
+        _ide_application_load_plugin (self, plugin_info);
+    }
+}
+
+/**
+ * _ide_application_load_plugins:
+ * @self: a #IdeApplication
+ *
+ * This function loads any additional plugins that have not yet been
+ * loaded during early startup.
+ *
+ * Since: 3.32
+ */
+void
+_ide_application_load_plugins (IdeApplication *self)
+{
+  g_autofree gchar *user_plugins_dir = NULL;
+  g_autoptr(GError) error = NULL;
+  const GList *plugins;
+  PeasEngine *engine;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  engine = peas_engine_get_default ();
+
+  /* Now that we have gotten past our startup plugins (which must be
+   * embedded into the gnome-builder executable, we can enable the
+   * system plugins that are loaded from disk.
+   */
+  peas_engine_prepend_search_path (engine,
+                                   PACKAGE_LIBDIR"/gnome-builder/plugins",
+                                   PACKAGE_DATADIR"/gnome-builder/plugins");
+
+  if (ide_is_flatpak ())
+    {
+      g_autofree gchar *plugins_dir = g_build_filename (g_get_home_dir (),
+                                                        ".local",
+                                                        "share",
+                                                        "gnome-builder",
+                                                        "plugins",
+                                                        NULL);
+      peas_engine_prepend_search_path (engine, plugins_dir, plugins_dir);
+    }
+
+  user_plugins_dir = g_build_filename (g_get_user_data_dir (),
+                                       "gnome-builder",
+                                       "plugins",
+                                       NULL);
+  peas_engine_prepend_search_path (engine, user_plugins_dir, NULL);
+
+  /* Ensure that we have all our required GObject Introspection packages
+   * loaded so that plugins don't need to require_version() as that is
+   * tedious and annoying to keep up to date.
+   *
+   * If we can't load any of our dependent packages, then fail to load
+   * python3 plugins altogether to avoid loading anything improper into
+   * the process space.
+   */
+  g_irepository_prepend_search_path (PACKAGE_LIBDIR"/gnome-builder/girepository-1.0");
+  if (!g_irepository_require (NULL, "GtkSource", "4", 0, &error) ||
+      !g_irepository_require (NULL, "Gio", "2.0", 0, &error) ||
+      !g_irepository_require (NULL, "GLib", "2.0", 0, &error) ||
+      !g_irepository_require (NULL, "Gtk", "3.0", 0, &error) ||
+      !g_irepository_require (NULL, "Dazzle", "1.0", 0, &error) ||
+      !g_irepository_require (NULL, "Jsonrpc", "1.0", 0, &error) ||
+      !g_irepository_require (NULL, "Template", "1.0", 0, &error) ||
+      !g_irepository_require (NULL, "Ide", PACKAGE_ABI_S, 0, &error))
+    g_critical ("Cannot enable Python 3 plugins: %s", error->message);
+  else
+    peas_engine_enable_loader (engine, "python3");
+
+  plugins = peas_engine_get_plugin_list (engine);
+
+  for (const GList *iter = plugins; iter; iter = iter->next)
+    {
+      PeasPluginInfo *plugin_info = iter->data;
+
+      if (!peas_plugin_info_is_loaded (plugin_info))
+        _ide_application_load_plugin (self, plugin_info);
+    }
+}
+
+static void
+ide_application_addin_added_cb (PeasExtensionSet *set,
+                                PeasPluginInfo   *plugin_info,
+                                PeasExtension    *exten,
+                                gpointer          user_data)
+{
+  IdeApplicationAddin *addin = (IdeApplicationAddin *)exten;
+  IdeApplication *self = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (self));
+
+  ide_application_addin_load (addin, self);
+}
+
+static void
+ide_application_addin_removed_cb (PeasExtensionSet *set,
+                                  PeasPluginInfo   *plugin_info,
+                                  PeasExtension    *exten,
+                                  gpointer          user_data)
+{
+  IdeApplicationAddin *addin = (IdeApplicationAddin *)exten;
+  IdeApplication *self = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (self));
+
+  ide_application_addin_unload (addin, self);
+}
+
+/**
+ * _ide_application_load_addins:
+ * @self: a #IdeApplication
+ *
+ * Loads the #IdeApplicationAddin's for this application.
+ *
+ * Since: 3.32
+ */
+void
+_ide_application_load_addins (IdeApplication *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (self->addins == NULL);
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_APPLICATION_ADDIN,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_application_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_application_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_application_addin_added_cb,
+                              self);
+}
+
+/**
+ * _ide_application_unload_addins:
+ * @self: a #IdeApplication
+ *
+ * Unloads all of the previously loaded #IdeApplicationAddin.
+ *
+ * Since: 3.32
+ */
+void
+_ide_application_unload_addins (IdeApplication *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (self->addins != NULL);
+
+  g_clear_object (&self->addins);
+}
diff --git a/src/libide/gui/ide-application-private.h b/src/libide/gui/ide-application-private.h
new file mode 100644
index 000000000..ba11c6a3d
--- /dev/null
+++ b/src/libide/gui/ide-application-private.h
@@ -0,0 +1,122 @@
+/* ide-application-private.h
+ *
+ * Copyright 2018-2019 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 <dazzle.h>
+#include <libpeas/peas.h>
+
+#include "ide-application.h"
+#include "ide-keybindings.h"
+#include "ide-worker-manager.h"
+
+G_BEGIN_DECLS
+
+struct _IdeApplication
+{
+  DzlApplication parent_instance;
+
+  /* Array of all of our IdeWorkebench instances (loaded projects and
+   * their application windows).
+   */
+  GPtrArray *workbenches;
+
+  /* We keep a hashtable of GSettings for each of the loaded plugins
+   * so that we can keep track if they are manually disabled using
+   * the org.gnome.builder.plugin gschema.
+   */
+  GHashTable *plugin_settings;
+
+  /* Addins which are created and destroyed with the application. We
+   * create them in ::startup() (after early stage operations have
+   * completed) and destroy them in ::shutdown().
+   */
+  PeasExtensionSet *addins;
+
+  /* DBus Proxy used to track color settings (Night Light) */
+  GDBusProxy *color_proxy;
+
+  /* org.gnome.Builder GSettings object to avoid creating a bunch
+   * of them (and ensuring it lives long enough to trigger signals
+   * for various keys.
+   */
+  GSettings *settings;
+
+  /* Tracks changes to plugins and updates the available keybindings
+   * to ensure they are loaded correctly (including .css files).
+   */
+  IdeKeybindings *keybindings;
+
+  /* We need to track the GResource files that were manually loaded for
+   * plugins on disk (generally Python plugins that need resources). That
+   * way we can remove them when the plugin is unloaded.
+   */
+  GHashTable *plugin_gresources;
+
+  /* We need to stash the unmodified argv for the application somewhere
+   * so that we can pass it to a remote instance. Otherwise we lose
+   * the ability by cmdline-addins to determine if any options were
+   * delivered to the program.
+   */
+  gchar **argv;
+
+  /* The time the application was started */
+  GDateTime *started_at;
+
+  /* Multi-process worker manager */
+  IdeWorkerManager *worker_manager;
+
+  /* Our type of process (optionally set to "worker" */
+  gchar *type;
+
+  /* The single plugin to load within a worker */
+  gchar *plugin;
+
+  /* The dbus-address for worker mode */
+  gchar *dbus_address;
+
+  /* Sets the type of workspace to create when creating the next workspace
+   * (such as when processing command line arguments).
+   */
+  GType workspace_type;
+
+  /* If we've detected we lost network access */
+  GNetworkMonitor *network_monitor;
+  guint has_network : 1;
+};
+
+IdeApplication *_ide_application_new                      (gboolean                 standalone,
+                                                           const gchar             *type,
+                                                           const gchar             *plugin,
+                                                           const gchar             *dbus_address);
+void            _ide_application_init_color               (IdeApplication          *self);
+void            _ide_application_init_actions             (IdeApplication          *self);
+void            _ide_application_init_shortcuts           (IdeApplication          *self);
+void            _ide_application_load_addins              (IdeApplication          *self);
+void            _ide_application_unload_addins            (IdeApplication          *self);
+void            _ide_application_load_plugin              (IdeApplication          *self,
+                                                           PeasPluginInfo          *plugin_info);
+void            _ide_application_add_option_entries       (IdeApplication          *self);
+void            _ide_application_load_plugins_for_startup (IdeApplication          *self);
+void            _ide_application_load_plugins             (IdeApplication          *self);
+void            _ide_application_command_line             (IdeApplication          *self,
+                                                           GApplicationCommandLine *cmdline);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-application-shortcuts.c b/src/libide/gui/ide-application-shortcuts.c
new file mode 100644
index 000000000..c604e64e8
--- /dev/null
+++ b/src/libide/gui/ide-application-shortcuts.c
@@ -0,0 +1,75 @@
+/* ide-application-shortcuts.c
+ *
+ * Copyright 2017 Sebastien Lafargue <slafargue gnome org>
+ *
+ * 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 "ide-application-shortcuts"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <dazzle.h>
+
+#include "ide-application-private.h"
+
+#define I_(s) (g_intern_static_string(s))
+
+void
+_ide_application_init_shortcuts (IdeApplication *self)
+{
+  DzlShortcutManager *manager;
+  DzlShortcutTheme *theme;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  manager = dzl_application_get_shortcut_manager (DZL_APPLICATION (self));
+  theme = dzl_shortcut_manager_get_theme_by_name (manager, "internal");
+
+  dzl_shortcut_manager_add_action (manager,
+                                   I_("app.help"),
+                                   NC_("shortcut window", "Workbench shortcuts"),
+                                   NC_("shortcut window", "Help"),
+                                   NC_("shortcut window", "Show the help window"),
+                                   NULL);
+  dzl_shortcut_theme_set_accel_for_action (theme,
+                                           "app.help",
+                                           "F1",
+                                           DZL_SHORTCUT_PHASE_GLOBAL);
+
+  dzl_shortcut_manager_add_action (manager,
+                                   I_("app.preferences"),
+                                   NC_("shortcut window", "Workbench shortcuts"),
+                                   NC_("shortcut window", "Preferences"),
+                                   NC_("shortcut window", "Show the preferences window"),
+                                   NULL);
+  dzl_shortcut_theme_set_accel_for_action (theme,
+                                           "app.preferences",
+                                           "<Primary>comma",
+                                           DZL_SHORTCUT_PHASE_GLOBAL);
+
+  dzl_shortcut_manager_add_action (manager,
+                                   I_("app.shortcuts"),
+                                   NC_("shortcut window", "Workbench shortcuts"),
+                                   NC_("shortcut window", "Help"),
+                                   NC_("shortcut window", "Show the shortcuts window"),
+                                   NULL);
+  dzl_shortcut_theme_set_accel_for_action (theme,
+                                           "app.shortcuts",
+                                           "<Primary>question",
+                                           DZL_SHORTCUT_PHASE_GLOBAL);
+}
diff --git a/src/libide/gui/ide-application.c b/src/libide/gui/ide-application.c
new file mode 100644
index 000000000..b19e96c4b
--- /dev/null
+++ b/src/libide/gui/ide-application.c
@@ -0,0 +1,617 @@
+/* ide-application.c
+ *
+ * Copyright 2014-2019 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 "ide-application"
+
+#include "config.h"
+
+#ifdef __linux__
+# include <sys/prctl.h>
+#endif
+
+#include <glib/gi18n.h>
+#include <libpeas/peas-autocleanups.h>
+#include <libide-themes.h>
+
+#include "ide-language-defaults.h"
+
+#include "ide-application.h"
+#include "ide-application-addin.h"
+#include "ide-application-private.h"
+#include "ide-gui-global.h"
+#include "ide-primary-workspace.h"
+#include "ide-worker.h"
+
+G_DEFINE_TYPE (IdeApplication, ide_application, DZL_TYPE_APPLICATION)
+
+#define IS_UI_PROCESS(app) ((app)->type == NULL)
+
+static void
+ide_application_add_platform_data (GApplication    *app,
+                                   GVariantBuilder *builder)
+{
+  IdeApplication *self = (IdeApplication *)app;
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (self->argv != NULL);
+
+  G_APPLICATION_CLASS (ide_application_parent_class)->add_platform_data (app, builder);
+
+  g_variant_builder_add (builder,
+                         "{sv}",
+                         "gnome-builder-version",
+                         g_variant_new_string (IDE_VERSION_S));
+  g_variant_builder_add (builder,
+                         "{sv}",
+                         "argv",
+                         g_variant_new_strv ((const gchar * const *)self->argv, -1));
+}
+
+static gint
+ide_application_command_line (GApplication            *app,
+                              GApplicationCommandLine *cmdline)
+{
+  IdeApplication *self = (IdeApplication *)app;
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  /* Allow plugins to handle command-line */
+  _ide_application_command_line (self, cmdline);
+
+  return G_APPLICATION_CLASS (ide_application_parent_class)->command_line (app, cmdline);
+}
+
+static gboolean
+ide_application_local_command_line (GApplication   *app,
+                                    gchar        ***arguments,
+                                    gint           *exit_status)
+{
+  IdeApplication *self = (IdeApplication *)app;
+
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (arguments != NULL);
+  g_assert (exit_status != NULL);
+  g_assert (self->argv == NULL);
+
+  /* Save these for later, to use by cmdline addins */
+  self->argv = g_strdupv (*arguments);
+
+  return G_APPLICATION_CLASS (ide_application_parent_class)->local_command_line (app, arguments, 
exit_status);
+}
+
+static void
+ide_application_register_keybindings (IdeApplication *self)
+{
+  g_autoptr(GSettings) settings = NULL;
+  g_autofree gchar *name = NULL;
+
+  g_assert (IDE_IS_APPLICATION (self));
+
+  settings = g_settings_new ("org.gnome.builder.editor");
+  name = g_settings_get_string (settings, "keybindings");
+  self->keybindings = ide_keybindings_new (name);
+  g_settings_bind (settings, "keybindings", self->keybindings, "mode", G_SETTINGS_BIND_GET);
+}
+
+static void
+ide_application_startup (GApplication *app)
+{
+  IdeApplication *self = (IdeApplication *)app;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+
+  /*
+   * We require a desktop session that provides a properly working
+   * DBus environment. Bail if for some reason that is not the case.
+   */
+  if (g_getenv ("DBUS_SESSION_BUS_ADDRESS") == NULL)
+    g_error ("%s",
+             _("GNOME Builder requires a desktop session with D-Bus. Please set DBUS_SESSION_BUS_ADDRESS."));
+
+  G_APPLICATION_CLASS (ide_application_parent_class)->startup (app);
+
+  if (IS_UI_PROCESS (self))
+    {
+      /* Setup access to private icons dir */
+      gtk_icon_theme_prepend_search_path (gtk_icon_theme_get_default (), PACKAGE_ICONDIR);
+
+      /* Load color settings (Night Light, Dark Mode, etc) */
+      _ide_application_init_color (self);
+    }
+
+  /* And now we can load the rest of our plugins for startup. */
+  _ide_application_load_plugins (self);
+
+  if (IS_UI_PROCESS (self))
+    {
+      /* Make sure our shorcuts are registered */
+      _ide_application_init_shortcuts (self);
+
+      /* Load keybindings from plugins and what not */
+      ide_application_register_keybindings (self);
+
+      /* Load language defaults into gsettings */
+      ide_language_defaults_init_async (NULL, NULL, NULL);
+    }
+}
+
+static void
+ide_application_shutdown (GApplication *app)
+{
+  IdeApplication *self = (IdeApplication *)app;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+
+  _ide_application_unload_addins (self);
+
+  g_clear_pointer (&self->plugin_settings, g_hash_table_unref);
+  g_clear_object (&self->addins);
+  g_clear_object (&self->color_proxy);
+  g_clear_object (&self->settings);
+  g_clear_object (&self->keybindings);
+
+  G_APPLICATION_CLASS (ide_application_parent_class)->shutdown (app);
+}
+
+static void
+ide_application_activate_worker (IdeApplication *self)
+{
+  g_autoptr(GDBusConnection) connection = NULL;
+  g_autoptr(GError) error = NULL;
+  PeasPluginInfo *plugin_info;
+  PeasExtension *extension;
+  PeasEngine *engine;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (ide_str_equal0 (self->type, "worker"));
+  g_assert (self->dbus_address != NULL);
+  g_assert (self->plugin != NULL);
+
+#ifdef __linux__
+  prctl (PR_SET_PDEATHSIG, SIGHUP);
+#endif
+
+  IDE_TRACE_MSG ("Connecting to %s", self->dbus_address);
+
+  connection = g_dbus_connection_new_for_address_sync (self->dbus_address,
+                                                       (G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
+                                                        G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING),
+                                                       NULL, NULL, &error);
+
+  if (error != NULL)
+    {
+      g_error ("DBus failure: %s", error->message);
+      IDE_EXIT;
+    }
+
+  engine = peas_engine_get_default ();
+
+  if (!(plugin_info = peas_engine_get_plugin_info (engine, self->plugin)))
+    {
+      g_error ("No such plugin \"%s\"", self->plugin);
+      IDE_EXIT;
+    }
+
+  if (!(extension = peas_engine_create_extension (engine, plugin_info, IDE_TYPE_WORKER, NULL)))
+    {
+      g_error ("Failed to create \"%s\" worker", self->plugin);
+      IDE_EXIT;
+    }
+
+  ide_worker_register_service (IDE_WORKER (extension), connection);
+  g_application_hold (G_APPLICATION (self));
+  g_dbus_connection_start_message_processing (connection);
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_activate (GApplication *app)
+{
+  IdeApplication *self = (IdeApplication *)app;
+  GtkWindow *window;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_APPLICATION (self));
+
+  if (ide_str_equal0 (self->type, "worker"))
+    {
+      ide_application_activate_worker (self);
+      return;
+    }
+
+  if ((window = gtk_application_get_active_window (GTK_APPLICATION (self))))
+    ide_gtk_window_present (window);
+
+  IDE_EXIT;
+}
+
+static void
+ide_application_dispose (GObject *object)
+{
+  IdeApplication *self = (IdeApplication *)object;
+
+  /* We don't necessarily get startup/shutdown called when we are
+   * the remote process, so ensure they get cleared here rather than
+   * in ::shutdown.
+   */
+  g_clear_pointer (&self->started_at, g_date_time_unref);
+  g_clear_pointer (&self->workbenches, g_ptr_array_unref);
+  g_clear_pointer (&self->plugin_settings, g_hash_table_unref);
+  g_clear_pointer (&self->plugin_gresources, g_hash_table_unref);
+  g_clear_pointer (&self->argv, g_strfreev);
+  g_clear_pointer (&self->plugin, g_free);
+  g_clear_pointer (&self->type, g_free);
+  g_clear_pointer (&self->dbus_address, g_free);
+  g_clear_object (&self->addins);
+  g_clear_object (&self->color_proxy);
+  g_clear_object (&self->settings);
+  g_clear_object (&self->network_monitor);
+  g_clear_object (&self->worker_manager);
+
+  G_OBJECT_CLASS (ide_application_parent_class)->dispose (object);
+}
+
+static void
+ide_application_class_init (IdeApplicationClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GApplicationClass *app_class = G_APPLICATION_CLASS (klass);
+
+  object_class->dispose = ide_application_dispose;
+
+  app_class->activate = ide_application_activate;
+  app_class->add_platform_data = ide_application_add_platform_data;
+  app_class->command_line = ide_application_command_line;
+  app_class->local_command_line = ide_application_local_command_line;
+  app_class->startup = ide_application_startup;
+  app_class->shutdown = ide_application_shutdown;
+}
+
+static void
+ide_application_init (IdeApplication *self)
+{
+  self->started_at = g_date_time_new_now_local ();
+  self->workspace_type = IDE_TYPE_PRIMARY_WORKSPACE;
+  self->workbenches = g_ptr_array_new_with_free_func (g_object_unref);
+  self->settings = g_settings_new ("org.gnome.builder");
+  self->plugin_gresources = g_hash_table_new_full (g_str_hash, g_str_equal, g_free,
+                                                   (GDestroyNotify)g_resource_unref);
+
+  g_application_set_default (G_APPLICATION (self));
+  gtk_window_set_default_icon_name (ide_get_application_id ());
+  ide_themes_init ();
+
+  /* Ensure our core data is loaded early. */
+  dzl_application_add_resources (DZL_APPLICATION (self), "resource:///org/gnome/libide-sourceview/");
+  dzl_application_add_resources (DZL_APPLICATION (self), "resource:///org/gnome/libide-gui/");
+
+  /* Make sure our GAction are available */
+  _ide_application_init_actions (self);
+}
+
+IdeApplication *
+_ide_application_new (gboolean     standalone,
+                      const gchar *type,
+                      const gchar *plugin,
+                      const gchar *dbus_address)
+{
+  GApplicationFlags flags = G_APPLICATION_HANDLES_COMMAND_LINE;
+  IdeApplication *self;
+
+  if (standalone || ide_str_equal0 (type, "worker"))
+    flags |= G_APPLICATION_NON_UNIQUE;
+
+  self = g_object_new (IDE_TYPE_APPLICATION,
+                       "application-id", ide_get_application_id (),
+                       "flags", flags,
+                       "resource-base-path", "/org/gnome/builder",
+                       NULL);
+
+  self->type = g_strdup (type);
+  self->plugin = g_strdup (plugin);
+  self->dbus_address = g_strdup (dbus_address);
+
+  /* Load plugins indicating they support startup features */
+  _ide_application_load_plugins_for_startup (self);
+
+  /* Now that early plugins are loaded, we can activate app addins. We'll
+   * load additional plugins later after post-early stage startup
+   */
+  _ide_application_load_addins (self);
+
+  /* Register command-line options, possibly from plugins. */
+  _ide_application_add_option_entries (self);
+
+  return g_steal_pointer (&self);
+}
+
+static void
+ide_application_add_workbench_cb (PeasExtensionSet *set,
+                                  PeasPluginInfo   *plugin_info,
+                                  PeasExtension    *exten,
+                                  gpointer          user_data)
+{
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_APPLICATION_ADDIN (exten));
+  g_assert (IDE_IS_WORKBENCH (user_data));
+
+  ide_application_addin_workbench_added (IDE_APPLICATION_ADDIN (exten), user_data);
+}
+
+void
+ide_application_add_workbench (IdeApplication *self,
+                               IdeWorkbench   *workbench)
+{
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+
+  g_ptr_array_add (self->workbenches, g_object_ref (workbench));
+
+  peas_extension_set_foreach (self->addins,
+                              ide_application_add_workbench_cb,
+                              workbench);
+}
+
+static void
+ide_application_remove_workbench_cb (PeasExtensionSet *set,
+                                     PeasPluginInfo   *plugin_info,
+                                     PeasExtension    *exten,
+                                     gpointer          user_data)
+{
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_APPLICATION_ADDIN (exten));
+  g_assert (IDE_IS_WORKBENCH (user_data));
+
+  ide_application_addin_workbench_removed (IDE_APPLICATION_ADDIN (exten), user_data);
+}
+
+void
+ide_application_remove_workbench (IdeApplication *self,
+                                  IdeWorkbench   *workbench)
+{
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+
+  peas_extension_set_foreach (self->addins,
+                              ide_application_remove_workbench_cb,
+                              workbench);
+
+  g_ptr_array_remove (self->workbenches, workbench);
+}
+
+/**
+ * ide_application_foreach_workbench:
+ * @self: an #IdeApplication
+ * @callback: (scope call): a #GFunc callback
+ * @user_data: user data for @callback
+ *
+ * Calls @callback for each of the registered workbenches.
+ *
+ * Since: 3.32
+ */
+void
+ide_application_foreach_workbench (IdeApplication *self,
+                                   GFunc           callback,
+                                   gpointer        user_data)
+{
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+  g_return_if_fail (callback != NULL);
+
+  for (guint i = self->workbenches->len; i > 0; i--)
+    {
+      IdeWorkbench *workbench = g_ptr_array_index (self->workbenches, i - 1);
+
+      callback (workbench, user_data);
+    }
+}
+
+/**
+ * ide_application_set_workspace_type:
+ * @self: a #IdeApplication
+ *
+ * Sets the #GType of an #IdeWorkspace that should be used when creating the
+ * next workspace upon handling files from command-line arguments. This is
+ * reset after the files are opened and is generally only useful from
+ * #IdeApplicationAddin's who need to alter the default workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_application_set_workspace_type (IdeApplication *self,
+                                    GType           workspace_type)
+{
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+  g_return_if_fail (g_type_is_a (workspace_type, IDE_TYPE_WORKSPACE));
+
+  self->workspace_type = workspace_type;
+}
+
+static void
+ide_application_network_changed_cb (IdeApplication  *self,
+                                    gboolean         available,
+                                    GNetworkMonitor *monitor)
+{
+  g_assert (IDE_IS_APPLICATION (self));
+  g_assert (G_IS_NETWORK_MONITOR (monitor));
+
+  self->has_network = !!available;
+}
+
+/**
+ * ide_application_has_network:
+ * @self: (nullable): a #IdeApplication
+ *
+ * This is a helper that uses an internal #GNetworkMonitor to track if we
+ * have access to the network. It works around some issues we've seen in
+ * the wild that make determining if we have network access difficult.
+ *
+ * Returns: %TRUE if we think there is network access.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_application_has_network (IdeApplication *self)
+{
+  g_return_val_if_fail (!self || IDE_IS_APPLICATION (self), FALSE);
+
+  if (self == NULL)
+    self = IDE_APPLICATION_DEFAULT;
+
+  if (self->network_monitor == NULL)
+    {
+      self->network_monitor = g_object_ref (g_network_monitor_get_default ());
+
+      g_signal_connect_object (self->network_monitor,
+                               "network-changed",
+                               G_CALLBACK (ide_application_network_changed_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      self->has_network = g_network_monitor_get_network_available (self->network_monitor);
+
+      /*
+       * FIXME: Ignore the network portal initially for now.
+       *
+       * See https://gitlab.gnome.org/GNOME/glib/merge_requests/227 for more
+       * information about when this is fixed.
+       */
+      if (!self->has_network && ide_is_flatpak ())
+        self->has_network = TRUE;
+    }
+
+  return self->has_network;
+}
+
+/**
+ * ide_application_get_started_at:
+ * @self: a #IdeApplication
+ *
+ * Gets the time the application was started.
+ *
+ * Returns: (transfer none): a #GDateTime
+ *
+ * Since: 3.32
+ */
+GDateTime *
+ide_application_get_started_at (IdeApplication *self)
+{
+  g_return_val_if_fail (IDE_IS_APPLICATION (self), NULL);
+
+  return self->started_at;
+}
+
+static void
+ide_application_get_worker_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  IdeWorkerManager *worker_manager = (IdeWorkerManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GDBusProxy *proxy;
+
+  g_assert (IDE_IS_WORKER_MANAGER (worker_manager));
+
+  if (!(proxy = ide_worker_manager_get_worker_finish (worker_manager, result, &error)))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task, g_steal_pointer (&proxy), g_object_unref);
+}
+
+/**
+ * ide_application_get_worker_async:
+ * @self: an #IdeApplication
+ * @plugin_name: The name of the plugin.
+ * @cancellable: (allow-none): a #GCancellable or %NULL.
+ * @callback: a #GAsyncReadyCallback or %NULL.
+ * @user_data: user data for @callback.
+ *
+ * Asynchronously requests a #GDBusProxy to a service provided in a worker
+ * process. The worker should be an #IdeWorker implemented by the plugin named
+ * @plugin_name. The #IdeWorker is responsible for created both the service
+ * registered on the bus and the proxy to it.
+ *
+ * The #IdeApplication is responsible for spawning a subprocess for the worker.
+ *
+ * @callback should call ide_application_get_worker_finish() with the result
+ * provided to retrieve the result.
+ *
+ * Since: 3.32
+ */
+void
+ide_application_get_worker_async (IdeApplication      *self,
+                                  const gchar         *plugin_name,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_APPLICATION (self));
+  g_return_if_fail (plugin_name != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (self->worker_manager == NULL)
+    self->worker_manager = ide_worker_manager_new ();
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_application_get_worker_async);
+
+  ide_worker_manager_get_worker_async (self->worker_manager,
+                                       plugin_name,
+                                       cancellable,
+                                       ide_application_get_worker_cb,
+                                       g_steal_pointer (&task));
+}
+
+/**
+ * ide_application_get_worker_finish:
+ * @self: an #IdeApplication.
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL.
+ *
+ * Completes an asynchronous request to get a proxy to a worker process.
+ *
+ * Returns: (transfer full): a #GDBusProxy or %NULL.
+ *
+ * Since: 3.32
+ */
+GDBusProxy *
+ide_application_get_worker_finish (IdeApplication  *self,
+                                   GAsyncResult    *result,
+                                   GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_APPLICATION (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
diff --git a/src/libide/gui/ide-application.h b/src/libide/gui/ide-application.h
new file mode 100644
index 000000000..46a231c52
--- /dev/null
+++ b/src/libide/gui/ide-application.h
@@ -0,0 +1,83 @@
+/* ide-application.h
+ *
+ * Copyright 2014-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+#include <libide-projects.h>
+
+#include "ide-workbench.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_APPLICATION    (ide_application_get_type())
+#define IDE_APPLICATION_DEFAULT IDE_APPLICATION(g_application_get_default())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeApplication, ide_application, IDE, APPLICATION, DzlApplication)
+
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_application_has_network         (IdeApplication           *self);
+IDE_AVAILABLE_IN_3_32
+gchar          **ide_application_get_argv            (IdeApplication           *self,
+                                                      GApplicationCommandLine  *cmdline);
+IDE_AVAILABLE_IN_3_32
+GDateTime       *ide_application_get_started_at      (IdeApplication           *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_application_open_project_async  (IdeApplication           *self,
+                                                      IdeProjectInfo           *project_info,
+                                                      GType                     workspace_type,
+                                                      GCancellable             *cancellable,
+                                                      GAsyncReadyCallback       callback,
+                                                      gpointer                  user_data);
+IDE_AVAILABLE_IN_3_32
+IdeWorkbench    *ide_application_open_project_finish (IdeApplication           *self,
+                                                      GAsyncResult             *result,
+                                                      GError                  **error);
+IDE_AVAILABLE_IN_3_32
+void             ide_application_set_workspace_type  (IdeApplication           *self,
+                                                      GType                     workspace_type);
+IDE_AVAILABLE_IN_3_32
+void             ide_application_add_workbench       (IdeApplication           *self,
+                                                      IdeWorkbench             *workbench);
+IDE_AVAILABLE_IN_3_32
+void             ide_application_remove_workbench    (IdeApplication           *self,
+                                                      IdeWorkbench             *workbench);
+IDE_AVAILABLE_IN_3_32
+void             ide_application_foreach_workbench   (IdeApplication           *self,
+                                                      GFunc                     callback,
+                                                      gpointer                  user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_application_get_worker_async    (IdeApplication           *self,
+                                                      const gchar              *plugin_name,
+                                                      GCancellable             *cancellable,
+                                                      GAsyncReadyCallback       callback,
+                                                      gpointer                  user_data);
+IDE_AVAILABLE_IN_3_32
+GDBusProxy      *ide_application_get_worker_finish   (IdeApplication           *self,
+                                                      GAsyncResult             *result,
+                                                      GError                  **error);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-cell-renderer-fancy.c b/src/libide/gui/ide-cell-renderer-fancy.c
new file mode 100644
index 000000000..212cb58f0
--- /dev/null
+++ b/src/libide/gui/ide-cell-renderer-fancy.c
@@ -0,0 +1,393 @@
+/* ide-cell-renderer-fancy.c
+ *
+ * Copyright 2017-2019 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 "ide-cell-renderer-fancy"
+
+#include "config.h"
+
+#include "ide-cell-renderer-fancy.h"
+
+#define TITLE_SPACING 3
+
+struct _IdeCellRendererFancy
+{
+  GtkCellRenderer parent_instance;
+
+  gchar *title;
+  gchar *body;
+};
+
+enum {
+  PROP_0,
+  PROP_BODY,
+  PROP_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeCellRendererFancy, ide_cell_renderer_fancy, GTK_TYPE_CELL_RENDERER)
+
+static GParamSpec *properties [N_PROPS];
+
+static PangoLayout *
+get_layout (IdeCellRendererFancy *self,
+            GtkWidget            *widget,
+            const gchar          *text,
+            gboolean              is_title,
+            GtkCellRendererState  flags)
+{
+  PangoLayout *l;
+  PangoAttrList *attrs;
+  GtkStyleContext *style = gtk_widget_get_style_context (widget);
+  GtkStateFlags state = gtk_style_context_get_state (style);
+  GdkRGBA rgba;
+
+  l = gtk_widget_create_pango_layout (widget, text);
+
+  if (text == NULL || *text == 0)
+    return l;
+
+  attrs = pango_attr_list_new ();
+
+  gtk_style_context_get_color (style, state, &rgba);
+  pango_attr_list_insert (attrs,
+                          pango_attr_foreground_new (rgba.red * 65535,
+                                                     rgba.green * 65535,
+                                                     rgba.blue * 65535));
+
+  if (is_title)
+    {
+      pango_attr_list_insert (attrs, pango_attr_scale_new (0.8333));
+      pango_attr_list_insert (attrs, pango_attr_foreground_alpha_new (65536 * 0.5));
+    }
+
+  pango_layout_set_attributes (l, attrs);
+  pango_attr_list_unref (attrs);
+
+  return l;
+}
+
+static GtkSizeRequestMode
+ide_cell_renderer_fancy_get_request_mode (GtkCellRenderer *cell)
+{
+  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+ide_cell_renderer_fancy_get_preferred_width (GtkCellRenderer *cell,
+                                             GtkWidget       *widget,
+                                             gint            *min_width,
+                                             gint            *nat_width)
+{
+  IdeCellRendererFancy *self = (IdeCellRendererFancy *)cell;
+  PangoLayout *body;
+  PangoLayout *title;
+  gint body_width = 0;
+  gint title_width = 0;
+  gint dummy;
+  gint xpad;
+  gint ypad;
+
+  if (min_width == NULL)
+    min_width = &dummy;
+
+  if (nat_width == NULL)
+    nat_width = &dummy;
+
+  g_assert (IDE_IS_CELL_RENDERER_FANCY (self));
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (min_width != NULL);
+  g_assert (nat_width != NULL);
+
+  gtk_cell_renderer_get_padding (cell, &xpad, &ypad);
+
+  body = get_layout (self, widget, self->body, FALSE, 0);
+  title = get_layout (self, widget, self->title, TRUE, 0);
+
+  pango_layout_set_width (body, -1);
+  pango_layout_set_width (title, -1);
+
+  pango_layout_get_pixel_size (body, &body_width, NULL);
+  pango_layout_get_pixel_size (title, &title_width, NULL);
+
+  *min_width = xpad * 2;
+  *nat_width = (xpad * 2) + MAX (title_width, body_width);
+
+  g_object_unref (body);
+  g_object_unref (title);
+}
+
+static void
+ide_cell_renderer_fancy_get_preferred_height_for_width (GtkCellRenderer *cell,
+                                                        GtkWidget       *widget,
+                                                        gint             width,
+                                                        gint            *min_height,
+                                                        gint            *nat_height)
+{
+  IdeCellRendererFancy *self = (IdeCellRendererFancy *)cell;
+  PangoLayout *body;
+  PangoLayout *title;
+  GtkAllocation alloc;
+  gint body_height = 0;
+  gint title_height = 0;
+  gint xpad;
+  gint ypad;
+  gint dummy;
+
+  if (min_height == NULL)
+    min_height = &dummy;
+
+  if (nat_height == NULL)
+    nat_height = &dummy;
+
+  g_assert (IDE_IS_CELL_RENDERER_FANCY (self));
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (min_height != NULL);
+  g_assert (nat_height != NULL);
+
+  gtk_cell_renderer_get_padding (cell, &xpad, &ypad);
+
+  /*
+   * HACK: @width is the min_width returned in our get_preferred_width()
+   *       function. That results in pretty bad values here, so we will
+   *       do this by assuming we are the onl widget in the tree view.
+   *
+   *       This makes this cell very much not usable for generic situations,
+   *       but it does make it so we can do text wrapping without resorting
+   *       to GtkListBox *for our exact usecase only*.
+   *
+   *       The problem here is that we require the widget to already be
+   *       realized and allocated and that we are the only renderer
+   *       within the only column (and also, in a treeview) without
+   *       exotic styling.
+   *
+   *       If we get something absurdly small (like 50) that is because we
+   *       are hitting our minimum size of (xpad * 2). So this works around
+   *       the issue and tries to get something reasonable with wrapping
+   *       at the 200px mark (our ~default width for panels).
+   *
+   *       Furthermore, we need to queue a resize when the column size
+   *       changes (as it will from resizing the widget). So the tree
+   *       view must also call gtk_tree_view_column_queue_resize().
+   */
+  gtk_widget_get_allocation (widget, &alloc);
+  if (alloc.width > width)
+    width = alloc.width - (xpad * 2);
+  else if (alloc.width < 50)
+    width = 200;
+
+  body = get_layout (self, widget, self->body, FALSE, 0);
+  title = get_layout (self, widget, self->title, TRUE, 0);
+
+  pango_layout_set_width (body, width * PANGO_SCALE);
+  pango_layout_set_width (title, width * PANGO_SCALE);
+  pango_layout_get_pixel_size (title, NULL, &title_height);
+  pango_layout_get_pixel_size (body, NULL, &body_height);
+  *min_height = *nat_height = (ypad * 2) + title_height + TITLE_SPACING + body_height;
+
+  g_object_unref (body);
+  g_object_unref (title);
+}
+
+static void
+ide_cell_renderer_fancy_render (GtkCellRenderer      *renderer,
+                                cairo_t              *cr,
+                                GtkWidget            *widget,
+                                const GdkRectangle   *bg_area,
+                                const GdkRectangle   *cell_area,
+                                GtkCellRendererState  flags)
+{
+  IdeCellRendererFancy *self = (IdeCellRendererFancy *)renderer;
+  PangoLayout *body;
+  PangoLayout *title;
+  gint xpad;
+  gint ypad;
+  gint height;
+
+  g_assert (IDE_IS_CELL_RENDERER_FANCY (self));
+  g_assert (cr != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (bg_area != NULL);
+  g_assert (cell_area != NULL);
+
+  gtk_cell_renderer_get_padding (renderer, &xpad, &ypad);
+
+  body = get_layout (self, widget, self->body, FALSE, flags);
+  title = get_layout (self, widget, self->title, TRUE, flags);
+
+  pango_layout_set_width (title, (cell_area->width - (xpad * 2)) * PANGO_SCALE);
+  pango_layout_set_width (body, (cell_area->width - (xpad * 2)) * PANGO_SCALE);
+
+  cairo_move_to (cr, cell_area->x + xpad, cell_area->y + ypad);
+  pango_cairo_show_layout (cr, title);
+
+  pango_layout_get_pixel_size (title, NULL, &height);
+  cairo_move_to (cr, cell_area->x + xpad, cell_area->y +ypad + + height + TITLE_SPACING);
+  pango_cairo_show_layout (cr, body);
+
+  g_object_unref (body);
+  g_object_unref (title);
+}
+
+static void
+ide_cell_renderer_fancy_finalize (GObject *object)
+{
+  IdeCellRendererFancy *self = (IdeCellRendererFancy *)object;
+
+  g_clear_pointer (&self->body, g_free);
+  g_clear_pointer (&self->title, g_free);
+
+  G_OBJECT_CLASS (ide_cell_renderer_fancy_parent_class)->finalize (object);
+}
+
+static void
+ide_cell_renderer_fancy_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeCellRendererFancy *self = IDE_CELL_RENDERER_FANCY (object);
+
+  switch (prop_id)
+    {
+    case PROP_BODY:
+      g_value_set_string (value, self->body);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, self->title);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_cell_renderer_fancy_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeCellRendererFancy *self = IDE_CELL_RENDERER_FANCY (object);
+
+  switch (prop_id)
+    {
+    case PROP_BODY:
+      ide_cell_renderer_fancy_set_body (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_cell_renderer_fancy_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_cell_renderer_fancy_class_init (IdeCellRendererFancyClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkCellRendererClass *cell_class = GTK_CELL_RENDERER_CLASS (klass);
+
+  object_class->finalize = ide_cell_renderer_fancy_finalize;
+  object_class->get_property = ide_cell_renderer_fancy_get_property;
+  object_class->set_property = ide_cell_renderer_fancy_set_property;
+
+  cell_class->get_request_mode = ide_cell_renderer_fancy_get_request_mode;
+  cell_class->get_preferred_width = ide_cell_renderer_fancy_get_preferred_width;
+  cell_class->get_preferred_height_for_width = ide_cell_renderer_fancy_get_preferred_height_for_width;
+  cell_class->render = ide_cell_renderer_fancy_render;
+
+  /* Note that we do not emit notify for these properties */
+
+  properties [PROP_BODY] =
+    g_param_spec_string ("body",
+                         "Body",
+                         "The body of the renderer",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the renderer",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_cell_renderer_fancy_init (IdeCellRendererFancy *self)
+{
+}
+
+const gchar *
+ide_cell_renderer_fancy_get_title (IdeCellRendererFancy *self)
+{
+  return self->title;
+}
+
+/**
+ * ide_cell_renderer_fancy_take_title:
+ * @self: a #IdeCellRendererFancy
+ * @title: (transfer full) (nullable): the new title
+ *
+ * Like ide_cell_renderer_fancy_set_title() but takes ownership
+ * of @title, saving a string copy.
+ *
+ * Since: 3.32
+ */
+void
+ide_cell_renderer_fancy_take_title (IdeCellRendererFancy *self,
+                                    gchar                *title)
+{
+  if (self->title != title)
+    {
+      g_free (self->title);
+      self->title = title;
+    }
+}
+
+void
+ide_cell_renderer_fancy_set_title (IdeCellRendererFancy *self,
+                                   const gchar          *title)
+{
+  ide_cell_renderer_fancy_take_title (self, g_strdup (title));
+}
+
+const gchar *
+ide_cell_renderer_fancy_get_body (IdeCellRendererFancy *self)
+{
+  return self->body;
+}
+
+void
+ide_cell_renderer_fancy_set_body (IdeCellRendererFancy *self,
+                                  const gchar          *body)
+{
+  if (self->body != body)
+    {
+      g_free (self->body);
+      self->body = g_strdup (body);
+    }
+}
diff --git a/src/libide/gui/ide-cell-renderer-fancy.h b/src/libide/gui/ide-cell-renderer-fancy.h
new file mode 100644
index 000000000..2d768612f
--- /dev/null
+++ b/src/libide/gui/ide-cell-renderer-fancy.h
@@ -0,0 +1,53 @@
+/* ide-cell-renderer-fancy.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CELL_RENDERER_FANCY (ide_cell_renderer_fancy_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeCellRendererFancy, ide_cell_renderer_fancy, IDE, CELL_RENDERER_FANCY, 
GtkCellRenderer)
+
+IDE_AVAILABLE_IN_3_32
+GtkCellRenderer *ide_cell_renderer_fancy_new        (void);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_cell_renderer_fancy_get_body   (IdeCellRendererFancy *self);
+IDE_AVAILABLE_IN_3_32
+const gchar     *ide_cell_renderer_fancy_get_title  (IdeCellRendererFancy *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_cell_renderer_fancy_take_title (IdeCellRendererFancy *self,
+                                                     gchar                *title);
+IDE_AVAILABLE_IN_3_32
+void             ide_cell_renderer_fancy_set_title  (IdeCellRendererFancy *self,
+                                                     const gchar          *title);
+IDE_AVAILABLE_IN_3_32
+void             ide_cell_renderer_fancy_set_body   (IdeCellRendererFancy *self,
+                                                     const gchar          *body);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-command-provider.c b/src/libide/gui/ide-command-provider.c
new file mode 100644
index 000000000..8b0177ab9
--- /dev/null
+++ b/src/libide/gui/ide-command-provider.c
@@ -0,0 +1,103 @@
+/* ide-command-provider.c
+ *
+ * Copyright 2018-2019 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 "ide-command-provider"
+
+#include "config.h"
+
+#include "ide-command-provider.h"
+
+G_DEFINE_INTERFACE (IdeCommandProvider, ide_command_provider, G_TYPE_OBJECT)
+
+static void
+ide_command_provider_real_query_async (IdeCommandProvider  *self,
+                                       IdeWorkspace        *workspace,
+                                       const gchar         *typed_text,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_command_provider_real_query_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Querying is not supported by this provider");
+}
+
+static GPtrArray *
+ide_command_provider_real_query_finish (IdeCommandProvider  *self,
+                                        GAsyncResult        *result,
+                                        GError             **error)
+{
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+ide_command_provider_default_init (IdeCommandProviderInterface *iface)
+{
+  iface->query_async = ide_command_provider_real_query_async;
+  iface->query_finish = ide_command_provider_real_query_finish;
+}
+
+void
+ide_command_provider_query_async (IdeCommandProvider  *self,
+                                  IdeWorkspace        *workspace,
+                                  const gchar         *typed_text,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_COMMAND_PROVIDER (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+  g_return_if_fail (typed_text != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_COMMAND_PROVIDER_GET_IFACE (self)->query_async (self,
+                                                      workspace,
+                                                      typed_text,
+                                                      cancellable,
+                                                      callback,
+                                                      user_data);
+}
+
+/**
+ * ide_command_provider_query_finish:
+ * @self: a #IdeCommandProvider
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to locate all the commands matching the
+ * users typed text.
+ *
+ * Returns: (transfer full) (element-type IdeCommand): a #GPtrArray of
+ *   #IdeCommand, or %NULL.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_command_provider_query_finish (IdeCommandProvider  *self,
+                                   GAsyncResult        *result,
+                                   GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND_PROVIDER (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_COMMAND_PROVIDER_GET_IFACE (self)->query_finish (self, result, error);
+}
diff --git a/src/libide/gui/ide-command-provider.h b/src/libide/gui/ide-command-provider.h
new file mode 100644
index 000000000..5fc08f06b
--- /dev/null
+++ b/src/libide/gui/ide-command-provider.h
@@ -0,0 +1,66 @@
+/* ide-command-provider.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-command.h"
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMMAND_PROVIDER (ide_command_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeCommandProvider, ide_command_provider, IDE, COMMAND_PROVIDER, GObject)
+
+struct _IdeCommandProviderInterface
+{
+  GTypeInterface parent_iface;
+
+  void       (*query_async)  (IdeCommandProvider   *self,
+                              IdeWorkspace         *workspace,
+                              const gchar          *typed_text,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data);
+  GPtrArray *(*query_finish) (IdeCommandProvider   *self,
+                              GAsyncResult         *result,
+                              GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void       ide_command_provider_query_async  (IdeCommandProvider   *self,
+                                              IdeWorkspace         *workspace,
+                                              const gchar          *typed_text,
+                                              GCancellable         *cancellable,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GPtrArray *ide_command_provider_query_finish (IdeCommandProvider   *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-command.c b/src/libide/gui/ide-command.c
new file mode 100644
index 000000000..8d99d7638
--- /dev/null
+++ b/src/libide/gui/ide-command.c
@@ -0,0 +1,153 @@
+/* ide-command.c
+ *
+ * Copyright 2018-2019 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 "ide-command"
+
+#include "config.h"
+
+#include "ide-command.h"
+
+G_DEFINE_INTERFACE (IdeCommand, ide_command, IDE_TYPE_OBJECT)
+
+static void
+ide_command_real_run_async (IdeCommand          *self,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_COMMAND (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_command_real_run_async);
+  g_task_return_new_error (task,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "The operation is not supported");
+}
+
+static gboolean
+ide_command_real_run_finish (IdeCommand    *self,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_command_default_init (IdeCommandInterface *iface)
+{
+  iface->run_async = ide_command_real_run_async;
+  iface->run_finish = ide_command_real_run_finish;
+}
+
+/**
+ * ide_command_run_async:
+ * @self: an #IdeCommand
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Runs the command, asynchronously.
+ *
+ * Use ide_command_run_finish() to get the result of the operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_command_run_async (IdeCommand          *self,
+                       GCancellable        *cancellable,
+                       GAsyncReadyCallback  callback,
+                       gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_COMMAND (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_COMMAND_GET_IFACE (self)->run_async (self, cancellable, callback, user_data);
+}
+
+/**
+ * ide_command_run_finish:
+ * @self: an #IdeCommand
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Returns: %TRUE if the command was successful; otherwise %FALSE
+ *   and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_command_run_finish (IdeCommand    *self,
+                        GAsyncResult  *result,
+                        GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_COMMAND_GET_IFACE (self)->run_finish (self, result, error);
+}
+
+/**
+ * ide_command_get_title:
+ * @self: a #IdeCommand
+ *
+ * Gets the title for the command.
+ *
+ * Returns: a string containing the title
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_command_get_title (IdeCommand *self)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND (self), NULL);
+
+  if (IDE_COMMAND_GET_IFACE (self)->get_title)
+    return IDE_COMMAND_GET_IFACE (self)->get_title (self);
+
+  return NULL;
+}
+
+/**
+ * ide_command_get_subtitle:
+ * @self: a #IdeCommand
+ *
+ * Gets the subtitle for the command.
+ *
+ * Returns: a string containing the subtitle
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_command_get_subtitle (IdeCommand *self)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND (self), NULL);
+
+  if (IDE_COMMAND_GET_IFACE (self)->get_subtitle)
+    return IDE_COMMAND_GET_IFACE (self)->get_subtitle (self);
+
+  return NULL;
+}
diff --git a/src/libide/gui/ide-command.h b/src/libide/gui/ide-command.h
new file mode 100644
index 000000000..0b9f389c7
--- /dev/null
+++ b/src/libide/gui/ide-command.h
@@ -0,0 +1,66 @@
+/* ide-command.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMMAND (ide_command_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeCommand, ide_command, IDE, COMMAND, IdeObject)
+
+struct _IdeCommandInterface
+{
+  GTypeInterface parent_iface;
+
+  gchar    *(*get_title)    (IdeCommand           *self);
+  gchar    *(*get_subtitle) (IdeCommand           *self);
+  void      (*run_async)    (IdeCommand           *self,
+                             GCancellable         *cancellable,
+                             GAsyncReadyCallback   callback,
+                             gpointer              user_data);
+  gboolean  (*run_finish)   (IdeCommand           *self,
+                             GAsyncResult         *result,
+                             GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+gchar   *ide_command_get_title    (IdeCommand           *self);
+IDE_AVAILABLE_IN_3_32
+gchar   *ide_command_get_subtitle (IdeCommand           *self);
+IDE_AVAILABLE_IN_3_32
+void     ide_command_run_async    (IdeCommand           *self,
+                                   GCancellable         *cancellable,
+                                   GAsyncReadyCallback   callback,
+                                   gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_command_run_finish   (IdeCommand           *self,
+                                   GAsyncResult         *result,
+                                   GError              **error);
+
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-config-view-addin.c b/src/libide/gui/ide-config-view-addin.c
new file mode 100644
index 000000000..eefc65ec1
--- /dev/null
+++ b/src/libide/gui/ide-config-view-addin.c
@@ -0,0 +1,46 @@
+/* ide-config-view-addin.c
+ *
+ * Copyright 2018-2019 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 "ide-config-view-addin"
+
+#include "config.h"
+
+#include "ide-config-view-addin.h"
+
+G_DEFINE_INTERFACE (IdeConfigViewAddin, ide_config_view_addin, G_TYPE_OBJECT)
+
+static void
+ide_config_view_addin_default_init (IdeConfigViewAddinInterface *iface)
+{
+}
+
+void
+ide_config_view_addin_load (IdeConfigViewAddin *self,
+                            DzlPreferences     *preferences,
+                            IdeConfiguration   *configuration)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_CONFIG_VIEW_ADDIN (self));
+  g_return_if_fail (DZL_IS_PREFERENCES (preferences));
+  g_return_if_fail (IDE_IS_CONFIGURATION (configuration));
+
+  if (IDE_CONFIG_VIEW_ADDIN_GET_IFACE (self)->load)
+    IDE_CONFIG_VIEW_ADDIN_GET_IFACE (self)->load (self, preferences, configuration);
+}
diff --git a/src/libide/gui/ide-config-view-addin.h b/src/libide/gui/ide-config-view-addin.h
new file mode 100644
index 000000000..8786c80e6
--- /dev/null
+++ b/src/libide/gui/ide-config-view-addin.h
@@ -0,0 +1,48 @@
+/* ide-config-view-addin.h
+ *
+ * Copyright 2018-2019 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 <dazzle.h>
+#include <libide-core.h>
+#include <libide-foundry.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_CONFIG_VIEW_ADDIN (ide_config_view_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeConfigViewAddin, ide_config_view_addin, IDE, CONFIG_VIEW_ADDIN, GObject)
+
+struct _IdeConfigViewAddinInterface
+{
+  GTypeInterface parent_iface;
+
+  void (*load) (IdeConfigViewAddin *self,
+                DzlPreferences     *preferences,
+                IdeConfiguration   *configuration);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_config_view_addin_load (IdeConfigViewAddin *self,
+                                 DzlPreferences     *preferences,
+                                 IdeConfiguration   *configuration);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-environment-editor-row.c b/src/libide/gui/ide-environment-editor-row.c
new file mode 100644
index 000000000..5e39f9da1
--- /dev/null
+++ b/src/libide/gui/ide-environment-editor-row.c
@@ -0,0 +1,278 @@
+/* ide-environment-editor-row.c
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-environment-editor-row"
+
+#include "config.h"
+
+#include "ide-environment-editor-row.h"
+
+struct _IdeEnvironmentEditorRow
+{
+  GtkListBoxRow           parent_instance;
+
+  IdeEnvironmentVariable *variable;
+
+  GtkEntry               *key_entry;
+  GtkEntry               *value_entry;
+  GtkButton              *delete_button;
+
+  GBinding               *key_binding;
+  GBinding               *value_binding;
+};
+
+enum {
+  PROP_0,
+  PROP_VARIABLE,
+  LAST_PROP
+};
+
+enum {
+  DELETE,
+  LAST_SIGNAL
+};
+
+G_DEFINE_TYPE (IdeEnvironmentEditorRow, ide_environment_editor_row, GTK_TYPE_LIST_BOX_ROW)
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [LAST_SIGNAL];
+
+static gboolean
+null_safe_mapping (GBinding     *binding,
+                   const GValue *from_value,
+                   GValue       *to_value,
+                   gpointer      user_data)
+{
+  const gchar *str = g_value_get_string (from_value);
+  g_value_set_string (to_value, str ?: "");
+  return TRUE;
+}
+
+static void
+ide_environment_editor_row_connect (IdeEnvironmentEditorRow *self)
+{
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+  g_assert (IDE_IS_ENVIRONMENT_VARIABLE (self->variable));
+
+  self->key_binding =
+    g_object_bind_property_full (self->variable, "key", self->key_entry, "text",
+                                 G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
+                                 null_safe_mapping, NULL, NULL, NULL);
+
+  self->value_binding =
+    g_object_bind_property_full (self->variable, "value", self->value_entry, "text",
+                                 G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
+                                 null_safe_mapping, NULL, NULL, NULL);
+}
+
+static void
+ide_environment_editor_row_disconnect (IdeEnvironmentEditorRow *self)
+{
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+  g_assert (IDE_IS_ENVIRONMENT_VARIABLE (self->variable));
+
+  g_clear_pointer (&self->key_binding, g_binding_unbind);
+  g_clear_pointer (&self->value_binding, g_binding_unbind);
+}
+
+static void
+delete_button_clicked (GtkButton               *button,
+                       IdeEnvironmentEditorRow *self)
+{
+  g_assert (GTK_IS_BUTTON (button));
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+
+  g_signal_emit (self, signals [DELETE], 0);
+}
+
+static void
+key_entry_activate (GtkWidget               *entry,
+                    IdeEnvironmentEditorRow *self)
+{
+  g_assert (GTK_IS_ENTRY (entry));
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->value_entry));
+}
+
+static void
+value_entry_activate (GtkWidget               *entry,
+                      IdeEnvironmentEditorRow *self)
+{
+  GtkWidget *parent;
+
+  g_assert (GTK_IS_ENTRY (entry));
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self));
+  parent = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_LIST_BOX);
+  g_signal_emit_by_name (parent, "move-cursor", GTK_MOVEMENT_DISPLAY_LINES, 1);
+}
+
+static void
+ide_environment_editor_row_destroy (GtkWidget *widget)
+{
+  IdeEnvironmentEditorRow *self = (IdeEnvironmentEditorRow *)widget;
+
+  if (self->variable != NULL)
+    {
+      ide_environment_editor_row_disconnect (self);
+      g_clear_object (&self->variable);
+    }
+
+  GTK_WIDGET_CLASS (ide_environment_editor_row_parent_class)->destroy (widget);
+}
+
+static void
+ide_environment_editor_row_get_property (GObject    *object,
+                                         guint       prop_id,
+                                         GValue     *value,
+                                         GParamSpec *pspec)
+{
+  IdeEnvironmentEditorRow *self = IDE_ENVIRONMENT_EDITOR_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_VARIABLE:
+      g_value_set_object (value, ide_environment_editor_row_get_variable (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_environment_editor_row_set_property (GObject      *object,
+                                         guint         prop_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec)
+{
+  IdeEnvironmentEditorRow *self = IDE_ENVIRONMENT_EDITOR_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_VARIABLE:
+      ide_environment_editor_row_set_variable (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_environment_editor_row_class_init (IdeEnvironmentEditorRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_environment_editor_row_get_property;
+  object_class->set_property = ide_environment_editor_row_set_property;
+
+  widget_class->destroy = ide_environment_editor_row_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-environment-editor-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeEnvironmentEditorRow, delete_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeEnvironmentEditorRow, key_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdeEnvironmentEditorRow, value_entry);
+
+  properties [PROP_VARIABLE] =
+    g_param_spec_object ("variable",
+                         "Variable",
+                         "Variable",
+                         IDE_TYPE_ENVIRONMENT_VARIABLE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  signals [DELETE] =
+    g_signal_new ("delete",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL, G_TYPE_NONE, 0);
+}
+
+static void
+ide_environment_editor_row_init (IdeEnvironmentEditorRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect (self->delete_button,
+                    "clicked",
+                    G_CALLBACK (delete_button_clicked),
+                    self);
+
+  g_signal_connect (self->key_entry,
+                    "activate",
+                    G_CALLBACK (key_entry_activate),
+                    self);
+
+  g_signal_connect (self->value_entry,
+                    "activate",
+                    G_CALLBACK (value_entry_activate),
+                    self);
+}
+
+/**
+ * ide_environment_editor_row_get_variable:
+ *
+ * Returns: (transfer none) (nullable): An #IdeEnvironmentVariable.
+ */
+IdeEnvironmentVariable *
+ide_environment_editor_row_get_variable (IdeEnvironmentEditorRow *self)
+{
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT_EDITOR_ROW (self), NULL);
+
+  return self->variable;
+}
+
+void
+ide_environment_editor_row_set_variable (IdeEnvironmentEditorRow *self,
+                                         IdeEnvironmentVariable  *variable)
+{
+  g_return_if_fail (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+  g_return_if_fail (!variable || IDE_IS_ENVIRONMENT_VARIABLE (variable));
+
+  if (variable != self->variable)
+    {
+      if (self->variable != NULL)
+        {
+          ide_environment_editor_row_disconnect (self);
+          g_clear_object (&self->variable);
+        }
+
+      if (variable != NULL)
+        {
+          self->variable = g_object_ref (variable);
+          ide_environment_editor_row_connect (self);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VARIABLE]);
+    }
+}
+
+void
+ide_environment_editor_row_start_editing (IdeEnvironmentEditorRow *self)
+{
+  g_return_if_fail (IDE_IS_ENVIRONMENT_EDITOR_ROW (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->key_entry));
+}
diff --git a/src/libide/gui/ide-environment-editor-row.h b/src/libide/gui/ide-environment-editor-row.h
new file mode 100644
index 000000000..01ae2b83e
--- /dev/null
+++ b/src/libide/gui/ide-environment-editor-row.h
@@ -0,0 +1,37 @@
+/* ide-environment-editor-row.h
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <gtk/gtk.h>
+#include <libide-threading.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_ENVIRONMENT_EDITOR_ROW (ide_environment_editor_row_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeEnvironmentEditorRow, ide_environment_editor_row, IDE, ENVIRONMENT_EDITOR_ROW, 
GtkListBoxRow)
+
+IdeEnvironmentVariable *ide_environment_editor_row_get_variable  (IdeEnvironmentEditorRow *self);
+void                    ide_environment_editor_row_set_variable  (IdeEnvironmentEditorRow *self,
+                                                                  IdeEnvironmentVariable  *variable);
+void                    ide_environment_editor_row_start_editing (IdeEnvironmentEditorRow *self);
+
+G_END_DECLS
diff --git a/src/libide/buildui/ide-environment-editor-row.ui b/src/libide/gui/ide-environment-editor-row.ui
similarity index 100%
rename from src/libide/buildui/ide-environment-editor-row.ui
rename to src/libide/gui/ide-environment-editor-row.ui
diff --git a/src/libide/gui/ide-environment-editor.c b/src/libide/gui/ide-environment-editor.c
new file mode 100644
index 000000000..31c4e5b27
--- /dev/null
+++ b/src/libide/gui/ide-environment-editor.c
@@ -0,0 +1,317 @@
+/* ide-environment-editor.c
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-environment-editor"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-environment-editor.h"
+#include "ide-environment-editor-row.h"
+
+struct _IdeEnvironmentEditor
+{
+  GtkListBox      parent_instance;
+  IdeEnvironment *environment;
+  GtkWidget      *dummy_row;
+
+  IdeEnvironmentVariable *dummy;
+};
+
+G_DEFINE_TYPE (IdeEnvironmentEditor, ide_environment_editor, GTK_TYPE_LIST_BOX)
+
+enum {
+  PROP_0,
+  PROP_ENVIRONMENT,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+ide_environment_editor_delete_row (IdeEnvironmentEditor    *self,
+                                   IdeEnvironmentEditorRow *row)
+{
+  IdeEnvironmentVariable *variable;
+
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR_ROW (row));
+
+  variable = ide_environment_editor_row_get_variable (row);
+  ide_environment_remove (self->environment, variable);
+}
+
+static GtkWidget *
+ide_environment_editor_create_dummy_row (IdeEnvironmentEditor *self)
+{
+  GtkWidget *row;
+  GtkWidget *label;
+
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label", _("New variable…"),
+                        "visible", TRUE,
+                        "xalign", 0.0f,
+                        NULL);
+  gtk_style_context_add_class (gtk_widget_get_style_context (label), "dim-label");
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "child", label,
+                      "visible", TRUE,
+                      NULL);
+
+  return row;
+}
+
+static GtkWidget *
+ide_environment_editor_create_row (gpointer item,
+                                   gpointer user_data)
+{
+  IdeEnvironmentVariable *variable = item;
+  IdeEnvironmentEditor *self = user_data;
+  IdeEnvironmentEditorRow *row;
+
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_assert (IDE_IS_ENVIRONMENT_VARIABLE (variable));
+
+  row = g_object_new (IDE_TYPE_ENVIRONMENT_EDITOR_ROW,
+                      "variable", variable,
+                      "visible", TRUE,
+                      NULL);
+
+  g_signal_connect_object (row,
+                           "delete",
+                           G_CALLBACK (ide_environment_editor_delete_row),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  return GTK_WIDGET (row);
+}
+
+static void
+ide_environment_editor_disconnect (IdeEnvironmentEditor *self)
+{
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_assert (IDE_IS_ENVIRONMENT (self->environment));
+
+  gtk_list_box_bind_model (GTK_LIST_BOX (self), NULL, NULL, NULL, NULL);
+
+  g_clear_object (&self->dummy);
+}
+
+static void
+ide_environment_editor_connect (IdeEnvironmentEditor *self)
+{
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_assert (IDE_IS_ENVIRONMENT (self->environment));
+
+  gtk_list_box_bind_model (GTK_LIST_BOX (self),
+                           G_LIST_MODEL (self->environment),
+                           ide_environment_editor_create_row, self, NULL);
+
+  self->dummy_row = ide_environment_editor_create_dummy_row (self);
+  gtk_container_add (GTK_CONTAINER (self), self->dummy_row);
+}
+
+static void
+find_row_cb (GtkWidget *widget,
+             gpointer   data)
+{
+  struct {
+    IdeEnvironmentVariable  *variable;
+    IdeEnvironmentEditorRow *row;
+  } *lookup = data;
+
+  g_assert (lookup != NULL);
+  g_assert (GTK_IS_LIST_BOX_ROW (widget));
+
+  if (IDE_IS_ENVIRONMENT_EDITOR_ROW (widget))
+    {
+      IdeEnvironmentVariable *variable;
+
+      variable = ide_environment_editor_row_get_variable (IDE_ENVIRONMENT_EDITOR_ROW (widget));
+
+      if (variable == lookup->variable)
+        lookup->row = IDE_ENVIRONMENT_EDITOR_ROW (widget);
+    }
+}
+
+static IdeEnvironmentEditorRow *
+find_row (IdeEnvironmentEditor   *self,
+          IdeEnvironmentVariable *variable)
+{
+  struct {
+    IdeEnvironmentVariable  *variable;
+    IdeEnvironmentEditorRow *row;
+  } lookup = { variable, NULL };
+
+  g_assert (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_assert (IDE_IS_ENVIRONMENT_VARIABLE (variable));
+
+  gtk_container_foreach (GTK_CONTAINER (self), find_row_cb, &lookup);
+
+  return lookup.row;
+}
+
+static void
+ide_environment_editor_row_activated (GtkListBox    *list_box,
+                                      GtkListBoxRow *row)
+{
+  IdeEnvironmentEditor *self = (IdeEnvironmentEditor *)list_box;
+
+  g_assert (GTK_IS_LIST_BOX (list_box));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+
+  if (self->environment == NULL)
+    return;
+
+  if (self->dummy_row == GTK_WIDGET (row))
+    {
+      g_autoptr(IdeEnvironmentVariable) variable = NULL;
+
+      variable = ide_environment_variable_new (NULL, NULL);
+      ide_environment_append (self->environment, variable);
+      ide_environment_editor_row_start_editing (find_row (self, variable));
+    }
+}
+
+static void
+ide_environment_editor_destroy (GtkWidget *widget)
+{
+  IdeEnvironmentEditor *self = (IdeEnvironmentEditor *)widget;
+
+  GTK_WIDGET_CLASS (ide_environment_editor_parent_class)->destroy (widget);
+
+  g_clear_object (&self->environment);
+}
+
+static void
+ide_environment_editor_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  IdeEnvironmentEditor *self = IDE_ENVIRONMENT_EDITOR(object);
+
+  switch (prop_id)
+    {
+    case PROP_ENVIRONMENT:
+      g_value_set_object (value, ide_environment_editor_get_environment (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_environment_editor_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  IdeEnvironmentEditor *self = IDE_ENVIRONMENT_EDITOR(object);
+
+  switch (prop_id)
+    {
+    case PROP_ENVIRONMENT:
+      ide_environment_editor_set_environment (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_environment_editor_class_init (IdeEnvironmentEditorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkListBoxClass *list_box_class = GTK_LIST_BOX_CLASS (klass);
+
+  object_class->get_property = ide_environment_editor_get_property;
+  object_class->set_property = ide_environment_editor_set_property;
+
+  widget_class->destroy = ide_environment_editor_destroy;
+
+  list_box_class->row_activated = ide_environment_editor_row_activated;
+
+  properties [PROP_ENVIRONMENT] =
+    g_param_spec_object ("environment",
+                         "Environment",
+                         "Environment",
+                         IDE_TYPE_ENVIRONMENT,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_environment_editor_init (IdeEnvironmentEditor *self)
+{
+  gtk_list_box_set_selection_mode (GTK_LIST_BOX (self), GTK_SELECTION_NONE);
+}
+
+GtkWidget *
+ide_environment_editor_new (void)
+{
+  return g_object_new (IDE_TYPE_ENVIRONMENT_EDITOR, NULL);
+}
+
+void
+ide_environment_editor_set_environment (IdeEnvironmentEditor *self,
+                                        IdeEnvironment       *environment)
+{
+  g_return_if_fail (IDE_IS_ENVIRONMENT_EDITOR (self));
+  g_return_if_fail (IDE_IS_ENVIRONMENT (environment));
+
+  if (self->environment != environment)
+    {
+      if (self->environment != NULL)
+        {
+          ide_environment_editor_disconnect (self);
+          g_clear_object (&self->environment);
+        }
+
+      if (environment != NULL)
+        {
+          self->environment = g_object_ref (environment);
+          ide_environment_editor_connect (self);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ENVIRONMENT]);
+    }
+}
+
+/**
+ * ide_environment_editor_get_environment:
+ *
+ * Returns: (nullable) (transfer none): An #IdeEnvironment or %NULL.
+ */
+IdeEnvironment *
+ide_environment_editor_get_environment (IdeEnvironmentEditor *self)
+{
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT_EDITOR (self), NULL);
+
+  return self->environment;
+}
diff --git a/src/libide/gui/ide-environment-editor.h b/src/libide/gui/ide-environment-editor.h
new file mode 100644
index 000000000..2a5731da2
--- /dev/null
+++ b/src/libide/gui/ide-environment-editor.h
@@ -0,0 +1,42 @@
+/* ide-environment-editor.h
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <gtk/gtk.h>
+#include <libide-core.h>
+#include <libide-threading.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_ENVIRONMENT_EDITOR (ide_environment_editor_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeEnvironmentEditor, ide_environment_editor, IDE, ENVIRONMENT_EDITOR, GtkListBox)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget      *ide_environment_editor_new             (void);
+IDE_AVAILABLE_IN_3_32
+IdeEnvironment *ide_environment_editor_get_environment (IdeEnvironmentEditor *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_environment_editor_set_environment (IdeEnvironmentEditor *self,
+                                                        IdeEnvironment       *environment);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-fancy-tree-view.c b/src/libide/gui/ide-fancy-tree-view.c
new file mode 100644
index 000000000..30cf656c7
--- /dev/null
+++ b/src/libide/gui/ide-fancy-tree-view.c
@@ -0,0 +1,201 @@
+/* ide-fancy-tree-view.c
+ *
+ * Copyright 2017-2019 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 "ide-fancy-tree-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-cell-renderer-fancy.h"
+#include "ide-fancy-tree-view.h"
+
+/**
+ * SECTION:ide-fancy-tree-view:
+ * @title: IdeFancyTreeView
+ * @short_description: a stylized treeview for use in sidebars
+ *
+ * This is a helper #GtkTreeView that matches the style that
+ * Builder uses for treeviews which can reflow text. It is a
+ * useful base class because it does all of the hacks necessary
+ * to make this work without ruining your code.
+ *
+ * It only has a single column, and comes setup with a single
+ * cell (an #IdeCellRendererFancy) to render the conten.
+ *
+ * Since: 3.32
+ */
+
+typedef struct
+{
+  gint  last_width;
+  guint relayout_source;
+} IdeFancyTreeViewPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeFancyTreeView, ide_fancy_tree_view, GTK_TYPE_TREE_VIEW)
+
+static void
+ide_fancy_tree_view_destroy (GtkWidget *widget)
+{
+  IdeFancyTreeView *self = (IdeFancyTreeView *)widget;
+  IdeFancyTreeViewPrivate *priv = ide_fancy_tree_view_get_instance_private (self);
+
+  dzl_clear_source (&priv->relayout_source);
+
+  GTK_WIDGET_CLASS (ide_fancy_tree_view_parent_class)->destroy (widget);
+}
+
+static gboolean
+queue_relayout_in_idle (gpointer user_data)
+{
+  IdeFancyTreeView *self = user_data;
+  IdeFancyTreeViewPrivate *priv = ide_fancy_tree_view_get_instance_private (self);
+  GtkAllocation alloc;
+  guint n_columns;
+
+  g_assert (IDE_IS_FANCY_TREE_VIEW (self));
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  if (alloc.width == priv->last_width)
+    goto cleanup;
+
+  priv->last_width = alloc.width;
+
+  n_columns = gtk_tree_view_get_n_columns (GTK_TREE_VIEW (self));
+
+  for (guint i = 0; i < n_columns; i++)
+    {
+      GtkTreeViewColumn *column;
+
+      column = gtk_tree_view_get_column (GTK_TREE_VIEW (self), i);
+      gtk_tree_view_column_queue_resize (column);
+    }
+
+cleanup:
+  priv->relayout_source = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+
+static void
+ide_fancy_tree_view_size_allocate (GtkWidget *widget,
+                                   GtkAllocation *alloc)
+{
+  IdeFancyTreeView *self = (IdeFancyTreeView *)widget;
+  IdeFancyTreeViewPrivate *priv = ide_fancy_tree_view_get_instance_private (self);
+
+  g_assert (IDE_IS_FANCY_TREE_VIEW (self));
+
+  GTK_WIDGET_CLASS (ide_fancy_tree_view_parent_class)->size_allocate (widget, alloc);
+
+  if (priv->last_width != alloc->width)
+    {
+      /*
+       * We must perform our queued relayout from an idle callback
+       * so that we don't affect this draw cycle. If we do that, we
+       * will get empty content flashes for the current frame. This
+       * allows us to draw the current frame slightly incorrect but
+       * fixup on the next frame (which looks much nicer from a user
+       * point of view).
+       */
+      if (priv->relayout_source == 0)
+        priv->relayout_source =
+          gdk_threads_add_idle_full (G_PRIORITY_HIGH,
+                                     queue_relayout_in_idle,
+                                     g_object_ref (self),
+                                     g_object_unref);
+    }
+}
+
+static void
+ide_fancy_tree_view_class_init (IdeFancyTreeViewClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->size_allocate = ide_fancy_tree_view_size_allocate;
+  widget_class->destroy = ide_fancy_tree_view_destroy;
+}
+
+static void
+ide_fancy_tree_view_init (IdeFancyTreeView *self)
+{
+  GtkTreeViewColumn *column;
+  GtkCellRenderer *cell;
+
+  g_object_set (self,
+                "activate-on-single-click", TRUE,
+                "headers-visible", FALSE,
+                NULL);
+
+  column = g_object_new (GTK_TYPE_TREE_VIEW_COLUMN,
+                         "expand", TRUE,
+                         "visible", TRUE,
+                         NULL);
+  gtk_tree_view_append_column (GTK_TREE_VIEW (self), column);
+
+  cell = g_object_new (IDE_TYPE_CELL_RENDERER_FANCY,
+                       "visible", TRUE,
+                       "xalign", 0.0f,
+                       "xpad", 4,
+                       "ypad", 6,
+                       NULL);
+  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+}
+
+GtkWidget *
+ide_fancy_tree_view_new (void)
+{
+  return g_object_new (IDE_TYPE_FANCY_TREE_VIEW, NULL);
+}
+
+/**
+ * ide_fancy_tree_view_set_data_func:
+ * @self: a #IdeFancyTreeView
+ * @func: (closure func_data) (scope async) (nullable): a callback
+ * @func_data: data for @func
+ * @func_data_destroy: destroy notify for @func_data
+ *
+ * Sets the data func to use to update the text for the
+ * #IdeCellRendererFancy cell renderer.
+ *
+ * Since: 3.32
+ */
+void
+ide_fancy_tree_view_set_data_func (IdeFancyTreeView      *self,
+                                   GtkCellLayoutDataFunc  func,
+                                   gpointer               func_data,
+                                   GDestroyNotify         func_data_destroy)
+{
+  GtkTreeViewColumn *column;
+  GList *cells;
+
+  g_return_if_fail (IDE_IS_FANCY_TREE_VIEW (self));
+
+  column = gtk_tree_view_get_column (GTK_TREE_VIEW (self), 0);
+  cells = gtk_cell_layout_get_cells (GTK_CELL_LAYOUT (column));
+
+  if (cells->data != NULL)
+    gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cells->data,
+                                        func, func_data, func_data_destroy);
+
+  g_list_free (cells);
+}
diff --git a/src/libide/gui/ide-fancy-tree-view.h b/src/libide/gui/ide-fancy-tree-view.h
new file mode 100644
index 000000000..e8c7caf0b
--- /dev/null
+++ b/src/libide/gui/ide-fancy-tree-view.h
@@ -0,0 +1,53 @@
+/* ide-fancy-tree-view.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FANCY_TREE_VIEW (ide_fancy_tree_view_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeFancyTreeView, ide_fancy_tree_view, IDE, FANCY_TREE_VIEW, GtkTreeView)
+
+struct _IdeFancyTreeViewClass
+{
+  GtkTreeViewClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_fancy_tree_view_new           (void);
+IDE_AVAILABLE_IN_3_32
+void       ide_fancy_tree_view_set_data_func (IdeFancyTreeView      *self,
+                                              GtkCellLayoutDataFunc  func,
+                                              gpointer               func_data,
+                                              GDestroyNotify         func_data_destroy);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-frame-actions.c b/src/libide/gui/ide-frame-actions.c
new file mode 100644
index 000000000..ea2af0f3e
--- /dev/null
+++ b/src/libide/gui/ide-frame-actions.c
@@ -0,0 +1,429 @@
+/* ide-frame-actions.c
+ *
+ * Copyright 2017-2019 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 "ide-frame-actions"
+
+#include "config.h"
+
+#include "ide-frame.h"
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-workbench.h"
+
+static void
+ide_frame_actions_next_page (GSimpleAction *action,
+                             GVariant      *variant,
+                             gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+
+  g_assert (IDE_IS_FRAME (self));
+
+  g_signal_emit_by_name (self, "change-current-page", 1);
+}
+
+static void
+ide_frame_actions_previous_page (GSimpleAction *action,
+                                 GVariant      *variant,
+                                 gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+
+  g_assert (IDE_IS_FRAME (self));
+
+  g_signal_emit_by_name (self, "change-current-page", -1);
+}
+
+static void
+ide_frame_actions_close_page (GSimpleAction *action,
+                              GVariant      *variant,
+                              gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+  IdePage *page;
+
+  g_assert (IDE_IS_FRAME (self));
+
+  page = ide_frame_get_visible_child (self);
+  if (page != NULL)
+    _ide_frame_request_close (self, page);
+}
+
+static void
+ide_frame_actions_move (IdeFrame *self,
+                        gint      direction)
+{
+  IdePage *page;
+  IdeFrame *dest;
+  GtkWidget *grid;
+  GtkWidget *column;
+  gint index = 0;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (direction == 1 || direction == -1);
+
+  page = ide_frame_get_visible_child (self);
+
+  g_return_if_fail (page != NULL);
+  g_return_if_fail (IDE_IS_PAGE (page));
+
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
+  g_return_if_fail (grid != NULL);
+  g_return_if_fail (IDE_IS_GRID (grid));
+
+  column = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID_COLUMN);
+  g_return_if_fail (column != NULL);
+  g_return_if_fail (IDE_IS_GRID_COLUMN (column));
+
+  gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (column),
+                           "index", &index,
+                           NULL);
+
+  dest = _ide_grid_get_nth_stack (IDE_GRID (grid), index + direction);
+
+  g_return_if_fail (dest != NULL);
+  g_return_if_fail (dest != self);
+  g_return_if_fail (IDE_IS_FRAME (dest));
+
+  _ide_frame_transfer (self, dest, page);
+}
+
+static void
+ide_frame_actions_move_right (GSimpleAction *action,
+                              GVariant      *variant,
+                              gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+
+  ide_frame_actions_move (self, 1);
+}
+
+static void
+ide_frame_actions_move_left (GSimpleAction *action,
+                             GVariant      *variant,
+                             gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+
+  ide_frame_actions_move (self, -1);
+}
+
+static void
+ide_frame_actions_split_page (GSimpleAction *action,
+                              GVariant      *variant,
+                              gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+  g_autoptr(GFile) file = NULL;
+  IdeBufferManager *bufmgr;
+  GObjectClass *klass;
+  const gchar *path;
+  GParamSpec *pspec;
+  IdeContext *context;
+  IdeBuffer *buffer;
+  GtkWidget *column;
+  GtkWidget *grid;
+  IdeFrame *dest;
+  IdePage *page;
+  IdePage *split_page;
+  gint index = 0;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING));
+
+  column = gtk_widget_get_parent (GTK_WIDGET (self));
+
+  if (column == NULL || !IDE_IS_GRID_COLUMN (column))
+    {
+      g_warning ("Failed to locate ancestor grid column");
+      return;
+    }
+
+  if (!(grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID)))
+    {
+      g_warning ("Failed to locate ancestor grid");
+      return;
+    }
+
+  if (!(page = ide_frame_get_visible_child (self)))
+    {
+      g_warning ("No page available to split");
+      return;
+    }
+
+  if ((path = g_variant_get_string (variant, NULL)) &&
+      !ide_str_empty0 (path) &&
+      (context = ide_widget_get_context (GTK_WIDGET (self))) &&
+      (bufmgr = ide_buffer_manager_from_context (context)) &&
+      (file = g_file_new_for_path (path)) &&
+      (buffer = ide_buffer_manager_find_buffer (bufmgr, file)) &&
+      (klass = G_OBJECT_GET_CLASS (page)) &&
+      (pspec = g_object_class_find_property (klass, "buffer")) &&
+      g_type_is_a (pspec->value_type, IDE_TYPE_BUFFER))
+    {
+      split_page = g_object_new (G_OBJECT_TYPE (page),
+                                 "buffer", buffer,
+                                 "visible", TRUE,
+                                 NULL);
+    }
+  else
+    {
+      if (!ide_page_get_can_split (page))
+        {
+          g_warning ("Attempt to split a page that cannot be split");
+          return;
+        }
+
+      if (!(split_page = ide_page_create_split (page)))
+        {
+          g_warning ("%s failed to create a split",
+                     G_OBJECT_TYPE_NAME (page));
+          return;
+        }
+    }
+
+  g_assert (IDE_IS_PAGE (split_page));
+  g_assert (IDE_IS_GRID_COLUMN (column));
+
+  gtk_container_child_get (GTK_CONTAINER (column), GTK_WIDGET (self),
+                           "index", &index,
+                           NULL);
+
+  dest = _ide_grid_get_nth_stack_for_column (IDE_GRID (grid),
+                                             IDE_GRID_COLUMN (column),
+                                             ++index);
+
+  g_assert (IDE_IS_FRAME (dest));
+
+  gtk_container_add (GTK_CONTAINER (dest), GTK_WIDGET (split_page));
+}
+
+static void
+ide_frame_actions_open_in_new_frame (GSimpleAction *action,
+                                     GVariant      *variant,
+                                     gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+  const gchar *filepath;
+  GtkWidget *grid;
+  GtkWidget *column;
+  IdeFrame *dest;
+  IdePage *page;
+  gint index = 0;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING));
+
+  filepath = g_variant_get_string (variant, NULL);
+  page = ide_frame_get_visible_child (self);
+
+  g_return_if_fail (page != NULL);
+  g_return_if_fail (IDE_IS_PAGE (page));
+
+  if (!ide_str_empty0 (filepath))
+    {
+      g_autoptr (GFile) file = NULL;
+      IdeBufferManager *buffer_manager;
+      IdeContext *context;
+      IdeBuffer *buffer;
+
+      context = ide_widget_get_context (GTK_WIDGET (self));
+      buffer_manager = ide_buffer_manager_from_context (context);
+      file = g_file_new_for_path (filepath);
+
+      if ((buffer = ide_buffer_manager_find_buffer (buffer_manager, file)))
+        page = g_object_new (G_OBJECT_TYPE (page),
+                             "buffer", buffer,
+                             "visible", TRUE,
+                             NULL);
+      else
+        return;
+    }
+  else
+    {
+      g_return_if_fail (ide_page_get_can_split (page));
+
+      page = ide_page_create_split (page);
+    }
+
+  if (page == NULL)
+    {
+      g_warning ("Requested split page but NULL was returned");
+      return;
+    }
+
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
+
+  g_return_if_fail (grid != NULL);
+  g_return_if_fail (IDE_IS_GRID (grid));
+
+  column = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID_COLUMN);
+
+  g_return_if_fail (column != NULL);
+  g_return_if_fail (IDE_IS_GRID_COLUMN (column));
+
+  gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (column),
+                           "index", &index,
+                           NULL);
+
+  dest = _ide_grid_get_nth_stack (IDE_GRID (grid), ++index);
+
+  g_return_if_fail (dest != NULL);
+  g_return_if_fail (IDE_IS_FRAME (dest));
+
+  gtk_container_add (GTK_CONTAINER (dest), GTK_WIDGET (page));
+}
+
+static void
+ide_frame_actions_close_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeFrame *self = (IdeFrame *)object;
+  GtkWidget *parent;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (!ide_frame_agree_to_close_finish (self, result, NULL))
+    return;
+
+  /* Things might have changed during the async op */
+  parent = gtk_widget_get_parent (GTK_WIDGET (self));
+  if (!IDE_IS_GRID_COLUMN (parent))
+    return;
+
+  /* Make sure there is still more than a single stack */
+  if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (parent)) > 1)
+    gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+static void
+ide_frame_actions_close_stack (GSimpleAction *action,
+                               GVariant      *variant,
+                               gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+
+  ide_frame_agree_to_close_async (self,
+                                         NULL,
+                                         ide_frame_actions_close_cb,
+                                         NULL);
+}
+
+static void
+ide_frame_actions_show_list (GSimpleAction *action,
+                             GVariant      *variant,
+                             gpointer       user_data)
+{
+  IdeFrame *self = user_data;
+  IdeFrameHeader *header;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_FRAME (self));
+
+  header = IDE_FRAME_HEADER (ide_frame_get_titlebar (self));
+  _ide_frame_header_focus_list (header);
+}
+
+static const GActionEntry actions[] = {
+  { "open-in-new-frame", ide_frame_actions_open_in_new_frame, "s" },
+  { "close-stack",       ide_frame_actions_close_stack },
+  { "close-page",        ide_frame_actions_close_page },
+  { "next-page",         ide_frame_actions_next_page },
+  { "previous-page",     ide_frame_actions_previous_page },
+  { "move-right",        ide_frame_actions_move_right },
+  { "move-left",         ide_frame_actions_move_left },
+  { "split-page",        ide_frame_actions_split_page, "s" },
+  { "show-list",         ide_frame_actions_show_list },
+};
+
+void
+_ide_frame_update_actions (IdeFrame *self)
+{
+  IdePage *page;
+  GtkWidget *parent;
+  gboolean has_page = FALSE;
+  gboolean can_split_page = FALSE;
+  gboolean can_close_stack = FALSE;
+
+  g_return_if_fail (IDE_IS_FRAME (self));
+
+  page = ide_frame_get_visible_child (self);
+
+  if (page != NULL)
+    {
+      has_page = TRUE;
+      can_split_page = ide_page_get_can_split (page);
+    }
+
+  /* If there is more than one stack in the column, then we can close
+   * this stack directly without involving the column.
+   */
+  parent = gtk_widget_get_parent (GTK_WIDGET (self));
+  if (IDE_IS_GRID_COLUMN (parent))
+    can_close_stack = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (parent)) > 1;
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "frame", "move-right",
+                             "enabled", has_page,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "frame", "move-left",
+                             "enabled", has_page,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "frame", "open-in-new-frame",
+                             "enabled", can_split_page,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "frame", "split-page",
+                             "enabled", can_split_page,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "frame", "close-stack",
+                             "enabled", can_close_stack,
+                             NULL);
+}
+
+void
+_ide_frame_init_actions (IdeFrame *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  g_return_if_fail (IDE_IS_FRAME (self));
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self),
+                                  "frame",
+                                  G_ACTION_GROUP (group));
+
+  _ide_frame_update_actions (self);
+}
diff --git a/src/libide/gui/ide-frame-addin.c b/src/libide/gui/ide-frame-addin.c
new file mode 100644
index 000000000..81893a80a
--- /dev/null
+++ b/src/libide/gui/ide-frame-addin.c
@@ -0,0 +1,111 @@
+/* ide-frame-addin.c
+ *
+ * Copyright 2017-2019 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 "ide-frame-addin"
+
+#include "config.h"
+
+#include "ide-frame-addin.h"
+
+/**
+ * SECTION:ide-frame-addin
+ * @title: IdeFrameAddin
+ * @short_description: addins created for every #IdeFrame
+ *
+ * Since: 3.32
+ */
+
+G_DEFINE_INTERFACE (IdeFrameAddin, ide_frame_addin, G_TYPE_OBJECT)
+
+static void
+ide_frame_addin_default_init (IdeFrameAddinInterface *iface)
+{
+}
+
+/**
+ * ide_frame_addin_load:
+ * @self: An #IdeFrameAddin
+ * @frame: An #IdeFrame
+ *
+ * This function should be implemented by #IdeFrameAddin plugins
+ * in #IdeFrameAddinInterface.
+ *
+ * This virtual method is called when the plugin should load itself.
+ * A new instance of the plugin is created for every #IdeFrame
+ * that is created in Builder.
+ *
+ * Since: 3.32
+ */
+void
+ide_frame_addin_load (IdeFrameAddin *self,
+                      IdeFrame      *frame)
+{
+  g_return_if_fail (IDE_IS_FRAME_ADDIN (self));
+  g_return_if_fail (IDE_IS_FRAME (frame));
+
+  if (IDE_FRAME_ADDIN_GET_IFACE (self)->load)
+    IDE_FRAME_ADDIN_GET_IFACE (self)->load (self, frame);
+}
+
+/**
+ * ide_frame_addin_unload:
+ * @self: An #IdeFrameAddin
+ * @frame: An #IdeFrame
+ *
+ * This function should be implemented by #IdeFrameAddin plugins
+ * in #IdeFrameAddinInterface.
+ *
+ * This virtual method is called when the plugin should unload itself.
+ * It should revert anything performed via ide_frame_addin_load().
+ *
+ * Since: 3.32
+ */
+void
+ide_frame_addin_unload (IdeFrameAddin *self,
+                        IdeFrame      *frame)
+{
+  g_return_if_fail (IDE_IS_FRAME_ADDIN (self));
+  g_return_if_fail (IDE_IS_FRAME (frame));
+
+  if (IDE_FRAME_ADDIN_GET_IFACE (self)->unload)
+    IDE_FRAME_ADDIN_GET_IFACE (self)->unload (self, frame);
+}
+
+/**
+ * ide_frame_addin_set_page:
+ * @self: an #IdeFrameAddin
+ * @page: (nullable): An #IdePage or %NULL.
+ *
+ * This virtual method is called whenever the active page changes
+ * in the #IdePage. Plugins may want to alter what controls
+ * are displayed on the frame based on the current page.
+ *
+ * Since: 3.32
+ */
+void
+ide_frame_addin_set_page (IdeFrameAddin *self,
+                          IdePage       *page)
+{
+  g_return_if_fail (IDE_IS_FRAME_ADDIN (self));
+  g_return_if_fail (!page || IDE_IS_PAGE (page));
+
+  if (IDE_FRAME_ADDIN_GET_IFACE (self)->set_page)
+    IDE_FRAME_ADDIN_GET_IFACE (self)->set_page (self, page);
+}
diff --git a/src/libide/gui/ide-frame-addin.h b/src/libide/gui/ide-frame-addin.h
new file mode 100644
index 000000000..819b9c551
--- /dev/null
+++ b/src/libide/gui/ide-frame-addin.h
@@ -0,0 +1,65 @@
+/* ide-frame-addin.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-frame.h"
+#include "ide-page.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FRAME_ADDIN (ide_frame_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeFrameAddin, ide_frame_addin, IDE, FRAME_ADDIN, GObject)
+
+struct _IdeFrameAddinInterface
+{
+  GTypeInterface parent_iface;
+
+  void (*load)     (IdeFrameAddin *self,
+                    IdeFrame      *frame);
+  void (*unload)   (IdeFrameAddin *self,
+                    IdeFrame      *frame);
+  void (*set_page) (IdeFrameAddin *self,
+                    IdePage       *page);
+};
+
+IDE_AVAILABLE_IN_3_32
+void                 ide_frame_addin_load          (IdeFrameAddin *self,
+                                                    IdeFrame      *frame);
+IDE_AVAILABLE_IN_3_32
+void                 ide_frame_addin_unload        (IdeFrameAddin *self,
+                                                    IdeFrame      *frame);
+IDE_AVAILABLE_IN_3_32
+void                 ide_frame_addin_set_page      (IdeFrameAddin *self,
+                                                    IdePage       *page);
+IDE_AVAILABLE_IN_3_32
+IdeFrameAddin *ide_frame_addin_find_by_module_name (IdeFrame      *frame,
+                                                    const gchar   *module_name);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-frame-header.c b/src/libide/gui/ide-frame-header.c
new file mode 100644
index 000000000..9943fd10e
--- /dev/null
+++ b/src/libide/gui/ide-frame-header.c
@@ -0,0 +1,767 @@
+/* ide-frame-header.c
+ *
+ * Copyright 2016-2019 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 "ide-frame-header"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-gui-private.h"
+#include "ide-frame-header.h"
+
+#define CSS_PROVIDER_PRIORITY (GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 100)
+
+/**
+ * SECTION:ide-frame-header
+ * @title: IdeFrameHeader
+ * @short_description: The header above document stacks
+ *
+ * The IdeFrameHeader is the titlebar widget above stacks of documents.
+ * It is used to add state when a given document is in view.
+ *
+ * It can also track the primary color of the content and update it's
+ * styling to match.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeFrameHeader
+{
+  DzlPriorityBox  parent_instance;
+
+  GtkCssProvider *css_provider;
+  guint           update_css_handler;
+
+  GdkRGBA         background_rgba;
+  GdkRGBA         foreground_rgba;
+
+  guint           background_rgba_set : 1;
+  guint           foreground_rgba_set : 1;
+
+  GtkButton      *close_button;
+  DzlMenuButton  *document_button;
+  GtkMenuButton  *title_button;
+  GtkPopover     *title_popover;
+  GtkListBox     *title_list_box;
+  DzlPriorityBox *title_box;
+  GtkLabel       *title_label;
+  GtkLabel       *title_modified;
+  GtkBox         *title_views_box;
+
+  DzlJoinedMenu  *menu;
+};
+
+enum {
+  PROP_0,
+  PROP_BACKGROUND_RGBA,
+  PROP_FOREGROUND_RGBA,
+  PROP_MODIFIED,
+  PROP_SHOW_CLOSE_BUTTON,
+  PROP_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeFrameHeader, ide_frame_header, DZL_TYPE_PRIORITY_BOX)
+
+static GParamSpec *properties [N_PROPS];
+
+void
+_ide_frame_header_focus_list (IdeFrameHeader *self)
+{
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
+
+  gtk_popover_popup (self->title_popover);
+  gtk_widget_grab_focus (GTK_WIDGET (self->title_list_box));
+}
+
+void
+_ide_frame_header_hide (IdeFrameHeader *self)
+{
+  GtkPopover *popover;
+
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
+
+  /* This is like _ide_frame_header_popdown() but we hide the
+   * popovers immediately without performing the popdown animation.
+   */
+
+  popover = gtk_menu_button_get_popover (GTK_MENU_BUTTON (self->document_button));
+  if (popover != NULL)
+    gtk_widget_hide (GTK_WIDGET (popover));
+
+  gtk_widget_hide (GTK_WIDGET (self->title_popover));
+}
+
+void
+_ide_frame_header_popdown (IdeFrameHeader *self)
+{
+  GtkPopover *popover;
+
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
+
+  popover = gtk_menu_button_get_popover (GTK_MENU_BUTTON (self->document_button));
+  if (popover != NULL)
+    gtk_popover_popdown (popover);
+
+  gtk_popover_popdown (self->title_popover);
+}
+
+void
+_ide_frame_header_update (IdeFrameHeader *self,
+                                 IdePage        *view)
+{
+  const gchar *action = "frame.close-page";
+
+  g_assert (IDE_IS_FRAME_HEADER (self));
+  g_assert (!view || IDE_IS_PAGE (view));
+
+  /*
+   * Update our menus for the document to include the menu type needed for the
+   * newly focused view. Make sure we keep the Frame section at the end which
+   * is always the last section in the joined menus.
+   */
+
+  while (dzl_joined_menu_get_n_joined (self->menu) > 1)
+    dzl_joined_menu_remove_index (self->menu, 0);
+
+  if (view != NULL)
+    {
+      const gchar *menu_id = ide_page_get_menu_id (view);
+
+      if (menu_id != NULL)
+        {
+          GMenu *menu = dzl_application_get_menu_by_id (DZL_APPLICATION_DEFAULT, menu_id);
+
+          dzl_joined_menu_prepend_menu (self->menu, G_MENU_MODEL (menu));
+        }
+    }
+
+  /*
+   * Hide the document selectors if there are no views to select (which is
+   * indicated by us having a NULL view here.
+   */
+  gtk_widget_set_visible (GTK_WIDGET (self->title_views_box), view != NULL);
+
+  /*
+   * The close button acts differently depending on the grid stage.
+   *
+   *  - Last column, single stack => do nothing (action will be disabled)
+   *  - No more views and more than one stack in column (close just the stack)
+   *  - No more views and single stack in column and more than one column (close the column)
+   */
+
+  if (view == NULL)
+    {
+      GtkWidget *stack;
+      GtkWidget *column;
+
+      action = "gridcolumn.close";
+      stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
+      column = gtk_widget_get_ancestor (GTK_WIDGET (stack), IDE_TYPE_GRID_COLUMN);
+
+      if (stack != NULL && column != NULL)
+        {
+          if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)) > 1)
+            action = "frame.close-stack";
+        }
+    }
+
+  gtk_actionable_set_action_name (GTK_ACTIONABLE (self->close_button), action);
+
+  /*
+   * Hide any popovers that we know about. If we got here from closing
+   * documents, we should hide the popover after the last document is closed
+   * (inidicated by NULL view).
+   */
+  if (view == NULL)
+    _ide_frame_header_popdown (self);
+}
+
+static void
+close_view_cb (GtkButton            *button,
+               IdeFrameHeader *self)
+{
+  GtkWidget *stack;
+  GtkWidget *row;
+  GtkWidget *view;
+
+  g_assert (GTK_IS_BUTTON (button));
+  g_assert (IDE_IS_FRAME_HEADER (self));
+
+  row = gtk_widget_get_ancestor (GTK_WIDGET (button), GTK_TYPE_LIST_BOX_ROW);
+  if (row == NULL)
+    return;
+
+  view = g_object_get_data (G_OBJECT (row), "IDE_PAGE");
+  if (view == NULL)
+    return;
+
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
+  if (stack == NULL)
+    return;
+
+  _ide_frame_request_close (IDE_FRAME (stack), IDE_PAGE (view));
+}
+
+static GtkWidget *
+create_document_row (gpointer item,
+                     gpointer user_data)
+{
+  IdeFrameHeader *self = user_data;
+  GtkListBoxRow *row;
+  GtkButton *close_button;
+  GtkLabel *label;
+  GtkImage *image;
+  GtkBox *box;
+
+  g_assert (IDE_IS_PAGE (item));
+  g_assert (IDE_IS_FRAME_HEADER (self));
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+  box = g_object_new (GTK_TYPE_BOX,
+                      "visible", TRUE,
+                      NULL);
+  image = g_object_new (GTK_TYPE_IMAGE,
+                        "icon-size", GTK_ICON_SIZE_MENU,
+                        "visible", TRUE,
+                        NULL);
+  label = g_object_new (DZL_TYPE_BOLDING_LABEL,
+                        "hexpand", TRUE,
+                        "xalign", 0.0f,
+                        "visible", TRUE,
+                        NULL);
+  close_button = g_object_new (GTK_TYPE_BUTTON,
+                               "child", g_object_new (GTK_TYPE_IMAGE,
+                                                      "icon-name", "window-close-symbolic",
+                                                      "visible", TRUE,
+                                                      NULL),
+                               "visible", TRUE,
+                               NULL);
+  g_signal_connect_object (close_button,
+                           "clicked",
+                           G_CALLBACK (close_view_cb),
+                           self, 0);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (close_button), "image-button");
+
+  g_object_bind_property (item, "icon-name", image, "icon-name", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (item, "modified", label, "bold", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (item, "title", label, "label", G_BINDING_SYNC_CREATE);
+  g_object_set_data (G_OBJECT (row), "IDE_PAGE", item);
+
+  gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (box));
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (image));
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (label));
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (close_button));
+
+  return GTK_WIDGET (row);
+}
+
+void
+_ide_frame_header_set_pages (IdeFrameHeader *self,
+                                    GListModel           *model)
+{
+  g_assert (IDE_IS_FRAME_HEADER (self));
+  g_assert (!model || G_IS_LIST_MODEL (model));
+
+  gtk_list_box_bind_model (self->title_list_box,
+                           model,
+                           create_document_row,
+                           self, NULL);
+}
+
+static void
+ide_frame_header_view_row_activated (GtkListBox           *list_box,
+                                            GtkListBoxRow        *row,
+                                            IdeFrameHeader *self)
+{
+  GtkWidget *stack;
+  GtkWidget *page;
+
+  g_assert (GTK_IS_LIST_BOX (list_box));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+  g_assert (IDE_IS_FRAME_HEADER (self));
+
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_FRAME);
+  page = g_object_get_data (G_OBJECT (row), "IDE_PAGE");
+
+  if (stack != NULL && page != NULL)
+    {
+      ide_frame_set_visible_child (IDE_FRAME (stack), IDE_PAGE (page));
+      gtk_widget_grab_focus (page);
+    }
+
+  _ide_frame_header_popdown (self);
+}
+
+static gboolean
+ide_frame_header_update_css (IdeFrameHeader *self)
+{
+  g_autoptr(GString) str = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_FRAME_HEADER (self));
+  g_assert (self->css_provider != NULL);
+  g_assert (GTK_IS_CSS_PROVIDER (self->css_provider));
+
+  str = g_string_new (NULL);
+
+  /*
+   * We set various styles on this provider so that we can update multiple
+   * widgets using the same CSS style. That includes ourself, various buttons,
+   * labels, and some images.
+   */
+
+  if (self->background_rgba_set)
+    {
+      g_autofree gchar *bgstr = gdk_rgba_to_string (&self->background_rgba);
+
+      g_string_append        (str, "ideframeheader {\n");
+      g_string_append        (str, "  background: none;\n");
+      g_string_append_printf (str, "  background-color: %s;\n", bgstr);
+      g_string_append        (str, "  transition: background-color 400ms;\n");
+      g_string_append        (str, "  transition-timing-function: ease; }\n");
+      g_string_append        (str, "button { background: transparent; }\n");
+      g_string_append        (str, "button:hover, button:checked {\n");
+      g_string_append_printf (str, "  background: none; background-color: shade(%s,.85); }\n", bgstr);
+
+      /* only use foreground when background is set */
+      if (self->foreground_rgba_set)
+        {
+          static const gchar *names[] = { "image", "label" };
+          g_autofree gchar *fgstr = gdk_rgba_to_string (&self->foreground_rgba);
+
+          for (guint i = 0; i < G_N_ELEMENTS (names); i++)
+            {
+              g_string_append_printf (str, "%s { ", names[i]);
+              g_string_append        (str, "  -gtk-icon-shadow: none;\n");
+              g_string_append        (str, "  text-shadow: none;\n");
+              g_string_append_printf (str, "  text-shadow: 0 -1px alpha(%s,0.05);\n", fgstr);
+              g_string_append_printf (str, "  color: %s;\n", fgstr);
+              g_string_append        (str, "}\n");
+            }
+        }
+    }
+
+  /* Use -1 for length so CSS provider knows the string is NULL terminated
+   * and there-by avoid a string copy.
+   */
+  if (!gtk_css_provider_load_from_data (self->css_provider, str->str, -1, &error))
+    g_warning ("Failed to load CSS: '%s': %s", str->str, error->message);
+
+  self->update_css_handler = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_frame_header_queue_update_css (IdeFrameHeader *self)
+{
+  g_assert (IDE_IS_FRAME_HEADER (self));
+
+  if (self->update_css_handler == 0)
+    self->update_css_handler =
+      gdk_threads_add_idle_full (G_PRIORITY_HIGH,
+                                 (GSourceFunc) ide_frame_header_update_css,
+                                 g_object_ref (self),
+                                 g_object_unref);
+}
+
+void
+_ide_frame_header_set_background_rgba (IdeFrameHeader *self,
+                                              const GdkRGBA        *background_rgba)
+{
+  GdkRGBA old;
+  gboolean old_set;
+
+  g_assert (IDE_IS_FRAME_HEADER (self));
+
+  old_set = self->background_rgba_set;
+  old = self->background_rgba;
+
+  if (background_rgba != NULL)
+    self->background_rgba = *background_rgba;
+
+  self->background_rgba_set = !!background_rgba;
+
+  if (self->background_rgba_set != old_set || !gdk_rgba_equal (&self->background_rgba, &old))
+    ide_frame_header_queue_update_css (self);
+}
+
+void
+_ide_frame_header_set_foreground_rgba (IdeFrameHeader *self,
+                                              const GdkRGBA        *foreground_rgba)
+{
+  GdkRGBA old;
+  gboolean old_set;
+
+  g_assert (IDE_IS_FRAME_HEADER (self));
+
+  old_set = self->foreground_rgba_set;
+  old = self->foreground_rgba;
+
+  if (foreground_rgba != NULL)
+    self->foreground_rgba = *foreground_rgba;
+
+  self->foreground_rgba_set = !!foreground_rgba;
+
+  if (self->background_rgba_set != old_set || !gdk_rgba_equal (&self->foreground_rgba, &old))
+    ide_frame_header_queue_update_css (self);
+}
+
+static void
+update_widget_providers (GtkWidget            *widget,
+                         IdeFrameHeader *self)
+{
+  g_assert (IDE_IS_FRAME_HEADER (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  /*
+   * The goal here is to explore the widget hierarchy a bit to find widget
+   * types that we care about styling. This is the second half to our CSS
+   * strategy to assign specific CSS providers to widgets instead of a global
+   * CSS provider. The goal here is to avoid the giant CSS invalidation that
+   * happens when invalidating the global CSS tree.
+   */
+
+  if (GTK_IS_BUTTON (widget) ||
+      GTK_IS_LABEL (widget) ||
+      GTK_IS_IMAGE (widget) ||
+      DZL_IS_SIMPLE_LABEL (widget))
+    {
+      GtkStyleContext *style_context;
+
+      style_context = gtk_widget_get_style_context (widget);
+      gtk_style_context_add_provider (style_context,
+                                      GTK_STYLE_PROVIDER (self->css_provider),
+                                      CSS_PROVIDER_PRIORITY);
+    }
+
+  if (GTK_IS_CONTAINER (widget))
+    gtk_container_foreach (GTK_CONTAINER (widget),
+                           (GtkCallback) update_widget_providers,
+                           self);
+}
+
+static void
+ide_frame_header_add (GtkContainer *container,
+                             GtkWidget    *widget)
+{
+  IdeFrameHeader *self = (IdeFrameHeader *)container;
+
+  g_assert (IDE_IS_FRAME_HEADER (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  GTK_CONTAINER_CLASS (ide_frame_header_parent_class)->add (container, widget);
+
+  update_widget_providers (widget, self);
+}
+
+static void
+ide_frame_header_get_preferred_width (GtkWidget *widget,
+                                             gint      *min_width,
+                                             gint      *nat_width)
+{
+  g_assert (IDE_IS_FRAME_HEADER (widget));
+  g_assert (min_width != NULL);
+  g_assert (nat_width != NULL);
+
+  GTK_WIDGET_CLASS (ide_frame_header_parent_class)->get_preferred_width (widget, min_width, nat_width);
+
+  /*
+   * We don't want changes to the natural width to influence our positioning of
+   * the grid separators (unless necessary). So instead, we always return our
+   * minimum position as our natural size and let the grid expand as necessary.
+   */
+  *nat_width = *min_width;
+}
+
+static void
+ide_frame_header_destroy (GtkWidget *widget)
+{
+  IdeFrameHeader *self = (IdeFrameHeader *)widget;
+
+  g_assert (IDE_IS_FRAME_HEADER (self));
+
+  dzl_clear_source (&self->update_css_handler);
+  g_clear_object (&self->css_provider);
+
+  if (self->title_list_box != NULL)
+    gtk_list_box_bind_model (self->title_list_box, NULL, NULL, NULL, NULL);
+
+  g_clear_object (&self->menu);
+
+  GTK_WIDGET_CLASS (ide_frame_header_parent_class)->destroy (widget);
+}
+
+static void
+ide_frame_header_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeFrameHeader *self = IDE_FRAME_HEADER (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODIFIED:
+      g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (self->title_modified)));
+      break;
+
+    case PROP_SHOW_CLOSE_BUTTON:
+      g_value_set_boolean (value, gtk_widget_get_visible (GTK_WIDGET (self->close_button)));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_label (GTK_LABEL (self->title_label)));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_frame_header_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeFrameHeader *self = IDE_FRAME_HEADER (object);
+
+  switch (prop_id)
+    {
+    case PROP_BACKGROUND_RGBA:
+      _ide_frame_header_set_background_rgba (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_FOREGROUND_RGBA:
+      _ide_frame_header_set_foreground_rgba (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_MODIFIED:
+      _ide_frame_header_set_modified (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_SHOW_CLOSE_BUTTON:
+      gtk_widget_set_visible (GTK_WIDGET (self->close_button), g_value_get_boolean (value));
+      break;
+
+    case PROP_TITLE:
+      _ide_frame_header_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_frame_header_class_init (IdeFrameHeaderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->get_property = ide_frame_header_get_property;
+  object_class->set_property = ide_frame_header_set_property;
+
+  widget_class->destroy = ide_frame_header_destroy;
+  widget_class->get_preferred_width = ide_frame_header_get_preferred_width;
+
+  container_class->add = ide_frame_header_add;
+
+  /**
+   * IdeFrameHeader:background-rgba:
+   *
+   * The "background-rgba" property can be used to set the background
+   * color of the header. This should be set to the
+   * #IdePage:primary-color of the active view.
+   *
+   * Set to %NULL to unset the primary-color.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_BACKGROUND_RGBA] =
+    g_param_spec_boxed ("background-rgba",
+                        "Background RGBA",
+                        "The background color to use for the header",
+                        GDK_TYPE_RGBA,
+                        (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeFrameHeader:foreground-rgba:
+   *
+   * Sets the foreground color to use when
+   * #IdeFrameHeader:background-rgba is used for the background.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_FOREGROUND_RGBA] =
+    g_param_spec_boxed ("foreground-rgba",
+                        "Foreground RGBA",
+                        "The foreground color to use with background-rgba",
+                        GDK_TYPE_RGBA,
+                        (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SHOW_CLOSE_BUTTON] =
+    g_param_spec_boolean ("show-close-button",
+                          "Show Close Button",
+                          "If the close button should be displayed",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MODIFIED] =
+    g_param_spec_boolean ("modified",
+                          "Modified",
+                          "If the current document is modified",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the current document or view",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "ideframeheader");
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-frame-header.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, close_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, document_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_label);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_list_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_modified);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_popover);
+  gtk_widget_class_bind_template_child (widget_class, IdeFrameHeader, title_views_box);
+}
+
+static void
+ide_frame_header_init (IdeFrameHeader *self)
+{
+  GtkStyleContext *style_context;
+  GMenu *frame_section;
+
+  /*
+   * To keep our foreground/background colors up to date, we use a CSS
+   * provider. However, attaching the provider globally causes much CSS
+   * style cascading exactly at the moment we want to animate. To avbid
+   * that, and keep animations snappy, we add the provider directly to
+   * our widget and to the children widgets we care about (buttons, their
+   * labels, etc).
+   */
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  self->css_provider = gtk_css_provider_new ();
+  gtk_style_context_add_provider (style_context,
+                                  GTK_STYLE_PROVIDER (self->css_provider),
+                                  CSS_PROVIDER_PRIORITY);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  /*
+   * Create our menu for the document controls popover. It has two sections.
+   * The top section is based on the document and is updated whenever the
+   * visible child is changed. The bottom, are the frame controls are and
+   * static, but setup by us here.
+   */
+
+  self->menu = dzl_joined_menu_new ();
+  dzl_menu_button_set_model (self->document_button, G_MENU_MODEL (self->menu));
+  frame_section = dzl_application_get_menu_by_id (DZL_APPLICATION_DEFAULT,
+                                                  "ide-frame-menu");
+  dzl_joined_menu_append_menu (self->menu, G_MENU_MODEL (frame_section));
+
+  /*
+   * When a row is selected, we want to change the current view and
+   * hide the popover.
+   */
+
+  g_signal_connect_object (self->title_list_box,
+                           "row-activated",
+                           G_CALLBACK (ide_frame_header_view_row_activated),
+                           self, 0);
+
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+  gtk_container_set_reallocate_redraws (GTK_CONTAINER (self), TRUE);
+  G_GNUC_END_IGNORE_DEPRECATIONS;
+}
+
+GtkWidget *
+ide_frame_header_new (void)
+{
+  return g_object_new (IDE_TYPE_FRAME_HEADER, NULL);
+}
+
+/**
+ * ide_frame_header_add_custom_title:
+ * @self: a #IdeFrameHeader
+ * @widget: a #GtkWidget
+ * @priority: the sort priority
+ *
+ * This will add @widget to the title area with @priority determining the
+ * sort order of the child.
+ *
+ * All "title" widgets in the #IdeFrameHeader are expanded to the
+ * same size. So if you don't need that, you should just use the normal
+ * gtk_container_add_with_properties() API to specify your widget with
+ * a given priority.
+ *
+ * Since: 3.32
+ */
+void
+ide_frame_header_add_custom_title (IdeFrameHeader *self,
+                                          GtkWidget            *widget,
+                                          gint                  priority)
+{
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->title_box), widget,
+                                     "priority", priority,
+                                     NULL);
+
+  update_widget_providers (widget, self);
+}
+
+void
+_ide_frame_header_set_title (IdeFrameHeader *self,
+                                    const gchar          *title)
+{
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
+
+  gtk_label_set_label (GTK_LABEL (self->title_label), title);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+}
+
+void
+_ide_frame_header_set_modified (IdeFrameHeader *self,
+                                       gboolean              modified)
+{
+  g_return_if_fail (IDE_IS_FRAME_HEADER (self));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->title_modified), modified);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODIFIED]);
+}
diff --git a/src/libide/gui/ide-frame-header.h b/src/libide/gui/ide-frame-header.h
new file mode 100644
index 000000000..26419db07
--- /dev/null
+++ b/src/libide/gui/ide-frame-header.h
@@ -0,0 +1,44 @@
+/* ide-frame-header.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FRAME_HEADER (ide_frame_header_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeFrameHeader, ide_frame_header, IDE, FRAME_HEADER, DzlPriorityBox)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_frame_header_new              (void);
+IDE_AVAILABLE_IN_3_32
+void       ide_frame_header_add_custom_title (IdeFrameHeader *self,
+                                              GtkWidget      *widget,
+                                              gint            priority);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-frame-header.ui b/src/libide/gui/ide-frame-header.ui
new file mode 100644
index 000000000..a9a8b776d
--- /dev/null
+++ b/src/libide/gui/ide-frame-header.ui
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <object class="GtkPopover" id="title_popover">
+    <property name="width-request">350</property>
+    <property name="border-width">18</property>
+    <style>
+      <class name="title-popover"/>
+    </style>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+        <property name="spacing">12</property>
+        <child>
+          <object class="GtkBox" id="title_views_box">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+            <property name="spacing">12</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="label" translatable="yes" comments="List of pages that are open">Open 
Pages</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow">
+                <property name="propagate-natural-height">true</property>
+                <property name="propagate-natural-width">true</property>
+                <property name="max-content-height">600</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkListBox" id="title_list_box">
+                    <property name="selection-mode">none</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="homogeneous">true</property>
+            <property name="spacing">6</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkButton">
+                <property name="tooltip-text" translatable="yes">Open file</property>
+                <property name="action-name">editor.open-file</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">document-open-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="visible">true</property>
+                <property name="tooltip-text" translatable="yes">New file</property>
+                <property name="action-name">editor.new-file</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">document-new-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="tooltip-text" translatable="yes">New terminal</property>
+                <property name="action-name">win.new-terminal</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">utilities-terminal-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="tooltip-text" translatable="yes">New documentation</property>
+                <property name="action-name">devhelp.new-view</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">org.gnome.Devhelp-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+  <template class="IdeFrameHeader" parent="DzlPriorityBox">
+    <child>
+      <object class="DzlPriorityBox" id="title_box">
+        <property name="hexpand">true</property>
+        <property name="homogeneous">true</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkMenuButton" id="title_button">
+            <property name="popover">title_popover</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">true</property>
+                <child type="center">
+                  <object class="GtkLabel" id="title_label">
+                    <property name="margin-start">6</property>
+                    <property name="margin-end">6</property>
+                    <property name="ellipsize">start</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="title_modified">
+                    <property name="halign">start</property>
+                    <property name="hexpand">true</property>
+                    <property name="margin-start">8</property>
+                    <property name="margin-end">8</property>
+                    <property name="label">•</property>
+                  </object>
+                  <packing>
+                    <property name="pack-type">end</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton" id="close_button">
+        <property name="action-name">gridcolumn.close</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">window-close-symbolic</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+      <packing>
+        <property name="pack-type">end</property>
+      </packing>
+    </child>
+    <child>
+      <object class="DzlMenuButton" id="document_button">
+        <property name="focus-on-click">false</property>
+        <property name="icon-name">pan-down-symbolic</property>
+        <property name="show-accels">true</property>
+        <property name="show-arrow">false</property>
+        <property name="show-icons">true</property>
+        <property name="visible">true</property>
+      </object>
+      <packing>
+        <property name="pack-type">end</property>
+      </packing>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-frame-shortcuts.c b/src/libide/gui/ide-frame-shortcuts.c
new file mode 100644
index 000000000..eef3655b6
--- /dev/null
+++ b/src/libide/gui/ide-frame-shortcuts.c
@@ -0,0 +1,113 @@
+/* ide-frame-shortcuts.c
+ *
+ * Copyright 2017-2019 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
+ */
+
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-frame.h"
+#include "ide-gui-private.h"
+
+#define I_(s) g_intern_static_string(s)
+
+static const DzlShortcutEntry frame_shortcuts[] = {
+  { "org.gnome.builder.frame.move-right",
+    DZL_SHORTCUT_PHASE_CAPTURE,
+    NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Files"),
+    NC_("shortcut window", "Move document to the right") },
+
+  { "org.gnome.builder.frame.move-left",
+    DZL_SHORTCUT_PHASE_CAPTURE,
+    NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Files"),
+    NC_("shortcut window", "Move document to the left") },
+
+  { "org.gnome.builder.frame.previous-document",
+    DZL_SHORTCUT_PHASE_CAPTURE,
+    NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Files"),
+    NC_("shortcut window", "Switch to the previous document") },
+
+  { "org.gnome.builder.frame.next-document",
+    DZL_SHORTCUT_PHASE_CAPTURE,
+    NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Files"),
+    NC_("shortcut window", "Switch to the next document") },
+
+  { "org.gnome.builder.frame.close-page",
+    DZL_SHORTCUT_PHASE_BUBBLE,
+    NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Files"),
+    NC_("shortcut window", "Close the document") },
+};
+
+void
+_ide_frame_init_shortcuts (IdeFrame *self)
+{
+  DzlShortcutController *controller;
+
+  g_return_if_fail (IDE_IS_FRAME (self));
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             frame_shortcuts,
+                                             G_N_ELEMENTS (frame_shortcuts),
+                                             GETTEXT_PACKAGE);
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.frame.move-right"),
+                                              I_("<Primary><Alt>Page_Down"),
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("frame.move-right"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.frame.move-left"),
+                                              I_("<Primary><Alt>Page_Up"),
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("frame.move-left"));
+
+  dzl_shortcut_controller_add_command_signal (controller,
+                                              I_("org.gnome.builder.frame.next-document"),
+                                              I_("<Primary><Shift>Page_Down"),
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("change-current-page"),
+                                              1, G_TYPE_INT, 1);
+
+  dzl_shortcut_controller_add_command_signal (controller,
+                                              I_("org.gnome.builder.frame.previous-document"),
+                                              I_("<Primary><Shift>Page_Up"),
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("change-current-page"),
+                                              1, G_TYPE_INT, -1);
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.frame.close-page"),
+                                              I_("<Primary>w"),
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("frame.close-page"));
+}
diff --git a/src/libide/gui/ide-frame-wrapper.c b/src/libide/gui/ide-frame-wrapper.c
new file mode 100644
index 000000000..aacd241f5
--- /dev/null
+++ b/src/libide/gui/ide-frame-wrapper.c
@@ -0,0 +1,124 @@
+/* ide-frame-wrapper.c
+ *
+ * Copyright 2018-2019 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 "ide-frame-wrapper"
+
+#include "config.h"
+
+#include "ide-frame-wrapper.h"
+
+/*
+ * This is just a GtkStack wrapper that allows us to override
+ * GtkContainer::remove() so that we can transition to the previously
+ * focused child first.
+ */
+
+struct _IdeFrameWrapper
+{
+  GtkStack parent_instance;
+  GQueue   history;
+};
+
+G_DEFINE_TYPE (IdeFrameWrapper, ide_frame_wrapper, GTK_TYPE_STACK)
+
+static void
+ide_frame_wrapper_add (GtkContainer *container,
+                              GtkWidget    *widget)
+{
+  IdeFrameWrapper *self = (IdeFrameWrapper *)container;
+
+  g_assert (IDE_IS_FRAME_WRAPPER (container));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (gtk_widget_get_visible (widget))
+    g_queue_push_head (&self->history, widget);
+  else
+    g_queue_push_tail (&self->history, widget);
+
+  GTK_CONTAINER_CLASS (ide_frame_wrapper_parent_class)->add (container, widget);
+}
+
+static void
+ide_frame_wrapper_remove (GtkContainer *container,
+                                 GtkWidget    *widget)
+{
+  IdeFrameWrapper *self = (IdeFrameWrapper *)container;
+
+  g_assert (IDE_IS_FRAME_WRAPPER (container));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  /* Remove the widget from our history chain, and then see if we need to
+   * first change the visible child before removing. If we don't we risk,
+   * focusing the wrong "next" widget as part of the removal.
+   */
+
+  g_queue_remove (&self->history, widget);
+
+  if (self->history.length > 0)
+    {
+      GtkWidget *new_fg = g_queue_peek_head (&self->history);
+
+      if (new_fg != gtk_stack_get_visible_child (GTK_STACK (self)))
+        gtk_stack_set_visible_child (GTK_STACK (self), new_fg);
+    }
+
+  GTK_CONTAINER_CLASS (ide_frame_wrapper_parent_class)->remove (container, widget);
+}
+
+static void
+ide_frame_wrapper_notify_visible_child (IdeFrameWrapper *self,
+                                               GParamSpec            *pspec)
+{
+  GtkWidget *visible_child;
+
+  g_assert (IDE_IS_FRAME_WRAPPER (self));
+  g_assert (pspec != NULL);
+
+  if ((visible_child = gtk_stack_get_visible_child (GTK_STACK (self))))
+    {
+      if (visible_child != g_queue_peek_head (&self->history))
+        {
+          GList *link_ = g_queue_find (&self->history, visible_child);
+
+          g_assert (link_ != NULL);
+
+          g_queue_unlink (&self->history, link_);
+          g_queue_push_head_link (&self->history, link_);
+        }
+    }
+}
+
+static void
+ide_frame_wrapper_class_init (IdeFrameWrapperClass *klass)
+{
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  container_class->add = ide_frame_wrapper_add;
+  container_class->remove = ide_frame_wrapper_remove;
+}
+
+static void
+ide_frame_wrapper_init (IdeFrameWrapper *self)
+{
+  g_signal_connect (self,
+                    "notify::visible-child",
+                    G_CALLBACK (ide_frame_wrapper_notify_visible_child),
+                    NULL);
+}
diff --git a/src/libide/gui/ide-frame-wrapper.h b/src/libide/gui/ide-frame-wrapper.h
new file mode 100644
index 000000000..093aaa780
--- /dev/null
+++ b/src/libide/gui/ide-frame-wrapper.h
@@ -0,0 +1,31 @@
+/* ide-frame-wrapper.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FRAME_WRAPPER (ide_frame_wrapper_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeFrameWrapper, ide_frame_wrapper, IDE, FRAME_WRAPPER, GtkStack)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-frame.c b/src/libide/gui/ide-frame.c
new file mode 100644
index 000000000..15f477930
--- /dev/null
+++ b/src/libide/gui/ide-frame.c
@@ -0,0 +1,1413 @@
+/* ide-frame.c
+ *
+ * Copyright 2017-2019 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 "ide-frame"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-core.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+
+#include "ide-frame.h"
+#include "ide-frame-addin.h"
+#include "ide-frame-header.h"
+#include "ide-frame-wrapper.h"
+#include "ide-gui-private.h"
+
+#define TRANSITION_DURATION 300
+#define DISTANCE_THRESHOLD(alloc) (MIN(250, (gint)((alloc)->width * .333)))
+
+/**
+ * SECTION:ide-frame
+ * @title: IdeFrame
+ * @short_description: A stack of #IdePage
+ *
+ * This widget is used to represent a stack of #IdePage widgets.  it
+ * includes an #IdeFrameHeader at the top, and then a stack of pages
+ * below.
+ *
+ * If there are no #IdePage visibile, then an empty state widget is
+ * displayed with some common information for the user.
+ *
+ * To simplify integration with other systems, #IdeFrame implements
+ * the #GListModel interface for each of the #IdePage.
+ *
+ * Since: 3.32
+ */
+
+typedef struct
+{
+  DzlBindingGroup      *bindings;
+  DzlSignalGroup       *signals;
+  GPtrArray            *pages;
+  GPtrArray            *in_transition;
+  PeasExtensionSet     *addins;
+
+  /*
+   * Our gestures are used to do interactive moves when the user
+   * does a three finger swipe. We create the dummy gesture to
+   * ensure things work, because it for some reason does not without
+   * the dummy gesture set.
+   *
+   * https://bugzilla.gnome.org/show_bug.cgi?id=788914
+   */
+  GtkGesture           *dummy;
+  GtkGesture           *pan;
+  DzlBoxTheatric       *pan_theatric;
+  IdePage              *pan_page;
+
+  /* Template references */
+  DzlBox               *empty_state;
+  DzlEmptyState        *failed_state;
+  IdeFrameHeader       *header;
+  GtkStack             *stack;
+  GtkStack             *top_stack;
+  GtkEventBox          *event_box;
+} IdeFramePrivate;
+
+typedef struct
+{
+  IdeFrame *source;
+  IdeFrame *dest;
+  IdePage  *page;
+  DzlBoxTheatric *theatric;
+} AnimationState;
+
+enum {
+  PROP_0,
+  PROP_HAS_VIEW,
+  PROP_VISIBLE_CHILD,
+  N_PROPS
+};
+
+enum {
+  CHNAGE_CURRENT_PAGE,
+  N_SIGNALS
+};
+
+static void list_model_iface_init    (GListModelInterface *iface);
+static void animation_state_complete (gpointer             data);
+
+G_DEFINE_TYPE_WITH_CODE (IdeFrame, ide_frame, GTK_TYPE_BOX,
+                         G_ADD_PRIVATE (IdeFrame)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static inline gboolean
+is_uninitialized (GtkAllocation *alloc)
+{
+  return (alloc->x == -1 && alloc->y == -1 &&
+          alloc->width == 1 && alloc->height == 1);
+}
+
+static void
+ide_frame_set_cursor (IdeFrame    *self,
+                      const gchar *name)
+{
+  GdkWindow *window;
+  GdkDisplay *display;
+  GdkCursor *cursor;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (name != NULL);
+
+  window = gtk_widget_get_window (GTK_WIDGET (self));
+  display = gtk_widget_get_display (GTK_WIDGET (self));
+  cursor = gdk_cursor_new_from_name (display, name);
+
+  gdk_window_set_cursor (window, cursor);
+
+  g_clear_object (&cursor);
+}
+
+static void
+ide_frame_page_failed (IdeFrame   *self,
+                       GParamSpec *pspec,
+                       IdePage    *page)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (IDE_IS_PAGE (page));
+
+  if (ide_page_get_failed (page))
+    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->failed_state));
+  else
+    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->stack));
+}
+
+static void
+ide_frame_bindings_notify_source (IdeFrame        *self,
+                                  GParamSpec      *pspec,
+                                  DzlBindingGroup *bindings)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  GObject *source;
+
+  g_assert (DZL_IS_BINDING_GROUP (bindings));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_FRAME (self));
+
+  source = dzl_binding_group_get_source (bindings);
+
+  if (source == NULL)
+    {
+      _ide_frame_header_set_title (priv->header, _("No Open Pages"));
+      _ide_frame_header_set_modified (priv->header, FALSE);
+      _ide_frame_header_set_background_rgba (priv->header, NULL);
+      _ide_frame_header_set_foreground_rgba (priv->header, NULL);
+    }
+}
+
+static void
+ide_frame_notify_addin_of_page (PeasExtensionSet *set,
+                                PeasPluginInfo   *plugin_info,
+                                PeasExtension    *exten,
+                                gpointer          user_data)
+{
+  IdeFrameAddin *addin = (IdeFrameAddin *)exten;
+  IdePage *page = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_FRAME_ADDIN (addin));
+  g_assert (!page || IDE_IS_PAGE (page));
+
+  ide_frame_addin_set_page (addin, page);
+}
+
+static void
+ide_frame_notify_visible_child (IdeFrame   *self,
+                                GParamSpec *pspec,
+                                GtkStack   *stack)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  GtkWidget *visible_child;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (GTK_IS_STACK (stack));
+
+  if (gtk_widget_in_destruction (GTK_WIDGET (self)))
+    return;
+
+  if ((visible_child = gtk_stack_get_visible_child (priv->stack)))
+    {
+      if (gtk_widget_in_destruction (visible_child))
+        visible_child = NULL;
+    }
+
+  /*
+   * Mux/Proxy actions to our level so that they also be activated
+   * from the header bar without any weirdness by the View.
+   */
+  dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self), visible_child,
+                                    "IDE_FRAME_MUXED_ACTION");
+
+  /* Update our bindings targets */
+  dzl_binding_group_set_source (priv->bindings, visible_child);
+  dzl_signal_group_set_target (priv->signals, visible_child);
+
+  /* Show either the empty state, failed state, or actual page */
+  if (visible_child != NULL &&
+      ide_page_get_failed (IDE_PAGE (visible_child)))
+    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->failed_state));
+  else if (visible_child != NULL)
+    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->stack));
+  else
+    gtk_stack_set_visible_child (priv->top_stack, GTK_WIDGET (priv->empty_state));
+
+  /* Allow the header to update settings */
+  _ide_frame_header_update (priv->header, IDE_PAGE (visible_child));
+
+  /* Ensure action state is up to date */
+  _ide_frame_update_actions (self);
+
+  if (priv->addins != NULL)
+    peas_extension_set_foreach (priv->addins,
+                                ide_frame_notify_addin_of_page,
+                                visible_child);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VISIBLE_CHILD]);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HAS_VIEW]);
+}
+
+static void
+collect_widgets (GtkWidget *widget,
+                 gpointer   user_data)
+{
+  g_ptr_array_add (user_data, widget);
+}
+
+static void
+ide_frame_change_current_page (IdeFrame *self,
+                               gint      direction)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  g_autoptr(GPtrArray) ar = NULL;
+  GtkWidget *visible_child;
+  gint position = 0;
+
+  g_assert (IDE_IS_FRAME (self));
+
+  visible_child = gtk_stack_get_visible_child (priv->stack);
+
+  if (visible_child == NULL)
+    return;
+
+  gtk_container_child_get (GTK_CONTAINER (priv->stack), visible_child,
+                           "position", &position,
+                           NULL);
+
+  ar = g_ptr_array_new ();
+  gtk_container_foreach (GTK_CONTAINER (priv->stack), collect_widgets, ar);
+  if (ar->len == 0)
+    g_return_if_reached ();
+
+  visible_child = g_ptr_array_index (ar, (position + direction) % ar->len);
+  gtk_stack_set_visible_child (priv->stack, visible_child);
+}
+
+static void
+ide_frame_add (GtkContainer *container,
+               GtkWidget    *widget)
+{
+  IdeFrame *self = (IdeFrame *)container;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_FRAME (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  if (IDE_IS_PAGE (widget))
+    gtk_container_add (GTK_CONTAINER (priv->stack), widget);
+  else
+    GTK_CONTAINER_CLASS (ide_frame_parent_class)->add (container, widget);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+ide_frame_page_added (IdeFrame *self,
+                      IdePage  *page)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  guint position;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (IDE_IS_PAGE (page));
+
+  /*
+   * Make sure that the header has dismissed all of the popovers immediately.
+   * We don't want them lingering while we do other UI work which might want to
+   * grab focus, etc.
+   */
+  _ide_frame_header_popdown (priv->header);
+
+  /* Notify GListModel consumers of the new page and it's position within
+   * our stack of page widgets.
+   */
+  position = priv->pages->len;
+  g_ptr_array_add (priv->pages, page);
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+
+  /*
+   * Now ensure that the page is displayed and focus the widget so the
+   * user can immediately start typing.
+   */
+  ide_frame_set_visible_child (self, page);
+  gtk_widget_grab_focus (GTK_WIDGET (page));
+}
+
+static void
+ide_frame_page_removed (IdeFrame *self,
+                        IdePage  *page)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (IDE_IS_PAGE (page));
+
+  if (priv->pages != NULL)
+    {
+      guint position = 0;
+
+      /* If this is the last page, hide the popdown now.  We use our hide
+       * variant instead of popdown so that we don't have jittery animations.
+       */
+      if (priv->pages->len == 1)
+        _ide_frame_header_hide (priv->header);
+
+      /*
+       * Only remove the page if it is not in transition. We hold onto the
+       * page during the transition so that we keep the list stable.
+       */
+      if (!g_ptr_array_find_with_equal_func (priv->in_transition, page, NULL, &position))
+        {
+          for (guint i = 0; i < priv->pages->len; i++)
+            {
+              if ((gpointer)page == g_ptr_array_index (priv->pages, i))
+                {
+                  g_ptr_array_remove_index (priv->pages, i);
+                  g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+                }
+            }
+        }
+    }
+}
+
+static void
+ide_frame_real_agree_to_close_async (IdeFrame            *self,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_frame_real_agree_to_close_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_frame_real_agree_to_close_finish (IdeFrame      *self,
+                                      GAsyncResult  *result,
+                                      GError       **error)
+{
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_frame_addin_added (PeasExtensionSet *set,
+                       PeasPluginInfo   *plugin_info,
+                       PeasExtension    *exten,
+                       gpointer          user_data)
+{
+  IdeFrameAddin *addin = (IdeFrameAddin *)exten;
+  IdeFrame *self = user_data;
+  IdePage *visible_child;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_FRAME_ADDIN (addin));
+
+  ide_frame_addin_load (addin, self);
+
+  visible_child = ide_frame_get_visible_child (self);
+
+  if (visible_child != NULL)
+    ide_frame_addin_set_page (addin, visible_child);
+}
+
+static void
+ide_frame_addin_removed (PeasExtensionSet *set,
+                         PeasPluginInfo   *plugin_info,
+                         PeasExtension    *exten,
+                         gpointer          user_data)
+{
+  IdeFrameAddin *addin = (IdeFrameAddin *)exten;
+  IdeFrame *self = user_data;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_FRAME_ADDIN (addin));
+
+  ide_frame_addin_set_page (addin, NULL);
+  ide_frame_addin_unload (addin, self);
+}
+
+static gboolean
+ide_frame_pan_begin (IdeFrame         *self,
+                     GdkEventSequence *sequence,
+                     GtkGesturePan    *gesture)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  GtkAllocation alloc;
+  cairo_surface_t *surface = NULL;
+  IdePage *page;
+  GdkWindow *window;
+  GtkWidget *grid;
+  cairo_t *cr;
+  gdouble x, y;
+  gboolean enable_animations;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (GTK_IS_GESTURE_PAN (gesture));
+  g_assert (priv->pan_theatric == NULL);
+
+  page = ide_frame_get_visible_child (self);
+  if (page != NULL)
+    gtk_widget_get_allocation (GTK_WIDGET (page), &alloc);
+
+  g_object_get (gtk_settings_get_default (),
+                "gtk-enable-animations", &enable_animations,
+                NULL);
+
+  if (sequence != NULL ||
+      page == NULL ||
+      !enable_animations ||
+      is_uninitialized (&alloc) ||
+      NULL == (window = gtk_widget_get_window (GTK_WIDGET (page))) ||
+      NULL == (surface = gdk_window_create_similar_surface (window,
+                                                            CAIRO_CONTENT_COLOR,
+                                                            alloc.width,
+                                                            alloc.height)))
+    {
+      if (sequence != NULL)
+        gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
+      IDE_RETURN (FALSE);
+    }
+
+  gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y);
+
+  cr = cairo_create (surface);
+  gtk_widget_draw (GTK_WIDGET (page), cr);
+  cairo_destroy (cr);
+
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
+  gtk_widget_translate_coordinates (GTK_WIDGET (priv->top_stack), grid, 0, 0,
+                                    &alloc.x, &alloc.y);
+
+  priv->pan_page = g_object_ref (page);
+  priv->pan_theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
+                                     "surface", surface,
+                                     "target", grid,
+                                     "x", alloc.x + (gint)x,
+                                     "y", alloc.y,
+                                     "width", alloc.width,
+                                     "height", alloc.height,
+                                     NULL);
+
+  g_clear_pointer (&surface, cairo_surface_destroy);
+
+  /* Hide the page while we begin the possible transition to another
+   * layout stack.
+   */
+  gtk_widget_hide (GTK_WIDGET (priv->pan_page));
+
+  /*
+   * Hide the mouse cursor until ide_frame_pan_end() is called.
+   * It can be distracting otherwise (and we want to warp it to the new
+   * grid column too).
+   */
+  ide_frame_set_cursor (self, "none");
+
+  IDE_RETURN (TRUE);
+}
+
+static void
+ide_frame_pan_update (IdeFrame         *self,
+                      GdkEventSequence *sequence,
+                      GtkGestureSwipe  *gesture)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  GtkAllocation alloc;
+  GtkWidget *grid;
+  gdouble x, y;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (GTK_IS_GESTURE_PAN (gesture));
+  g_assert (!priv->pan_theatric || DZL_IS_BOX_THEATRIC (priv->pan_theatric));
+
+  if (priv->pan_theatric == NULL)
+    {
+      if (sequence != NULL)
+        gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_DENIED);
+      IDE_EXIT;
+    }
+
+  gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y);
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
+  gtk_widget_translate_coordinates (GTK_WIDGET (priv->top_stack), grid, 0, 0,
+                                    &alloc.x, &alloc.y);
+
+  g_object_set (priv->pan_theatric,
+                "x", alloc.x + (gint)x,
+                NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_frame_pan_end (IdeFrame         *self,
+                   GdkEventSequence *sequence,
+                   GtkGesturePan    *gesture)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  IdeFramePrivate *dest_priv;
+  IdeFrame *dest;
+  GtkAllocation alloc;
+  GtkWidget *grid;
+  GtkWidget *column;
+  gdouble x, y;
+  gint direction;
+  gint index = 0;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (GTK_IS_GESTURE_PAN (gesture));
+
+  if (priv->pan_theatric == NULL || priv->pan_page == NULL)
+    IDE_GOTO (cleanup);
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  gtk_gesture_drag_get_offset (GTK_GESTURE_DRAG (gesture), &x, &y);
+
+  if (x > DISTANCE_THRESHOLD (&alloc))
+    direction = 1;
+  else if (x < -DISTANCE_THRESHOLD (&alloc))
+    direction = -1;
+  else
+    direction = 0;
+
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
+  g_assert (grid != NULL);
+  g_assert (IDE_IS_GRID (grid));
+
+  column = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID_COLUMN);
+  g_assert (column != NULL);
+  g_assert (IDE_IS_GRID_COLUMN (column));
+
+  gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (column),
+                           "index", &index,
+                           NULL);
+
+  dest = _ide_grid_get_nth_stack (IDE_GRID (grid), index + direction);
+  dest_priv = ide_frame_get_instance_private (dest);
+  g_assert (dest != NULL);
+  g_assert (IDE_IS_FRAME (dest));
+
+  gtk_widget_get_allocation (GTK_WIDGET (dest), &alloc);
+
+  if (!is_uninitialized (&alloc))
+    {
+      AnimationState *state;
+
+      state = g_slice_new0 (AnimationState);
+      state->source = g_object_ref (self);
+      state->dest = g_object_ref (dest);
+      state->page = g_object_ref (priv->pan_page);
+      state->theatric = priv->pan_theatric;
+
+      gtk_widget_translate_coordinates (GTK_WIDGET (dest_priv->top_stack), grid, 0, 0,
+                                        &alloc.x, &alloc.y);
+
+      /*
+       * Use EASE_OUT_CUBIC, because user initiated the beginning of the
+       * acceleration curve just by swiping. No need to duplicate.
+       */
+      dzl_object_animate_full (state->theatric,
+                               DZL_ANIMATION_EASE_OUT_CUBIC,
+                               TRANSITION_DURATION,
+                               gtk_widget_get_frame_clock (GTK_WIDGET (self)),
+                               animation_state_complete,
+                               state,
+                               "x", alloc.x,
+                               "width", alloc.width,
+                               NULL);
+
+      if (dest != self)
+        {
+          g_ptr_array_add (priv->in_transition, g_object_ref (priv->pan_page));
+          gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (priv->pan_page));
+        }
+
+      IDE_TRACE_MSG ("Animating transition to %s column",
+                     dest != self ? "another" : "same");
+    }
+  else
+    {
+      g_autoptr(IdePage) page = g_object_ref (priv->pan_page);
+
+      IDE_TRACE_MSG ("Moving page to a previously non-existant column");
+
+      gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page));
+      gtk_widget_show (GTK_WIDGET (page));
+      gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (page));
+    }
+
+cleanup:
+  g_clear_object (&priv->pan_theatric);
+  g_clear_object (&priv->pan_page);
+
+  gtk_widget_queue_draw (gtk_widget_get_toplevel (GTK_WIDGET (self)));
+
+  ide_frame_set_cursor (self, "arrow");
+
+  IDE_EXIT;
+}
+
+static void
+ide_frame_constructed (GObject *object)
+{
+  IdeFrame *self = (IdeFrame *)object;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_assert (IDE_IS_FRAME (self));
+
+  G_OBJECT_CLASS (ide_frame_parent_class)->constructed (object);
+
+  priv->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_FRAME_ADDIN,
+                                         NULL);
+
+  g_signal_connect (priv->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_frame_addin_added),
+                    self);
+
+  g_signal_connect (priv->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_frame_addin_removed),
+                    self);
+
+  peas_extension_set_foreach (priv->addins,
+                              ide_frame_addin_added,
+                              self);
+
+  gtk_widget_add_events (GTK_WIDGET (priv->event_box), GDK_TOUCH_MASK);
+  priv->pan = g_object_new (GTK_TYPE_GESTURE_PAN,
+                            "widget", priv->event_box,
+                            "orientation", GTK_ORIENTATION_HORIZONTAL,
+                            "n-points", 3,
+                            NULL);
+  g_signal_connect_swapped (priv->pan,
+                            "begin",
+                            G_CALLBACK (ide_frame_pan_begin),
+                            self);
+  g_signal_connect_swapped (priv->pan,
+                            "update",
+                            G_CALLBACK (ide_frame_pan_update),
+                            self);
+  g_signal_connect_swapped (priv->pan,
+                            "end",
+                            G_CALLBACK (ide_frame_pan_end),
+                            self);
+  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->pan),
+                                              GTK_PHASE_BUBBLE);
+
+  /*
+   * FIXME: Our priv->pan gesture does not activate unless we add another
+   *        dummy gesture. I currently have no idea why that is.
+   *
+   *        https://bugzilla.gnome.org/show_bug.cgi?id=788914
+   */
+  priv->dummy = gtk_gesture_rotate_new (GTK_WIDGET (priv->event_box));
+  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (priv->dummy),
+                                              GTK_PHASE_BUBBLE);
+}
+
+static void
+ide_frame_grab_focus (GtkWidget *widget)
+{
+  IdeFrame *self = (IdeFrame *)widget;
+  IdePage *child;
+
+  g_assert (IDE_IS_FRAME (self));
+
+  child = ide_frame_get_visible_child (self);
+
+  if (child != NULL)
+    gtk_widget_grab_focus (GTK_WIDGET (child));
+  else
+    GTK_WIDGET_CLASS (ide_frame_parent_class)->grab_focus (widget);
+}
+
+static void
+ide_frame_destroy (GtkWidget *widget)
+{
+  IdeFrame *self = (IdeFrame *)widget;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_assert (IDE_IS_FRAME (self));
+
+  g_clear_object (&priv->addins);
+
+  g_clear_pointer (&priv->in_transition, g_ptr_array_unref);
+
+  if (priv->pages != NULL)
+    {
+      g_list_model_items_changed (G_LIST_MODEL (self), 0, priv->pages->len, 0);
+      g_clear_pointer (&priv->pages, g_ptr_array_unref);
+    }
+
+  if (priv->bindings != NULL)
+    {
+      dzl_binding_group_set_source (priv->bindings, NULL);
+      g_clear_object (&priv->bindings);
+    }
+
+  if (priv->signals != NULL)
+    {
+      dzl_signal_group_set_target (priv->signals, NULL);
+      g_clear_object (&priv->signals);
+    }
+
+  g_clear_object (&priv->pan);
+
+  GTK_WIDGET_CLASS (ide_frame_parent_class)->destroy (widget);
+}
+
+static void
+ide_frame_get_property (GObject    *object,
+                        guint       prop_id,
+                        GValue     *value,
+                        GParamSpec *pspec)
+{
+  IdeFrame *self = IDE_FRAME (object);
+
+  switch (prop_id)
+    {
+    case PROP_HAS_VIEW:
+      g_value_set_boolean (value, ide_frame_get_has_page (self));
+      break;
+
+    case PROP_VISIBLE_CHILD:
+      g_value_set_object (value, ide_frame_get_visible_child (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_frame_set_property (GObject      *object,
+                        guint         prop_id,
+                        const GValue *value,
+                        GParamSpec   *pspec)
+{
+  IdeFrame *self = IDE_FRAME (object);
+
+  switch (prop_id)
+    {
+    case PROP_VISIBLE_CHILD:
+      ide_frame_set_visible_child (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_frame_class_init (IdeFrameClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->constructed = ide_frame_constructed;
+  object_class->get_property = ide_frame_get_property;
+  object_class->set_property = ide_frame_set_property;
+
+  widget_class->destroy = ide_frame_destroy;
+  widget_class->grab_focus = ide_frame_grab_focus;
+
+  container_class->add = ide_frame_add;
+
+  klass->agree_to_close_async = ide_frame_real_agree_to_close_async;
+  klass->agree_to_close_finish = ide_frame_real_agree_to_close_finish;
+
+  properties [PROP_HAS_VIEW] =
+    g_param_spec_boolean ("has-page", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_VISIBLE_CHILD] =
+    g_param_spec_object ("visible-child",
+                         "Visible Child",
+                         "The current page to be displayed",
+                         IDE_TYPE_PAGE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHNAGE_CURRENT_PAGE] =
+    g_signal_new_class_handler ("change-current-page",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (ide_frame_change_current_page),
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__INT,
+                                G_TYPE_NONE, 1, G_TYPE_INT);
+
+  gtk_widget_class_set_css_name (widget_class, "ideframe");
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-frame.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, empty_state);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, failed_state);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, header);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, stack);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, top_stack);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeFrame, event_box);
+
+  g_type_ensure (IDE_TYPE_FRAME_HEADER);
+  g_type_ensure (IDE_TYPE_FRAME_WRAPPER);
+  g_type_ensure (IDE_TYPE_SHORTCUT_LABEL);
+}
+
+static void
+ide_frame_init (IdeFrame *self)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  _ide_frame_init_actions (self);
+  _ide_frame_init_shortcuts (self);
+
+  priv->pages = g_ptr_array_new ();
+  priv->in_transition = g_ptr_array_new_with_free_func (g_object_unref);
+
+  priv->signals = dzl_signal_group_new (IDE_TYPE_PAGE);
+
+  dzl_signal_group_connect_swapped (priv->signals,
+                                    "notify::failed",
+                                    G_CALLBACK (ide_frame_page_failed),
+                                    self);
+
+  priv->bindings = dzl_binding_group_new ();
+
+  g_signal_connect_object (priv->bindings,
+                           "notify::source",
+                           G_CALLBACK (ide_frame_bindings_notify_source),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  dzl_binding_group_bind (priv->bindings, "title",
+                          priv->header, "title",
+                          G_BINDING_SYNC_CREATE);
+
+  dzl_binding_group_bind (priv->bindings, "modified",
+                          priv->header, "modified",
+                          G_BINDING_SYNC_CREATE);
+
+  dzl_binding_group_bind (priv->bindings, "primary-color-bg",
+                          priv->header, "background-rgba",
+                          G_BINDING_SYNC_CREATE);
+
+  dzl_binding_group_bind (priv->bindings, "primary-color-fg",
+                          priv->header, "foreground-rgba",
+                          G_BINDING_SYNC_CREATE);
+
+  g_signal_connect_object (priv->stack,
+                           "notify::visible-child",
+                           G_CALLBACK (ide_frame_notify_visible_child),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->stack,
+                           "add",
+                           G_CALLBACK (ide_frame_page_added),
+                           self,
+                           G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+  g_signal_connect_object (priv->stack,
+                           "remove",
+                           G_CALLBACK (ide_frame_page_removed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  _ide_frame_header_set_pages (priv->header, G_LIST_MODEL (self));
+  _ide_frame_header_update (priv->header, NULL);
+}
+
+GtkWidget *
+ide_frame_new (void)
+{
+  return g_object_new (IDE_TYPE_FRAME, NULL);
+}
+
+/**
+ * ide_frame_set_visible_child:
+ * @self: a #IdeFrame
+ *
+ * Sets the current page for the stack.
+ *
+ * Since: 3.32
+ */
+void
+ide_frame_set_visible_child (IdeFrame *self,
+                             IdePage  *page)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_FRAME (self));
+  g_return_if_fail (IDE_IS_PAGE (page));
+  g_return_if_fail (gtk_widget_get_parent (GTK_WIDGET (page)) == (GtkWidget *)priv->stack);
+
+  gtk_stack_set_visible_child (priv->stack, GTK_WIDGET (page));
+}
+
+/**
+ * ide_frame_get_visible_child:
+ * @self: a #IdeFrame
+ *
+ * Gets the visible #IdePage if there is one; otherwise %NULL.
+ *
+ * Returns: (nullable) (transfer none): An #IdePage or %NULL
+ *
+ * Since: 3.32
+ */
+IdePage *
+ide_frame_get_visible_child (IdeFrame *self)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_FRAME (self), NULL);
+
+  return IDE_PAGE (gtk_stack_get_visible_child (priv->stack));
+}
+
+/**
+ * ide_frame_get_titlebar:
+ * @self: a #IdeFrame
+ *
+ * Gets the #IdeFrameHeader header that is at the top of the stack.
+ *
+ * Returns: (transfer none) (type IdeFrameHeader): The layout stack header.
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_frame_get_titlebar (IdeFrame *self)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_FRAME (self), NULL);
+
+  return GTK_WIDGET (priv->header);
+}
+
+/**
+ * ide_frame_get_has_page:
+ * @self: an #IdeFrame
+ *
+ * Gets the "has-page" property.
+ *
+ * This property is a convenience to allow widgets to easily bind
+ * properties based on whether or not a page is visible in the stack.
+ *
+ * Returns: %TRUE if the stack has a page
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_frame_get_has_page (IdeFrame *self)
+{
+  IdePage *visible_child;
+
+  g_return_val_if_fail (IDE_IS_FRAME (self), FALSE);
+
+  visible_child = ide_frame_get_visible_child (self);
+
+  return visible_child != NULL;
+}
+
+static void
+ide_frame_close_page_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  IdePage *page = (IdePage *)object;
+  g_autoptr(IdeFrame) self = user_data;
+  g_autoptr(GError) error = NULL;
+  GtkWidget *toplevel;
+  GtkWidget *focus;
+  gboolean had_focus = FALSE;
+
+  g_assert (IDE_IS_PAGE (page));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_FRAME (self));
+
+  if (!ide_page_agree_to_close_finish (page, result, &error))
+    {
+      g_message ("%s", error->message);
+      return;
+    }
+
+  /* Keep track of whether or not the widget had focus (which
+   * would happen if we were activated from a keybinding.
+   */
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (page));
+  if (GTK_IS_WINDOW (toplevel) &&
+      NULL != (focus = gtk_window_get_focus (GTK_WINDOW (toplevel))) &&
+      (focus == GTK_WIDGET (page) ||
+       gtk_widget_is_ancestor (focus, GTK_WIDGET (page))))
+    had_focus = TRUE;
+
+  /* Now we can destroy the child */
+  gtk_widget_destroy (GTK_WIDGET (page));
+
+  /* We don't want to leave the widget focus in an indeterminate
+   * state so we immediately focus the next child in the stack.
+   * But only do so if we had focus previously.
+   */
+  if (had_focus)
+    {
+      IdePage *visible_child = ide_frame_get_visible_child (self);
+
+      if (visible_child != NULL)
+        gtk_widget_grab_focus (GTK_WIDGET (visible_child));
+    }
+}
+
+void
+_ide_frame_request_close (IdeFrame *self,
+                          IdePage  *page)
+{
+  g_return_if_fail (IDE_IS_FRAME (self));
+  g_return_if_fail (IDE_IS_PAGE (page));
+
+  ide_page_agree_to_close_async (page,
+                                        NULL,
+                                        ide_frame_close_page_cb,
+                                        g_object_ref (self));
+}
+
+static GType
+ide_frame_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_PAGE;
+}
+
+static guint
+ide_frame_get_n_items (GListModel *model)
+{
+  IdeFrame *self = (IdeFrame *)model;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_assert (IDE_IS_FRAME (self));
+
+  return priv->pages ? priv->pages->len : 0;
+}
+
+static gpointer
+ide_frame_get_item (GListModel *model,
+                    guint       position)
+{
+  IdeFrame *self = (IdeFrame *)model;
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_assert (IDE_IS_FRAME (self));
+  g_assert (position < priv->pages->len);
+
+  return g_object_ref (g_ptr_array_index (priv->pages, position));
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_n_items = ide_frame_get_n_items;
+  iface->get_item = ide_frame_get_item;
+  iface->get_item_type = ide_frame_get_item_type;
+}
+
+void
+ide_frame_agree_to_close_async (IdeFrame            *self,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_FRAME (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_FRAME_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
+}
+
+gboolean
+ide_frame_agree_to_close_finish (IdeFrame      *self,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_FRAME (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_FRAME_GET_CLASS (self)->agree_to_close_finish (self, result, error);
+}
+
+static void
+animation_state_complete (gpointer data)
+{
+  IdeFramePrivate *priv;
+  AnimationState *state = data;
+
+  g_assert (state != NULL);
+  g_assert (IDE_IS_FRAME (state->source));
+  g_assert (IDE_IS_FRAME (state->dest));
+  g_assert (IDE_IS_PAGE (state->page));
+
+  /* Add the widget to the new stack */
+  if (state->dest != state->source)
+    {
+      gtk_container_add (GTK_CONTAINER (state->dest), GTK_WIDGET (state->page));
+
+      /* Now remove it from our temporary transition. Be careful in case we were
+       * destroyed in the mean time.
+       */
+      priv = ide_frame_get_instance_private (state->source);
+
+      if (priv->in_transition != NULL)
+        {
+          guint position = 0;
+
+          if (g_ptr_array_find_with_equal_func (priv->pages, state->page, NULL, &position))
+            {
+              g_ptr_array_remove (priv->in_transition, state->page);
+              g_ptr_array_remove_index (priv->pages, position);
+              g_list_model_items_changed (G_LIST_MODEL (state->source), position, 1, 0);
+            }
+        }
+    }
+
+  /*
+   * We might need to reshow the widget in cases where we are in a
+   * three-finger-swipe of the page. There is also a chance that we
+   * aren't the proper visible child and that needs to be restored now.
+   */
+  gtk_widget_show (GTK_WIDGET (state->page));
+  ide_frame_set_visible_child (state->dest, state->page);
+
+  g_clear_object (&state->source);
+  g_clear_object (&state->dest);
+  g_clear_object (&state->page);
+  g_clear_object (&state->theatric);
+  g_slice_free (AnimationState, state);
+}
+
+void
+_ide_frame_transfer (IdeFrame *self,
+                     IdeFrame *dest,
+                     IdePage  *page)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+  IdeFramePrivate *dest_priv = ide_frame_get_instance_private (dest);
+  const GdkRGBA *fg;
+  const GdkRGBA *bg;
+
+  g_return_if_fail (IDE_IS_FRAME (self));
+  g_return_if_fail (IDE_IS_FRAME (dest));
+  g_return_if_fail (IDE_IS_PAGE (page));
+  g_return_if_fail (GTK_WIDGET (priv->stack) == gtk_widget_get_parent (GTK_WIDGET (page)));
+
+  /*
+   * Inform the destination stack about our new primary colors so that it can
+   * begin a transition to the new colors. We also want to do this upfront so
+   * that we can reduce the amount of style invalidation caused during the
+   * transitions.
+   */
+
+  fg = ide_page_get_primary_color_fg (page);
+  bg = ide_page_get_primary_color_bg (page);
+  _ide_frame_header_set_foreground_rgba (dest_priv->header, fg);
+  _ide_frame_header_set_background_rgba (dest_priv->header, bg);
+
+  /*
+   * If both the old and the new stacks are mapped, we can animate
+   * between them using a snapshot of the page. Well, we also need
+   * to be sure they have a valid allocation, but that check is done
+   * slightly after this because it makes things easier.
+   */
+  if (gtk_widget_get_mapped (GTK_WIDGET (self)) &&
+      gtk_widget_get_mapped (GTK_WIDGET (dest)) &&
+      gtk_widget_get_mapped (GTK_WIDGET (page)))
+    {
+      GtkAllocation alloc, dest_alloc;
+      cairo_surface_t *surface = NULL;
+      GdkWindow *window;
+      GtkWidget *grid;
+      gboolean enable_animations;
+
+      grid = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GRID);
+
+      gtk_widget_get_allocation (GTK_WIDGET (page), &alloc);
+      gtk_widget_get_allocation (GTK_WIDGET (dest), &dest_alloc);
+
+      g_object_get (gtk_settings_get_default (),
+                    "gtk-enable-animations", &enable_animations,
+                    NULL);
+
+      if (enable_animations &&
+          grid != NULL &&
+          !is_uninitialized (&alloc) &&
+          !is_uninitialized (&dest_alloc) &&
+          dest_alloc.width > 0 && dest_alloc.height > 0 &&
+          NULL != (window = gtk_widget_get_window (GTK_WIDGET (page))) &&
+          NULL != (surface = gdk_window_create_similar_surface (window,
+                                                                CAIRO_CONTENT_COLOR,
+                                                                alloc.width,
+                                                                alloc.height)))
+        {
+          DzlBoxTheatric *theatric = NULL;
+          AnimationState *state;
+          cairo_t *cr;
+
+          cr = cairo_create (surface);
+          gtk_widget_draw (GTK_WIDGET (page), cr);
+          cairo_destroy (cr);
+
+          gtk_widget_translate_coordinates (GTK_WIDGET (priv->stack), grid, 0, 0,
+                                            &alloc.x, &alloc.y);
+          gtk_widget_translate_coordinates (GTK_WIDGET (dest_priv->stack), grid, 0, 0,
+                                            &dest_alloc.x, &dest_alloc.y);
+
+          theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
+                                   "surface", surface,
+                                   "height", alloc.height,
+                                   "target", grid,
+                                   "width", alloc.width,
+                                   "x", alloc.x,
+                                   "y", alloc.y,
+                                   NULL);
+
+          state = g_slice_new0 (AnimationState);
+          state->source = g_object_ref (self);
+          state->dest = g_object_ref (dest);
+          state->page = g_object_ref (page);
+          state->theatric = theatric;
+
+          dzl_object_animate_full (theatric,
+                                   DZL_ANIMATION_EASE_IN_OUT_CUBIC,
+                                   TRANSITION_DURATION,
+                                   gtk_widget_get_frame_clock (GTK_WIDGET (self)),
+                                   animation_state_complete,
+                                   state,
+                                   "x", dest_alloc.x,
+                                   "width", dest_alloc.width,
+                                   "y", dest_alloc.y,
+                                   "height", dest_alloc.height,
+                                   NULL);
+
+          /*
+           * Mark the page as in-transition so that when we remove it
+           * we can ignore the items-changed until the animation completes.
+           */
+          g_ptr_array_add (priv->in_transition, g_object_ref (page));
+          gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page));
+
+          cairo_surface_destroy (surface);
+
+          return;
+        }
+    }
+
+  g_object_ref (page);
+  gtk_container_remove (GTK_CONTAINER (priv->stack), GTK_WIDGET (page));
+  gtk_container_add (GTK_CONTAINER (dest_priv->stack), GTK_WIDGET (page));
+  g_object_unref (page);
+}
+
+/**
+ * ide_frame_foreach_page:
+ * @self: a #IdeFrame
+ * @callback: (scope call) (closure user_data): A callback for each page
+ * @user_data: user data for @callback
+ *
+ * This function will call @callback for every page found in @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_frame_foreach_page (IdeFrame    *self,
+                        GtkCallback  callback,
+                        gpointer     user_data)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_FRAME (self));
+  g_return_if_fail (callback != NULL);
+
+  gtk_container_foreach (GTK_CONTAINER (priv->stack), callback, user_data);
+}
+
+/**
+ * ide_frame_addin_find_by_module_name:
+ * @frame: An #IdeFrame
+ * @module_name: the module name which provides the addin
+ *
+ * This function will locate the #IdeFrameAddin that was registered by
+ * the plugin named @module_name (which should match the "Module" field
+ * provided in the .plugin file).
+ *
+ * If no module was found or that module does not implement the
+ * #IdeFrameAddinInterface, then %NULL is returned.
+ *
+ * Returns: (transfer none) (nullable): An #IdeFrameAddin or %NULL
+ *
+ * Since: 3.32
+ */
+IdeFrameAddin *
+ide_frame_addin_find_by_module_name (IdeFrame    *frame,
+                                     const gchar *module_name)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (frame);
+  PeasExtension *ret = NULL;
+  PeasPluginInfo *plugin_info;
+
+  g_return_val_if_fail (IDE_IS_FRAME (frame), NULL);
+  g_return_val_if_fail (priv->addins != NULL, NULL);
+  g_return_val_if_fail (module_name != NULL, NULL);
+
+  plugin_info = peas_engine_get_plugin_info (peas_engine_get_default (), module_name);
+
+  if (plugin_info != NULL)
+    ret = peas_extension_set_get_extension (priv->addins, plugin_info);
+  else
+    g_warning ("No addin could be found matching module \"%s\"", module_name);
+
+  return ret ? IDE_FRAME_ADDIN (ret) : NULL;
+}
+
+void
+ide_frame_add_with_depth (IdeFrame  *self,
+                          GtkWidget *widget,
+                          guint      position)
+{
+  IdeFramePrivate *priv = ide_frame_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_FRAME (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (priv->stack), widget,
+                                     "position", position,
+                                     NULL);
+}
diff --git a/src/libide/gui/ide-frame.h b/src/libide/gui/ide-frame.h
new file mode 100644
index 000000000..36e586d7c
--- /dev/null
+++ b/src/libide/gui/ide-frame.h
@@ -0,0 +1,84 @@
+/* ide-frame.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-page.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FRAME (ide_frame_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeFrame, ide_frame, IDE, FRAME, GtkBox)
+
+struct _IdeFrameClass
+{
+  GtkBoxClass parent_class;
+
+  void     (*agree_to_close_async)  (IdeFrame             *stack,
+                                     GCancellable         *cancellable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              user_data);
+  gboolean (*agree_to_close_finish) (IdeFrame             *stack,
+                                     GAsyncResult         *result,
+                                     GError              **error);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_frame_new                   (void);
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_frame_get_titlebar          (IdeFrame             *self);
+IDE_AVAILABLE_IN_3_32
+IdePage   *ide_frame_get_visible_child     (IdeFrame             *self);
+IDE_AVAILABLE_IN_3_32
+void       ide_frame_set_visible_child     (IdeFrame             *self,
+                                            IdePage              *page);
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_frame_get_has_page          (IdeFrame             *self);
+IDE_AVAILABLE_IN_3_32
+void       ide_frame_agree_to_close_async  (IdeFrame             *self,
+                                            GCancellable         *cancellable,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_frame_agree_to_close_finish (IdeFrame             *self,
+                                            GAsyncResult         *result,
+                                            GError              **error);
+IDE_AVAILABLE_IN_3_32
+void       ide_frame_foreach_page          (IdeFrame             *self,
+                                            GtkCallback           callback,
+                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void       ide_frame_add_with_depth        (IdeFrame             *self,
+                                            GtkWidget            *widget,
+                                            guint                 position);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-frame.ui b/src/libide/gui/ide-frame.ui
new file mode 100644
index 000000000..3dc728752
--- /dev/null
+++ b/src/libide/gui/ide-frame.ui
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeFrame" parent="GtkBox">
+    <property name="orientation">vertical</property>
+    <child>
+      <object class="IdeFrameHeader" id="header">
+        <property name="show-close-button">true</property>
+        <property name="title" translatable="yes">No Open Pages</property>
+        <property name="visible">true</property>
+      </object>
+    </child>
+    <child>
+      <object class="GtkEventBox" id="event_box">
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkStack" id="top_stack">
+            <property name="expand">true</property>
+            <property name="homogeneous">false</property>
+            <property name="interpolate-size">false</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="DzlBox" id="empty_state">
+                <property name="expand">false</property>
+                <property name="halign">center</property>
+                <property name="orientation">vertical</property>
+                <property name="valign">center</property>
+                <property name="max-width-request">275</property>
+                <property name="margin">32</property>
+                <property name="spacing">6</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">Open a File or Terminal</property>
+                    <property name="margin-bottom">6</property>
+                    <property name="visible">true</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                      <attribute name="scale" value="1.2"/>
+                    </attributes>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">Use the page switcher above or use one of the 
following:</property>
+                    <property name="justify">center</property>
+                    <property name="margin-bottom">18</property>
+                    <property name="visible">true</property>
+                    <property name="wrap">true</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                    <attributes>
+                      <attribute name="scale" value=".909"/>
+                    </attributes>
+                  </object>
+                </child>
+                <child>
+                  <object class="IdeShortcutLabel">
+                    <property name="title" translatable="yes">Search</property>
+                    <property name="action">win.global-search</property>
+                    <property name="visible">true</property>
+                    <!-- Remove after auto accel tracking -->
+                    <property name="accel">Ctrl+.</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="IdeShortcutLabel">
+                    <property name="title" translatable="yes">Project sidebar</property>
+                    <property name="action">editor.sidebar</property>
+                    <property name="visible">true</property>
+                    <!-- Remove after auto accel tracking -->
+                    <property name="accel">F9</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="IdeShortcutLabel">
+                    <property name="title" translatable="yes">File chooser</property>
+                    <property name="action">editor.open-file</property>
+                    <property name="visible">true</property>
+                    <!-- Remove after auto accel tracking -->
+                    <property name="accel">Ctrl+O</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="IdeShortcutLabel">
+                    <property name="title" translatable="yes">New terminal</property>
+                    <property name="action">win.new-terminal</property>
+                    <property name="visible">true</property>
+                    <!-- Remove after auto accel tracking -->
+                    <property name="accel">Ctrl+Shift+T</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="homogeneous">true</property>
+                    <property name="margin-top">18</property>
+                    <property name="spacing">6</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkButton">
+                        <property name="action-name">editor.open-file</property>
+                        <property name="label" translatable="yes">Open File…</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton">
+                        <property name="action-name">win.new-terminal</property>
+                        <property name="label" translatable="yes">New Terminal</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="DzlEmptyState" id="failed_state">
+                <property name="icon-name">computer-fail-symbolic</property>
+                <property name="pixel-size">160</property>
+                <property name="title" translatable="yes">Uh oh, something went wrong</property>
+                <property name="subtitle" translatable="yes">There was a failure while trying to perform the 
operation.</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="IdeFrameWrapper" id="stack">
+                <property name="expand">true</property>
+                <property name="homogeneous">false</property>
+                <property name="interpolate-size">false</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-grid-actions.c b/src/libide/gui/ide-grid-actions.c
new file mode 100644
index 000000000..472f9e4d8
--- /dev/null
+++ b/src/libide/gui/ide-grid-actions.c
@@ -0,0 +1,73 @@
+/* ide-grid-actions.c
+ *
+ * Copyright 2017-2019 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 "ide-grid"
+
+#include "config.h"
+
+#include "ide-grid.h"
+#include "ide-gui-private.h"
+
+static void
+ide_grid_actions_focus_neighbor (GSimpleAction *action,
+                                        GVariant      *variant,
+                                        gpointer       user_data)
+{
+  IdeGrid *self = user_data;
+  GtkDirectionType dir;
+
+  g_return_if_fail (G_IS_SIMPLE_ACTION (action));
+  g_return_if_fail (variant != NULL);
+  g_return_if_fail (g_variant_is_of_type (variant, G_VARIANT_TYPE_INT32));
+  g_return_if_fail (IDE_IS_GRID (self));
+
+  dir = (GtkDirectionType)g_variant_get_int32 (variant);
+
+  switch (dir)
+    {
+    case GTK_DIR_TAB_FORWARD:
+    case GTK_DIR_TAB_BACKWARD:
+    case GTK_DIR_UP:
+    case GTK_DIR_DOWN:
+    case GTK_DIR_LEFT:
+    case GTK_DIR_RIGHT:
+      ide_grid_focus_neighbor (self, dir);
+      break;
+
+    default:
+      g_return_if_reached ();
+    }
+}
+
+static const GActionEntry actions[] = {
+  { "focus-neighbor", ide_grid_actions_focus_neighbor, "i" },
+};
+
+void
+_ide_grid_init_actions (IdeGrid *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  g_return_if_fail (IDE_IS_GRID (self));
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group), actions, G_N_ELEMENTS (actions), self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "grid", G_ACTION_GROUP (group));
+}
diff --git a/src/libide/gui/ide-grid-column-actions.c b/src/libide/gui/ide-grid-column-actions.c
new file mode 100644
index 000000000..54786ae8b
--- /dev/null
+++ b/src/libide/gui/ide-grid-column-actions.c
@@ -0,0 +1,81 @@
+/* ide-grid-column-actions.c
+ *
+ * Copyright 2017-2019 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 "ide-grid-column-actions"
+
+#include "config.h"
+
+#include "ide-gui-private.h"
+
+static void
+ide_grid_column_actions_close (GSimpleAction *action,
+                               GVariant      *variant,
+                               gpointer       user_data)
+{
+  IdeGridColumn *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_GRID_COLUMN (self));
+
+  _ide_grid_column_try_close (self);
+}
+
+static const GActionEntry grid_column_actions[] = {
+  { "close", ide_grid_column_actions_close },
+};
+
+void
+_ide_grid_column_update_actions (IdeGridColumn *self)
+{
+  GtkWidget *grid;
+  gboolean can_close;
+
+  g_assert (IDE_IS_GRID_COLUMN (self));
+
+  grid = gtk_widget_get_parent (GTK_WIDGET (self));
+
+  if (grid == NULL || !IDE_IS_GRID (grid))
+    {
+      g_warning ("Attempt to update actions in unowned grid column");
+      return;
+    }
+
+  can_close = (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (grid)) > 1);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "gridcolumn", "close",
+                             "enabled", can_close,
+                             NULL);
+}
+
+void
+_ide_grid_column_init_actions (IdeGridColumn *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  g_assert (IDE_IS_GRID_COLUMN (self));
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                  grid_column_actions,
+                                  G_N_ELEMENTS (grid_column_actions),
+                                  self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self),
+                                  "gridcolumn",
+                                  G_ACTION_GROUP (group));
+}
diff --git a/src/libide/gui/ide-grid-column.c b/src/libide/gui/ide-grid-column.c
new file mode 100644
index 000000000..1fbee766a
--- /dev/null
+++ b/src/libide/gui/ide-grid-column.c
@@ -0,0 +1,394 @@
+/* ide-grid-column.c
+ *
+ * Copyright 2017-2019 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 "ide-grid-column"
+
+#include "config.h"
+
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#include "ide-grid-column.h"
+#include "ide-gui-private.h"
+#include "ide-page.h"
+
+struct _IdeGridColumn
+{
+  DzlMultiPaned parent_instance;
+  GQueue        focus_stack;
+};
+
+typedef struct
+{
+  GList *stacks;
+  IdeTask *backpointer;
+} TryCloseState;
+
+G_DEFINE_TYPE (IdeGridColumn, ide_grid_column, DZL_TYPE_MULTI_PANED)
+
+static void ide_grid_column_try_close_pump (IdeTask *task);
+
+enum {
+  PROP_0,
+  PROP_CURRENT_STACK,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+try_close_state_free (gpointer data)
+{
+  TryCloseState *state = data;
+
+  g_assert (state != NULL);
+
+  g_list_free_full (state->stacks, g_object_unref);
+  state->stacks = NULL;
+  state->backpointer = NULL;
+
+  g_slice_free (TryCloseState, state);
+}
+
+static void
+ide_grid_column_add (GtkContainer *container,
+                     GtkWidget    *widget)
+{
+  IdeGridColumn *self = (IdeGridColumn *)container;
+
+  g_assert (IDE_IS_GRID_COLUMN (self));
+
+  if (IDE_IS_PAGE (widget))
+    {
+      GtkWidget *child;
+
+      g_assert (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (container)) > 0);
+
+      child = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (container), 0);
+      gtk_container_add (GTK_CONTAINER (child), widget);
+    }
+  else if (IDE_IS_FRAME (widget))
+    {
+      GtkWidget *grid;
+
+      g_queue_push_head (&self->focus_stack, widget);
+      GTK_CONTAINER_CLASS (ide_grid_column_parent_class)->add (container, widget);
+
+      if (IDE_IS_GRID (grid = gtk_widget_get_parent (GTK_WIDGET (self))))
+        _ide_grid_stack_added (IDE_GRID (grid), IDE_FRAME (widget));
+    }
+  else
+    {
+      g_warning ("%s only supports adding IdePage or IdeFrame",
+                 G_OBJECT_TYPE_NAME (self));
+      return;
+    }
+}
+
+static void
+ide_grid_column_remove (GtkContainer *container,
+                        GtkWidget    *widget)
+{
+  IdeGridColumn *self = (IdeGridColumn *)container;
+  GtkWidget *grid;
+
+  g_assert (IDE_IS_GRID_COLUMN (self));
+  g_assert (IDE_IS_FRAME (widget));
+
+  if (IDE_IS_GRID (grid = gtk_widget_get_parent (GTK_WIDGET (self))))
+    _ide_grid_stack_removed (IDE_GRID (grid), IDE_FRAME (widget));
+
+  g_queue_remove (&self->focus_stack, widget);
+
+  GTK_CONTAINER_CLASS (ide_grid_column_parent_class)->remove (container, widget);
+}
+
+static void
+ide_grid_column_grab_focus (GtkWidget *widget)
+{
+  IdeGridColumn *self = (IdeGridColumn *)widget;
+  IdeFrame *stack;
+
+  g_assert (IDE_IS_GRID_COLUMN (self));
+
+  stack = ide_grid_column_get_current_stack (self);
+
+  if (stack != NULL)
+    gtk_widget_grab_focus (GTK_WIDGET (stack));
+  else
+    GTK_WIDGET_CLASS (ide_grid_column_parent_class)->grab_focus (widget);
+}
+
+static void
+ide_grid_column_finalize (GObject *object)
+{
+#ifndef G_DISABLE_ASSERT
+  IdeGridColumn *self = (IdeGridColumn *)object;
+
+  g_assert (self->focus_stack.head == NULL);
+  g_assert (self->focus_stack.tail == NULL);
+  g_assert (self->focus_stack.length == 0);
+#endif
+
+  G_OBJECT_CLASS (ide_grid_column_parent_class)->finalize (object);
+}
+
+static void
+ide_grid_column_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeGridColumn *self = IDE_GRID_COLUMN (object);
+
+  switch (prop_id)
+    {
+    case PROP_CURRENT_STACK:
+      g_value_set_object (value, ide_grid_column_get_current_stack (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_grid_column_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeGridColumn *self = IDE_GRID_COLUMN (object);
+
+  switch (prop_id)
+    {
+    case PROP_CURRENT_STACK:
+      ide_grid_column_set_current_stack (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_grid_column_class_init (IdeGridColumnClass *klass)
+{
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_grid_column_finalize;
+  object_class->get_property = ide_grid_column_get_property;
+  object_class->set_property = ide_grid_column_set_property;
+
+  widget_class->grab_focus = ide_grid_column_grab_focus;
+
+  container_class->add = ide_grid_column_add;
+  container_class->remove = ide_grid_column_remove;
+
+  properties [PROP_CURRENT_STACK] =
+    g_param_spec_object ("current-stack",
+                         "Current Stack",
+                         "The most recently focused stack within the column",
+                         IDE_TYPE_FRAME,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "idegridcolumn");
+}
+
+static void
+ide_grid_column_init (IdeGridColumn *self)
+{
+  _ide_grid_column_init_actions (self);
+}
+
+GtkWidget *
+ide_grid_column_new (void)
+{
+  return g_object_new (IDE_TYPE_GRID_COLUMN, NULL);
+}
+
+static void
+ide_grid_column_try_close_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  IdeFrame *stack = (IdeFrame *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_FRAME (stack));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_frame_agree_to_close_finish (stack, result, &error))
+    {
+      g_debug ("Cannot close stack now due to: %s", error->message);
+      gtk_widget_grab_focus (GTK_WIDGET (stack));
+      ide_task_return_boolean (task, FALSE);
+      return;
+    }
+
+  gtk_widget_destroy (GTK_WIDGET (stack));
+
+  ide_grid_column_try_close_pump (g_steal_pointer (&task));
+}
+
+static void
+ide_grid_column_try_close_pump (IdeTask *_task)
+{
+  g_autoptr(IdeTask) task = _task;
+  g_autoptr(IdeFrame) stack = NULL;
+  TryCloseState *state;
+  GCancellable *cancellable;
+
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+  g_assert (state != NULL);
+  g_assert (state->backpointer == task);
+
+  if (state->stacks == NULL)
+    {
+      IdeGridColumn *self = ide_task_get_source_object (task);
+
+      g_assert (IDE_IS_GRID_COLUMN (self));
+      gtk_widget_destroy (GTK_WIDGET (self));
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  stack = state->stacks->data;
+  state->stacks = g_list_remove (state->stacks, stack);
+  g_assert (IDE_IS_FRAME (stack));
+
+  cancellable = ide_task_get_cancellable (task);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_frame_agree_to_close_async (stack,
+                                         cancellable,
+                                         ide_grid_column_try_close_cb,
+                                         g_steal_pointer (&task));
+}
+
+void
+_ide_grid_column_try_close (IdeGridColumn *self)
+{
+  TryCloseState state = { 0 };
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_GRID_COLUMN (self));
+
+  state.stacks = gtk_container_get_children (GTK_CONTAINER (self));
+
+  if (state.stacks == NULL)
+    {
+      /* Implausible and should not happen because we should always
+       * have a stack inside the grid when the action is activated.
+       */
+      gtk_widget_destroy (GTK_WIDGET (self));
+      g_return_if_reached ();
+    }
+
+  task = ide_task_new (self, NULL, NULL, NULL);
+  ide_task_set_source_tag (task, _ide_grid_column_try_close);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  g_list_foreach (state.stacks, (GFunc)g_object_ref, NULL);
+  state.backpointer = task;
+  ide_task_set_task_data (task, g_slice_dup (TryCloseState, &state), try_close_state_free);
+
+  ide_grid_column_try_close_pump (g_steal_pointer (&task));
+}
+
+gboolean
+_ide_grid_column_is_empty (IdeGridColumn *self)
+{
+  g_return_val_if_fail (IDE_IS_GRID_COLUMN (self), FALSE);
+
+  /*
+   * Check if we only have a single stack and it is empty.
+   * That means we are in our "initial/empty" state.
+   */
+
+  if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)) == 1)
+    {
+      GtkWidget *child = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), 0);
+
+      g_assert (IDE_IS_FRAME (child));
+
+      return !ide_frame_get_has_page (IDE_FRAME (child));
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_grid_column_get_current_stack:
+ * @self: a #IdeGridColumn
+ *
+ * Gets the most recently focused stack. If no stack has been added, then
+ * %NULL is returned.
+ *
+ * Returns: (transfer none) (nullable): an #IdeFrame or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeFrame *
+ide_grid_column_get_current_stack (IdeGridColumn *self)
+{
+  g_return_val_if_fail (IDE_IS_GRID_COLUMN (self), NULL);
+
+  return self->focus_stack.head ? self->focus_stack.head->data : NULL;
+}
+
+void
+ide_grid_column_set_current_stack (IdeGridColumn *self,
+                                   IdeFrame      *stack)
+{
+  GList *iter;
+
+  g_return_if_fail (IDE_IS_GRID_COLUMN (self));
+  g_return_if_fail (!stack || IDE_IS_FRAME (stack));
+
+  /* If there is nothing to do, short-circuit. */
+  if (stack == NULL ||
+      (self->focus_stack.head != NULL &&
+       self->focus_stack.head->data == (gpointer)stack))
+    return;
+
+  /*
+   * If we are already in the stack, we can just move our element
+   * without having to setup signal handling.
+   */
+  if (NULL != (iter = g_queue_find (&self->focus_stack, stack)))
+    {
+      g_queue_unlink (&self->focus_stack, iter);
+      g_queue_push_head_link (&self->focus_stack, iter);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_STACK]);
+      return;
+    }
+
+  g_warning ("%s was not found within %s",
+             G_OBJECT_TYPE_NAME (stack), G_OBJECT_TYPE_NAME (self));
+}
diff --git a/src/libide/gui/ide-grid-column.h b/src/libide/gui/ide-grid-column.h
new file mode 100644
index 000000000..ab2810fe0
--- /dev/null
+++ b/src/libide/gui/ide-grid-column.h
@@ -0,0 +1,47 @@
+/* ide-grid-column.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+#include "ide-frame.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_GRID_COLUMN (ide_grid_column_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeGridColumn, ide_grid_column, IDE, GRID_COLUMN, DzlMultiPaned)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_grid_column_new               (void);
+IDE_AVAILABLE_IN_3_32
+IdeFrame  *ide_grid_column_get_current_stack (IdeGridColumn *self);
+IDE_AVAILABLE_IN_3_32
+void       ide_grid_column_set_current_stack (IdeGridColumn *self,
+                                              IdeFrame      *stack);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-grid.c b/src/libide/gui/ide-grid.c
new file mode 100644
index 000000000..47b8dc63a
--- /dev/null
+++ b/src/libide/gui/ide-grid.c
@@ -0,0 +1,1533 @@
+/* ide-grid.c
+ *
+ * Copyright 2017-2019 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 "ide-grid"
+
+#include "config.h"
+
+#include <string.h>
+
+#include "ide-grid.h"
+#include "ide-gui-private.h"
+
+/**
+ * SECTION:ide-grid
+ * @title: IdeGrid
+ * @short_description: A grid for #IdePage
+ *
+ * The #IdeGrid provides a grid of pages that the user may
+ * manipulate.
+ *
+ * Internally, this is implemented with #IdeGrid at the top
+ * containing one or more of #IdeGridColumn. Those columns
+ * contain one or more #IdeFrame. The stack can contain many
+ * #IdePage.
+ *
+ * #IdeGrid implements the #GListModel interface to simplify
+ * the process of listing (with deduplication) the pages that are
+ * contianed within the #IdeGrid. If you would instead like
+ * to see all possible pages in the stack, use the
+ * ide_grid_foreach_page() API.
+ *
+ * Since: 3.32
+ */
+
+typedef struct
+{
+  /* Owned references */
+  DzlSignalGroup *toplevel_signals;
+  GQueue          focus_column;
+  GArray         *stack_info;
+
+  /*
+   * This owned reference is our box highlight theatric that we
+   * animate while doing a DnD drop interaction.
+   */
+  DzlBoxTheatric *drag_theatric;
+  DzlAnimation   *drag_anim;
+
+  /*
+   * This unowned reference is simply used to compare to a new focus
+   * page to see if we have changed our current page. It is not to
+   * be used directly, only for pointer comparison.
+   */
+  IdePage  *_last_focused_page;
+
+  /*
+   * A GSource that is used to remove empty stacks that are unnecessary
+   * (after a last stack item is removed).
+   */
+  guint cull_source;
+} IdeGridPrivate;
+
+typedef struct
+{
+  IdeGridColumn *column;
+  IdeFrame      *stack;
+  GdkRectangle         area;
+  gint                 drop;
+  gint                 x;
+  gint                 y;
+} DropLocate;
+
+typedef struct
+{
+  IdeFrame *stack;
+  guint           len;
+} StackInfo;
+
+enum {
+  PROP_0,
+  PROP_CURRENT_COLUMN,
+  PROP_CURRENT_STACK,
+  PROP_CURRENT_PAGE,
+  N_PROPS
+};
+
+enum {
+  CREATE_STACK,
+  CREATE_VIEW,
+  N_SIGNALS
+};
+
+enum {
+  DROP_ONTO,
+  DROP_ABOVE,
+  DROP_BELOW,
+  DROP_LEFT_OF,
+  DROP_RIGHT_OF,
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeGrid, ide_grid, DZL_TYPE_MULTI_PANED,
+                         G_ADD_PRIVATE (IdeGrid)
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_grid_cull (IdeGrid *self)
+{
+  guint n_columns;
+
+  g_assert (IDE_IS_GRID (self));
+
+  n_columns = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
+
+  for (guint i = n_columns; i > 0; i--)
+    {
+      IdeGridColumn *column;
+      guint n_stacks;
+
+      column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i - 1));
+      n_stacks = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column));
+
+      if (n_columns == 1 && n_stacks == 1)
+        return;
+
+      for (guint j = n_stacks; j > 0; j--)
+        {
+          IdeFrame *stack;
+          guint n_items;
+
+          stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), j - 1));
+          n_items = g_list_model_get_n_items (G_LIST_MODEL (stack));
+
+          if (n_items == 0)
+            gtk_widget_destroy (GTK_WIDGET (stack));
+        }
+
+      if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)) == 0)
+        gtk_widget_destroy (GTK_WIDGET (column));
+    }
+}
+
+static gboolean
+ide_grid_do_cull (gpointer data)
+{
+  IdeGrid *self = data;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+
+  g_assert (IDE_IS_GRID (self));
+
+  priv->cull_source = 0;
+
+  ide_grid_cull (self);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_grid_queue_cull (IdeGrid *self)
+{
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+
+  g_assert (IDE_IS_GRID (self));
+
+  if (priv->cull_source != 0)
+    return;
+
+  priv->cull_source = gdk_threads_add_idle_full (G_PRIORITY_HIGH,
+                                                 ide_grid_do_cull,
+                                                 g_object_ref (self),
+                                                 g_object_unref);
+}
+
+static void
+ide_grid_update_actions (IdeGrid *self)
+{
+  guint n_children;
+
+  g_assert (IDE_IS_GRID (self));
+
+  n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
+
+  for (guint i = 0; i < n_children; i++)
+    {
+      GtkWidget *column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i);
+
+      g_assert (IDE_IS_GRID_COLUMN (column));
+
+      _ide_grid_column_update_actions (IDE_GRID_COLUMN (column));
+    }
+}
+
+static IdeFrame *
+ide_grid_real_create_frame (IdeGrid *self)
+{
+  return g_object_new (IDE_TYPE_FRAME,
+                       "expand", TRUE,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static GtkWidget *
+ide_grid_create_frame (IdeGrid *self)
+{
+  IdeFrame *ret = NULL;
+
+  g_assert (IDE_IS_GRID (self));
+
+  g_signal_emit (self, signals [CREATE_STACK], 0, &ret);
+  g_return_val_if_fail (IDE_IS_FRAME (ret), NULL);
+  return GTK_WIDGET (ret);
+}
+
+static GtkWidget *
+ide_grid_create_column (IdeGrid *self)
+{
+  GtkWidget *stack;
+
+  g_assert (IDE_IS_GRID (self));
+
+  stack = ide_grid_create_frame (self);
+
+  if (stack != NULL)
+    {
+      GtkWidget *column = g_object_new (IDE_TYPE_GRID_COLUMN,
+                                        "visible", TRUE,
+                                        NULL);
+      gtk_container_add (GTK_CONTAINER (column), stack);
+      return column;
+    }
+
+  return NULL;
+}
+
+static void
+ide_grid_after_set_focus (IdeGrid *self,
+                                 GtkWidget     *widget,
+                                 GtkWidget     *toplevel)
+{
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (!widget || GTK_IS_WIDGET (widget));
+  g_assert (GTK_IS_WINDOW (toplevel));
+
+  if (widget != NULL)
+    {
+      GtkWidget *column = NULL;
+      GtkWidget *page;
+
+      if (gtk_widget_is_ancestor (widget, GTK_WIDGET (self)))
+        {
+          column = gtk_widget_get_ancestor (widget, IDE_TYPE_GRID_COLUMN);
+
+          if (column != NULL)
+            ide_grid_set_current_column (self, IDE_GRID_COLUMN (column));
+        }
+
+      /*
+       * self->_last_focused_page is an unowned reference, we only
+       * use it for pointer comparison, nothing more.
+       */
+      page = gtk_widget_get_ancestor (widget, IDE_TYPE_PAGE);
+      if (page != (GtkWidget *)priv->_last_focused_page)
+        {
+          priv->_last_focused_page = (IdePage *)page;
+          ide_object_notify_in_main (self, properties [PROP_CURRENT_PAGE]);
+
+          if (page != NULL && column != NULL)
+            {
+              GtkWidget *stack;
+
+              stack = gtk_widget_get_ancestor (GTK_WIDGET (page), IDE_TYPE_FRAME);
+              if (stack != NULL)
+                ide_grid_column_set_current_stack (IDE_GRID_COLUMN (column),
+                                                          IDE_FRAME (stack));
+            }
+        }
+    }
+}
+
+static void
+ide_grid_hierarchy_changed (GtkWidget *widget,
+                                   GtkWidget *old_toplevel)
+{
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));
+
+  /*
+   * Setup focus tracking so that we can update our "current stack" when the
+   * user selected focus changes.
+   */
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (GTK_IS_WINDOW (toplevel))
+    dzl_signal_group_set_target (priv->toplevel_signals, toplevel);
+  else
+    dzl_signal_group_set_target (priv->toplevel_signals, NULL);
+
+  /*
+   * If we've been added to a widget and still do not have a stack added, then
+   * we'll emit our ::create-stack signal to create that now. We do this here
+   * to allow the consumer to connect to ::create-stack before adding the
+   * widget to the hierarchy.
+   */
+
+  if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (widget)) == 0)
+    {
+      GtkWidget *column = ide_grid_create_column (self);
+      gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (column));
+    }
+}
+
+static void
+ide_grid_add (GtkContainer *container,
+                     GtkWidget    *widget)
+{
+  IdeGrid *self = (IdeGrid *)container;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (IDE_IS_GRID_COLUMN (widget))
+    {
+      GList *children;
+
+      /* Add our column to the grid */
+      g_queue_push_head (&priv->focus_column, widget);
+      GTK_CONTAINER_CLASS (ide_grid_parent_class)->add (container, widget);
+      ide_grid_set_current_column (self, IDE_GRID_COLUMN (widget));
+      _ide_grid_column_update_actions (IDE_GRID_COLUMN (widget));
+
+      /* Start monitoring all the stacks in the grid for pages */
+      children = gtk_container_get_children (GTK_CONTAINER (widget));
+      for (const GList *iter = children; iter; iter = iter->next)
+        if (IDE_IS_FRAME (iter->data))
+          _ide_grid_stack_added (self, iter->data);
+      g_list_free (children);
+    }
+  else if (IDE_IS_FRAME (widget))
+    {
+      IdeGridColumn *column;
+
+      column = ide_grid_get_current_column (self);
+      gtk_container_add (GTK_CONTAINER (column), widget);
+      ide_grid_set_current_column (self, column);
+    }
+  else if (IDE_IS_PAGE (widget))
+    {
+      IdeGridColumn *column = NULL;
+      guint n_columns;
+
+      /* If we have an empty layout stack, we'll prefer to add the
+       * page to that. If we don't find an empty stack, we'll add
+       * the page to the most recently focused stack.
+       */
+
+      n_columns = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
+
+      for (guint i = 0; i < n_columns; i++)
+        {
+          GtkWidget *ele = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i);
+
+          g_assert (IDE_IS_GRID_COLUMN (ele));
+
+          if (_ide_grid_column_is_empty (IDE_GRID_COLUMN (ele)))
+            {
+              column = IDE_GRID_COLUMN (ele);
+              break;
+            }
+        }
+
+      if (column == NULL)
+        column = ide_grid_get_current_column (self);
+
+      g_assert (IDE_IS_GRID_COLUMN (column));
+
+      gtk_container_add (GTK_CONTAINER (column), widget);
+    }
+  else
+    {
+      g_warning ("%s must be one of IdeFrame, IdePage, or IdeGrid",
+                 G_OBJECT_TYPE_NAME (self));
+      return;
+    }
+
+  ide_grid_update_actions (self);
+}
+
+static void
+ide_grid_remove (GtkContainer *container,
+                        GtkWidget    *widget)
+{
+  IdeGrid *self = (IdeGrid *)container;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  gboolean notify = FALSE;
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (IDE_IS_GRID_COLUMN (widget));
+
+  notify = g_queue_peek_head (&priv->focus_column) == (gpointer)widget;
+  g_queue_remove (&priv->focus_column, widget);
+
+  GTK_CONTAINER_CLASS (ide_grid_parent_class)->remove (container, widget);
+
+  ide_grid_update_actions (self);
+
+  if (notify)
+    {
+      GtkWidget *head = g_queue_peek_head (&priv->focus_column);
+
+      if (head != NULL)
+        gtk_widget_grab_focus (head);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_COLUMN]);
+    }
+}
+
+static gboolean
+ide_grid_get_drop_area (IdeGrid        *self,
+                               gint                  x,
+                               gint                  y,
+                               GdkRectangle         *out_area,
+                               IdeGridColumn **out_column,
+                               IdeFrame      **out_stack,
+                               gint                 *out_drop)
+{
+  GtkAllocation alloc;
+  GtkWidget *column;
+  GtkWidget *stack = NULL;
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (out_area != NULL);
+  g_assert (out_column != NULL);
+  g_assert (out_stack != NULL);
+  g_assert (out_drop != NULL);
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  column = dzl_multi_paned_get_at_point (DZL_MULTI_PANED (self), x + alloc.x, 0);
+  if (column != NULL)
+    stack = dzl_multi_paned_get_at_point (DZL_MULTI_PANED (column), 0, y + alloc.y);
+
+  if (column != NULL && stack != NULL)
+    {
+      GtkAllocation stack_alloc;
+
+      gtk_widget_get_allocation (stack, &stack_alloc);
+
+      gtk_widget_translate_coordinates (stack,
+                                        GTK_WIDGET (self),
+                                        0, 0,
+                                        &stack_alloc.x, &stack_alloc.y);
+
+      *out_area = stack_alloc;
+      *out_column = IDE_GRID_COLUMN (column);
+      *out_stack = IDE_FRAME (stack);
+      *out_drop = DROP_ONTO;
+
+      gtk_widget_translate_coordinates (GTK_WIDGET (self), stack, x, y, &x, &y);
+
+      if (FALSE) {}
+      else if (x < (stack_alloc.width / 4))
+        {
+          out_area->y = 0;
+          out_area->height = alloc.height;
+          out_area->width = stack_alloc.width / 4;
+          *out_drop = DROP_LEFT_OF;
+        }
+      else if (x > (stack_alloc.width / 4 * 3))
+        {
+          out_area->y = 0;
+          out_area->height = alloc.height;
+          out_area->x = dzl_cairo_rectangle_x2 (&stack_alloc) - (stack_alloc.width / 4);
+          out_area->width = stack_alloc.width / 4;
+          *out_drop = DROP_RIGHT_OF;
+        }
+      else if (y < (stack_alloc.height / 4))
+        {
+          out_area->height = stack_alloc.height / 4;
+          *out_drop = DROP_ABOVE;
+        }
+      else if (y > (stack_alloc.height / 4 * 3))
+        {
+          out_area->y = dzl_cairo_rectangle_y2 (&stack_alloc) - (stack_alloc.height / 4);
+          out_area->height = stack_alloc.height / 4;
+          *out_drop = DROP_BELOW;
+        }
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gboolean
+ide_grid_drag_motion (GtkWidget      *widget,
+                             GdkDragContext *context,
+                             gint            x,
+                             gint            y,
+                             guint           time_)
+{
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  IdeGridColumn *column = NULL;
+  IdeFrame *stack = NULL;
+  DzlAnimation *drag_anim;
+  GdkRectangle area = {0};
+  GtkAllocation alloc;
+  gint drop = DROP_ONTO;
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (GDK_IS_DRAG_CONTEXT (context));
+
+  if (priv->drag_anim != NULL)
+    {
+      dzl_animation_stop (priv->drag_anim);
+      g_clear_weak_pointer (&priv->drag_anim);
+    }
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  if (!ide_grid_get_drop_area (self, x, y, &area, &column, &stack, &drop))
+    return GDK_EVENT_PROPAGATE;
+
+  if (priv->drag_theatric == NULL)
+    {
+      priv->drag_theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
+                                          "x", area.x,
+                                          "y", area.y,
+                                          "width", area.width,
+                                          "height", area.height,
+                                          "alpha", 0.3,
+                                          "background", "#729fcf",
+                                          "target", self,
+                                          NULL);
+      return GDK_EVENT_STOP;
+    }
+
+  drag_anim = dzl_object_animate (priv->drag_theatric,
+                                  DZL_ANIMATION_EASE_OUT_CUBIC,
+                                  100,
+                                  gtk_widget_get_frame_clock (GTK_WIDGET (self)),
+                                  "x", area.x,
+                                  "width", area.width,
+                                  "y", area.y,
+                                  "height", area.height,
+                                  NULL);
+  g_set_weak_pointer (&priv->drag_anim, drag_anim);
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+
+  return GDK_EVENT_STOP;
+}
+
+static void
+ide_grid_drag_data_received (GtkWidget        *widget,
+                                    GdkDragContext   *context,
+                                    gint              x,
+                                    gint              y,
+                                    GtkSelectionData *data,
+                                    guint             info,
+                                    guint             time_)
+{
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridColumn *column = NULL;
+  IdeFrame *stack = NULL;
+  g_auto(GStrv) uris = NULL;
+  GdkRectangle area = {0};
+  gint drop = DROP_ONTO;
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (GDK_IS_DRAG_CONTEXT (context));
+
+  if (!ide_grid_get_drop_area (self, x, y, &area, &column, &stack, &drop))
+    return;
+
+  g_assert (IDE_IS_GRID_COLUMN (column));
+  g_assert (IDE_IS_FRAME (stack));
+
+  if (!(uris = gtk_selection_data_get_uris (data)))
+    return;
+
+  for (guint i = 0; uris[i] != NULL; i++)
+    {
+      const gchar *uri = uris[i];
+      IdePage *page = NULL;
+      gint column_index = 0;
+      gint stack_index = 0;
+
+      g_signal_emit (self, signals [CREATE_VIEW], 0, uri, &page);
+
+      if (page == NULL)
+        {
+          g_debug ("Failed to load IdePage for \"%s\"", uri);
+          continue;
+        }
+
+      gtk_container_child_get (GTK_CONTAINER (self), GTK_WIDGET (column),
+                               "index", &column_index,
+                               NULL);
+      gtk_container_child_get (GTK_CONTAINER (column), GTK_WIDGET (stack),
+                               "index", &stack_index,
+                               NULL);
+
+      switch (drop)
+        {
+        case DROP_ONTO:
+          gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page));
+          break;
+
+        case DROP_ABOVE:
+          stack = IDE_FRAME (ide_grid_create_frame (self));
+          gtk_container_add_with_properties (GTK_CONTAINER (column), GTK_WIDGET (stack),
+                                             "index", stack_index,
+                                             NULL);
+          gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page));
+          break;
+
+        case DROP_BELOW:
+          stack = IDE_FRAME (ide_grid_create_frame (self));
+          gtk_container_add_with_properties (GTK_CONTAINER (column), GTK_WIDGET (stack),
+                                             "index", stack_index + 1,
+                                             NULL);
+          gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (page));
+          break;
+
+        case DROP_LEFT_OF:
+          column = IDE_GRID_COLUMN (ide_grid_create_column (self));
+          gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (column),
+                                             "index", column_index,
+                                             NULL);
+          gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (page));
+          break;
+
+        case DROP_RIGHT_OF:
+          column = IDE_GRID_COLUMN (ide_grid_create_column (self));
+          gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (column),
+                                             "index", column_index + 1,
+                                             NULL);
+          gtk_container_add (GTK_CONTAINER (column), GTK_WIDGET (page));
+          break;
+
+        default:
+          g_assert_not_reached ();
+        }
+    }
+}
+
+static void
+ide_grid_drag_leave (GtkWidget      *widget,
+                            GdkDragContext *context,
+                            guint           time_)
+{
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (GDK_IS_DRAG_CONTEXT (context));
+
+  if (priv->drag_anim != NULL)
+    {
+      dzl_animation_stop (priv->drag_anim);
+      g_clear_weak_pointer (&priv->drag_anim);
+    }
+
+  g_clear_object (&priv->drag_theatric);
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static gboolean
+ide_grid_drag_failed (GtkWidget      *widget,
+                             GdkDragContext *context,
+                             GtkDragResult   result)
+{
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (GDK_IS_DRAG_CONTEXT (context));
+
+  if (priv->drag_anim != NULL)
+    {
+      dzl_animation_stop (priv->drag_anim);
+      g_clear_weak_pointer (&priv->drag_anim);
+    }
+
+  g_clear_object (&priv->drag_theatric);
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_grid_grab_focus (GtkWidget *widget)
+{
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeFrame *stack;
+
+  g_assert (IDE_IS_GRID (self));
+
+  stack = ide_grid_get_current_stack (self);
+
+  if (stack != NULL)
+    gtk_widget_grab_focus (GTK_WIDGET (stack));
+  else
+    GTK_WIDGET_CLASS (ide_grid_parent_class)->grab_focus (widget);
+}
+
+static void
+ide_grid_destroy (GtkWidget *widget)
+{
+  IdeGrid *self = (IdeGrid *)widget;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+
+  dzl_clear_source (&priv->cull_source);
+
+  GTK_WIDGET_CLASS (ide_grid_parent_class)->destroy (widget);
+}
+
+static void
+ide_grid_finalize (GObject *object)
+{
+  IdeGrid *self = (IdeGrid *)object;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (priv->focus_column.head == NULL);
+  g_assert (priv->focus_column.tail == NULL);
+  g_assert (priv->focus_column.length == 0);
+
+  g_clear_pointer (&priv->stack_info, g_array_unref);
+  g_clear_object (&priv->toplevel_signals);
+
+  G_OBJECT_CLASS (ide_grid_parent_class)->finalize (object);
+}
+
+static void
+ide_grid_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
+{
+  IdeGrid *self = IDE_GRID (object);
+
+  switch (prop_id)
+    {
+    case PROP_CURRENT_COLUMN:
+      g_value_set_object (value, ide_grid_get_current_column (self));
+      break;
+
+    case PROP_CURRENT_STACK:
+      g_value_set_object (value, ide_grid_get_current_stack (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_grid_set_property (GObject      *object,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
+{
+  IdeGrid *self = IDE_GRID (object);
+
+  switch (prop_id)
+    {
+    case PROP_CURRENT_COLUMN:
+      ide_grid_set_current_column (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_grid_class_init (IdeGridClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->finalize = ide_grid_finalize;
+  object_class->get_property = ide_grid_get_property;
+  object_class->set_property = ide_grid_set_property;
+
+  widget_class->destroy = ide_grid_destroy;
+  widget_class->drag_data_received = ide_grid_drag_data_received;
+  widget_class->drag_motion = ide_grid_drag_motion;
+  widget_class->drag_leave = ide_grid_drag_leave;
+  widget_class->drag_failed = ide_grid_drag_failed;
+  widget_class->grab_focus = ide_grid_grab_focus;
+  widget_class->hierarchy_changed = ide_grid_hierarchy_changed;
+
+  container_class->add = ide_grid_add;
+  container_class->remove = ide_grid_remove;
+
+  klass->create_frame = ide_grid_real_create_frame;
+
+  properties [PROP_CURRENT_COLUMN] =
+    g_param_spec_object ("current-column",
+                         "Current Column",
+                         "The most recently focused grid column",
+                         IDE_TYPE_GRID_COLUMN,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CURRENT_STACK] =
+    g_param_spec_object ("current-stack",
+                         "Current Stack",
+                         "The most recently focused IdeFrame",
+                         IDE_TYPE_FRAME,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CURRENT_PAGE] =
+    g_param_spec_object ("current-page",
+                         "Current View",
+                         "The most recently focused IdePage",
+                         IDE_TYPE_PAGE,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "idegrid");
+
+  /**
+   * IdeGrid::create-stack:
+   * @self: an #IdeGrid
+   *
+   * Creates a new stack to be added to the grid.
+   *
+   * Returns: (transfer full): A newly created #IdeFrame
+   *
+   * Since: 3.32
+   */
+  signals [CREATE_STACK] =
+    g_signal_new (g_intern_static_string ("create-stack"),
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeGridClass, create_frame),
+                  g_signal_accumulator_first_wins, NULL, NULL,
+                  IDE_TYPE_FRAME, 0);
+
+  /**
+   * IdeGrid::create-page:
+   * @self: an #IdeGrid
+   * @uri: the URI to open
+   *
+   * Creates a new page for @uri to be added to the grid.
+   *
+   * Returns: (transfer full): A newly created #IdePage
+   *
+   * Since: 3.32
+   */
+  signals [CREATE_VIEW] =
+    g_signal_new (g_intern_static_string ("create-page"),
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeGridClass, create_page),
+                  g_signal_accumulator_first_wins, NULL, NULL,
+                  IDE_TYPE_PAGE,
+                  1,
+                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+static void
+ide_grid_init (IdeGrid *self)
+{
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  static const GtkTargetEntry target_entries[] = {
+    { (gchar *)"text/uri-list", 0, 0 },
+  };
+
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self),
+                                  GTK_ORIENTATION_HORIZONTAL);
+
+  gtk_drag_dest_set (GTK_WIDGET (self),
+                     GTK_DEST_DEFAULT_MOTION | GTK_DEST_DEFAULT_DROP,
+                     target_entries,
+                     G_N_ELEMENTS (target_entries),
+                     GDK_ACTION_COPY);
+
+  priv->stack_info = g_array_new (FALSE, FALSE, sizeof (StackInfo));
+
+  priv->toplevel_signals = dzl_signal_group_new (GTK_TYPE_WINDOW);
+
+  dzl_signal_group_connect_object (priv->toplevel_signals,
+                                   "set-focus",
+                                   G_CALLBACK (ide_grid_after_set_focus),
+                                   self,
+                                   G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+  _ide_grid_init_actions (self);
+}
+
+/**
+ * ide_grid_new:
+ *
+ * Creates a new #IdeGrid.
+ *
+ * Returns: (transfer full): A newly created #IdeGrid
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_grid_new (void)
+{
+  return g_object_new (IDE_TYPE_GRID, NULL);
+}
+
+/**
+ * ide_grid_get_current_stack:
+ * @self: a #IdeGrid
+ *
+ * Gets the most recently focused stack. This is useful when you want to open
+ * a document on the stack the user last focused.
+ *
+ * Returns: (transfer none) (nullable): an #IdeFrame or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeFrame *
+ide_grid_get_current_stack (IdeGrid *self)
+{
+  IdeGridColumn *column;
+
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
+
+  column = ide_grid_get_current_column (self);
+  if (column != NULL)
+    return ide_grid_column_get_current_stack (column);
+
+  return NULL;
+}
+
+/**
+ * ide_grid_get_nth_column:
+ * @self: a #IdeGrid
+ * @nth: the index of the column, or -1
+ *
+ * Gets the @nth column from the grid.
+ *
+ * If @nth is -1, then a new column at the beginning of the
+ * grid is created.
+ *
+ * If @nth is >= the number of columns in the grid, then a new
+ * column at the end of the grid is created.
+ *
+ * Returns: (transfer none): An #IdeGridColumn.
+ *
+ * Since: 3.32
+ */
+IdeGridColumn *
+ide_grid_get_nth_column (IdeGrid *self,
+                                gint           nth)
+{
+  GtkWidget *column;
+
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
+
+  if (nth < 0)
+    {
+      column = ide_grid_create_column (self);
+      gtk_container_add_with_properties (GTK_CONTAINER (self), column,
+                                         "index", 0,
+                                         NULL);
+    }
+  else if (nth >= dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)))
+    {
+      column = ide_grid_create_column (self);
+      gtk_container_add (GTK_CONTAINER (self), column);
+    }
+  else
+    {
+      column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), nth);
+    }
+
+  g_return_val_if_fail (IDE_IS_GRID_COLUMN (column), NULL);
+
+  return IDE_GRID_COLUMN (column);
+}
+
+/*
+ * _ide_grid_get_nth_stack:
+ *
+ * This will get the @nth stack. If it does not yet exist,
+ * it will be created.
+ *
+ * If nth == -1, a new stack will be created at index 0.
+ *
+ * If nth >= the number of stacks, a new stack will be created
+ * at the end of the grid.
+ *
+ * Returns: (not nullable) (transfer none): An #IdeFrame.
+ */
+IdeFrame *
+_ide_grid_get_nth_stack (IdeGrid *self,
+                                gint           nth)
+{
+  IdeGridColumn *column;
+  IdeFrame *stack;
+
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
+
+  column = ide_grid_get_nth_column (self, nth);
+  stack = ide_grid_column_get_current_stack (IDE_GRID_COLUMN (column));
+
+  g_return_val_if_fail (IDE_IS_FRAME (stack), NULL);
+
+  return stack;
+}
+
+/**
+ * _ide_grid_get_nth_stack_for_column:
+ * @self: an #IdeGrid
+ * @column: an #IdeGridColumn
+ * @nth: the index of the column, between -1 and G_MAXINT
+ *
+ * This will get the @nth stack within @column. If a matching stack
+ * cannot be found, it will be created.
+ *
+ * If @nth is less-than 0, a new column will be inserted at the top.
+ *
+ * If @nth is greater-than the number of stacks, then a new stack
+ * will be created at the bottom.
+ *
+ * Returns: (not nullable) (transfer none): An #IdeFrame.
+ *
+ * Since: 3.32
+ */
+IdeFrame *
+_ide_grid_get_nth_stack_for_column (IdeGrid       *self,
+                                           IdeGridColumn *column,
+                                           gint                 nth)
+{
+  GtkWidget *stack;
+
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
+  g_return_val_if_fail (IDE_IS_GRID_COLUMN (column), NULL);
+  g_return_val_if_fail (gtk_widget_get_parent (GTK_WIDGET (column)) == GTK_WIDGET (self), NULL);
+
+  if (nth < 0)
+    {
+      stack = ide_grid_create_frame (self);
+      gtk_container_add_with_properties (GTK_CONTAINER (column), stack,
+                                         "index", 0,
+                                         NULL);
+    }
+  else if (nth >= dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column)))
+    {
+      stack = ide_grid_create_frame (self);
+      gtk_container_add (GTK_CONTAINER (self), stack);
+    }
+  else
+    {
+      stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), nth);
+    }
+
+  g_assert (IDE_IS_FRAME (stack));
+
+  return IDE_FRAME (stack);
+}
+
+/**
+ * ide_grid_get_current_column:
+ * @self: a #IdeGrid
+ *
+ * Gets the most recently focused column of the grid.
+ *
+ * Returns: (transfer none) (not nullable): An #IdeGridColumn
+ *
+ * Since: 3.32
+ */
+IdeGridColumn *
+ide_grid_get_current_column (IdeGrid *self)
+{
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  GtkWidget *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
+
+  if (priv->focus_column.head != NULL)
+    ret = priv->focus_column.head->data;
+  else if (dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self)) > 0)
+    ret = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), 0);
+
+  if (ret == NULL)
+    {
+      ret = ide_grid_create_column (self);
+      gtk_container_add (GTK_CONTAINER (self), ret);
+    }
+
+  g_return_val_if_fail (IDE_IS_GRID_COLUMN (ret), NULL);
+
+  return IDE_GRID_COLUMN (ret);
+}
+
+/**
+ * ide_grid_set_current_column:
+ * @self: an #IdeGrid
+ * @column: (nullable): an #IdeGridColumn or %NULL
+ *
+ * Sets the current column for the grid. Generally this is automatically
+ * updated for you when the focus changes within the workbench.
+ *
+ * @column can be %NULL out of convenience.
+ *
+ * Since: 3.32
+ */
+void
+ide_grid_set_current_column (IdeGrid       *self,
+                                    IdeGridColumn *column)
+{
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  GList *iter;
+
+  g_return_if_fail (IDE_IS_GRID (self));
+  g_return_if_fail (!column || IDE_IS_GRID_COLUMN (column));
+
+  if (column == NULL)
+    return;
+
+  if (gtk_widget_get_parent (GTK_WIDGET (column)) != GTK_WIDGET (self))
+    {
+      g_warning ("Attempt to set current column with non-descendant");
+      return;
+    }
+
+  if (NULL != (iter = g_queue_find (&priv->focus_column, column)))
+    {
+      g_queue_unlink (&priv->focus_column, iter);
+      g_queue_push_head_link (&priv->focus_column, iter);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CURRENT_COLUMN]);
+      ide_grid_update_actions (self);
+      return;
+    }
+
+  g_warning ("%s does not contain %s",
+             G_OBJECT_TYPE_NAME (self), G_OBJECT_TYPE_NAME (column));
+}
+
+/**
+ * ide_grid_get_current_page:
+ * @self: a #IdeGrid
+ *
+ * Gets the most recent page used by the user as determined by tracking
+ * the window focus.
+ *
+ * Returns: (transfer none): An #IdePage or %NULL
+ *
+ * Since: 3.32
+ */
+IdePage *
+ide_grid_get_current_page (IdeGrid *self)
+{
+  IdeFrame *stack;
+
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
+
+  stack = ide_grid_get_current_stack (self);
+
+  if (stack != NULL)
+    return ide_frame_get_visible_child (stack);
+
+  return NULL;
+}
+
+static void
+collect_pages (GtkWidget *widget,
+               GPtrArray *ar)
+{
+  if (IDE_IS_PAGE (widget))
+    g_ptr_array_add (ar, widget);
+}
+
+/**
+ * ide_grid_foreach_page:
+ * @self: a #IdeGrid
+ * @callback: (scope call) (closure user_data): A callback for each page
+ * @user_data: user data for @callback
+ *
+ * This function will call @callback for every page found in @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_grid_foreach_page (IdeGrid     *self,
+                       GtkCallback  callback,
+                       gpointer     user_data)
+{
+  g_autoptr(GPtrArray) pages = NULL;
+  guint n_columns;
+
+  g_return_if_fail (IDE_IS_GRID (self));
+  g_return_if_fail (callback != NULL);
+
+  pages = g_ptr_array_new ();
+
+  n_columns = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
+
+  for (guint i = 0; i < n_columns; i++)
+    {
+      GtkWidget *column = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), i);
+      guint n_stacks;
+
+      g_assert (IDE_IS_GRID_COLUMN (column));
+
+      n_stacks = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column));
+
+      for (guint j = 0; j < n_stacks; j++)
+        {
+          GtkWidget *stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), j);
+
+          g_assert (IDE_IS_FRAME (stack));
+
+          ide_frame_foreach_page (IDE_FRAME (stack),
+                                  (GtkCallback) collect_pages,
+                                  pages);
+        }
+    }
+
+  for (guint i = 0; i < pages->len; i++)
+    callback (g_ptr_array_index (pages, i), user_data);
+}
+
+static GType
+ide_grid_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_PAGE;
+}
+
+static guint
+ide_grid_get_n_items (GListModel *model)
+{
+  IdeGrid *self = (IdeGrid *)model;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  guint n_items = 0;
+
+  g_assert (IDE_IS_GRID (self));
+
+  for (guint i = 0; i < priv->stack_info->len; i++)
+    n_items += g_array_index (priv->stack_info, StackInfo, i).len;
+
+  return n_items;
+}
+
+static gpointer
+ide_grid_get_item (GListModel *model,
+                          guint       position)
+{
+  IdeGrid *self = (IdeGrid *)model;
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (position < ide_grid_get_n_items (model));
+
+  for (guint i = 0; i < priv->stack_info->len; i++)
+    {
+      const StackInfo *info = &g_array_index (priv->stack_info, StackInfo, i);
+
+      if (position >= info->len)
+        {
+          position -= info->len;
+          continue;
+        }
+
+      return g_list_model_get_item (G_LIST_MODEL (info->stack), position);
+    }
+
+  g_warning ("Failed to locate position %u within %s",
+             position, G_OBJECT_TYPE_NAME (self));
+
+  return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_grid_get_item_type;
+  iface->get_n_items = ide_grid_get_n_items;
+  iface->get_item = ide_grid_get_item;
+}
+
+static void
+ide_grid_stack_items_changed (IdeGrid  *self,
+                                     guint           position,
+                                     guint           removed,
+                                     guint           added,
+                                     IdeFrame *stack)
+{
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  guint real_position = 0;
+
+  g_assert (IDE_IS_GRID (self));
+  g_assert (IDE_IS_FRAME (stack));
+
+  for (guint i = 0; i < priv->stack_info->len; i++)
+    {
+      StackInfo *info = &g_array_index (priv->stack_info, StackInfo, i);
+
+      if (info->stack == stack)
+        {
+          info->len -= removed;
+          info->len += added;
+
+          g_list_model_items_changed (G_LIST_MODEL (self),
+                                      real_position + position,
+                                      removed,
+                                      added);
+
+          ide_object_notify_in_main (G_OBJECT (self), properties [PROP_CURRENT_PAGE]);
+
+          ide_grid_queue_cull (self);
+
+          return;
+        }
+
+      real_position += info->len;
+    }
+
+  g_warning ("Failed to locate %s within %s",
+             G_OBJECT_TYPE_NAME (stack), G_OBJECT_TYPE_NAME (self));
+}
+
+void
+_ide_grid_stack_added (IdeGrid  *self,
+                              IdeFrame *stack)
+{
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  StackInfo info = { 0 };
+  guint n_items;
+
+  g_return_if_fail (IDE_IS_GRID (self));
+  g_return_if_fail (IDE_IS_FRAME (stack));
+  g_return_if_fail (G_IS_LIST_MODEL (stack));
+
+  info.stack = stack;
+  info.len = 0;
+
+  g_array_append_val (priv->stack_info, info);
+
+  g_signal_connect_object (stack,
+                           "items-changed",
+                           G_CALLBACK (ide_grid_stack_items_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (stack));
+  ide_grid_stack_items_changed (self, 0, 0, n_items, stack);
+}
+
+void
+_ide_grid_stack_removed (IdeGrid  *self,
+                                IdeFrame *stack)
+{
+  IdeGridPrivate *priv = ide_grid_get_instance_private (self);
+  guint position = 0;
+
+  g_return_if_fail (IDE_IS_GRID (self));
+  g_return_if_fail (IDE_IS_FRAME (stack));
+
+  g_signal_handlers_disconnect_by_func (stack,
+                                        G_CALLBACK (ide_grid_stack_items_changed),
+                                        self);
+
+  for (guint i = 0; i < priv->stack_info->len; i++)
+    {
+      const StackInfo info = g_array_index (priv->stack_info, StackInfo, i);
+
+      if (info.stack == stack)
+        {
+          g_array_remove_index (priv->stack_info, i);
+          g_list_model_items_changed (G_LIST_MODEL (self), position, info.len, 0);
+          break;
+        }
+    }
+}
+
+static void
+count_pages_cb (GtkWidget *widget,
+                gpointer   data)
+{
+  (*(guint *)data)++;
+}
+
+guint
+ide_grid_count_pages (IdeGrid *self)
+{
+  guint count = 0;
+
+  g_return_val_if_fail (IDE_IS_GRID (self), 0);
+
+  ide_grid_foreach_page (self, count_pages_cb, &count);
+
+  return count;
+}
+
+/**
+ * ide_grid_focus_neighbor:
+ * @self: An #IdeGrid
+ * @dir: the direction for the focus change
+ *
+ * Attempts to focus a neighbor #IdePage in the grid based on
+ * the direction requested.
+ *
+ * If an #IdePage was focused, it will be returned to the caller.
+ *
+ * Returns: (transfer none) (nullable): An #IdePage or %NULL
+ *
+ * Since: 3.32
+ */
+IdePage *
+ide_grid_focus_neighbor (IdeGrid    *self,
+                                GtkDirectionType  dir)
+{
+  IdeGridColumn *column;
+  IdeFrame *stack;
+  IdePage *page = NULL;
+  guint stack_pos = 0;
+  guint column_pos = 0;
+  guint n_children;
+
+  g_return_val_if_fail (IDE_IS_GRID (self), NULL);
+  g_return_val_if_fail (dir <= GTK_DIR_RIGHT, NULL);
+
+  /* Make sure we have a current page and stack */
+  if (NULL == (stack = ide_grid_get_current_stack (self)) ||
+      NULL == (column = ide_grid_get_current_column (self)))
+    return NULL;
+
+  gtk_container_child_get (GTK_CONTAINER (self), GTK_WIDGET (column),
+                           "index", &column_pos,
+                           NULL);
+
+  gtk_container_child_get (GTK_CONTAINER (column), GTK_WIDGET (stack),
+                           "index", &stack_pos,
+                           NULL);
+
+  switch (dir)
+    {
+    case GTK_DIR_DOWN:
+      n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (column));
+      if (n_children - stack_pos == 1)
+        return NULL;
+      stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), stack_pos + 1));
+      page = ide_frame_get_visible_child (stack);
+      break;
+
+    case GTK_DIR_RIGHT:
+      n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
+      if (n_children - column_pos == 1)
+        return NULL;
+      column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), column_pos + 1));
+      stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
+      page = ide_frame_get_visible_child (stack);
+      break;
+
+    case GTK_DIR_UP:
+      if (stack_pos == 0)
+        return NULL;
+      stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), stack_pos - 1));
+      page = ide_frame_get_visible_child (stack);
+      break;
+
+    case GTK_DIR_LEFT:
+      if (column_pos == 0)
+        return NULL;
+      column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), column_pos - 1));
+      stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
+      page = ide_frame_get_visible_child (stack);
+      break;
+
+    case GTK_DIR_TAB_FORWARD:
+      if (!ide_grid_focus_neighbor (self, GTK_DIR_DOWN) &&
+          !ide_grid_focus_neighbor (self, GTK_DIR_RIGHT))
+        {
+          column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), 0));
+          stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
+          page = ide_frame_get_visible_child (stack);
+        }
+      break;
+
+    case GTK_DIR_TAB_BACKWARD:
+      if (!ide_grid_focus_neighbor (self, GTK_DIR_UP) &&
+          !ide_grid_focus_neighbor (self, GTK_DIR_LEFT))
+        {
+          n_children = dzl_multi_paned_get_n_children (DZL_MULTI_PANED (self));
+          column = IDE_GRID_COLUMN (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (self), n_children - 1));
+          stack = IDE_FRAME (dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (column), 0));
+          page = ide_frame_get_visible_child (stack);
+        }
+      break;
+
+    default:
+      g_assert_not_reached ();
+    }
+
+  if (page != NULL)
+    gtk_widget_child_focus (GTK_WIDGET (page), GTK_DIR_TAB_FORWARD);
+
+  return page;
+}
diff --git a/src/libide/gui/ide-grid.h b/src/libide/gui/ide-grid.h
new file mode 100644
index 000000000..04516e6b5
--- /dev/null
+++ b/src/libide/gui/ide-grid.h
@@ -0,0 +1,77 @@
+/* ide-grid.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+#include "ide-grid-column.h"
+#include "ide-frame.h"
+#include "ide-page.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_GRID (ide_grid_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeGrid, ide_grid, IDE, GRID, DzlMultiPaned)
+
+struct _IdeGridClass
+{
+  DzlMultiPanedClass parent_class;
+
+  IdeFrame *(*create_frame) (IdeGrid     *self);
+  IdePage  *(*create_page)  (IdeGrid     *self,
+                             const gchar *uri);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget     *ide_grid_new                (void);
+IDE_AVAILABLE_IN_3_32
+IdeGridColumn *ide_grid_get_nth_column     (IdeGrid          *self,
+                                            gint              nth);
+IDE_AVAILABLE_IN_3_32
+IdePage       *ide_grid_focus_neighbor     (IdeGrid          *self,
+                                            GtkDirectionType  dir);
+IDE_AVAILABLE_IN_3_32
+IdeGridColumn *ide_grid_get_current_column (IdeGrid          *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_grid_set_current_column (IdeGrid          *self,
+                                            IdeGridColumn    *column);
+IDE_AVAILABLE_IN_3_32
+IdeFrame      *ide_grid_get_current_stack  (IdeGrid          *self);
+IDE_AVAILABLE_IN_3_32
+IdePage       *ide_grid_get_current_page   (IdeGrid          *self);
+IDE_AVAILABLE_IN_3_32
+guint          ide_grid_count_pages        (IdeGrid          *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_grid_foreach_page       (IdeGrid          *self,
+                                            GtkCallback       callback,
+                                            gpointer          user_data);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-gui-global.c b/src/libide/gui/ide-gui-global.c
new file mode 100644
index 000000000..bf4063367
--- /dev/null
+++ b/src/libide/gui/ide-gui-global.c
@@ -0,0 +1,358 @@
+/* ide-gui-global.c
+ *
+ * Copyright 2018-2019 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 "ide-gui-global"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-threading.h>
+
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-workspace.h"
+
+static GQuark quark_handler;
+static GQuark quark_where_context_was;
+
+static void ide_widget_notify_context    (GtkWidget  *toplevel,
+                                          GParamSpec *pspec,
+                                          GtkWidget  *widget);
+static void ide_widget_hierarchy_changed (GtkWidget  *widget,
+                                          GtkWidget  *previous_toplevel,
+                                          gpointer    user_data);
+
+static void
+ide_widget_notify_context (GtkWidget  *toplevel,
+                           GParamSpec *pspec,
+                           GtkWidget  *widget)
+{
+  IdeWidgetContextHandler handler;
+  IdeContext *old_context;
+  IdeContext *context;
+
+  g_assert (GTK_IS_WIDGET (toplevel));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  handler = g_object_get_qdata (G_OBJECT (widget), quark_handler);
+  old_context = g_object_get_qdata (G_OBJECT (widget), quark_where_context_was);
+
+  if (handler == NULL)
+    return;
+
+  context = ide_widget_get_context (toplevel);
+
+  if (context == old_context)
+    return;
+
+  g_object_set_qdata (G_OBJECT (widget), quark_where_context_was, context);
+
+  g_signal_handlers_disconnect_by_func (toplevel,
+                                        G_CALLBACK (ide_widget_notify_context),
+                                        widget);
+  g_signal_handlers_disconnect_by_func (widget,
+                                        G_CALLBACK (ide_widget_hierarchy_changed),
+                                        NULL);
+
+  handler (widget, context);
+}
+
+static gboolean
+has_context_property (GtkWidget *widget)
+{
+  GParamSpec *pspec;
+
+  g_assert (GTK_IS_WIDGET (widget));
+
+  pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (widget), "context");
+  return pspec != NULL && g_type_is_a (pspec->value_type, IDE_TYPE_CONTEXT);
+}
+
+static void
+ide_widget_hierarchy_changed (GtkWidget *widget,
+                              GtkWidget *previous_toplevel,
+                              gpointer   user_data)
+{
+  GtkWidget *toplevel;
+
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (GTK_IS_WINDOW (previous_toplevel))
+    g_signal_handlers_disconnect_by_func (previous_toplevel,
+                                          G_CALLBACK (ide_widget_notify_context),
+                                          widget);
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (GTK_IS_WINDOW (toplevel) && has_context_property (toplevel))
+    {
+      g_signal_connect_object (toplevel,
+                               "notify::context",
+                               G_CALLBACK (ide_widget_notify_context),
+                               widget,
+                               0);
+      ide_widget_notify_context (toplevel, NULL, widget);
+    }
+}
+
+/**
+ * ide_widget_set_context_handler:
+ * @widget: (type Gtk.Widget): a #GtkWidget
+ * @handler: (scope async): A callback to handle the context
+ *
+ * Calls @handler when the #IdeContext has been set for @widget.
+ *
+ * Since: 3.32
+ */
+void
+ide_widget_set_context_handler (gpointer                widget,
+                                IdeWidgetContextHandler handler)
+{
+  GtkWidget *toplevel;
+
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  /* Ensure we have our quarks for quick key lookup */
+  if G_UNLIKELY (quark_handler == 0)
+    quark_handler = g_quark_from_static_string ("IDE_CONTEXT_HANDLER");
+
+  if G_UNLIKELY (quark_where_context_was == 0)
+    quark_where_context_was = g_quark_from_static_string ("IDE_CONTEXT");
+
+  g_object_set_qdata (G_OBJECT (widget), quark_handler, handler);
+
+  g_signal_connect (widget,
+                    "hierarchy-changed",
+                    G_CALLBACK (ide_widget_hierarchy_changed),
+                    NULL);
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (GTK_IS_WINDOW (toplevel))
+    ide_widget_hierarchy_changed (widget, NULL, NULL);
+}
+
+/**
+ * ide_widget_get_context:
+ * @widget: a #GtkWidget
+ *
+ * Gets the context for the widget.
+ *
+ * Returns: (nullable) (transfer none): an #IdeContext, or %NULL
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_widget_get_context (GtkWidget *widget)
+{
+  GtkWidget *toplevel;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (IDE_IS_WORKSPACE (toplevel))
+    return ide_workspace_get_context (IDE_WORKSPACE (toplevel));
+
+  return NULL;
+}
+
+/**
+ * ide_widget_get_workbench:
+ * @widget: a #GtkWidget
+ *
+ * Gets the #IdeWorkbench that contains @widget.
+ *
+ * Returns: (transfer none) (nullable): an #IdeWorkbench or %NULL
+ *
+ * Since: 3.32
+ */
+IdeWorkbench *
+ide_widget_get_workbench (GtkWidget *widget)
+{
+  GtkWidget *toplevel;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (GTK_IS_WINDOW (toplevel))
+    {
+      GtkWindowGroup *group = gtk_window_get_group (GTK_WINDOW (toplevel));
+
+      if (IDE_IS_WORKBENCH (group))
+        return IDE_WORKBENCH (group);
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_widget_get_workspace:
+ * @widget: a #GtkWidget
+ *
+ * Gets the #IdeWorkspace containing @widget.
+ *
+ * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL
+ *
+ * Since: 3.32
+ */
+IdeWorkspace *
+ide_widget_get_workspace (GtkWidget *widget)
+{
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  return (IdeWorkspace *)dzl_gtk_widget_get_relative (widget, IDE_TYPE_WORKSPACE);
+}
+
+static gboolean
+ide_gtk_progress_bar_tick_cb (gpointer data)
+{
+  GtkProgressBar *progress = data;
+
+  g_assert (GTK_IS_PROGRESS_BAR (progress));
+
+  gtk_progress_bar_pulse (progress);
+  gtk_widget_queue_draw (GTK_WIDGET (progress));
+
+  return G_SOURCE_CONTINUE;
+}
+
+void
+_ide_gtk_progress_bar_stop_pulsing (GtkProgressBar *progress)
+{
+  guint tick_id;
+
+  g_return_if_fail (GTK_IS_PROGRESS_BAR (progress));
+
+  tick_id = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (progress), "PULSE_ID"));
+
+  if (tick_id != 0)
+    {
+      g_source_remove (tick_id);
+      g_object_set_data (G_OBJECT (progress), "PULSE_ID", NULL);
+    }
+
+  gtk_progress_bar_set_fraction (progress, 0.0);
+}
+
+void
+_ide_gtk_progress_bar_start_pulsing (GtkProgressBar *progress)
+{
+  guint tick_id;
+
+  g_return_if_fail (GTK_IS_PROGRESS_BAR (progress));
+
+  if (g_object_get_data (G_OBJECT (progress), "PULSE_ID"))
+    return;
+
+  gtk_progress_bar_set_fraction (progress, 0.0);
+  gtk_progress_bar_set_pulse_step (progress, .5);
+
+  /* We want lower than the frame rate, because that is all that is needed */
+  tick_id = dzl_frame_source_add_full (G_PRIORITY_DEFAULT,
+                                       2,
+                                       ide_gtk_progress_bar_tick_cb,
+                                       g_object_ref (progress),
+                                       g_object_unref);
+  g_object_set_data (G_OBJECT (progress), "PULSE_ID", GUINT_TO_POINTER (tick_id));
+  ide_gtk_progress_bar_tick_cb (progress);
+}
+
+gboolean
+ide_gtk_show_uri_on_window (GtkWindow    *window,
+                            const gchar  *uri,
+                            gint64        timestamp,
+                            GError      **error)
+{
+  g_return_val_if_fail (!window || GTK_IS_WINDOW (window), FALSE);
+  g_return_val_if_fail (uri != NULL, FALSE);
+
+  if (ide_is_flatpak ())
+    {
+      g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+      g_autoptr(IdeSubprocess) subprocess = NULL;
+
+      /* We can't currently trust gtk_show_uri_on_window() because it tries
+       * to open our HTML page with Builder inside our current flatpak
+       * environment! We need to ensure this is fixed upstream, but it's
+       * currently unclear how to do so since we register handles for html.
+       */
+
+      launcher = ide_subprocess_launcher_new (0);
+      ide_subprocess_launcher_set_run_on_host (launcher, TRUE);
+      ide_subprocess_launcher_set_clear_env (launcher, FALSE);
+      ide_subprocess_launcher_push_argv (launcher, "xdg-open");
+      ide_subprocess_launcher_push_argv (launcher, uri);
+
+      if (!(subprocess = ide_subprocess_launcher_spawn (launcher, NULL, error)))
+        return FALSE;
+    }
+  else
+    {
+      /* XXX: Workaround for wayland timestamp issue */
+      if (!gtk_show_uri_on_window (window, uri, timestamp / 1000L, error))
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+static void
+show_parents (GtkWidget *widget)
+{
+  GtkWidget *workspace;
+  GtkWidget *parent;
+
+  g_assert (GTK_IS_WIDGET (widget));
+
+  workspace = gtk_widget_get_ancestor (widget, IDE_TYPE_WORKSPACE);
+  parent = gtk_widget_get_parent (widget);
+
+  if (DZL_IS_DOCK_REVEALER (widget))
+    dzl_dock_revealer_set_reveal_child (DZL_DOCK_REVEALER (widget), TRUE);
+
+  if (IDE_IS_SURFACE (widget))
+    ide_workspace_set_visible_surface (IDE_WORKSPACE (workspace), IDE_SURFACE (widget));
+
+  if (GTK_IS_STACK (parent))
+    gtk_stack_set_visible_child (GTK_STACK (parent), widget);
+
+  if (parent != NULL)
+    show_parents (parent);
+}
+
+void
+ide_widget_reveal_and_grab (GtkWidget *widget)
+{
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  show_parents (widget);
+  gtk_widget_grab_focus (widget);
+}
+
+void
+ide_gtk_window_present (GtkWindow *window)
+{
+  /* TODO: We need the last event time to do this properly. Until then,
+   * we'll just fake some timing info to workaround wayland issues.
+   */
+  gtk_window_present_with_time (window, g_get_monotonic_time () / 1000L);
+}
diff --git a/src/libide/gui/ide-gui-global.h b/src/libide/gui/ide-gui-global.h
new file mode 100644
index 000000000..399d9f769
--- /dev/null
+++ b/src/libide/gui/ide-gui-global.h
@@ -0,0 +1,58 @@
+/* ide-gui-global.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-workbench.h"
+
+G_BEGIN_DECLS
+
+#define ide_widget_warning(instance, format, ...)                                                   \
+  G_STMT_START {                                                                                    \
+    IdeContext *context = ide_widget_get_context (GTK_WIDGET (instance));                           \
+    ide_context_log (context, G_LOG_LEVEL_WARNING, G_LOG_DOMAIN, format __VA_OPT__(,) __VA_ARGS__); \
+  } G_STMT_END
+
+typedef void (*IdeWidgetContextHandler) (GtkWidget  *widget,
+                                         IdeContext *context);
+
+IDE_AVAILABLE_IN_3_32
+void          ide_widget_set_context_handler (gpointer                 widget,
+                                              IdeWidgetContextHandler  handler);
+IDE_AVAILABLE_IN_3_32
+IdeContext   *ide_widget_get_context         (GtkWidget               *widget);
+IDE_AVAILABLE_IN_3_32
+void          ide_widget_reveal_and_grab     (GtkWidget               *widget);
+IDE_AVAILABLE_IN_3_32
+IdeWorkbench *ide_widget_get_workbench       (GtkWidget               *widget);
+IDE_AVAILABLE_IN_3_32
+IdeWorkspace *ide_widget_get_workspace       (GtkWidget               *widget);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_gtk_show_uri_on_window     (GtkWindow               *window,
+                                              const gchar             *uri,
+                                              gint64                   timestamp,
+                                              GError                 **error);
+IDE_AVAILABLE_IN_3_32
+void          ide_gtk_window_present         (GtkWindow               *window);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-gui-private.h b/src/libide/gui/ide-gui-private.h
new file mode 100644
index 000000000..634312a3d
--- /dev/null
+++ b/src/libide/gui/ide-gui-private.h
@@ -0,0 +1,103 @@
+/* ide-gui-private.h
+ *
+ * Copyright 2017-2019 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 <dazzle.h>
+#include <gtk/gtk.h>
+#include <libpeas/peas.h>
+#include <libpeas/peas-autocleanups.h>
+#include <libide-core.h>
+#include <libide-projects.h>
+
+#include "ide-frame.h"
+#include "ide-frame-header.h"
+#include "ide-grid.h"
+#include "ide-grid-column.h"
+#include "ide-header-bar.h"
+#include "ide-notification-list-box-row-private.h"
+#include "ide-notification-stack-private.h"
+#include "ide-notification-view-private.h"
+#include "ide-page.h"
+#include "ide-primary-workspace.h"
+#include "ide-shortcut-label-private.h"
+#include "ide-workbench.h"
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+void      _ide_frame_init_actions               (IdeFrame            *self);
+void      _ide_frame_init_shortcuts             (IdeFrame            *self);
+void      _ide_frame_update_actions             (IdeFrame            *self);
+void      _ide_frame_transfer                   (IdeFrame            *self,
+                                                 IdeFrame            *dest,
+                                                 IdePage             *view);
+void      _ide_grid_column_init_actions         (IdeGridColumn       *self);
+void      _ide_grid_column_update_actions       (IdeGridColumn       *self);
+gboolean  _ide_grid_column_is_empty             (IdeGridColumn       *self);
+void      _ide_grid_column_try_close            (IdeGridColumn       *self);
+IdeFrame *_ide_grid_get_nth_stack               (IdeGrid             *self,
+                                                 gint                 nth);
+IdeFrame *_ide_grid_get_nth_stack_for_column    (IdeGrid             *self,
+                                                 IdeGridColumn       *column,
+                                                 gint                 nth);
+void      _ide_grid_init_actions                (IdeGrid             *self);
+void      _ide_grid_stack_added                 (IdeGrid             *self,
+                                                 IdeFrame            *stack);
+void      _ide_grid_stack_removed               (IdeGrid             *self,
+                                                 IdeFrame            *stack);
+void      _ide_frame_request_close              (IdeFrame            *stack,
+                                                 IdePage             *view);
+void      _ide_frame_header_update              (IdeFrameHeader      *self,
+                                                 IdePage             *view);
+void      _ide_frame_header_focus_list          (IdeFrameHeader      *self);
+void      _ide_frame_header_hide                (IdeFrameHeader      *self);
+void      _ide_frame_header_popdown             (IdeFrameHeader      *self);
+void      _ide_frame_header_set_pages           (IdeFrameHeader      *self,
+                                                 GListModel          *model);
+void      _ide_frame_header_set_title           (IdeFrameHeader      *self,
+                                                 const gchar         *title);
+void      _ide_frame_header_set_modified        (IdeFrameHeader      *self,
+                                                 gboolean             modified);
+void      _ide_frame_header_set_background_rgba (IdeFrameHeader      *self,
+                                                 const GdkRGBA       *background_rgba);
+void      _ide_frame_header_set_foreground_rgba (IdeFrameHeader      *self,
+                                                 const GdkRGBA       *foreground_rgba);
+void      _ide_primary_workspace_init_actions   (IdePrimaryWorkspace *self);
+void      _ide_workspace_init_actions           (IdeWorkspace        *self);
+GList    *_ide_workspace_get_mru_link           (IdeWorkspace        *self);
+void      _ide_workspace_add_page_mru           (IdeWorkspace        *self,
+                                                 GList               *mru_link);
+void      _ide_workspace_remove_page_mru        (IdeWorkspace        *self,
+                                                 GList               *mru_link);
+void      _ide_workspace_move_front_page_mru    (IdeWorkspace        *workspace,
+                                                 GList               *mru_link);
+void      _ide_workspace_set_context            (IdeWorkspace        *workspace,
+                                                 IdeContext          *context);
+gboolean  _ide_workbench_is_last_workspace      (IdeWorkbench        *self,
+                                                 IdeWorkspace        *workspace);
+void      _ide_header_bar_init_shortcuts        (IdeHeaderBar        *self);
+void      _ide_header_bar_show_menu             (IdeHeaderBar        *self);
+void      _ide_gtk_progress_bar_start_pulsing   (GtkProgressBar      *progress);
+void      _ide_gtk_progress_bar_stop_pulsing    (GtkProgressBar      *progress);
+void      _ide_surface_set_fullscreen           (IdeSurface          *self,
+                                                 gboolean             fullscreen);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-header-bar-shortcuts.c b/src/libide/gui/ide-header-bar-shortcuts.c
new file mode 100644
index 000000000..a0a3230ec
--- /dev/null
+++ b/src/libide/gui/ide-header-bar-shortcuts.c
@@ -0,0 +1,68 @@
+/* ide-header-bar-shortcuts.c
+ *
+ * Copyright 2018-2019 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 "ide-header-bar-shortcuts"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-gui-private.h"
+
+#define I_(s) (g_intern_static_string(s))
+
+static DzlShortcutEntry workspace_shortcuts[] = {
+  { "org.gnome.builder.workspace.show-menu",
+    0, NULL,
+    NC_("shortcut window", "Window shortcuts"),
+    NC_("shortcut window", "General"),
+    NC_("shortcut window", "Show window menu") },
+
+  { "org.gnome.builder.workspace.fullscreen",
+    0, NULL,
+    NC_("shortcut window", "Window shortcuts"),
+    NC_("shortcut window", "General"),
+    NC_("shortcut window", "Toggle window to fullscreen") },
+};
+
+void
+_ide_header_bar_init_shortcuts (IdeHeaderBar *self)
+{
+  DzlShortcutController *controller;
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.workspace.show-menu"),
+                                              "F10",
+                                              DZL_SHORTCUT_PHASE_BUBBLE | DZL_SHORTCUT_PHASE_GLOBAL,
+                                              I_("win.show-menu"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.workspace.fullscreen"),
+                                              "F11",
+                                              DZL_SHORTCUT_PHASE_DISPATCH | DZL_SHORTCUT_PHASE_GLOBAL,
+                                              I_("win.fullscreen"));
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             workspace_shortcuts,
+                                             G_N_ELEMENTS (workspace_shortcuts),
+                                             GETTEXT_PACKAGE);
+}
diff --git a/src/libide/gui/ide-header-bar.c b/src/libide/gui/ide-header-bar.c
new file mode 100644
index 000000000..245eb5c5e
--- /dev/null
+++ b/src/libide/gui/ide-header-bar.c
@@ -0,0 +1,469 @@
+/* ide-header-bar.c
+ *
+ * Copyright 2014-2019 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 "ide-header-bar"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-gui-private.h"
+#include "ide-header-bar.h"
+
+typedef struct
+{
+  gchar              *menu_id;
+
+  GtkToggleButton    *fullscreen_button;
+  GtkImage           *fullscreen_image;
+  DzlShortcutTooltip *fullscreen_tooltip;
+  DzlMenuButton      *menu_button;
+  DzlShortcutTooltip *menu_tooltip;
+  GtkBox             *primary;
+  GtkBox             *secondary;
+
+  guint               show_fullscreen_button : 1;
+} IdeHeaderBarPrivate;
+
+enum {
+  PROP_0,
+  PROP_MENU_ID,
+  PROP_SHOW_FULLSCREEN_BUTTON,
+  N_PROPS
+};
+
+static void buildable_iface_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeHeaderBar, ide_header_bar, GTK_TYPE_HEADER_BAR,
+                         G_ADD_PRIVATE (IdeHeaderBar)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
+
+static GParamSpec        *properties [N_PROPS];
+static GtkBuildableIface *buildable_parent;
+
+static void
+on_fullscreen_toggled_cb (GtkToggleButton *button,
+                          GParamSpec      *pspec,
+                          GtkImage        *image)
+{
+  const gchar *icon_name;
+
+  g_assert (GTK_IS_TOGGLE_BUTTON (button));
+  g_assert (GTK_IS_IMAGE (image));
+
+  if (gtk_toggle_button_get_active (button))
+    icon_name = "view-restore-symbolic";
+  else
+    icon_name = "view-fullscreen-symbolic";
+
+  g_object_set (image, "icon-name", icon_name, NULL);
+}
+
+static void
+ide_header_bar_finalize (GObject *object)
+{
+  IdeHeaderBar *self = (IdeHeaderBar *)object;
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_clear_pointer (&priv->menu_id, g_free);
+
+  G_OBJECT_CLASS (ide_header_bar_parent_class)->finalize (object);
+}
+
+static void
+ide_header_bar_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeHeaderBar *self = IDE_HEADER_BAR (object);
+
+  switch (prop_id)
+    {
+    case PROP_MENU_ID:
+      g_value_set_string (value, ide_header_bar_get_menu_id (self));
+      break;
+
+    case PROP_SHOW_FULLSCREEN_BUTTON:
+      g_value_set_boolean (value, ide_header_bar_get_show_fullscreen_button (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_header_bar_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeHeaderBar *self = IDE_HEADER_BAR (object);
+
+  switch (prop_id)
+    {
+    case PROP_MENU_ID:
+      ide_header_bar_set_menu_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_SHOW_FULLSCREEN_BUTTON:
+      ide_header_bar_set_show_fullscreen_button (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_header_bar_class_init (IdeHeaderBarClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_header_bar_finalize;
+  object_class->get_property = ide_header_bar_get_property;
+  object_class->set_property = ide_header_bar_set_property;
+
+  properties [PROP_SHOW_FULLSCREEN_BUTTON] =
+    g_param_spec_boolean ("show-fullscreen-button",
+                          "Show Fullscreen Button",
+                          "If the fullscreen button should be shown",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MENU_ID] =
+    g_param_spec_string ("menu-id",
+                         "Menu ID",
+                         "The id of the menu to display with the window",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-header-bar.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, fullscreen_button);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, fullscreen_image);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, fullscreen_tooltip);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, menu_button);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, menu_tooltip);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, primary);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeHeaderBar, secondary);
+
+  g_type_ensure (DZL_TYPE_PRIORITY_BOX);
+  g_type_ensure (DZL_TYPE_SHORTCUT_TOOLTIP);
+}
+
+static void
+ide_header_bar_init (IdeHeaderBar *self)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (priv->fullscreen_button,
+                           "notify::active",
+                           G_CALLBACK (on_fullscreen_toggled_cb),
+                           priv->fullscreen_image,
+                           0);
+
+  _ide_header_bar_init_shortcuts (self);
+}
+
+GtkWidget *
+ide_header_bar_new (void)
+{
+  return g_object_new (IDE_TYPE_HEADER_BAR, NULL);
+}
+
+/**
+ * ide_header_bar_get_show_fullscreen_button:
+ * @self: a #IdeHeaderBar
+ *
+ * Gets if the fullscreen button should be displayed in the header bar.
+ *
+ * Returns: %TRUE if it should be displayed
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_header_bar_get_show_fullscreen_button (IdeHeaderBar *self)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_HEADER_BAR (self), FALSE);
+
+  return priv->show_fullscreen_button;
+}
+
+/**
+ * ide_header_bar_set_show_fullscreen_button:
+ * @self: a #IdeHeaderBar
+ * @show_fullscreen_button: if the fullscreen button should be displayed
+ *
+ * Changes the visibility of the fullscreen button.
+ *
+ * Since: 3.32
+ */
+void
+ide_header_bar_set_show_fullscreen_button (IdeHeaderBar *self,
+                                           gboolean      show_fullscreen_button)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+
+  show_fullscreen_button = !!show_fullscreen_button;
+
+  if (show_fullscreen_button != priv->show_fullscreen_button)
+    {
+      const gchar *session;
+
+      priv->show_fullscreen_button = show_fullscreen_button;
+
+      session = g_getenv ("DESKTOP_SESSION");
+      if (ide_str_equal0 (session, "pantheon"))
+        show_fullscreen_button = FALSE;
+
+      gtk_widget_set_visible (GTK_WIDGET (priv->fullscreen_button), show_fullscreen_button);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_FULLSCREEN_BUTTON]);
+    }
+}
+
+/**
+ * ide_header_bar_get_menu_id:
+ * @self: a #IdeHeaderBar
+ *
+ * Gets the menu-id to show in the workspace window.
+ *
+ * Returns: (nullable): a string containing the menu-id, or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_header_bar_get_menu_id (IdeHeaderBar *self)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_HEADER_BAR (self), NULL);
+
+  return priv->menu_id;
+}
+
+/**
+ * ide_header_bar_set_menu_id:
+ * @self: a #IdeHeaderBar
+ *
+ * Sets the menu-id to display in the window.
+ *
+ * Set to %NULL to hide the workspace menu.
+ *
+ * Since: 3.32
+ */
+void
+ide_header_bar_set_menu_id (IdeHeaderBar *self,
+                            const gchar  *menu_id)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+
+  if (!ide_str_equal0 (menu_id, priv->menu_id))
+    {
+      g_free (priv->menu_id);
+      priv->menu_id = g_strdup (menu_id);
+      g_object_set (priv->menu_button, "menu-id", menu_id, NULL);
+      gtk_widget_set_visible (GTK_WIDGET (priv->menu_button), !ide_str_empty0 (menu_id));
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MENU_ID]);
+    }
+}
+
+/**
+ * ide_header_bar_add_primary:
+ * @self: a #IdeHeaderBar
+ *
+ * Adds a widget to the primary button section of the workspace header.
+ * This is the left, for LTR languages.
+ *
+ * Since: 3.32
+ */
+void
+ide_header_bar_add_primary (IdeHeaderBar *self,
+                            GtkWidget    *widget)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add (GTK_CONTAINER (priv->primary), widget);
+}
+
+void
+ide_header_bar_add_center_left (IdeHeaderBar *self,
+                                GtkWidget    *child)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (child));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (priv->primary), child,
+                                     "pack-type", GTK_PACK_END,
+                                     NULL);
+}
+
+/**
+ * ide_header_bar_add_secondary:
+ * @self: a #IdeHeaderBar
+ *
+ * Adds a widget to the secondary button section of the workspace header.
+ * This is the right, for LTR languages.
+ *
+ * Since: 3.32
+ */
+void
+ide_header_bar_add_secondary (IdeHeaderBar *self,
+                              GtkWidget    *widget)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add (GTK_CONTAINER (priv->secondary), widget);
+}
+
+void
+_ide_header_bar_show_menu (IdeHeaderBar *self)
+{
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_HEADER_BAR (self));
+
+  gtk_widget_activate (GTK_WIDGET (priv->menu_button));
+}
+
+static void
+ide_header_bar_add_child (GtkBuildable  *buildable,
+                          GtkBuilder    *builder,
+                          GObject       *child,
+                          const gchar   *type)
+{
+  IdeHeaderBar *self = (IdeHeaderBar *)buildable;
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_assert (IDE_IS_HEADER_BAR (self));
+  g_assert (GTK_IS_BUILDER (builder));
+  g_assert (G_IS_OBJECT (child));
+
+  if (ide_str_equal0 (type, "left-of-center"))
+    {
+      if (GTK_IS_WIDGET (child))
+        {
+          gtk_container_add_with_properties (GTK_CONTAINER (priv->primary), GTK_WIDGET (child),
+                                             "pack-type", GTK_PACK_END,
+                                             NULL);
+          return;
+        }
+
+      goto warning;
+    }
+
+  if (ide_str_equal0 (type, "left") || ide_str_equal0 (type, "primary"))
+    {
+      if (GTK_IS_WIDGET (child))
+        {
+          gtk_container_add_with_properties (GTK_CONTAINER (priv->primary), GTK_WIDGET (child),
+                                             "pack-type", GTK_PACK_START,
+                                             NULL);
+          return;
+        }
+
+      goto warning;
+    }
+
+  if (ide_str_equal0 (type, "right-of-center"))
+    {
+      if (GTK_IS_WIDGET (child))
+        {
+          gtk_container_add_with_properties (GTK_CONTAINER (priv->secondary), GTK_WIDGET (child),
+                                             "pack-type", GTK_PACK_START,
+                                             NULL);
+          return;
+        }
+
+      goto warning;
+    }
+
+  if (ide_str_equal0 (type, "right") || ide_str_equal0 (type, "secondary"))
+    {
+      if (GTK_IS_WIDGET (child))
+        {
+          gtk_container_add_with_properties (GTK_CONTAINER (priv->secondary), GTK_WIDGET (child),
+                                             "pack-type", GTK_PACK_END,
+                                             NULL);
+          return;
+        }
+
+      goto warning;
+    }
+
+  buildable_parent->add_child (buildable, builder, child, type);
+
+  return;
+
+warning:
+  g_warning ("'%s' child type must be a GtkWidget, not %s",
+             type, G_OBJECT_TYPE_NAME (child));
+}
+
+static GObject *
+ide_header_bar_get_internal_child (GtkBuildable *buildable,
+                                   GtkBuilder   *builder,
+                                   const gchar  *child_name)
+{
+  IdeHeaderBar *self = (IdeHeaderBar *)buildable;
+  IdeHeaderBarPrivate *priv = ide_header_bar_get_instance_private (self);
+
+  g_assert (IDE_IS_HEADER_BAR (self));
+  g_assert (GTK_IS_BUILDER (builder));
+
+  if (ide_str_equal0 (child_name, "primary"))
+    return G_OBJECT (priv->primary);
+
+  if (ide_str_equal0 (child_name, "secondary"))
+    return G_OBJECT (priv->secondary);
+
+  if (buildable_parent->get_internal_child)
+    return buildable_parent->get_internal_child (buildable, builder, child_name);
+
+  return NULL;
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  buildable_parent = g_type_interface_peek_parent (iface);
+  iface->add_child = ide_header_bar_add_child;
+  iface->get_internal_child = ide_header_bar_get_internal_child;
+}
diff --git a/src/libide/gui/ide-header-bar.h b/src/libide/gui/ide-header-bar.h
new file mode 100644
index 000000000..ec77fbd39
--- /dev/null
+++ b/src/libide/gui/ide-header-bar.h
@@ -0,0 +1,67 @@
+/* ide-header-bar.h
+ *
+ * Copyright 2014-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HEADER_BAR (ide_header_bar_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeHeaderBar, ide_header_bar, IDE, HEADER_BAR, GtkHeaderBar)
+
+struct _IdeHeaderBarClass
+{
+  GtkHeaderBarClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget   *ide_header_bar_new                       (void);
+IDE_AVAILABLE_IN_3_32
+void         ide_header_bar_add_primary               (IdeHeaderBar *self,
+                                                       GtkWidget    *widget);
+IDE_AVAILABLE_IN_3_32
+void         ide_header_bar_add_center_left           (IdeHeaderBar *self,
+                                                       GtkWidget    *widget);
+IDE_AVAILABLE_IN_3_32
+void         ide_header_bar_add_secondary             (IdeHeaderBar *self,
+                                                       GtkWidget    *widget);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_header_bar_get_menu_id               (IdeHeaderBar *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_header_bar_set_menu_id               (IdeHeaderBar *self,
+                                                       const gchar  *menu_id);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_header_bar_get_show_fullscreen_button (IdeHeaderBar *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_header_bar_set_show_fullscreen_button (IdeHeaderBar *self,
+                                                       gboolean      show_fullscreen_button);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-header-bar.ui b/src/libide/gui/ide-header-bar.ui
new file mode 100644
index 000000000..e55beb724
--- /dev/null
+++ b/src/libide/gui/ide-header-bar.ui
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.24 -->
+  <template class="IdeHeaderBar" parent="GtkHeaderBar">
+    <child>
+      <object class="DzlPriorityBox" id="primary">
+        <property name="hexpand">true</property>
+        <property name="margin-end">6</property>
+        <property name="orientation">horizontal</property>
+        <property name="spacing">6</property>
+        <property name="visible">true</property>
+      </object>
+      <packing>
+        <property name="pack-type">start</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="DzlPriorityBox" id="secondary">
+        <property name="hexpand">true</property>
+        <property name="margin-start">6</property>
+        <property name="spacing">6</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="spacing">6</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkToggleButton" id="fullscreen_button">
+                <property name="action-name">win.fullscreen</property>
+                <property name="focus-on-click">false</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+                <child>
+                  <object class="GtkImage" id="fullscreen_image">
+                    <property name="icon-name">view-fullscreen-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="DzlMenuButton" id="menu_button">
+                <property name="icon-name">open-menu-symbolic</property>
+                <property name="show-accels">true</property>
+                <property name="show-icons">true</property>
+                <style>
+                  <class name="image-button"/>
+                </style>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="pack-type">end</property>
+            <property name="priority">-1000000</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="pack-type">end</property>
+        <property name="position">0</property>
+      </packing>
+    </child>
+  </template>
+  <object class="DzlShortcutTooltip" id="fullscreen_tooltip">
+    <property name="command-id">org.gnome.builder.workspace.fullscreen</property>
+    <property name="widget">fullscreen_button</property>
+  </object>
+  <object class="DzlShortcutTooltip" id="menu_tooltip">
+    <property name="command-id">org.gnome.builder.workspace.show-menu</property>
+    <property name="widget">menu_button</property>
+  </object>
+</interface>
+
diff --git a/src/libide/gui/ide-keybindings.c b/src/libide/gui/ide-keybindings.c
new file mode 100644
index 000000000..f97638150
--- /dev/null
+++ b/src/libide/gui/ide-keybindings.c
@@ -0,0 +1,366 @@
+/* ide-keybindings.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-keybindings"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+#include <libide-core.h>
+
+#include "ide-keybindings.h"
+
+struct _IdeKeybindings
+{
+  GObject         parent_instance;
+
+  GtkCssProvider *css_provider;
+  gchar          *mode;
+  GHashTable     *plugin_providers;
+
+  guint           constructed : 1;
+};
+
+enum
+{
+  PROP_0,
+  PROP_MODE,
+  LAST_PROP
+};
+
+G_DEFINE_TYPE (IdeKeybindings, ide_keybindings, G_TYPE_OBJECT)
+
+static GParamSpec *properties [LAST_PROP];
+
+IdeKeybindings *
+ide_keybindings_new (const gchar *mode)
+{
+  return g_object_new (IDE_TYPE_KEYBINDINGS,
+                       "mode", mode,
+                       NULL);
+}
+
+static void
+ide_keybindings_load_plugin (IdeKeybindings *self,
+                             PeasPluginInfo *plugin_info,
+                             PeasEngine     *engine)
+{
+  g_autofree gchar *path = NULL;
+  const gchar *module_name;
+  g_autoptr(GBytes) bytes = NULL;
+  g_autoptr(GtkCssProvider) provider = NULL;
+
+  g_assert (IDE_IS_KEYBINDINGS (self));
+  g_assert (plugin_info != NULL);
+  g_assert (PEAS_IS_ENGINE (engine));
+
+  if (!self->mode || !self->plugin_providers)
+    return;
+
+  module_name = peas_plugin_info_get_module_name (plugin_info);
+  path = g_strdup_printf ("/plugins/%s/keybindings/%s.css", module_name, self->mode);
+  bytes = g_resources_lookup_data (path, 0, NULL);
+  if (bytes == NULL)
+    return;
+
+  IDE_TRACE_MSG ("Loading %s keybindings for \"%s\" plugin", self->mode, module_name);
+
+  provider = gtk_css_provider_new ();
+  gtk_css_provider_load_from_resource (provider, path);
+  gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+                                             GTK_STYLE_PROVIDER (provider),
+                                             GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1);
+  g_hash_table_insert (self->plugin_providers,
+                       g_strdup (module_name),
+                       g_steal_pointer (&provider));
+}
+
+static void
+ide_keybindings_unload_plugin (IdeKeybindings *self,
+                               PeasPluginInfo *plugin_info,
+                               PeasEngine     *engine)
+{
+  GtkStyleProvider *provider;
+  const gchar *module_name;
+
+  g_assert (IDE_IS_KEYBINDINGS (self));
+  g_assert (plugin_info != NULL);
+  g_assert (PEAS_IS_ENGINE (engine));
+
+  if (self->plugin_providers == NULL)
+    return;
+
+  module_name = peas_plugin_info_get_module_name (plugin_info);
+  provider = g_hash_table_lookup (self->plugin_providers, module_name);
+  if (provider == NULL)
+    return;
+
+  gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (), provider);
+  g_hash_table_remove (self->plugin_providers, module_name);
+}
+
+static void
+ide_keybindings_reload (IdeKeybindings *self)
+{
+  GdkScreen *screen;
+  PeasEngine *engine;
+  const GList *list;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_KEYBINDINGS (self));
+
+  {
+    g_autofree gchar *path = NULL;
+    g_autoptr(GBytes) bytes = NULL;
+    g_autoptr(GError) error = NULL;
+
+    if (self->mode == NULL)
+      self->mode = g_strdup ("default");
+
+    IDE_TRACE_MSG ("Loading %s keybindings", self->mode);
+    path = g_strdup_printf ("/org/gnome/builder/keybindings/%s.css", self->mode);
+    bytes = g_resources_lookup_data (path, G_RESOURCE_LOOKUP_FLAGS_NONE, &error);
+
+    if (bytes == NULL)
+      {
+        g_clear_pointer (&path, g_free);
+        path = g_strdup_printf ("/plugins/%s/keybindings/%s.css", self->mode, self->mode);
+        bytes = g_resources_lookup_data (path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
+        if (bytes != NULL)
+          g_clear_error (&error);
+      }
+
+    if (error == NULL)
+      {
+        /*
+         * We use -1 for the length so that the CSS provider knows that the
+         * string is \0 terminated. This is guaranteed to us by GResources so
+         * that interned data can be used as C strings.
+         */
+        gtk_css_provider_load_from_data (self->css_provider,
+                                         g_bytes_get_data (bytes, NULL),
+                                         -1,
+                                         &error);
+      }
+
+    if (error)
+      g_warning ("%s", error->message);
+  }
+
+  engine = peas_engine_get_default ();
+  screen = gdk_screen_get_default ();
+
+  if (self->plugin_providers != NULL)
+    {
+      GHashTableIter iter;
+      GtkStyleProvider *provider;
+
+      g_hash_table_iter_init (&iter, self->plugin_providers);
+      while (g_hash_table_iter_next (&iter, NULL, (gpointer *)&provider))
+        gtk_style_context_remove_provider_for_screen (screen, provider);
+
+      g_clear_pointer (&self->plugin_providers, g_hash_table_unref);
+    }
+
+  self->plugin_providers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+
+  list = peas_engine_get_plugin_list (engine);
+
+  for (; list != NULL; list = list->next)
+    {
+      PeasPluginInfo *plugin_info = list->data;
+
+      if (!peas_plugin_info_is_loaded (plugin_info))
+        continue;
+
+      ide_keybindings_load_plugin (self, plugin_info, engine);
+    }
+
+  IDE_EXIT;
+}
+
+const gchar *
+ide_keybindings_get_mode (IdeKeybindings *self)
+{
+  g_return_val_if_fail (IDE_IS_KEYBINDINGS (self), NULL);
+
+  return self->mode;
+}
+
+void
+ide_keybindings_set_mode (IdeKeybindings *self,
+                          const gchar    *mode)
+{
+  g_return_if_fail (IDE_IS_KEYBINDINGS (self));
+
+  if (!dzl_str_equal0 (self->mode, mode))
+    {
+      g_free (self->mode);
+      self->mode = g_strdup (mode);
+
+      if (self->constructed)
+        ide_keybindings_reload (self);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODE]);
+    }
+}
+
+static void
+ide_keybindings_parsing_error (GtkCssProvider *css_provider,
+                               GtkCssSection  *section,
+                               GError         *error,
+                               gpointer        user_data)
+{
+  g_autofree gchar *filename = NULL;
+  GFile *file;
+  guint start_line;
+  guint end_line;
+
+  file = gtk_css_section_get_file (section);
+  filename = g_file_get_uri (file);
+  start_line = gtk_css_section_get_start_line (section);
+  end_line = gtk_css_section_get_end_line (section);
+
+  g_warning ("CSS parsing error in %s between lines %u and %u", filename, start_line, end_line);
+}
+
+static void
+ide_keybindings_constructed (GObject *object)
+{
+  IdeKeybindings *self = (IdeKeybindings *)object;
+  PeasEngine *engine;
+  GdkScreen *screen;
+
+  IDE_ENTRY;
+
+  self->constructed = TRUE;
+
+  G_OBJECT_CLASS (ide_keybindings_parent_class)->constructed (object);
+
+  screen = gdk_screen_get_default ();
+  engine = peas_engine_get_default ();
+
+  g_signal_connect_object (engine,
+                           "load-plugin",
+                           G_CALLBACK (ide_keybindings_load_plugin),
+                           self,
+                           G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (engine,
+                           "unload-plugin",
+                           G_CALLBACK (ide_keybindings_unload_plugin),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_style_context_add_provider_for_screen (screen, GTK_STYLE_PROVIDER (self->css_provider),
+                                             GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+  ide_keybindings_reload (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_keybindings_finalize (GObject *object)
+{
+  IdeKeybindings *self = (IdeKeybindings *)object;
+
+  IDE_ENTRY;
+
+  g_clear_object (&self->css_provider);
+  g_clear_pointer (&self->mode, g_free);
+  g_clear_pointer (&self->plugin_providers, g_hash_table_unref);
+
+  G_OBJECT_CLASS (ide_keybindings_parent_class)->finalize (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_keybindings_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeKeybindings *self = IDE_KEYBINDINGS (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODE:
+      g_value_set_string (value, ide_keybindings_get_mode (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_keybindings_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeKeybindings *self = IDE_KEYBINDINGS (object);
+
+  switch (prop_id)
+    {
+    case PROP_MODE:
+      ide_keybindings_set_mode (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_keybindings_class_init (IdeKeybindingsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_keybindings_constructed;
+  object_class->finalize = ide_keybindings_finalize;
+  object_class->get_property = ide_keybindings_get_property;
+  object_class->set_property = ide_keybindings_set_property;
+
+  properties [PROP_MODE] =
+    g_param_spec_string ("mode",
+                         "Mode",
+                         "The name of the keybindings mode.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_keybindings_init (IdeKeybindings *self)
+{
+  self->css_provider = gtk_css_provider_new ();
+
+  g_signal_connect (self->css_provider,
+                    "parsing-error",
+                    G_CALLBACK (ide_keybindings_parsing_error),
+                    NULL);
+}
diff --git a/src/libide/gui/ide-keybindings.h b/src/libide/gui/ide-keybindings.h
new file mode 100644
index 000000000..5756734ea
--- /dev/null
+++ b/src/libide/gui/ide-keybindings.h
@@ -0,0 +1,36 @@
+/* ide-keybindings.h
+ *
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_KEYBINDINGS (ide_keybindings_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeKeybindings, ide_keybindings, IDE, KEYBINDINGS, GObject)
+
+IdeKeybindings *ide_keybindings_new      (const gchar    *mode);
+const gchar    *ide_keybindings_get_mode (IdeKeybindings *self);
+void            ide_keybindings_set_mode (IdeKeybindings *self,
+                                          const gchar    *name);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-marked-view.c b/src/libide/gui/ide-marked-view.c
new file mode 100644
index 000000000..0edfa8f1f
--- /dev/null
+++ b/src/libide/gui/ide-marked-view.c
@@ -0,0 +1,112 @@
+/* ide-marked-view.c
+ *
+ * Copyright 2018-2019 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 "ide-marked-view"
+
+#include "config.h"
+
+#include <webkit2/webkit2.h>
+
+#include "gs-markdown-private.h"
+#include "ide-marked-view.h"
+
+struct _IdeMarkedView
+{
+  GtkBin parent_instance;
+};
+
+G_DEFINE_TYPE (IdeMarkedView, ide_marked_view, GTK_TYPE_BIN)
+
+static void
+ide_marked_view_class_init (IdeMarkedViewClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_css_name (widget_class, "markedview");
+}
+
+static void
+ide_marked_view_init (IdeMarkedView *self)
+{
+}
+
+GtkWidget *
+ide_marked_view_new (IdeMarkedContent *content)
+{
+  g_autofree gchar *markup = NULL;
+  GtkWidget *child = NULL;
+  IdeMarkedView *self;
+  IdeMarkedKind kind;
+
+  g_return_val_if_fail (content != NULL, NULL);
+
+  self = g_object_new (IDE_TYPE_MARKED_VIEW, NULL);
+  kind = ide_marked_content_get_kind (content);
+  markup = ide_marked_content_as_string (content);
+
+  switch (kind)
+    {
+    default:
+    case IDE_MARKED_KIND_PLAINTEXT:
+    case IDE_MARKED_KIND_PANGO:
+      child = g_object_new (GTK_TYPE_LABEL,
+                            "max-width-chars", 80,
+                            "wrap", TRUE,
+                            "xalign", 0.0f,
+                            "visible", TRUE,
+                            "use-markup", kind == IDE_MARKED_KIND_PANGO,
+                            "label", markup,
+                            NULL);
+      break;
+
+    case IDE_MARKED_KIND_HTML:
+      child = g_object_new (WEBKIT_TYPE_WEB_VIEW,
+                            "visible", TRUE,
+                            NULL);
+      webkit_web_view_load_html (WEBKIT_WEB_VIEW (child), markup, NULL);
+      break;
+
+    case IDE_MARKED_KIND_MARKDOWN:
+      {
+        g_autoptr(GsMarkdown) md = gs_markdown_new (GS_MARKDOWN_OUTPUT_PANGO);
+        g_autofree gchar *parsed = NULL;
+
+        gs_markdown_set_smart_quoting (md, TRUE);
+        gs_markdown_set_autocode (md, TRUE);
+        gs_markdown_set_autolinkify (md, TRUE);
+
+        if ((parsed = gs_markdown_parse (md, markup)))
+          child = g_object_new (GTK_TYPE_LABEL,
+                                "max-width-chars", 80,
+                                "wrap", TRUE,
+                                "xalign", 0.0f,
+                                "visible", TRUE,
+                                "use-markup", TRUE,
+                                "label", parsed,
+                                NULL);
+      }
+      break;
+    }
+
+  if (child != NULL)
+    gtk_container_add (GTK_CONTAINER (self), child);
+
+  return GTK_WIDGET (self);
+}
diff --git a/src/libide/gui/ide-marked-view.h b/src/libide/gui/ide-marked-view.h
new file mode 100644
index 000000000..b424b868a
--- /dev/null
+++ b/src/libide/gui/ide-marked-view.h
@@ -0,0 +1,37 @@
+/* ide-marked-view.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+#include <libide-core.h>
+#include <libide-io.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_MARKED_VIEW (ide_marked_view_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeMarkedView, ide_marked_view, IDE, MARKED_VIEW, GtkBin)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_marked_view_new (IdeMarkedContent *content);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notification-list-box-row-private.h 
b/src/libide/gui/ide-notification-list-box-row-private.h
new file mode 100644
index 000000000..b0a603ff5
--- /dev/null
+++ b/src/libide/gui/ide-notification-list-box-row-private.h
@@ -0,0 +1,38 @@
+/* ide-notification-list-box-row-private.h
+ *
+ * Copyright 2018-2019 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-core.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATION_LIST_BOX_ROW (ide_notification_list_box_row_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeNotificationListBoxRow, ide_notification_list_box_row, IDE, 
NOTIFICATION_LIST_BOX_ROW, GtkListBoxRow)
+
+GtkWidget       *ide_notification_list_box_row_new              (IdeNotification           *notification);
+IdeNotification *ide_notification_list_box_row_get_notification (IdeNotificationListBoxRow *self);
+void             ide_notification_list_box_row_set_compact      (IdeNotificationListBoxRow *self,
+                                                                 gboolean                   compact);
+gboolean         ide_notification_list_box_row_get_compact      (IdeNotificationListBoxRow *self);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notification-list-box-row.c b/src/libide/gui/ide-notification-list-box-row.c
new file mode 100644
index 000000000..8bc0ca5fe
--- /dev/null
+++ b/src/libide/gui/ide-notification-list-box-row.c
@@ -0,0 +1,377 @@
+/* ide-notification-list-box-row.c
+ *
+ * Copyright 2018-2019 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 "ide-notification-list-box-row"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-gui-private.h"
+#include "ide-notification-list-box-row-private.h"
+
+struct _IdeNotificationListBoxRow
+{
+  GtkListBoxRow    parent_instance;
+
+  IdeNotification *notification;
+
+  GtkLabel        *body;
+  GtkLabel        *title;
+  GtkBox          *lower_button_area;
+  GtkBox          *side_button_area;
+  GtkBox          *buttons;
+  GtkProgressBar  *progress;
+
+  guint            compact : 1;
+};
+
+G_DEFINE_TYPE (IdeNotificationListBoxRow, ide_notification_list_box_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+  PROP_0,
+  PROP_COMPACT,
+  PROP_NOTIFICATION,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+setup_buttons_locked (IdeNotificationListBoxRow *self)
+{
+  g_autofree gchar *body = NULL;
+  g_autofree gchar *title = NULL;
+  guint n_buttons;
+
+  g_assert (IDE_IS_NOTIFICATION_LIST_BOX_ROW (self));
+  g_assert (self->notification != NULL);
+
+  title = ide_notification_dup_title (self->notification);
+  body = ide_notification_dup_body (self->notification);
+
+  n_buttons = ide_notification_get_n_buttons (self->notification);
+
+  for (guint i = 0; i < n_buttons; i++)
+    {
+      g_autofree gchar *action = NULL;
+      g_autofree gchar *label = NULL;
+      g_autoptr(GIcon) icon = NULL;
+      g_autoptr(GVariant) target = NULL;
+
+      if (ide_notification_get_button (self->notification, i, &label, &icon, &action, &target))
+        {
+          GtkButton *button;
+          GtkWidget *child = NULL;
+
+          if (action == NULL || (label == NULL && icon == NULL))
+            continue;
+
+          if (label != NULL && (!self->compact || icon == NULL))
+            child = g_object_new (GTK_TYPE_LABEL,
+                                  "label", label,
+                                  "visible", TRUE,
+                                  NULL);
+          else if (icon != NULL)
+            child = g_object_new (GTK_TYPE_IMAGE,
+                                  "icon-size", GTK_ICON_SIZE_MENU,
+                                  "gicon", icon,
+                                  "visible", TRUE,
+                                  NULL);
+
+          g_assert (GTK_IS_WIDGET (child));
+
+          button = g_object_new (GTK_TYPE_TOGGLE_BUTTON,
+                                 "child", child,
+                                 "action-name", action,
+                                 "action-target", target,
+                                 "visible", TRUE,
+                                 NULL);
+
+          if (!self->compact)
+            dzl_gtk_widget_add_style_class (GTK_WIDGET (button), "suggested-action");
+          else
+            dzl_gtk_widget_add_style_class (GTK_WIDGET (button), "circular");
+
+          g_assert (GTK_IS_WIDGET (button));
+
+          gtk_container_add_with_properties (GTK_CONTAINER (self->buttons), GTK_WIDGET (button),
+                                             "pack-type", GTK_PACK_END,
+                                             NULL);
+        }
+    }
+
+  /* Always show labels when compact+buttons for alignment. */
+  gtk_widget_set_visible (GTK_WIDGET (self->body),
+                          !ide_str_empty0 (body) || (self->compact && n_buttons > 0));
+  gtk_widget_set_visible (GTK_WIDGET (self->title),
+                          !ide_str_empty0 (title) || (self->compact && n_buttons > 0));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->buttons), n_buttons > 0);
+}
+
+/**
+ * ide_notification_list_box_row_new:
+ *
+ * Create a new #IdeNotificationListBoxRow.
+ *
+ * Returns: (transfer full): a newly created #IdeNotificationListBoxRow
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_notification_list_box_row_new (IdeNotification *notification)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (notification), NULL);
+
+  return g_object_new (IDE_TYPE_NOTIFICATION_LIST_BOX_ROW,
+                       "notification", notification,
+                       NULL);
+}
+
+static void
+ide_notification_list_box_row_constructed (GObject *object)
+{
+  IdeNotificationListBoxRow *self = (IdeNotificationListBoxRow *)object;
+  g_autofree gchar *body = NULL;
+  g_autofree gchar *title = NULL;
+
+  g_assert (IDE_IS_NOTIFICATION_LIST_BOX_ROW (self));
+
+  if (self->notification == NULL)
+    {
+      g_warning ("%s created without an IdeNotification!",
+                 G_OBJECT_TYPE_NAME (self));
+      goto chain_up;
+    }
+
+  ide_object_lock (IDE_OBJECT (self->notification));
+
+  body = ide_notification_dup_body (self->notification);
+  title = ide_notification_dup_title (self->notification);
+
+  g_object_bind_property (self->notification, "title", self->title, "label", G_BINDING_SYNC_CREATE);
+  g_object_bind_property (self->notification, "body", self->body, "label", G_BINDING_SYNC_CREATE);
+
+  /* Always show labels when compact+buttons for alignment. */
+  gtk_widget_set_visible (GTK_WIDGET (self->body),
+                          !ide_str_empty0 (body) ||
+                          (self->compact && ide_notification_get_n_buttons (self->notification)));
+  gtk_widget_set_visible (GTK_WIDGET (self->title),
+                          !ide_str_empty0 (title) ||
+                          (self->compact && ide_notification_get_n_buttons (self->notification)));
+
+  if (ide_notification_get_urgent (self->notification))
+    dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "needs-attention");
+
+  gtk_widget_set_visible (GTK_WIDGET (self->progress),
+                          ide_notification_get_has_progress (self->notification));
+  g_object_bind_property (self->notification, "progress",
+                          self->progress, "fraction",
+                          G_BINDING_SYNC_CREATE);
+
+  setup_buttons_locked (self);
+
+  if (ide_notification_get_progress_is_imprecise (self->notification))
+    _ide_gtk_progress_bar_start_pulsing (self->progress);
+
+  ide_object_unlock (IDE_OBJECT (self->notification));
+
+chain_up:
+  G_OBJECT_CLASS (ide_notification_list_box_row_parent_class)->constructed (object);
+}
+
+static void
+ide_notification_list_box_row_destroy (GtkWidget *widget)
+{
+  IdeNotificationListBoxRow *self = (IdeNotificationListBoxRow *)widget;
+
+  if (self->progress != NULL)
+    _ide_gtk_progress_bar_stop_pulsing (self->progress);
+
+  g_clear_object (&self->notification);
+
+  GTK_WIDGET_CLASS (ide_notification_list_box_row_parent_class)->destroy (widget);
+}
+
+static void
+ide_notification_list_box_row_get_property (GObject    *object,
+                                            guint       prop_id,
+                                            GValue     *value,
+                                            GParamSpec *pspec)
+{
+  IdeNotificationListBoxRow *self = IDE_NOTIFICATION_LIST_BOX_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMPACT:
+      g_value_set_boolean (value, ide_notification_list_box_row_get_compact (self));
+      break;
+
+    case PROP_NOTIFICATION:
+      g_value_set_object (value, self->notification);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_list_box_row_set_property (GObject      *object,
+                                            guint         prop_id,
+                                            const GValue *value,
+                                            GParamSpec   *pspec)
+{
+  IdeNotificationListBoxRow *self = IDE_NOTIFICATION_LIST_BOX_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMPACT:
+      ide_notification_list_box_row_set_compact (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_NOTIFICATION:
+      self->notification = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_list_box_row_class_init (IdeNotificationListBoxRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = ide_notification_list_box_row_constructed;
+  object_class->get_property = ide_notification_list_box_row_get_property;
+  object_class->set_property = ide_notification_list_box_row_set_property;
+
+  widget_class->destroy = ide_notification_list_box_row_destroy;
+
+  properties [PROP_COMPACT] =
+    g_param_spec_boolean ("compact",
+                          "Compact",
+                          "If the compact button mode should be used",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_NOTIFICATION] =
+    g_param_spec_object ("notification",
+                         "Notification",
+                         "The notification to display",
+                         IDE_TYPE_NOTIFICATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-notification-list-box-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, body);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, buttons);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, lower_button_area);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, progress);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, side_button_area);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationListBoxRow, title);
+}
+
+static void
+ide_notification_list_box_row_init (IdeNotificationListBoxRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+/**
+ * ide_notification_list_box_row_get_notification:
+ * @self: a #IdeNotificationListBoxRow
+ *
+ * Returns: (transfer none) (nullable): an #IdeNotification
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notification_list_box_row_get_notification (IdeNotificationListBoxRow *self)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_LIST_BOX_ROW (self), NULL);
+
+  return self->notification;
+}
+
+gboolean
+ide_notification_list_box_row_get_compact (IdeNotificationListBoxRow *self)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_LIST_BOX_ROW (self), FALSE);
+
+  return self->compact;
+}
+
+void
+ide_notification_list_box_row_set_compact (IdeNotificationListBoxRow *self,
+                                           gboolean                   compact)
+{
+  GtkBox *parent;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION_LIST_BOX_ROW (self));
+
+  if (self->compact != compact)
+    {
+      self->compact = compact;
+
+      g_object_ref (self->buttons);
+
+      gtk_container_foreach (GTK_CONTAINER (self->buttons),
+                             (GtkCallback)gtk_widget_destroy,
+                             NULL);
+
+      parent = GTK_BOX (gtk_widget_get_parent (GTK_WIDGET (self->buttons)));
+      gtk_container_remove (GTK_CONTAINER (parent), GTK_WIDGET (self->buttons));
+      gtk_widget_hide (GTK_WIDGET (parent));
+
+      if (compact)
+        parent = self->side_button_area;
+      else
+        parent = self->lower_button_area;
+
+      gtk_container_add_with_properties (GTK_CONTAINER (parent), GTK_WIDGET (self->buttons),
+                                         "pack-type", GTK_PACK_END,
+                                         NULL);
+
+      g_object_unref (self->buttons);
+
+      gtk_label_set_width_chars (self->title, self->compact ? 35 : 50);
+      gtk_label_set_max_width_chars (self->title, self->compact ? 35 : 50);
+
+      gtk_label_set_width_chars (self->body, self->compact ? 35 : 50);
+      gtk_label_set_max_width_chars (self->body, self->compact ? 35 : 50);
+
+      if (self->notification != NULL)
+        {
+          ide_object_lock (IDE_OBJECT (self->notification));
+          setup_buttons_locked (self);
+          gtk_widget_set_visible (GTK_WIDGET (parent),
+                                  ide_notification_get_n_buttons (self->notification) > 0);
+          ide_object_unlock (IDE_OBJECT (self->notification));
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMPACT]);
+    }
+}
diff --git a/src/libide/gui/ide-notification-list-box-row.ui b/src/libide/gui/ide-notification-list-box-row.ui
new file mode 100644
index 000000000..b74f8eaea
--- /dev/null
+++ b/src/libide/gui/ide-notification-list-box-row.ui
@@ -0,0 +1,112 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <template class="IdeNotificationListBoxRow" parent="GtkListBoxRow">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkGrid" id="grid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_start">12</property>
+        <property name="margin_end">12</property>
+        <property name="margin_top">12</property>
+        <property name="margin_bottom">12</property>
+        <property name="row_spacing">6</property>
+        <property name="column_spacing">12</property>
+        <property name="baseline_row">2</property>
+        <child>
+          <object class="GtkProgressBar" id="progress">
+            <property name="name">progress</property>
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="valign">baseline</property>
+            <property name="hexpand">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <style>
+              <class name="title"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="body">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="wrap">True</property>
+            <property name="width_chars">50</property>
+            <property name="max_width_chars">50</property>
+            <property name="xalign">0</property>
+            <style>
+              <class name="body"/>
+            </style>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="lower_button_area">
+            <property name="margin_top">6</property>
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="hexpand">True</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkBox" id="buttons">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <child>
+                  <placeholder/>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">3</property>
+            <property name="width">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="side_button_area">
+            <property name="can_focus">False</property>
+            <property name="valign">start</property>
+            <property name="margin_top">10</property>
+            <child>
+              <placeholder/>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">0</property>
+            <property name="height">3</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <style>
+      <class name="notification"/>
+    </style>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-notification-stack-private.h b/src/libide/gui/ide-notification-stack-private.h
new file mode 100644
index 000000000..df9f2e0ca
--- /dev/null
+++ b/src/libide/gui/ide-notification-stack-private.h
@@ -0,0 +1,44 @@
+/* ide-notification-stack-private.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATION_STACK (ide_notification_stack_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeNotificationStack, ide_notification_stack, IDE, NOTIFICATION_STACK, GtkStack)
+
+GtkWidget       *ide_notification_stack_new           (void);
+void             ide_notification_stack_bind_model    (IdeNotificationStack *self,
+                                                       GListModel           *notifications);
+gboolean         ide_notification_stack_is_empty      (IdeNotificationStack *self);
+gboolean         ide_notification_stack_get_can_move  (IdeNotificationStack *self);
+void             ide_notification_stack_move_next     (IdeNotificationStack *self);
+void             ide_notification_stack_move_previous (IdeNotificationStack *self);
+IdeNotification *ide_notification_stack_get_visible   (IdeNotificationStack *self);
+gdouble          ide_notification_stack_get_progress  (IdeNotificationStack *self);
+void             ide_notification_stack_set_progress  (IdeNotificationStack *self,
+                                                       gdouble               progress);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notification-stack.c b/src/libide/gui/ide-notification-stack.c
new file mode 100644
index 000000000..9d33b6f47
--- /dev/null
+++ b/src/libide/gui/ide-notification-stack.c
@@ -0,0 +1,405 @@
+/* ide-notification-stack.c
+ *
+ * Copyright 2018-2019 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 "ide-notification-stack"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-notification-stack-private.h"
+#include "ide-notification-view-private.h"
+
+#define CAROUSEL_TIMEOUT_SECS 5
+#define TRANSITION_DURATION   500
+
+struct _IdeNotificationStack
+{
+  GtkStack         parent_instance;
+  DzlSignalGroup  *signals;
+  DzlBindingGroup *bindings;
+  GListModel      *model;
+  gdouble          progress;
+  guint            carousel_source;
+  guint            in_carousel : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_PROGRESS,
+  N_PROPS
+};
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE (IdeNotificationStack, ide_notification_stack, GTK_TYPE_STACK)
+
+static guint signals [N_SIGNALS];
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_notification_stack_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  IdeNotificationStack *self = IDE_NOTIFICATION_STACK (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROGRESS:
+      g_value_set_double (value, ide_notification_stack_get_progress (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_stack_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  IdeNotificationStack *self = IDE_NOTIFICATION_STACK (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROGRESS:
+      ide_notification_stack_set_progress (self, g_value_get_double (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static gboolean
+ide_notification_stack_carousel_cb (gpointer data)
+{
+  IdeNotificationStack *self = data;
+
+  g_assert (IDE_IS_NOTIFICATION_STACK (self));
+
+  self->in_carousel = TRUE;
+  ide_notification_stack_move_next (self);
+  self->in_carousel = FALSE;
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+ide_notification_stack_items_changed_cb (IdeNotificationStack *self,
+                                         guint                 position,
+                                         guint                 removed,
+                                         guint                 added,
+                                         GListModel           *model)
+{
+  GtkWidget *urgent = NULL;
+  GList *children;
+  GList *iter;
+
+  g_assert (IDE_IS_NOTIFICATION_STACK (self));
+
+  children = gtk_container_get_children (GTK_CONTAINER (self));
+  iter = g_list_nth (children, position);
+
+  for (guint i = 0; i < removed; i++, iter = iter->next)
+    {
+      GtkWidget *child = iter->data;
+      gtk_widget_destroy (child);
+    }
+
+  g_list_free (children);
+
+  for (guint i = 0; i < added; i++)
+    {
+      g_autoptr(IdeNotification) notif = g_list_model_get_item (model, position + i);
+      GtkWidget *view = g_object_new (IDE_TYPE_NOTIFICATION_VIEW,
+                                      "notification", notif,
+                                      "visible", TRUE,
+                                      NULL);
+
+      gtk_container_add_with_properties (GTK_CONTAINER (self), view,
+                                         "position", position + i,
+                                         NULL);
+
+      if (!urgent && ide_notification_get_urgent (notif))
+        urgent = view;
+    }
+
+  if (urgent != NULL)
+    {
+      gtk_stack_set_visible_child (GTK_STACK (self), urgent);
+      g_clear_handle_id (&self->carousel_source, g_source_remove);
+    }
+
+  if (self->carousel_source == 0 && g_list_model_get_n_items (model))
+    self->carousel_source = g_timeout_add_seconds (CAROUSEL_TIMEOUT_SECS,
+                                                   ide_notification_stack_carousel_cb,
+                                                   self);
+
+  g_signal_emit (self, signals [CHANGED], 0);
+}
+
+static void
+ide_notification_stack_notify_visible_child (IdeNotificationStack *self)
+{
+  g_assert (IDE_IS_NOTIFICATION_STACK (self));
+
+  self->progress = 0.0;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+
+  dzl_binding_group_set_source (self->bindings,
+                                ide_notification_stack_get_visible (self));
+
+  g_signal_emit (self, signals [CHANGED], 0);
+}
+
+static void
+ide_notification_stack_destroy (GtkWidget *widget)
+{
+  IdeNotificationStack *self = (IdeNotificationStack *)widget;
+
+  if (self->signals != NULL)
+    dzl_signal_group_set_target (self->signals, NULL);
+
+  if (self->bindings != NULL)
+    dzl_binding_group_set_source (self->bindings, NULL);
+
+  g_clear_object (&self->bindings);
+  g_clear_object (&self->signals);
+  g_clear_handle_id (&self->carousel_source, g_source_remove);
+
+  GTK_WIDGET_CLASS (ide_notification_stack_parent_class)->destroy (widget);
+}
+
+static void
+ide_notification_stack_class_init (IdeNotificationStackClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_notification_stack_get_property;
+  object_class->set_property = ide_notification_stack_set_property;
+
+  widget_class->destroy = ide_notification_stack_destroy;
+
+  properties [PROP_PROGRESS] =
+    g_param_spec_double ("progress",
+                         "Progress",
+                         "The progress of the current item",
+                         0.0, 1.0, 0.0,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE, 0);
+
+  gtk_widget_class_set_css_name (widget_class, "notificationstack");
+}
+
+static void
+ide_notification_stack_init (IdeNotificationStack *self)
+{
+  self->signals = dzl_signal_group_new (G_TYPE_LIST_MODEL);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "items-changed",
+                                   G_CALLBACK (ide_notification_stack_items_changed_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  self->bindings = dzl_binding_group_new ();
+
+  dzl_binding_group_bind (self->bindings, "progress", self, "progress",
+                          G_BINDING_SYNC_CREATE);
+
+  gtk_stack_set_transition_duration (GTK_STACK (self), TRANSITION_DURATION);
+  gtk_stack_set_transition_type (GTK_STACK (self), GTK_STACK_TRANSITION_TYPE_SLIDE_UP_DOWN);
+
+  g_signal_connect (self,
+                    "notify::visible-child",
+                    G_CALLBACK (ide_notification_stack_notify_visible_child),
+                    NULL);
+}
+
+void
+ide_notification_stack_bind_model (IdeNotificationStack *self,
+                                   GListModel           *model)
+{
+  g_return_if_fail (IDE_IS_NOTIFICATION_STACK (self));
+  g_return_if_fail (!model || G_IS_LIST_MODEL (model));
+  g_return_if_fail (!model ||
+                    g_type_is_a (g_list_model_get_item_type (model), IDE_TYPE_NOTIFICATION));
+
+  if (g_set_object (&self->model, model))
+    {
+      guint n_items = 0;
+
+      if (model != NULL)
+        n_items = g_list_model_get_n_items (model);
+
+      gtk_container_foreach (GTK_CONTAINER (self), (GtkCallback)gtk_widget_destroy, NULL);
+      dzl_signal_group_set_target (self->signals, model);
+
+      if (n_items > 0)
+        ide_notification_stack_items_changed_cb (self, 0, 0, n_items, model);
+    }
+}
+
+gboolean
+ide_notification_stack_get_can_move (IdeNotificationStack *self)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_STACK (self), FALSE);
+
+  if (self->model != NULL)
+    return g_list_model_get_n_items (self->model) > 1;
+  else
+    return FALSE;
+}
+
+void
+ide_notification_stack_move_next (IdeNotificationStack *self)
+{
+  GtkWidget *child;
+  gint position;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION_STACK (self));
+
+  if ((child = gtk_stack_get_visible_child (GTK_STACK (self))))
+    {
+      GList *children;
+
+      gtk_container_child_get (GTK_CONTAINER (self), child,
+                               "position", &position,
+                               NULL);
+      children = gtk_container_get_children (GTK_CONTAINER (self));
+      if (!(child = g_list_nth_data (children, position + 1)))
+        child = children->data;
+      g_list_free (children);
+
+      gtk_stack_set_transition_type (GTK_STACK (self), GTK_STACK_TRANSITION_TYPE_SLIDE_DOWN);
+      gtk_stack_set_visible_child (GTK_STACK (self), child);
+      gtk_stack_set_transition_type (GTK_STACK (self), GTK_STACK_TRANSITION_TYPE_SLIDE_UP_DOWN);
+
+      if (!self->in_carousel)
+        g_clear_handle_id (&self->carousel_source, g_source_remove);
+    }
+}
+
+void
+ide_notification_stack_move_previous (IdeNotificationStack *self)
+{
+  GtkWidget *child;
+  gint position;
+
+  g_return_if_fail (IDE_IS_NOTIFICATION_STACK (self));
+
+  if ((child = gtk_stack_get_visible_child (GTK_STACK (self))))
+    {
+      GList *children;
+
+      gtk_container_child_get (GTK_CONTAINER (self), child,
+                               "position", &position,
+                               NULL);
+      children = gtk_container_get_children (GTK_CONTAINER (self));
+      if (position == 0)
+        child = g_list_last (children)->data;
+      else
+        child = g_list_nth_data (children, position - 1);
+      g_list_free (children);
+
+      gtk_stack_set_transition_type (GTK_STACK (self), GTK_STACK_TRANSITION_TYPE_SLIDE_UP);
+      gtk_stack_set_visible_child (GTK_STACK (self), child);
+      gtk_stack_set_transition_type (GTK_STACK (self), GTK_STACK_TRANSITION_TYPE_SLIDE_UP_DOWN);
+
+      if (!self->in_carousel)
+        g_clear_handle_id (&self->carousel_source, g_source_remove);
+    }
+}
+
+/**
+ * ide_notification_stack_get_visible:
+ * @self: a #IdeNotificationStack
+ *
+ * Gets the visible notification in the stack.
+ *
+ * Returns: (transfer none) (nullable): an #IdeNotification or %NULL
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notification_stack_get_visible (IdeNotificationStack *self)
+{
+  GtkWidget *child;
+
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_STACK (self), NULL);
+
+  if ((child = gtk_stack_get_visible_child (GTK_STACK (self))))
+    {
+      if (IDE_IS_NOTIFICATION_VIEW (child))
+        return ide_notification_view_get_notification (IDE_NOTIFICATION_VIEW (child));
+    }
+
+  return NULL;
+}
+
+gdouble
+ide_notification_stack_get_progress (IdeNotificationStack *self)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_STACK (self), 0.0);
+
+  return self->progress;
+}
+
+void
+ide_notification_stack_set_progress (IdeNotificationStack *self,
+                                     gdouble               progress)
+{
+  g_return_if_fail (IDE_IS_NOTIFICATION_STACK (self));
+
+  progress = CLAMP (progress, 0.0, 1.0);
+
+  if (progress != self->progress)
+    {
+      self->progress = progress;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROGRESS]);
+    }
+}
+
+gboolean
+ide_notification_stack_is_empty (IdeNotificationStack *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_STACK (self), FALSE);
+
+  return self->model == NULL || g_list_model_get_n_items (self->model) == 0;
+}
diff --git a/src/libide/gui/ide-notification-view-private.h b/src/libide/gui/ide-notification-view-private.h
new file mode 100644
index 000000000..017a83fa5
--- /dev/null
+++ b/src/libide/gui/ide-notification-view-private.h
@@ -0,0 +1,37 @@
+/* ide-notification-view-private.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATION_VIEW (ide_notification_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeNotificationView, ide_notification_view, IDE, NOTIFICATION_VIEW, GtkBin)
+
+GtkWidget       *ide_notification_view_new              (void);
+IdeNotification *ide_notification_view_get_notification (IdeNotificationView *self);
+void             ide_notification_view_set_notification (IdeNotificationView *self,
+                                                         IdeNotification     *notification);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notification-view.c b/src/libide/gui/ide-notification-view.c
new file mode 100644
index 000000000..8b160aee9
--- /dev/null
+++ b/src/libide/gui/ide-notification-view.c
@@ -0,0 +1,291 @@
+/* ide-notification-view.c
+ *
+ * Copyright 2018-2019 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 "ide-notification-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-notification-view-private.h"
+
+struct _IdeNotificationView
+{
+  GtkBin           parent_instance;
+
+  IdeNotification *notification;
+  DzlBindingGroup *bindings;
+
+  GtkLabel        *label;
+  GtkBox          *buttons;
+  GtkButton       *default_button;
+  GtkImage        *default_button_image;
+};
+
+G_DEFINE_TYPE (IdeNotificationView, ide_notification_view, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_NOTIFICATION,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_notification_view_notify_icon (IdeNotificationView *self,
+                                   GParamSpec          *pspec,
+                                   IdeNotification     *notif)
+{
+  g_autoptr(GIcon) icon = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_NOTIFICATION_VIEW (self));
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  icon = ide_notification_ref_icon (notif);
+  gtk_image_set_from_gicon (self->default_button_image, icon, GTK_ICON_SIZE_MENU);
+  gtk_widget_set_visible (GTK_WIDGET (self->default_button), icon != NULL);
+}
+
+static void
+connect_notification (IdeNotificationView *self,
+                      IdeNotification     *notification)
+{
+  g_autofree gchar *action_name = NULL;
+  g_autoptr(GVariant) target_value = NULL;
+  g_autoptr(GIcon) icon = NULL;
+  guint n_buttons;
+
+  g_assert (IDE_IS_NOTIFICATION_VIEW (self));
+  g_assert (!notification || IDE_IS_NOTIFICATION (notification));
+
+  gtk_container_foreach (GTK_CONTAINER (self->buttons), (GtkCallback)gtk_widget_destroy, NULL);
+
+  if (notification == NULL)
+    {
+      gtk_widget_hide (GTK_WIDGET (self->label));
+      gtk_widget_hide (GTK_WIDGET (self->default_button));
+      gtk_widget_hide (GTK_WIDGET (self->buttons));
+      return;
+    }
+
+  g_signal_connect_object (notification,
+                           "notify::icon",
+                           G_CALLBACK (ide_notification_view_notify_icon),
+                           self,
+                           G_CONNECT_SWAPPED);
+  ide_notification_view_notify_icon (self, NULL, notification);
+
+  /*
+   * Setup the default action button (which is shown right after the label
+   * containing notification title).
+   */
+
+  if (ide_notification_get_default_action (notification, &action_name, &target_value))
+    {
+      gtk_actionable_set_action_name (GTK_ACTIONABLE (self->default_button), action_name);
+      gtk_actionable_set_action_target_value (GTK_ACTIONABLE (self->default_button), target_value);
+    }
+
+  /*
+   * Now add all of the buttons requested by the notification.
+   */
+
+  ide_object_lock (IDE_OBJECT (notification));
+
+  n_buttons = ide_notification_get_n_buttons (notification);
+
+  for (guint i = 0; i < n_buttons; i++)
+    {
+      g_autofree gchar *action = NULL;
+      g_autofree gchar *label = NULL;
+      g_autoptr(GIcon) button_icon = NULL;
+      g_autoptr(GVariant) target = NULL;
+
+      if (ide_notification_get_button (notification, i, &label, &button_icon, &action, &target) &&
+          button_icon != NULL &&
+          action_name != NULL)
+        {
+          GtkButton *button;
+
+          button = g_object_new (GTK_TYPE_BUTTON,
+                                 "child", g_object_new (GTK_TYPE_IMAGE,
+                                                        "gicon", button_icon,
+                                                        "visible", TRUE,
+                                                        NULL),
+                                 "action-name", action,
+                                 "action-target", target,
+                                 "has-tooltip", TRUE,
+                                 "tooltip-text", label,
+                                 "visible", TRUE,
+                                 NULL);
+          gtk_container_add (GTK_CONTAINER (self->buttons), GTK_WIDGET (button));
+        }
+    }
+
+  ide_object_unlock (IDE_OBJECT (notification));
+}
+
+static void
+ide_notification_view_finalize (GObject *object)
+{
+  IdeNotificationView *self = (IdeNotificationView *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_object (&self->bindings);
+  g_clear_object (&self->notification);
+
+  G_OBJECT_CLASS (ide_notification_view_parent_class)->finalize (object);
+}
+
+static void
+ide_notification_view_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  IdeNotificationView *self = IDE_NOTIFICATION_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_NOTIFICATION:
+      g_value_set_object (value, ide_notification_view_get_notification (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_view_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  IdeNotificationView *self = IDE_NOTIFICATION_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_NOTIFICATION:
+      ide_notification_view_set_notification (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_notification_view_class_init (IdeNotificationViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_notification_view_finalize;
+  object_class->get_property = ide_notification_view_get_property;
+  object_class->set_property = ide_notification_view_set_property;
+
+  /**
+   * IdeNotificationView:notification:
+   *
+   * The "notification" property is the #IdeNotification to be displayed.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_NOTIFICATION] =
+    g_param_spec_object ("notification",
+                         "Notification",
+                         "The IdeNotification to be viewed",
+                         IDE_TYPE_NOTIFICATION,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-notification-view.ui");
+  gtk_widget_class_set_css_name (widget_class, "notification");
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationView, label);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationView, buttons);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationView, default_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationView, default_button_image);
+}
+
+static void
+ide_notification_view_init (IdeNotificationView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->bindings = dzl_binding_group_new ();
+
+  dzl_binding_group_bind (self->bindings, "title", self->label, "label", G_BINDING_SYNC_CREATE);
+}
+
+/**
+ * ide_notification_view_new:
+ *
+ * Create a new #IdeNotificationView to visualize a notification within
+ * the #IdeOmniBar.
+ *
+ * Returns: (transfer full): a newly created #IdeNotificationView
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_notification_view_new (void)
+{
+  return g_object_new (IDE_TYPE_NOTIFICATION_VIEW, NULL);
+}
+
+/**
+ * ide_notification_view_get_notification:
+ * @self: an #IdeNotificationView
+ *
+ * Gets the #IdeNotification that is being viewed.
+ *
+ * Returns: (transfer none) (nullable): an #IdeNotification or %NULL
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+ide_notification_view_get_notification (IdeNotificationView *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_NOTIFICATION_VIEW (self), NULL);
+
+  return self->notification;
+}
+
+void
+ide_notification_view_set_notification (IdeNotificationView *self,
+                                        IdeNotification     *notification)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_NOTIFICATION_VIEW (self));
+  g_return_if_fail (!notification || IDE_IS_NOTIFICATION (notification));
+
+  if (g_set_object (&self->notification, notification))
+    {
+      dzl_binding_group_set_source (self->bindings, notification);
+      connect_notification (self, notification);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NOTIFICATION]);
+    }
+}
diff --git a/src/libide/gui/ide-notification-view.ui b/src/libide/gui/ide-notification-view.ui
new file mode 100644
index 000000000..bc7b177cf
--- /dev/null
+++ b/src/libide/gui/ide-notification-view.ui
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <template class="IdeNotificationView" parent="GtkBin">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="spacing">6</property>
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="ellipsize">end</property>
+            <property name="margin-start">6</property>
+            <property name="visible">True</property>
+            <property name="width_chars">5</property>
+            <property name="can_focus">False</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="default_button">
+            <property name="can_focus">True</property>
+            <property name="focus_on_click">False</property>
+            <property name="receives_default">True</property>
+            <child>
+              <object class="GtkImage" id="default_button_image">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="stock">gtk-missing-image</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox" id="buttons">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">end</property>
+            <property name="hexpand">True</property>
+            <property name="spacing">6</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
+
diff --git a/src/libide/gui/ide-notifications-button-popover-private.h 
b/src/libide/gui/ide-notifications-button-popover-private.h
new file mode 100644
index 000000000..180506cfc
--- /dev/null
+++ b/src/libide/gui/ide-notifications-button-popover-private.h
@@ -0,0 +1,31 @@
+/* ide-notifications-button-popover-private.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATIONS_BUTTON_POPOVER (ide_notifications_button_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeNotificationsButtonPopover, ide_notifications_button_popover, IDE, 
NOTIFICATIONS_BUTTON_POPOVER, GtkPopover)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notifications-button-popover.c 
b/src/libide/gui/ide-notifications-button-popover.c
new file mode 100644
index 000000000..b89e45be9
--- /dev/null
+++ b/src/libide/gui/ide-notifications-button-popover.c
@@ -0,0 +1,51 @@
+/* ide-notifications-button-popover.c
+ *
+ * Copyright 2018-2019 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 "ide-notifications-button-popover"
+
+#include "config.h"
+
+#include "ide-notifications-button-popover-private.h"
+
+struct _IdeNotificationsButtonPopover
+{
+  GtkPopover parent_instance;
+};
+
+G_DEFINE_TYPE (IdeNotificationsButtonPopover, ide_notifications_button_popover, GTK_TYPE_POPOVER)
+
+static GtkSizeRequestMode
+ide_notifications_button_popover_get_request_mode (GtkWidget *widget)
+{
+  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+ide_notifications_button_popover_class_init (IdeNotificationsButtonPopoverClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->get_request_mode = ide_notifications_button_popover_get_request_mode;
+}
+
+static void
+ide_notifications_button_popover_init (IdeNotificationsButtonPopover *self)
+{
+}
diff --git a/src/libide/gui/ide-notifications-button.c b/src/libide/gui/ide-notifications-button.c
new file mode 100644
index 000000000..a9e47e41e
--- /dev/null
+++ b/src/libide/gui/ide-notifications-button.c
@@ -0,0 +1,217 @@
+/* ide-notifications-button.c
+ *
+ * Copyright 2018-2019 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 "ide-notifications-button"
+
+#include "config.h"
+
+#include "ide-notifications-button.h"
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+
+/**
+ * SECTION:ide-notifications-button:
+ * @title: IdeNotificationsButton
+ * @short_description: a popover menu button containing progress notifications
+ *
+ * The #IdeNotificationsButton shows ongoing notifications that have progress.
+ * The individual notifications are displayed in a popover with appropriate
+ * progress show for each.
+ *
+ * The button itself will show a "combined" progress of all the active
+ * notifications.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeNotificationsButton
+{
+  DzlProgressMenuButton  parent_instance;
+
+  GListModel            *model;
+  DzlListModelFilter    *filter;
+
+  /* Template widgets */
+  GtkPopover            *popover;
+  GtkListBox            *list_box;
+};
+
+G_DEFINE_TYPE (IdeNotificationsButton, ide_notifications_button, DZL_TYPE_PROGRESS_MENU_BUTTON)
+
+static GtkWidget *
+create_notification_row (gpointer item,
+                         gpointer user_data)
+{
+  IdeNotification *notif = item;
+  gboolean has_default;
+
+  g_assert (IDE_IS_NOTIFICATION (notif));
+  g_assert (IDE_IS_NOTIFICATIONS_BUTTON (user_data));
+
+  has_default = ide_notification_get_default_action (notif, NULL, NULL);
+
+  return g_object_new (IDE_TYPE_NOTIFICATION_LIST_BOX_ROW,
+                       "activatable", has_default,
+                       "compact", TRUE,
+                       "notification", item,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static gboolean
+filter_by_has_progress (GObject  *object,
+                        gpointer  user_data)
+{
+  IdeNotification *notif = (IdeNotification *)object;
+
+  g_assert (IDE_IS_NOTIFICATION (notif));
+  g_assert (user_data == NULL);
+
+  return ide_notification_get_has_progress (notif);
+}
+
+static void
+ide_notifications_button_bind_model (IdeNotificationsButton *self,
+                                     GListModel             *model)
+{
+  g_assert (IDE_IS_NOTIFICATIONS_BUTTON (self));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  if (g_set_object (&self->model, model))
+    {
+      g_clear_object (&self->filter);
+
+      self->filter = dzl_list_model_filter_new (model);
+      dzl_list_model_filter_set_filter_func (self->filter,
+                                             filter_by_has_progress,
+                                             NULL, NULL);
+
+      gtk_list_box_bind_model (self->list_box,
+                               G_LIST_MODEL (self->filter),
+                               create_notification_row,
+                               self, NULL);
+    }
+}
+
+static void
+ide_notifications_button_context_set_cb (GtkWidget  *widget,
+                                         IdeContext *context)
+{
+  IdeNotificationsButton *self = (IdeNotificationsButton *)widget;
+  g_autoptr(IdeNotifications) notifications = NULL;
+
+  g_assert (IDE_IS_NOTIFICATIONS_BUTTON (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  notifications = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_NOTIFICATIONS);
+  ide_notifications_button_bind_model (self, G_LIST_MODEL (notifications));
+
+  g_object_bind_property (notifications, "progress", self, "progress",
+                          G_BINDING_SYNC_CREATE);
+  g_object_bind_property (notifications, "has-progress", self, "visible",
+                          G_BINDING_SYNC_CREATE);
+  g_object_bind_property (notifications, "progress-is-imprecise", self, "show-progress",
+                          G_BINDING_INVERT_BOOLEAN | G_BINDING_SYNC_CREATE);
+}
+
+static void
+ide_notifications_button_row_activated (IdeNotificationsButton    *self,
+                                        IdeNotificationListBoxRow *row,
+                                        GtkListBox                *list_box)
+{
+  g_autofree gchar *default_action = NULL;
+  g_autoptr(GVariant) default_target = NULL;
+  IdeNotification *notif;
+
+  g_assert (IDE_IS_NOTIFICATIONS_BUTTON (self));
+  g_assert (IDE_IS_NOTIFICATION_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  notif = ide_notification_list_box_row_get_notification (row);
+
+  if (ide_notification_get_default_action (notif, &default_action, &default_target))
+    {
+      gchar *name = strchr (default_action, '.');
+      gchar *group = default_action;
+
+      if (name != NULL)
+        {
+          *name = '\0';
+          name++;
+        }
+      else
+        {
+          group = NULL;
+          name = default_action;
+        }
+
+      dzl_gtk_widget_action (GTK_WIDGET (list_box), group, name, default_target);
+    }
+}
+
+static void
+ide_notifications_button_destroy (GtkWidget *widget)
+{
+  IdeNotificationsButton *self = (IdeNotificationsButton *)widget;
+
+  g_assert (IDE_IS_NOTIFICATIONS_BUTTON (self));
+
+  g_clear_object (&self->filter);
+  g_clear_object (&self->model);
+
+  GTK_WIDGET_CLASS (ide_notifications_button_parent_class)->destroy (widget);
+}
+
+static void
+ide_notifications_button_class_init (IdeNotificationsButtonClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->destroy = ide_notifications_button_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-notifications-button.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationsButton, list_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeNotificationsButton, popover);
+  gtk_widget_class_bind_template_callback (widget_class, ide_notifications_button_row_activated);
+}
+
+static void
+ide_notifications_button_init (IdeNotificationsButton *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  ide_widget_set_context_handler (GTK_WIDGET (self),
+                                  ide_notifications_button_context_set_cb);
+}
+
+/**
+ * ide_notifications_button_new:
+ *
+ * Create a new #IdeNotificationsButton.
+ *
+ * Returns: (transfer full): a newly created #IdeNotificationsButton
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_notifications_button_new (void)
+{
+  return g_object_new (IDE_TYPE_NOTIFICATIONS_BUTTON, NULL);
+}
diff --git a/src/libide/gui/ide-notifications-button.h b/src/libide/gui/ide-notifications-button.h
new file mode 100644
index 000000000..703d63f4b
--- /dev/null
+++ b/src/libide/gui/ide-notifications-button.h
@@ -0,0 +1,40 @@
+/* ide-notifications-button.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <dazzle.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_NOTIFICATIONS_BUTTON (ide_notifications_button_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeNotificationsButton, ide_notifications_button, IDE, NOTIFICATIONS_BUTTON, 
DzlProgressMenuButton)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_notifications_button_new (void);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-notifications-button.ui b/src/libide/gui/ide-notifications-button.ui
new file mode 100644
index 000000000..ebf209a45
--- /dev/null
+++ b/src/libide/gui/ide-notifications-button.ui
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeNotificationsButton" parent="DzlProgressMenuButton">
+    <property name="show-progress">false</property>
+    <property name="popover">popover</property>
+  </template>
+  <object class="GtkPopover" id="popover">
+    <style>
+      <class name="notificationsbutton"/>
+    </style>
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="visible">true</property>
+        <property name="max-content-width">400</property>
+        <property name="min-content-width">400</property>
+        <property name="hscrollbar-policy">never</property>
+        <property name="propagate-natural-width">false</property>
+        <property name="propagate-natural-height">true</property>
+        <child>
+          <object class="GtkListBox" id="list_box">
+            <signal name="row-activated" handler="ide_notifications_button_row_activated" swapped="true" 
object="IdeNotificationsButton"/>
+            <property name="selection-mode">none</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
+
+
+
diff --git a/src/libide/gui/ide-omni-bar-addin.c b/src/libide/gui/ide-omni-bar-addin.c
new file mode 100644
index 000000000..49d6a3b30
--- /dev/null
+++ b/src/libide/gui/ide-omni-bar-addin.c
@@ -0,0 +1,89 @@
+/* ide-omni-bar-addin.c
+ *
+ * Copyright 2018-2019 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 "ide-omni-bar-addin"
+
+#include "config.h"
+
+#include "ide-omni-bar-addin.h"
+
+/**
+ * SECTION:ide-omni-bar-addin
+ * @title: IdeOmniBarAddin
+ * @short_description: addins to extend the #IdeOmniBar
+ *
+ * The #IdeOmniBarAddin allows plugins to extend how the #IdeOmniBar
+ * works. They can add additional components such as buttons, or more
+ * information to the popover.
+ *
+ * See #IdeOmniBar for information about what you can alter.
+ *
+ * Since: 3.32
+ */
+
+G_DEFINE_INTERFACE (IdeOmniBarAddin, ide_omni_bar_addin, G_TYPE_OBJECT)
+
+static void
+ide_omni_bar_addin_default_init (IdeOmniBarAddinInterface *iface)
+{
+}
+
+/**
+ * ide_omni_bar_addin_load:
+ * @self: an #IdeOmniBarAddin
+ * @omni_bar: an #IdeOmniBar
+ *
+ * Requests that the #IdeOmniBarAddin initialize, possibly modifying
+ * @omni_bar as necessary.
+ *
+ * Since: 3.32
+ */
+void
+ide_omni_bar_addin_load (IdeOmniBarAddin *self,
+                         IdeOmniBar      *omni_bar)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR_ADDIN (self));
+  g_return_if_fail (IDE_IS_OMNI_BAR (omni_bar));
+
+  if (IDE_OMNI_BAR_ADDIN_GET_IFACE (self)->load)
+    IDE_OMNI_BAR_ADDIN_GET_IFACE (self)->load (self, omni_bar);
+}
+
+/**
+ * ide_omni_bar_addin_unload:
+ * @self: an #IdeOmniBarAddin
+ * @omni_bar: an #IdeOmniBar
+ *
+ * Requests that the #IdeOmniBarAddin shutdown, possibly modifying
+ * @omni_bar as necessary to return it to the original state before
+ * the addin was loaded.
+ *
+ * Since: 3.32
+ */
+void
+ide_omni_bar_addin_unload (IdeOmniBarAddin *self,
+                           IdeOmniBar      *omni_bar)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR_ADDIN (self));
+  g_return_if_fail (IDE_IS_OMNI_BAR (omni_bar));
+
+  if (IDE_OMNI_BAR_ADDIN_GET_IFACE (self)->unload)
+    IDE_OMNI_BAR_ADDIN_GET_IFACE (self)->unload (self, omni_bar);
+}
diff --git a/src/libide/gui/ide-omni-bar-addin.h b/src/libide/gui/ide-omni-bar-addin.h
new file mode 100644
index 000000000..0b2290d39
--- /dev/null
+++ b/src/libide/gui/ide-omni-bar-addin.h
@@ -0,0 +1,55 @@
+/* ide-omni-bar-addin.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-omni-bar.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_OMNI_BAR_ADDIN (ide_omni_bar_addin_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeOmniBarAddin, ide_omni_bar_addin, IDE, OMNI_BAR_ADDIN, GObject)
+
+struct _IdeOmniBarAddinInterface
+{
+  GTypeInterface parent;
+
+  void (*load)   (IdeOmniBarAddin *self,
+                  IdeOmniBar      *omni_bar);
+  void (*unload) (IdeOmniBarAddin *self,
+                  IdeOmniBar      *omni_bar);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_omni_bar_addin_load   (IdeOmniBarAddin *self,
+                                IdeOmniBar      *omni_bar);
+IDE_AVAILABLE_IN_3_32
+void ide_omni_bar_addin_unload (IdeOmniBarAddin *self,
+                                IdeOmniBar      *omni_bar);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-omni-bar.c b/src/libide/gui/ide-omni-bar.c
new file mode 100644
index 000000000..fbf8c6399
--- /dev/null
+++ b/src/libide/gui/ide-omni-bar.c
@@ -0,0 +1,619 @@
+/* ide-omni-bar.c
+ *
+ * Copyright 2018-2019 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 "ide-omni-bar"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <dazzle.h>
+
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-notification-list-box-row-private.h"
+#include "ide-notification-stack-private.h"
+#include "ide-omni-bar-addin.h"
+#include "ide-omni-bar.h"
+
+struct _IdeOmniBar
+{
+  GtkEventBox           parent_instance;
+
+  PeasExtensionSet     *addins;
+  GtkGesture           *gesture;
+  GtkEventController   *motion;
+
+  GtkStack             *top_stack;
+  GtkPopover           *popover;
+  DzlEntryBox          *entry_box;
+  IdeNotificationStack *notification_stack;
+  GtkListBox           *notifications_list_box;
+  DzlPriorityBox       *inner_box;
+  DzlPriorityBox       *outer_box;
+  GtkProgressBar       *progress;
+  GtkWidget            *placeholder;
+  DzlPriorityBox       *sections_box;
+};
+
+static void ide_omni_bar_move_next     (IdeOmniBar        *self,
+                                        GVariant          *param);
+static void ide_omni_bar_move_previous (IdeOmniBar        *self,
+                                        GVariant          *param);
+static void buildable_iface_init       (GtkBuildableIface *iface);
+
+DZL_DEFINE_ACTION_GROUP (IdeOmniBar, ide_omni_bar, {
+  { "move-next", ide_omni_bar_move_next },
+  { "move-previous", ide_omni_bar_move_previous },
+})
+
+G_DEFINE_TYPE_WITH_CODE (IdeOmniBar, ide_omni_bar, GTK_TYPE_EVENT_BOX,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, ide_omni_bar_init_action_group)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+static void
+ide_omni_bar_popover_closed_cb (IdeOmniBar *self,
+                                GtkPopover *popover)
+{
+  GtkStyleContext *style_context;
+  GtkStateFlags state_flags;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_POPOVER (popover));
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  state_flags = gtk_style_context_get_state (style_context);
+
+  state_flags &= ~GTK_STATE_FLAG_ACTIVE;
+  state_flags &= ~GTK_STATE_FLAG_PRELIGHT;
+
+  gtk_style_context_set_state (style_context, state_flags);
+}
+
+static void
+multipress_pressed_cb (IdeOmniBar           *self,
+                       guint                 n_press,
+                       gdouble               x,
+                       gdouble               y,
+                       GtkGestureMultiPress *gesture)
+{
+  GtkStyleContext *style_context;
+  GtkStateFlags state_flags;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));
+
+  gtk_popover_popup (self->popover);
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  state_flags = gtk_style_context_get_state (style_context);
+  gtk_style_context_set_state (style_context, state_flags | GTK_STATE_FLAG_ACTIVE);
+
+  gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+static void
+ide_omni_bar_notification_stack_changed_cb (IdeOmniBar           *self,
+                                            IdeNotificationStack *stack)
+{
+  IdeNotification *notif;
+  gboolean enabled;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (IDE_IS_NOTIFICATION_STACK (stack));
+
+  enabled = ide_notification_stack_get_can_move (stack);
+
+  ide_omni_bar_set_action_enabled (self, "move-previous", enabled);
+  ide_omni_bar_set_action_enabled (self, "move-next", enabled);
+
+  _ide_gtk_progress_bar_stop_pulsing (self->progress);
+  gtk_widget_hide (GTK_WIDGET (self->progress));
+
+  if ((notif = ide_notification_stack_get_visible (stack)))
+    {
+      if (ide_notification_get_has_progress (notif))
+        {
+          if (ide_notification_get_progress_is_imprecise (notif))
+            _ide_gtk_progress_bar_start_pulsing (self->progress);
+          gtk_widget_show (GTK_WIDGET (self->progress));
+        }
+    }
+
+  if (ide_notification_stack_is_empty (stack))
+    gtk_stack_set_visible_child_name (self->top_stack, "placeholder");
+  else
+    gtk_stack_set_visible_child_name (self->top_stack, "notifications");
+}
+
+static void
+ide_omni_bar_extension_added_cb (PeasExtensionSet *set,
+                                 PeasPluginInfo   *plugin_info,
+                                 PeasExtension    *exten,
+                                 gpointer          user_data)
+{
+  IdeOmniBarAddin *addin = (IdeOmniBarAddin *)exten;
+  IdeOmniBar *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_OMNI_BAR_ADDIN (addin));
+  g_assert (IDE_IS_OMNI_BAR (self));
+
+  ide_omni_bar_addin_load (addin, self);
+}
+
+static void
+ide_omni_bar_extension_removed_cb (PeasExtensionSet *set,
+                                   PeasPluginInfo   *plugin_info,
+                                   PeasExtension    *exten,
+                                   gpointer          user_data)
+{
+  IdeOmniBarAddin *addin = (IdeOmniBarAddin *)exten;
+  IdeOmniBar *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_OMNI_BAR_ADDIN (addin));
+  g_assert (IDE_IS_OMNI_BAR (self));
+
+  ide_omni_bar_addin_unload (addin, self);
+}
+
+static GtkWidget *
+create_notification_row (gpointer item,
+                         gpointer user_data)
+{
+  IdeNotification *notif = item;
+  gboolean has_default;
+
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  has_default = ide_notification_get_default_action (notif, NULL, NULL);
+
+  return g_object_new (IDE_TYPE_NOTIFICATION_LIST_BOX_ROW,
+                       "activatable", has_default,
+                       "notification", notif,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static gboolean
+filter_for_popover (GObject  *object,
+                    gpointer  user_data)
+{
+  IdeNotification *notif = (IdeNotification *)object;
+
+  g_assert (IDE_IS_NOTIFICATION (notif));
+  g_assert (user_data == NULL);
+
+  return !ide_notification_get_has_progress (notif) &&
+         ide_notification_get_urgent (notif);
+}
+
+static void
+ide_omni_bar_context_set_cb (GtkWidget  *widget,
+                             IdeContext *context)
+{
+  IdeOmniBar *self = (IdeOmniBar *)widget;
+  g_autoptr(IdeObject) notifications = NULL;
+  g_autoptr(DzlListModelFilter) filter = NULL;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (IDE_IS_CONTEXT (context));
+  g_assert (self->addins == NULL);
+
+  notifications = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_NOTIFICATIONS);
+  ide_notification_stack_bind_model (self->notification_stack, G_LIST_MODEL (notifications));
+
+  filter = dzl_list_model_filter_new (G_LIST_MODEL (notifications));
+  dzl_list_model_filter_set_filter_func (filter, filter_for_popover, NULL, NULL);
+  gtk_list_box_bind_model (self->notifications_list_box,
+                           G_LIST_MODEL (filter),
+                           create_notification_row,
+                           NULL, NULL);
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_OMNI_BAR_ADDIN,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_omni_bar_extension_added_cb),
+                    self);
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_omni_bar_extension_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_omni_bar_extension_added_cb,
+                              self);
+}
+
+static void
+ide_omni_bar_motion_enter_cb (IdeOmniBar               *self,
+                              gdouble                   x,
+                              gdouble                   y,
+                              GtkEventControllerMotion *motion)
+{
+  GtkStyleContext *style_context;
+  GtkStateFlags state_flags;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  state_flags = gtk_style_context_get_state (style_context);
+
+  if ((state_flags & GTK_STATE_FLAG_PRELIGHT) == 0)
+    gtk_style_context_set_state (style_context, state_flags | GTK_STATE_FLAG_PRELIGHT);
+}
+
+static void
+ide_omni_bar_motion_leave_cb (IdeOmniBar               *self,
+                              GtkEventControllerMotion *motion)
+{
+  GtkStyleContext *style_context;
+  GtkStateFlags state_flags;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  state_flags = gtk_style_context_get_state (style_context);
+
+  if (state_flags & GTK_STATE_FLAG_PRELIGHT)
+    gtk_style_context_set_state (style_context, state_flags & ~GTK_STATE_FLAG_PRELIGHT);
+}
+
+static void
+ide_omni_bar_motion_cb (IdeOmniBar               *self,
+                        gdouble                   x,
+                        gdouble                   y,
+                        GtkEventControllerMotion *motion)
+{
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_EVENT_CONTROLLER_MOTION (motion));
+
+  /*
+   * Because of how crossing-events work with Gtk 3, we don't get reliable
+   * crossing events for the motion controller. So every motion (which we do
+   * seem to get semi-reliably), just re-run the enter-notify path to ensure
+   * we get proper state set.
+   */
+
+  ide_omni_bar_motion_enter_cb (self, x, y, motion);
+}
+
+static gboolean
+ide_omni_bar_query_tooltip (GtkWidget  *widget,
+                            gint        x,
+                            gint        y,
+                            gboolean    keyboard_mode,
+                            GtkTooltip *tooltip)
+{
+  IdeOmniBar *self = (IdeOmniBar *)widget;
+  IdeNotification *notif;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_TOOLTIP (tooltip));
+
+  if ((notif = ide_notification_stack_get_visible (self->notification_stack)))
+    {
+      g_autofree gchar *body = ide_notification_dup_body (notif);
+
+      if (body != NULL)
+        {
+          gtk_tooltip_set_text (tooltip, body);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+ide_omni_bar_notification_row_activated (IdeOmniBar                *self,
+                                         IdeNotificationListBoxRow *row,
+                                         GtkListBox                *list_box)
+{
+  g_autofree gchar *default_action = NULL;
+  g_autoptr(GVariant) default_target = NULL;
+  IdeNotification *notif;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (IDE_IS_NOTIFICATION_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  notif = ide_notification_list_box_row_get_notification (row);
+
+  if (ide_notification_get_default_action (notif, &default_action, &default_target))
+    {
+      gchar *name = strchr (default_action, '.');
+      gchar *group = default_action;
+
+      if (name != NULL)
+        {
+          *name = '\0';
+          name++;
+        }
+      else
+        {
+          group = NULL;
+          name = default_action;
+        }
+
+      dzl_gtk_widget_action (GTK_WIDGET (list_box), group, name, default_target);
+    }
+}
+
+static void
+ide_omni_bar_destroy (GtkWidget *widget)
+{
+  IdeOmniBar *self = (IdeOmniBar *)widget;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+
+  if (self->progress != NULL)
+    _ide_gtk_progress_bar_stop_pulsing (self->progress);
+
+  g_clear_object (&self->addins);
+  g_clear_object (&self->gesture);
+  g_clear_object (&self->motion);
+
+  GTK_WIDGET_CLASS (ide_omni_bar_parent_class)->destroy (widget);
+}
+
+static void
+ide_omni_bar_class_init (IdeOmniBarClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->destroy = ide_omni_bar_destroy;
+  widget_class->query_tooltip = ide_omni_bar_query_tooltip;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-omni-bar.ui");
+  gtk_widget_class_set_css_name (widget_class, "omnibar");
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, entry_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, inner_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, notification_stack);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, notifications_list_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, outer_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, popover);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, progress);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, sections_box);
+  gtk_widget_class_bind_template_child (widget_class, IdeOmniBar, top_stack);
+  gtk_widget_class_bind_template_callback (widget_class, ide_omni_bar_notification_row_activated);
+
+  g_type_ensure (DZL_TYPE_ENTRY_BOX);
+  g_type_ensure (IDE_TYPE_NOTIFICATION_STACK);
+}
+
+static void
+ide_omni_bar_init (IdeOmniBar *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_widget_set_has_tooltip (GTK_WIDGET (self), TRUE);
+
+  gtk_widget_add_events (GTK_WIDGET (self),
+                         (GDK_POINTER_MOTION_MASK |
+                          GDK_ENTER_NOTIFY_MASK |
+                          GDK_LEAVE_NOTIFY_MASK));
+
+  self->motion = gtk_event_controller_motion_new (GTK_WIDGET (self));
+  gtk_event_controller_set_propagation_phase (self->motion, GTK_PHASE_CAPTURE);
+
+  g_signal_connect_swapped (self->motion,
+                            "enter",
+                            G_CALLBACK (ide_omni_bar_motion_enter_cb),
+                            self);
+
+  g_signal_connect_swapped (self->motion,
+                            "motion",
+                            G_CALLBACK (ide_omni_bar_motion_cb),
+                            self);
+
+  g_signal_connect_swapped (self->motion,
+                            "leave",
+                            G_CALLBACK (ide_omni_bar_motion_leave_cb),
+                            self);
+
+  self->gesture = gtk_gesture_multi_press_new (GTK_WIDGET (self));
+
+  g_signal_connect_swapped (self->gesture,
+                            "pressed",
+                            G_CALLBACK (multipress_pressed_cb),
+                            self);
+
+  g_signal_connect_object (self->notification_stack,
+                           "changed",
+                           G_CALLBACK (ide_omni_bar_notification_stack_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->popover,
+                           "closed",
+                           G_CALLBACK (ide_omni_bar_popover_closed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "omnibar", G_ACTION_GROUP (self));
+
+  ide_widget_set_context_handler (GTK_WIDGET (self), ide_omni_bar_context_set_cb);
+}
+
+GtkWidget *
+ide_omni_bar_new (void)
+{
+  return g_object_new (IDE_TYPE_OMNI_BAR, NULL);
+}
+
+static void
+ide_omni_bar_move_next (IdeOmniBar *self,
+                        GVariant   *param)
+{
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (param == NULL);
+
+  ide_notification_stack_move_next (self->notification_stack);
+}
+
+static void
+ide_omni_bar_move_previous (IdeOmniBar *self,
+                            GVariant   *param)
+{
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (param == NULL);
+
+  ide_notification_stack_move_previous (self->notification_stack);
+}
+
+/**
+ * ide_omni_bar_add_status_icon:
+ * @self: a #IdeOmniBar
+ * @widget: the #GtkWidget to add
+ * @priority: the sort priority for @widget
+ *
+ * Adds a status-icon style widget to the end of the omnibar. Generally,
+ * you'll want this to be either a GtkButton, GtkLabel, or something simple.
+ *
+ * Since: 3.32
+ */
+void
+ide_omni_bar_add_status_icon (IdeOmniBar *self,
+                              GtkWidget  *widget,
+                              gint        priority)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->inner_box), widget,
+                                     "pack-type", GTK_PACK_END,
+                                     "priority", priority,
+                                     NULL);
+}
+
+void
+ide_omni_bar_add_button (IdeOmniBar  *self,
+                         GtkWidget   *widget,
+                         GtkPackType  pack_type,
+                         gint         priority)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+  g_return_if_fail (pack_type == GTK_PACK_START ||
+                    pack_type == GTK_PACK_END);
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->outer_box), widget,
+                                     "pack-type", pack_type,
+                                     "priority", priority,
+                                     NULL);
+}
+
+void
+ide_omni_bar_set_placeholder (IdeOmniBar *self,
+                              GtkWidget  *widget)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR (self));
+  g_return_if_fail (!widget || GTK_IS_WIDGET (widget));
+
+  if (self->placeholder == widget)
+    return;
+
+  if (self->placeholder)
+    gtk_widget_destroy (self->placeholder);
+
+  self->placeholder = widget;
+
+  if (self->placeholder)
+    {
+      g_signal_connect (self->placeholder,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        self->placeholder);
+      gtk_container_add_with_properties (GTK_CONTAINER (self->top_stack), self->placeholder,
+                                         "name", "placeholder",
+                                         NULL);
+      if (self->notification_stack == NULL ||
+          ide_notification_stack_is_empty (self->notification_stack))
+        gtk_stack_set_visible_child_name (self->top_stack, "placeholder");
+    }
+}
+
+static void
+ide_omni_bar_add_child (GtkBuildable *buildable,
+                          GtkBuilder   *builder,
+                        GObject      *child,
+                        const gchar  *type)
+{
+  IdeOmniBar *self = (IdeOmniBar *)buildable;
+
+  g_assert (IDE_IS_OMNI_BAR (self));
+  g_assert (GTK_IS_BUILDER (builder));
+  g_assert (G_IS_OBJECT (child));
+
+  if (ide_str_equal0 (type, "start") && GTK_IS_WIDGET (child))
+    ide_omni_bar_add_button (IDE_OMNI_BAR (self),
+                             GTK_WIDGET (child),
+                             GTK_PACK_START,
+                             0);
+  else if (ide_str_equal0 (type, "end") && GTK_IS_WIDGET (child))
+    ide_omni_bar_add_button (IDE_OMNI_BAR (self),
+                             GTK_WIDGET (child),
+                             GTK_PACK_END,
+                             0);
+  else if (ide_str_equal0 (type, "placeholder") && GTK_IS_WIDGET (child))
+    ide_omni_bar_set_placeholder (IDE_OMNI_BAR (self), GTK_WIDGET (child));
+  else
+    parent_buildable_iface->add_child (buildable, builder, child, type);
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  parent_buildable_iface = g_type_interface_peek_parent (iface);
+  iface->add_child = ide_omni_bar_add_child;
+}
+
+/**
+ * ide_omni_bar_add_popover_section:
+ * @self: an #IdeOmniBar
+ * @widget: a #GtkWidget
+ * @priority: sort priority for the section
+ *
+ * Adds @widget to the omnibar popover, sorted by @priority
+ *
+ * Since: 3.32
+ */
+void
+ide_omni_bar_add_popover_section (IdeOmniBar *self,
+                                  GtkWidget  *widget,
+                                  gint        priority)
+{
+  g_return_if_fail (IDE_IS_OMNI_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self->sections_box), widget,
+                                     "priority", priority,
+                                     NULL);
+}
diff --git a/src/libide/gui/ide-omni-bar.h b/src/libide/gui/ide-omni-bar.h
new file mode 100644
index 000000000..ef4a9e484
--- /dev/null
+++ b/src/libide/gui/ide-omni-bar.h
@@ -0,0 +1,56 @@
+/* ide-omni-bar.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_OMNI_BAR (ide_omni_bar_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeOmniBar, ide_omni_bar, IDE, OMNI_BAR, GtkEventBox)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_omni_bar_new                 (void);
+IDE_AVAILABLE_IN_3_32
+void       ide_omni_bar_add_status_icon     (IdeOmniBar  *self,
+                                             GtkWidget   *widget,
+                                             gint         priority);
+IDE_AVAILABLE_IN_3_32
+void       ide_omni_bar_add_button          (IdeOmniBar  *self,
+                                             GtkWidget   *widget,
+                                             GtkPackType  pack_type,
+                                             gint         priority);
+IDE_AVAILABLE_IN_3_32
+void       ide_omni_bar_set_placeholder     (IdeOmniBar  *self,
+                                             GtkWidget   *placeholder);
+IDE_AVAILABLE_IN_3_32
+void       ide_omni_bar_add_popover_section (IdeOmniBar  *self,
+                                             GtkWidget   *widget,
+                                             gint         priority);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-omni-bar.ui b/src/libide/gui/ide-omni-bar.ui
new file mode 100644
index 000000000..53591ff36
--- /dev/null
+++ b/src/libide/gui/ide-omni-bar.ui
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeOmniBar" parent="GtkEventBox">
+    <child>
+      <object class="DzlPriorityBox" id="outer_box">
+        <property name="hexpand">true</property>
+        <property name="visible">true</property>
+        <style>
+          <class name="linked"/>
+        </style>
+        <child type="center">
+          <object class="DzlEntryBox" id="entry_box">
+            <property name="max-width-chars">40</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkOverlay" id="overlay">
+                <property name="visible">true</property>
+                <child type="overlay">
+                  <object class="GtkProgressBar" id="progress">
+                    <property name="valign">end</property>
+                    <property name="hexpand">true</property>
+                    <property name="fraction" bind-source="notification_stack" bind-property="progress"/>
+                    <property name="visible">true</property>
+                    <style>
+                      <class name="osd"/>
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="DzlPriorityBox" id="inner_box">
+                    <property name="margin-top">1</property>
+                    <property name="spacing">3</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkBox">
+                        <property name="hexpand">false</property>
+                        <property name="vexpand">false</property>
+                        <property name="valign">center</property>
+                        <property name="orientation">vertical</property>
+                        <property name="visible">true</property>
+                        <style>
+                          <class name="pan"/>>
+                        </style>
+                        <child>
+                          <object class="GtkButton">
+                            <property name="action-name">omnibar.move-previous</property>
+                            <property name="visible">true</property>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="icon-name">pan-up-symbolic</property>
+                                <property name="pixel-size">12</property>
+                                <property name="visible">true</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkButton">
+                            <property name="action-name">omnibar.move-next</property>
+                            <property name="visible">true</property>
+                            <child>
+                              <object class="GtkImage">
+                                <property name="icon-name">pan-down-symbolic</property>
+                                <property name="pixel-size">12</property>
+                                <property name="visible">true</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkStack" id="top_stack">
+                        <property name="margin-start">3</property>
+                        <property name="margin-end">3</property>
+                        <property name="hexpand">true</property>
+                        <property name="visible">true</property>
+                        <child>
+                          <object class="IdeNotificationStack" id="notification_stack">
+                            <property name="visible">true</property>
+                          </object>
+                          <packing>
+                            <property name="name">notifications</property>
+                          </packing>
+                        </child>
+                      </object>
+                      <packing>
+                        <property name="expand">true</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkPopover" id="popover">
+    <property name="width-request">500</property>
+    <property name="relative-to">IdeOmniBar</property>
+    <property name="position">top</property>
+    <style>
+      <class name="omnibar"/>
+    </style>
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">12</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="DzlPriorityBox" id="sections_box">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkListBox" id="notifications_list_box">
+            <signal name="row-activated" swapped="true" object="IdeOmniBar" 
handler="ide_omni_bar_notification_row_activated"/>
+            <property name="selection-mode">none</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/src/libide/gui/ide-page.c b/src/libide/gui/ide-page.c
new file mode 100644
index 000000000..6e6c40925
--- /dev/null
+++ b/src/libide/gui/ide-page.c
@@ -0,0 +1,872 @@
+/* ide-page.c
+ *
+ * Copyright 2017-2019 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 "ide-page"
+
+#include "config.h"
+
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-page.h"
+#include "ide-workspace.h"
+
+typedef struct
+{
+  GList        mru_link;
+
+  const gchar *menu_id;
+  const gchar *icon_name;
+  gchar       *title;
+  GIcon       *icon;
+
+  GdkRGBA      primary_color_bg;
+  GdkRGBA      primary_color_fg;
+
+  guint        failed : 1;
+  guint        modified : 1;
+  guint        can_split : 1;
+  guint        primary_color_bg_set : 1;
+  guint        primary_color_fg_set : 1;
+} IdePagePrivate;
+
+enum {
+  PROP_0,
+  PROP_CAN_SPLIT,
+  PROP_FAILED,
+  PROP_ICON,
+  PROP_ICON_NAME,
+  PROP_MENU_ID,
+  PROP_MODIFIED,
+  PROP_PRIMARY_COLOR_BG,
+  PROP_PRIMARY_COLOR_FG,
+  PROP_TITLE,
+  N_PROPS
+};
+
+enum {
+  CREATE_SPLIT,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdePage, ide_page, GTK_TYPE_BOX)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_page_real_agree_to_close_async (IdePage             *self,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_PAGE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_source_tag (task, ide_page_agree_to_close_async);
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_page_real_agree_to_close_finish (IdePage       *self,
+                                     GAsyncResult  *result,
+                                     GError       **error)
+{
+  g_assert (IDE_IS_PAGE (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+find_focus_child (GtkWidget *widget,
+                  gboolean  *handled)
+{
+  if (!*handled)
+    *handled = gtk_widget_child_focus (widget, GTK_DIR_TAB_FORWARD);
+}
+
+static void
+ide_page_grab_focus (GtkWidget *widget)
+{
+  gboolean handled = FALSE;
+
+  g_assert (IDE_IS_PAGE (widget));
+
+  /*
+   * This default grab_focus override just looks for the first child (generally
+   * something like a scrolled window) and tries to move forward on focusing
+   * the child widget. In most cases, this should work without intervention
+   * from the child subclass.
+   */
+
+  gtk_container_foreach (GTK_CONTAINER (widget), (GtkCallback) find_focus_child, &handled);
+}
+
+static void
+ide_page_hierarchy_changed (GtkWidget *widget,
+                            GtkWidget *previous_toplevel)
+{
+  IdePage *self = (IdePage *)widget;
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_PAGE (self));
+  g_assert (!previous_toplevel || GTK_IS_WIDGET (previous_toplevel));
+
+  if (IDE_IS_WORKSPACE (previous_toplevel))
+    _ide_workspace_remove_page_mru (IDE_WORKSPACE (previous_toplevel), &priv->mru_link);
+
+  if (GTK_WIDGET_CLASS (ide_page_parent_class)->hierarchy_changed)
+    GTK_WIDGET_CLASS (ide_page_parent_class)->hierarchy_changed (widget, previous_toplevel);
+
+  toplevel = gtk_widget_get_toplevel (widget);
+
+  if (IDE_IS_WORKSPACE (toplevel))
+    _ide_workspace_add_page_mru (IDE_WORKSPACE (toplevel), &priv->mru_link);
+}
+
+/**
+ * ide_page_mark_used:
+ * @self: a #IdePage
+ *
+ * This function marks the page as used by updating it's position in the
+ * workspaces MRU (most-recently-used) queue.
+ *
+ * Pages should call this when their contents have been focused.
+ *
+ * Since: 3.32
+ */
+void
+ide_page_mark_used (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  IdeWorkspace *workspace;
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  if ((workspace = ide_widget_get_workspace (GTK_WIDGET (self))))
+    _ide_workspace_move_front_page_mru (workspace, &priv->mru_link);
+}
+
+static void
+ide_page_finalize (GObject *object)
+{
+  IdePage *self = (IdePage *)object;
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_clear_pointer (&priv->title, g_free);
+  g_clear_object (&priv->icon);
+
+  G_OBJECT_CLASS (ide_page_parent_class)->finalize (object);
+}
+
+static void
+ide_page_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
+{
+  IdePage *self = IDE_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CAN_SPLIT:
+      g_value_set_boolean (value, ide_page_get_can_split (self));
+      break;
+
+    case PROP_FAILED:
+      g_value_set_boolean (value, ide_page_get_failed (self));
+      break;
+
+    case PROP_ICON_NAME:
+      g_value_set_static_string (value, ide_page_get_icon_name (self));
+      break;
+
+    case PROP_ICON:
+      g_value_set_object (value, ide_page_get_icon (self));
+      break;
+
+    case PROP_MENU_ID:
+      g_value_set_static_string (value, ide_page_get_menu_id (self));
+      break;
+
+    case PROP_MODIFIED:
+      g_value_set_boolean (value, ide_page_get_modified (self));
+      break;
+
+    case PROP_PRIMARY_COLOR_BG:
+      g_value_set_boxed (value, ide_page_get_primary_color_bg (self));
+      break;
+
+    case PROP_PRIMARY_COLOR_FG:
+      g_value_set_boxed (value, ide_page_get_primary_color_fg (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, ide_page_get_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_page_set_property (GObject      *object,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
+{
+  IdePage *self = IDE_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CAN_SPLIT:
+      ide_page_set_can_split (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_FAILED:
+      ide_page_set_failed (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_ICON_NAME:
+      ide_page_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ICON:
+      ide_page_set_icon (self, g_value_get_object (value));
+      break;
+
+    case PROP_MENU_ID:
+      ide_page_set_menu_id (self, g_value_get_string (value));
+      break;
+
+    case PROP_MODIFIED:
+      ide_page_set_modified (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_PRIMARY_COLOR_BG:
+      ide_page_set_primary_color_bg (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_PRIMARY_COLOR_FG:
+      ide_page_set_primary_color_fg (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_TITLE:
+      ide_page_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_page_class_init (IdePageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_page_finalize;
+  object_class->get_property = ide_page_get_property;
+  object_class->set_property = ide_page_set_property;
+
+  widget_class->grab_focus = ide_page_grab_focus;
+  widget_class->hierarchy_changed = ide_page_hierarchy_changed;
+
+  klass->agree_to_close_async = ide_page_real_agree_to_close_async;
+  klass->agree_to_close_finish = ide_page_real_agree_to_close_finish;
+
+  properties [PROP_CAN_SPLIT] =
+    g_param_spec_boolean ("can-split",
+                          "Can Split",
+                          "If the view can be split into a second view",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FAILED] =
+    g_param_spec_boolean ("failed",
+                          "Failed",
+                          "If the view has failed or crashed",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ICON] =
+    g_param_spec_object ("icon",
+                         "Icon",
+                         "A GIcon for the view",
+                         G_TYPE_ICON,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The icon-name describing the view content",
+                         "text-x-generic-symbolic",
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MENU_ID] =
+    g_param_spec_string ("menu-id",
+                         "Menu ID",
+                         "The identifier of the GMenu to use in the document popover",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_MODIFIED] =
+    g_param_spec_boolean ("modified",
+                          "Modified",
+                          "If the view has been modified from the saved content",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdePage:primary-color-bg:
+   *
+   * The "primary-color-bg" property should describe the primary color
+   * of the content of the view (if any).
+   *
+   * This can be used by the layout stack to alter the color of the
+   * header to match that of the content.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PRIMARY_COLOR_BG] =
+    g_param_spec_boxed ("primary-color-bg",
+                        "Primary Color Background",
+                        "The primary foreground color of the content",
+                        GDK_TYPE_RGBA,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdePage:primary-color-fg:
+   *
+   * The "primary-color-fg" property should describe the foreground
+   * to use for content above primary-color-bg.
+   *
+   * This can be used by the layout stack to alter the color of the
+   * foreground to match that of the content.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PRIMARY_COLOR_FG] =
+    g_param_spec_boxed ("primary-color-fg",
+                        "Primary Color Foreground",
+                        "The primary foreground color of the content",
+                        GDK_TYPE_RGBA,
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title of the document or view",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdePage::create-split:
+   * @self: an #IdePage
+   *
+   * This signal is emitted when the view is requested to make a split
+   * version of itself. This happens when the user requests that a second
+   * version of the file to be displayed, often side-by-side.
+   *
+   * This signal will only be emitted when #IdePage:can-split is
+   * set to %TRUE. The default is %FALSE.
+   *
+   * Returns: (transfer full): A newly created #IdePage
+   *
+   * Since: 3.32
+   */
+  signals [CREATE_SPLIT] =
+    g_signal_new (g_intern_static_string ("create-split"),
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdePageClass, create_split),
+                  g_signal_accumulator_first_wins, NULL,
+                  NULL, IDE_TYPE_PAGE, 0);
+}
+
+static void
+ide_page_init (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  g_autoptr(GSimpleActionGroup) group = g_simple_action_group_new ();
+
+  gtk_orientable_set_orientation (GTK_ORIENTABLE (self), GTK_ORIENTATION_VERTICAL);
+
+  priv->mru_link.data = self;
+  priv->icon_name = g_intern_string ("text-x-generic-symbolic");
+
+  /* Add an action group out of convenience to plugins that want to
+   * stash a simple action somewhere.
+   */
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "view", G_ACTION_GROUP (group));
+}
+
+GtkWidget *
+ide_page_new (void)
+{
+  return g_object_new (IDE_TYPE_PAGE, NULL);
+}
+
+const gchar *
+ide_page_get_title (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+
+  return priv->title;
+}
+
+void
+ide_page_set_title (IdePage     *self,
+                    const gchar *title)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  if (g_strcmp0 (title, priv->title) != 0)
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+const gchar *
+ide_page_get_menu_id (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+
+  return priv->menu_id;
+}
+
+void
+ide_page_set_menu_id (IdePage     *self,
+                      const gchar *menu_id)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  menu_id = g_intern_string (menu_id);
+
+  if (menu_id != priv->menu_id)
+    {
+      priv->menu_id = menu_id;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MENU_ID]);
+    }
+}
+
+void
+ide_page_agree_to_close_async (IdePage             *self,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_PAGE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_PAGE_GET_CLASS (self)->agree_to_close_async (self, cancellable, callback, user_data);
+}
+
+gboolean
+ide_page_agree_to_close_finish (IdePage       *self,
+                                GAsyncResult  *result,
+                                GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_PAGE_GET_CLASS (self)->agree_to_close_finish (self, result, error);
+}
+
+gboolean
+ide_page_get_failed (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
+
+  return priv->failed;
+}
+
+void
+ide_page_set_failed (IdePage  *self,
+                     gboolean  failed)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  failed = !!failed;
+
+  if (failed != priv->failed)
+    {
+      priv->failed = failed;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FAILED]);
+    }
+}
+
+gboolean
+ide_page_get_modified (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
+
+  return priv->modified;
+}
+
+void
+ide_page_set_modified (IdePage  *self,
+                       gboolean  modified)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  modified = !!modified;
+
+  if (priv->modified != modified)
+    {
+      priv->modified = modified;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_MODIFIED]);
+    }
+}
+
+/**
+ * ide_page_get_icon:
+ * @self: a #IdePage
+ *
+ * Gets the #GIcon to represent the view.
+ *
+ * Returns: (transfer none) (nullable): A #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_page_get_icon (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+
+  if (priv->icon == NULL)
+    {
+      if (priv->icon_name != NULL)
+        priv->icon = g_icon_new_for_string (priv->icon_name, NULL);
+    }
+
+  return priv->icon;
+}
+
+void
+ide_page_set_icon (IdePage *self,
+                   GIcon   *icon)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  if (g_set_object (&priv->icon, icon))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON]);
+}
+
+const gchar *
+ide_page_get_icon_name (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+
+  return priv->icon_name;
+}
+
+void
+ide_page_set_icon_name (IdePage     *self,
+                        const gchar *icon_name)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  icon_name = g_intern_string (icon_name);
+
+  if (icon_name != priv->icon_name)
+    {
+      priv->icon_name = icon_name;
+      g_clear_object (&priv->icon);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+    }
+}
+
+gboolean
+ide_page_get_can_split (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PAGE (self), FALSE);
+
+  return priv->can_split;
+}
+
+void
+ide_page_set_can_split (IdePage  *self,
+                        gboolean  can_split)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  can_split = !!can_split;
+
+  if (priv->can_split != can_split)
+    {
+      priv->can_split = can_split;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CAN_SPLIT]);
+    }
+}
+
+/**
+ * ide_page_create_split:
+ * @self: an #IdePage
+ *
+ * This function requests that the #IdePage create a split version
+ * of itself so that the user may view the document in multiple views.
+ *
+ * The view should be added to an #IdeLayoutStack where appropriate.
+ *
+ * Returns: (nullable) (transfer full): A newly created #IdePage or %NULL.
+ *
+ * Since: 3.32
+ */
+IdePage *
+ide_page_create_split (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  IdePage *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+
+  if (priv->can_split)
+    {
+      g_signal_emit (self, signals [CREATE_SPLIT], 0, &ret);
+      g_return_val_if_fail (!ret || IDE_IS_PAGE (ret), NULL);
+    }
+
+  return ret;
+}
+
+/**
+ * ide_page_get_primary_color_bg:
+ * @self: a #IdePage
+ *
+ * Gets the #IdePage:primary-color-bg property if it has been set.
+ *
+ * The primary-color-bg can be used to alter the color of the layout
+ * stack header to match the document contents.
+ *
+ * Returns: (transfer none) (nullable): a #GdkRGBA or %NULL.
+ *
+ * Since: 3.32
+ */
+const GdkRGBA *
+ide_page_get_primary_color_bg (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+
+  return priv->primary_color_bg_set ?  &priv->primary_color_bg : NULL;
+}
+
+/**
+ * ide_page_set_primary_color_bg:
+ * @self: a #IdePage
+ * @primary_color_bg: (nullable): a #GdkRGBA or %NULL
+ *
+ * Sets the #IdePage:primary-color-bg property.
+ * If @primary_color_bg is %NULL, the property is unset.
+ *
+ * Since: 3.32
+ */
+void
+ide_page_set_primary_color_bg (IdePage       *self,
+                               const GdkRGBA *primary_color_bg)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  gboolean old_set;
+  GdkRGBA old;
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  old_set = priv->primary_color_bg_set;
+  old = priv->primary_color_bg;
+
+  if (primary_color_bg != NULL)
+    {
+      priv->primary_color_bg = *primary_color_bg;
+      priv->primary_color_bg_set = TRUE;
+    }
+  else
+    {
+      memset (&priv->primary_color_bg, 0, sizeof priv->primary_color_bg);
+      priv->primary_color_bg_set = FALSE;
+    }
+
+  if (old_set != priv->primary_color_bg_set ||
+      !gdk_rgba_equal (&old, &priv->primary_color_bg))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIMARY_COLOR_BG]);
+}
+
+/**
+ * ide_page_get_primary_color_fg:
+ * @self: a #IdePage
+ *
+ * Gets the #IdePage:primary-color-fg property if it has been set.
+ *
+ * The primary-color-fg can be used to alter the foreground color of the layout
+ * stack header to match the document contents.
+ *
+ * Returns: (transfer none) (nullable): a #GdkRGBA or %NULL.
+ *
+ * Since: 3.32
+ */
+const GdkRGBA *
+ide_page_get_primary_color_fg (IdePage *self)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PAGE (self), NULL);
+
+  return priv->primary_color_fg_set ?  &priv->primary_color_fg : NULL;
+}
+
+/**
+ * ide_page_set_primary_color_fg:
+ * @self: a #IdePage
+ * @primary_color_fg: (nullable): a #GdkRGBA or %NULL
+ *
+ * Sets the #IdePage:primary-color-fg property.
+ * If @primary_color_fg is %NULL, the property is unset.
+ *
+ * Since: 3.32
+ */
+void
+ide_page_set_primary_color_fg (IdePage       *self,
+                               const GdkRGBA *primary_color_fg)
+{
+  IdePagePrivate *priv = ide_page_get_instance_private (self);
+  gboolean old_set;
+  GdkRGBA old;
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  old_set = priv->primary_color_fg_set;
+  old = priv->primary_color_fg;
+
+  if (primary_color_fg != NULL)
+    {
+      priv->primary_color_fg = *primary_color_fg;
+      priv->primary_color_fg_set = TRUE;
+    }
+  else
+    {
+      memset (&priv->primary_color_fg, 0, sizeof priv->primary_color_fg);
+      priv->primary_color_fg_set = FALSE;
+    }
+
+  if (old_set != priv->primary_color_fg_set ||
+      !gdk_rgba_equal (&old, &priv->primary_color_fg))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PRIMARY_COLOR_FG]);
+}
+
+/**
+ * ide_page_report_error:
+ * @self: a #IdePage
+ * @format: a printf-style format string
+ *
+ * This function reports an error to the user in the layout view.
+ *
+ * @format should be a printf-style format string followed by the
+ * arguments for the format.
+ *
+ * Since: 3.32
+ */
+void
+ide_page_report_error (IdePage     *self,
+                       const gchar *format,
+                       ...)
+{
+  g_autofree gchar *message = NULL;
+  GtkInfoBar *infobar;
+  GtkWidget *content_area;
+  GtkLabel *label;
+  va_list args;
+
+  g_return_if_fail (IDE_IS_PAGE (self));
+
+  va_start (args, format);
+  message = g_strdup_vprintf (format, args);
+  va_end (args);
+
+  infobar = g_object_new (GTK_TYPE_INFO_BAR,
+                          "message-type", GTK_MESSAGE_WARNING,
+                          "show-close-button", TRUE,
+                          "visible", TRUE,
+                          NULL);
+  g_signal_connect (infobar,
+                    "response",
+                    G_CALLBACK (gtk_widget_destroy),
+                    NULL);
+  g_signal_connect (infobar,
+                    "close",
+                    G_CALLBACK (gtk_widget_destroy),
+                    NULL);
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label", message,
+                        "visible", TRUE,
+                        "wrap", TRUE,
+                        "xalign", 0.0f,
+                        NULL);
+
+  content_area = gtk_info_bar_get_content_area (infobar);
+  gtk_container_add (GTK_CONTAINER (content_area), GTK_WIDGET (label));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (infobar),
+                                     "position", 0,
+                                     NULL);
+}
diff --git a/src/libide/gui/ide-page.h b/src/libide/gui/ide-page.h
new file mode 100644
index 000000000..3a5c34631
--- /dev/null
+++ b/src/libide/gui/ide-page.h
@@ -0,0 +1,119 @@
+/* ide-page.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PAGE (ide_page_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdePage, ide_page, IDE, PAGE, GtkBox)
+
+struct _IdePageClass
+{
+  GtkBoxClass parent_class;
+
+  void           (*agree_to_close_async)  (IdePage              *self,
+                                           GCancellable         *cancellable,
+                                           GAsyncReadyCallback   callback,
+                                           gpointer              user_data);
+  gboolean       (*agree_to_close_finish) (IdePage              *self,
+                                           GAsyncResult         *result,
+                                           GError              **error);
+  IdePage       *(*create_split)          (IdePage              *self);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget     *ide_page_new                   (void);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_page_get_can_split         (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_can_split         (IdePage              *self,
+                                               gboolean              can_split);
+IDE_AVAILABLE_IN_3_32
+IdePage       *ide_page_create_split          (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_page_get_icon_name         (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_icon_name         (IdePage              *self,
+                                               const gchar          *icon_name);
+IDE_AVAILABLE_IN_3_32
+GIcon         *ide_page_get_icon              (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_icon              (IdePage              *self,
+                                               GIcon                *icon);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_page_get_failed            (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_failed            (IdePage              *self,
+                                               gboolean              failed);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_page_get_menu_id           (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_menu_id           (IdePage              *self,
+                                               const gchar          *menu_id);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_page_get_modified          (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_modified          (IdePage              *self,
+                                               gboolean              modified);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_page_get_title             (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_title             (IdePage              *self,
+                                               const gchar          *title);
+IDE_AVAILABLE_IN_3_32
+const GdkRGBA *ide_page_get_primary_color_bg  (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_primary_color_bg  (IdePage              *self,
+                                               const GdkRGBA        *primary_color_bg);
+IDE_AVAILABLE_IN_3_32
+const GdkRGBA *ide_page_get_primary_color_fg  (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_set_primary_color_fg  (IdePage              *self,
+                                               const GdkRGBA        *primary_color_fg);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_agree_to_close_async  (IdePage              *self,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_page_agree_to_close_finish (IdePage              *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_mark_used             (IdePage              *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_page_report_error          (IdePage              *self,
+                                               const gchar          *format,
+                                               ...) G_GNUC_PRINTF (2, 3);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-pane.c b/src/libide/gui/ide-pane.c
new file mode 100644
index 000000000..bb1c7ec0a
--- /dev/null
+++ b/src/libide/gui/ide-pane.c
@@ -0,0 +1,54 @@
+/* ide-pane.c
+ *
+ * Copyright 2018-2019 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 "ide-pane"
+
+#include "config.h"
+
+#include "ide-pane.h"
+
+G_DEFINE_TYPE (IdePane, ide_pane, DZL_TYPE_DOCK_WIDGET)
+
+static void
+ide_pane_class_init (IdePaneClass *klass)
+{
+}
+
+static void
+ide_pane_init (IdePane *self)
+{
+}
+
+/**
+ * ide_pane_new:
+ *
+ * Creates a new #IdePane widget.
+ *
+ * These widgets are meant to be added to #IdePanel widgets.
+ *
+ * Returns: (transfer full): a new #IdePane
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_pane_new (void)
+{
+  return g_object_new (IDE_TYPE_PANE, NULL);
+}
diff --git a/src/libide/gui/ide-pane.h b/src/libide/gui/ide-pane.h
new file mode 100644
index 000000000..0eed763d5
--- /dev/null
+++ b/src/libide/gui/ide-pane.h
@@ -0,0 +1,48 @@
+/* ide-pane.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PANE (ide_pane_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdePane, ide_pane, IDE, PANE, DzlDockWidget)
+
+struct _IdePaneClass
+{
+  DzlDockWidgetClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_pane_new (void);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-panel.c b/src/libide/gui/ide-panel.c
new file mode 100644
index 000000000..e3091fd85
--- /dev/null
+++ b/src/libide/gui/ide-panel.c
@@ -0,0 +1,85 @@
+/* ide-panel.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-panel"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "ide-panel.h"
+
+typedef struct
+{
+  DzlDockStack *dock_stack;
+} IdePanelPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdePanel, ide_panel, DZL_TYPE_DOCK_BIN_EDGE)
+
+static void
+ide_panel_add (GtkContainer *container,
+               GtkWidget    *widget)
+{
+  IdePanel *self = (IdePanel *)container;
+  IdePanelPrivate *priv = ide_panel_get_instance_private (self);
+
+  g_assert (IDE_IS_PANEL (self));
+
+  if (DZL_IS_DOCK_WIDGET (widget))
+    gtk_container_add (GTK_CONTAINER (priv->dock_stack), widget);
+  else
+    GTK_CONTAINER_CLASS (ide_panel_parent_class)->add (container, widget);
+}
+
+static void
+ide_panel_class_init (IdePanelClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  container_class->add = ide_panel_add;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-panel.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdePanel, dock_stack);
+}
+
+static void
+ide_panel_init (IdePanel *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+/**
+ * ide_panel_new:
+ *
+ * Creates a new #IdePanel widget.
+ *
+ * These are meant to be added to #IdeSurface widgets within a workspace.
+ *
+ * Returns: an #IdePanel
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_panel_new (void)
+{
+  return g_object_new (IDE_TYPE_PANEL, NULL);
+}
diff --git a/src/libide/gui/ide-panel.h b/src/libide/gui/ide-panel.h
new file mode 100644
index 000000000..50f99acc3
--- /dev/null
+++ b/src/libide/gui/ide-panel.h
@@ -0,0 +1,48 @@
+/* ide-panel.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PANEL (ide_panel_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdePanel, ide_panel, IDE, PANEL, DzlDockBinEdge)
+
+struct _IdePanelClass
+{
+  DzlDockBinEdgeClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_panel_new (void);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-panel.ui b/src/libide/gui/ide-panel.ui
new file mode 100644
index 000000000..4fd94fc62
--- /dev/null
+++ b/src/libide/gui/ide-panel.ui
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.24 -->
+  <template class="IdePanel" parent="DzlDockBinEdge">
+    <child>
+      <object class="DzlDockStack" id="dock_stack">
+        <property name="expand">true</property>
+        <property name="visible">true</property>
+      </object>
+    </child>
+  </template>
+</interface>
+
diff --git a/src/libide/gui/ide-preferences-addin.c b/src/libide/gui/ide-preferences-addin.c
new file mode 100644
index 000000000..eef7dd6c2
--- /dev/null
+++ b/src/libide/gui/ide-preferences-addin.c
@@ -0,0 +1,80 @@
+/* ide-preferences-addin.c
+ *
+ * Copyright 2015-2019 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 "ide-preferences-addin"
+
+#include "config.h"
+
+#include "ide-preferences-addin.h"
+
+G_DEFINE_INTERFACE (IdePreferencesAddin, ide_preferences_addin, G_TYPE_OBJECT)
+
+static void
+ide_preferences_addin_default_init (IdePreferencesAddinInterface *iface)
+{
+}
+
+/**
+ * ide_preferences_addin_load:
+ * @self: An #IdePreferencesAddin.
+ * @preferences: The preferences container implementation.
+ *
+ * This interface method is called when a preferences addin is initialized. It
+ * could be initialized from multiple preferences implementations, so consumers
+ * should use the #DzlPreferences interface to add their preferences controls
+ * to the container.
+ *
+ * Such implementations might include a preferences dialog window, or a
+ * preferences widget which could be rendered as a perspective.
+ *
+ * Since: 3.32
+ */
+void
+ide_preferences_addin_load (IdePreferencesAddin *self,
+                            DzlPreferences      *preferences)
+{
+  g_return_if_fail (IDE_IS_PREFERENCES_ADDIN (self));
+  g_return_if_fail (DZL_IS_PREFERENCES (preferences));
+
+  if (IDE_PREFERENCES_ADDIN_GET_IFACE (self)->load)
+    IDE_PREFERENCES_ADDIN_GET_IFACE (self)->load (self, preferences);
+}
+
+/**
+ * ide_preferences_addin_unload:
+ * @self: An #IdePreferencesAddin.
+ * @preferences: The preferences container implementation.
+ *
+ * This interface method is called when the preferences addin should remove all
+ * controls added to @preferences. This could happen during desctruction of
+ * @preferences, or when the plugin is unloaded.
+ *
+ * Since: 3.32
+ */
+void
+ide_preferences_addin_unload (IdePreferencesAddin *self,
+                              DzlPreferences      *preferences)
+{
+  g_return_if_fail (IDE_IS_PREFERENCES_ADDIN (self));
+  g_return_if_fail (DZL_IS_PREFERENCES (preferences));
+
+  if (IDE_PREFERENCES_ADDIN_GET_IFACE (self)->unload)
+    IDE_PREFERENCES_ADDIN_GET_IFACE (self)->unload (self, preferences);
+}
diff --git a/src/libide/gui/ide-preferences-addin.h b/src/libide/gui/ide-preferences-addin.h
new file mode 100644
index 000000000..70fa8f098
--- /dev/null
+++ b/src/libide/gui/ide-preferences-addin.h
@@ -0,0 +1,51 @@
+/* ide-preferences-addin.h
+ *
+ * Copyright 2015-2019 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 <dazzle.h>
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PREFERENCES_ADDIN (ide_preferences_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdePreferencesAddin, ide_preferences_addin, IDE, PREFERENCES_ADDIN, GObject)
+
+struct _IdePreferencesAddinInterface
+{
+  GTypeInterface parent_interface;
+
+  void (*load)   (IdePreferencesAddin *self,
+                  DzlPreferences      *preferences);
+  void (*unload) (IdePreferencesAddin *self,
+                  DzlPreferences      *preferences);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_preferences_addin_load   (IdePreferencesAddin *self,
+                                   DzlPreferences      *preferences);
+IDE_AVAILABLE_IN_3_32
+void ide_preferences_addin_unload (IdePreferencesAddin *self,
+                                   DzlPreferences      *preferences);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-builtin-private.h 
b/src/libide/gui/ide-preferences-builtin-private.h
new file mode 100644
index 000000000..ca3f8b5be
--- /dev/null
+++ b/src/libide/gui/ide-preferences-builtin-private.h
@@ -0,0 +1,29 @@
+/* ide-preferences-builtin.h
+ *
+ * Copyright 2015-2019 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 <dazzle.h>
+
+G_BEGIN_DECLS
+
+void _ide_preferences_builtin_register (DzlPreferences *preferences);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-builtin.c b/src/libide/gui/ide-preferences-builtin.c
new file mode 100644
index 000000000..1fdee4849
--- /dev/null
+++ b/src/libide/gui/ide-preferences-builtin.c
@@ -0,0 +1,571 @@
+/* ide-preferences-builtin.c
+ *
+ * Copyright 2015-2019 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 "ide-preferences-builtin"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+#include <libpeas/peas.h>
+
+#include "ide-preferences-builtin-private.h"
+#include "ide-preferences-language-row-private.h"
+
+static gint
+sort_plugin_info (gconstpointer a,
+                  gconstpointer b)
+{
+  PeasPluginInfo *plugin_info_a = (PeasPluginInfo *)a;
+  PeasPluginInfo *plugin_info_b = (PeasPluginInfo *)b;
+  const gchar *name_a = peas_plugin_info_get_name (plugin_info_a);
+  const gchar *name_b = peas_plugin_info_get_name (plugin_info_b);
+
+  if (name_a == NULL || name_b == NULL)
+    return g_strcmp0 (name_a, name_b);
+
+  return g_utf8_collate (name_a, name_b);
+}
+
+static void
+ide_preferences_builtin_register_plugins (DzlPreferences *preferences)
+{
+  PeasEngine *engine;
+  const GList *list;
+  GList *copy;
+  guint i = 0;
+
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  engine = peas_engine_get_default ();
+  list = peas_engine_get_plugin_list (engine);
+
+  dzl_preferences_add_page (preferences, "plugins", _("Extensions"), 700);
+  dzl_preferences_add_list_group (preferences, "plugins", "plugins", _("Extensions"), GTK_SELECTION_NONE, 
100);
+
+  copy = g_list_sort (g_list_copy ((GList *)list), sort_plugin_info);
+
+  for (const GList *iter = copy; iter; iter = iter->next, i++)
+    {
+      PeasPluginInfo *plugin_info = iter->data;
+      g_autofree gchar *path = NULL;
+      g_autofree gchar *keywords = NULL;
+      const gchar *desc;
+      const gchar *name;
+
+      if (peas_plugin_info_is_hidden (plugin_info))
+        continue;
+
+      name = peas_plugin_info_get_name (plugin_info);
+      desc = peas_plugin_info_get_description (plugin_info);
+      keywords = g_strdup_printf ("%s %s", name, desc);
+      path = g_strdup_printf ("/org/gnome/builder/plugins/%s/",
+                              peas_plugin_info_get_module_name (plugin_info));
+
+      dzl_preferences_add_switch (preferences, "plugins", "plugins", "org.gnome.builder.plugin", "enabled", 
path, NULL, name, desc, keywords, i);
+    }
+
+  g_list_free (copy);
+}
+
+static void
+ide_preferences_builtin_register_appearance (DzlPreferences *preferences)
+{
+  GtkSourceStyleSchemeManager *manager;
+  const gchar * const *scheme_ids;
+  GtkWidget *bin;
+  gint i;
+  gint dark_mode;
+
+  dzl_preferences_add_page (preferences, "appearance", _("Appearance"), 0);
+
+  dzl_preferences_add_list_group (preferences, "appearance", "basic", _("Themes"), GTK_SELECTION_NONE, 0);
+  dark_mode = dzl_preferences_add_switch (preferences, "appearance", "basic", "org.gnome.builder", 
"night-mode", NULL, NULL, _("Dark Mode"), _("Whether Builder should use a dark theme"), _("dark theme"), 0);
+  dzl_preferences_add_switch (preferences, "appearance", "basic", "org.gnome.builder", "follow-night-light", 
NULL, NULL, _("Night Light"), _("Automatically enable dark mode at night"), _("follow night light"), 5);
+  dzl_preferences_add_switch (preferences, "appearance", "basic", "org.gnome.builder.editor", 
"show-grid-lines", NULL, NULL, _("Grid Pattern"), _("Display a grid pattern underneath source code"), NULL, 
10);
+
+  dzl_preferences_add_list_group (preferences, "appearance", "font", _("Font"), GTK_SELECTION_NONE, 10);
+  dzl_preferences_add_font_button (preferences, "appearance", "font", "org.gnome.builder.editor", 
"font-name", _("Editor"), C_("Keywords", "editor font monospace"), 0);
+  /* XXX: This belongs in terminal addin */
+  dzl_preferences_add_font_button (preferences, "appearance", "font", "org.gnome.builder.terminal", 
"font-name", _("Terminal"), C_("Keywords", "terminal font monospace"), 1);
+  dzl_preferences_add_switch (preferences, "appearance", "font", "org.gnome.builder.terminal", "allow-bold", 
NULL, NULL, _("Bold text in terminals"), _("If terminals are allowed to display bold text"), C_("Keywords", 
"terminal allow bold"), 2);
+
+  manager = gtk_source_style_scheme_manager_get_default ();
+  scheme_ids = gtk_source_style_scheme_manager_get_scheme_ids (manager);
+
+  dzl_preferences_add_list_group (preferences, "appearance", "schemes", _("Color Scheme"), 
GTK_SELECTION_NONE, 20);
+
+  for (i = 0; scheme_ids [i]; i++)
+    {
+      g_autofree gchar *variant_str = NULL;
+      GtkSourceStyleScheme *scheme;
+      const gchar *title;
+
+      variant_str = g_strdup_printf ("\"%s\"", scheme_ids [i]);
+      scheme = gtk_source_style_scheme_manager_get_scheme (manager, scheme_ids [i]);
+      title = gtk_source_style_scheme_get_name (scheme);
+
+      dzl_preferences_add_radio (preferences, "appearance", "schemes", "org.gnome.builder.editor", 
"style-scheme-name", NULL, variant_str, title, NULL, title, i);
+    }
+
+  if (g_getenv ("GTK_THEME") != NULL)
+    {
+      bin = dzl_preferences_get_widget (preferences, dark_mode);
+      gtk_widget_set_sensitive (bin, FALSE);
+    }
+}
+
+static void
+ide_preferences_builtin_register_keyboard (DzlPreferences *preferences)
+{
+  dzl_preferences_add_page (preferences, "keyboard", _("Keyboard"), 400);
+
+  dzl_preferences_add_list_group (preferences, "keyboard", "mode", _("Emulation"), GTK_SELECTION_NONE, 0);
+  dzl_preferences_add_radio (preferences, "keyboard", "mode", "org.gnome.builder.editor", "keybindings", 
NULL, "\"default\"", _("Builder"), _("Default keybinding mode which mimics gedit"), NULL, 0);
+
+  dzl_preferences_add_list_group (preferences, "keyboard", "movements", _("Movement"), GTK_SELECTION_NONE, 
100);
+  dzl_preferences_add_switch (preferences, "keyboard", "movements", "org.gnome.builder.editor", 
"smart-home-end", NULL, NULL, _("Smart Home and End"), _("Home moves to first non-whitespace character"), 
NULL, 0);
+  dzl_preferences_add_switch (preferences, "keyboard", "movements", "org.gnome.builder.editor", 
"smart-backspace", NULL, NULL, _("Smart Backspace"), _("Backspace will remove extra space to keep you aligned 
with your indentation"), NULL, 100);
+}
+
+static void
+ide_preferences_builtin_register_editor (DzlPreferences *preferences)
+{
+  dzl_preferences_add_page (preferences, "editor", _("Editor"), 100);
+
+  dzl_preferences_add_list_group (preferences, "editor", "general", _("General"), GTK_SELECTION_NONE, -5);
+  dzl_preferences_add_switch (preferences, "editor", "general", "org.gnome.builder", "show-open-files", 
NULL, NULL, _("Display list of open files"), _("Display the list of all open files in the project sidebar"), 
NULL, 0);
+
+  dzl_preferences_add_list_group (preferences, "editor", "position", _("Cursor"), GTK_SELECTION_NONE, 0);
+  dzl_preferences_add_switch (preferences, "editor", "position", "org.gnome.builder.editor", 
"restore-insert-mark", NULL, NULL, _("Restore cursor position"), _("Restore cursor position when a file is 
reopened"), NULL, 0);
+  dzl_preferences_add_switch (preferences, "editor", "position", "org.gnome.builder.editor", "wrap-text", 
NULL, NULL, _("Enable text wrapping"), _("Wrap text that is too wide to display"), NULL, 5);
+  dzl_preferences_add_spin_button (preferences, "editor", "position", "org.gnome.builder.editor", 
"scroll-offset", NULL, _("Scroll Offset"), _("Minimum number of lines to keep above and below the cursor"), 
NULL, 10);
+  dzl_preferences_add_spin_button (preferences, "editor", "position", "org.gnome.builder.editor", 
"overscroll", NULL, _("Overscroll"), _("Allow the editor to scroll past the end of the buffer"), NULL, 20);
+
+  dzl_preferences_add_list_group (preferences, "editor", "line", _("Line Information"), GTK_SELECTION_NONE, 
50);
+  dzl_preferences_add_switch (preferences, "editor", "line", "org.gnome.builder.editor", 
"show-line-numbers", NULL, NULL, _("Line numbers"), _("Show line number at beginning of each line"), NULL, 0);
+  dzl_preferences_add_switch (preferences, "editor", "line", "org.gnome.builder.editor", 
"show-line-changes", NULL, NULL, _("Line changes"), _("Show if a line was added or modified next to line 
number"), NULL, 1);
+  dzl_preferences_add_switch (preferences, "editor", "line", "org.gnome.builder.editor", 
"show-line-diagnostics", NULL, NULL, _("Line diagnostics"), _("Show an icon next to line numbers indicating 
type of diagnostic"), NULL, 2);
+
+  dzl_preferences_add_list_group (preferences, "editor", "highlight", _("Highlight"), GTK_SELECTION_NONE, 
100);
+  dzl_preferences_add_switch (preferences, "editor", "highlight", "org.gnome.builder.editor", 
"highlight-current-line", NULL, NULL, _("Current line"), _("Make current line stand out with highlights"), 
NULL, 0);
+  dzl_preferences_add_switch (preferences, "editor", "highlight", "org.gnome.builder.editor", 
"highlight-matching-brackets", NULL, NULL, _("Matching brackets"), _("Highlight matching brackets based on 
cursor position"), NULL, 1);
+
+  dzl_preferences_add_list_group (preferences, "editor", "overview", _("Code Overview"), GTK_SELECTION_NONE, 
100);
+  dzl_preferences_add_switch (preferences, "editor", "overview", "org.gnome.builder.editor", "show-map", 
NULL, NULL, _("Show overview map"), _("A zoomed out view to enhance navigating source code"), NULL, 0);
+  dzl_preferences_add_switch (preferences, "editor", "overview", "org.gnome.builder.editor", 
"auto-hide-map", NULL, NULL, _("Automatically hide overview map"), _("Automatically hide map when editor 
loses focus"), NULL, 1);
+
+  dzl_preferences_add_list_group (preferences, "editor", "draw-spaces", _("Visible Whitespace Characters"), 
GTK_SELECTION_NONE, 400);
+  dzl_preferences_add_radio (preferences, "editor", "draw-spaces", "org.gnome.builder.editor", 
"draw-spaces", NULL, "\"space\"", _("Spaces"), NULL, NULL, 0);
+  dzl_preferences_add_radio (preferences, "editor", "draw-spaces", "org.gnome.builder.editor", 
"draw-spaces", NULL, "\"tab\"", _("Tabs"), NULL, NULL, 1);
+  dzl_preferences_add_radio (preferences, "editor", "draw-spaces", "org.gnome.builder.editor", 
"draw-spaces", NULL, "\"newline\"", _("New line and carriage return"), NULL, NULL, 2);
+  dzl_preferences_add_radio (preferences, "editor", "draw-spaces", "org.gnome.builder.editor", 
"draw-spaces", NULL, "\"nbsp\"", _("Non-breaking spaces"), NULL, NULL, 3);
+  dzl_preferences_add_radio (preferences, "editor", "draw-spaces", "org.gnome.builder.editor", 
"draw-spaces", NULL, "\"text\"", _("Spaces inside of text"), NULL, NULL, 4);
+  dzl_preferences_add_radio (preferences, "editor", "draw-spaces", "org.gnome.builder.editor", 
"draw-spaces", NULL, "\"trailing\"", _("Trailing Only"), NULL, NULL, 5);
+  dzl_preferences_add_radio (preferences, "editor", "draw-spaces", "org.gnome.builder.editor", 
"draw-spaces", NULL, "\"leading\"", _("Leading Only"), NULL, NULL, 6);
+
+  dzl_preferences_add_list_group (preferences, "editor", "autosave", _("Autosave"), GTK_SELECTION_NONE, 450);
+  dzl_preferences_add_switch (preferences, "editor", "autosave", "org.gnome.builder.editor", "auto-save", 
NULL, NULL,_("Autosave Enabled"), _("Enable or disable autosave feature"), NULL, 1);
+  dzl_preferences_add_spin_button (preferences, "editor", "autosave", "org.gnome.builder.editor", 
"auto-save-timeout", NULL, _("Autosave Frequency"), _("The number of seconds after modification before auto 
saving"), NULL, 60);
+}
+
+static void
+ide_preferences_builtin_register_code_insight (DzlPreferences *preferences)
+{
+  dzl_preferences_add_page (preferences, "code-insight", _("Code Insight"), 300);
+
+  dzl_preferences_add_list_group (preferences, "code-insight", "highlighting", _("Highlighting"), 
GTK_SELECTION_NONE, 0);
+  dzl_preferences_add_switch (preferences, "code-insight", "highlighting", "org.gnome.builder.code-insight", 
"semantic-highlighting", NULL, NULL, _("Semantic Highlighting"), _("Use code insight to highlight additional 
information discovered in source file"), NULL, 0);
+
+  dzl_preferences_add_list_group (preferences, "code-insight", "diagnostics", _("Diagnostics"), 
GTK_SELECTION_NONE, 200);
+}
+
+static void
+ide_preferences_builtin_register_completion (DzlPreferences *preferences)
+{
+  dzl_preferences_add_page (preferences, "completion", _("Completion"), 325);
+
+  dzl_preferences_add_list_group (preferences, "completion", "general", _("General"), GTK_SELECTION_NONE, 0);
+  dzl_preferences_add_spin_button (preferences, "completion", "general", "org.gnome.builder.editor", 
"completion-n-rows", NULL, _("Completions Display Size"), _("Number of completions to display"), NULL, -1);
+  dzl_preferences_add_switch (preferences, "completion", "general", "org.gnome.builder.editor", 
"interactive-completion", NULL, NULL, _("Interactive Completion"), _("Display code suggestions interactively 
as you type"), NULL, 0);
+
+  dzl_preferences_add_list_group (preferences, "completion", "providers", _("Completion Providers"), 
GTK_SELECTION_NONE, 100);
+}
+
+static void
+ide_preferences_builtin_register_snippets (DzlPreferences *preferences)
+{
+  dzl_preferences_add_page (preferences, "snippets", _("Snippets"), 350);
+
+  /* TODO: Add snippet editor widget + languages */
+}
+
+static void
+language_search_changed (GtkSearchEntry      *search,
+                         DzlPreferencesGroup *group)
+{
+  g_autoptr(DzlPatternSpec) spec = NULL;
+  const gchar *text;
+
+  g_assert (GTK_IS_SEARCH_ENTRY (search));
+  g_assert (DZL_IS_PREFERENCES_GROUP (group));
+
+  text = gtk_entry_get_text (GTK_ENTRY (search));
+
+  if (!dzl_str_empty0 (text))
+    {
+      g_autofree gchar *folded = g_utf8_casefold (text, -1);
+
+      spec = dzl_pattern_spec_new (folded);
+    }
+
+  /* FIXME:
+   *
+   * This is a bit of a leaky abstraction, but we can
+   * clean that up later. We need to get something out
+   * that is coherent for 3.22.
+   */
+
+  dzl_preferences_group_refilter (group, spec);
+}
+
+static void
+ide_preferences_builtin_register_languages (DzlPreferences *preferences)
+{
+  GtkSourceLanguageManager *manager;
+  const gchar * const *language_ids;
+  GtkSearchEntry *search;
+  GtkWidget *group = NULL;
+  GtkWidget *flow = NULL;
+
+  dzl_preferences_add_page (preferences, "languages", _("Programming Languages"), 200);
+
+  manager = gtk_source_language_manager_get_default ();
+  language_ids = gtk_source_language_manager_get_language_ids (manager);
+
+  g_assert (language_ids != NULL && language_ids[0] != NULL);
+
+  dzl_preferences_add_group (preferences, "languages", "search", NULL, 0);
+
+  search = g_object_new (GTK_TYPE_SEARCH_ENTRY,
+                         /* translators: placeholder string for the entry used to filter the languages in 
Preferences/Programming languages */
+                         "placeholder-text", _("Search languages…"),
+                         "visible", TRUE,
+                         NULL);
+  dzl_preferences_add_custom (preferences, "languages", "search", GTK_WIDGET (search), NULL, 0);
+
+  dzl_preferences_add_list_group (preferences, "languages", "languages", NULL, GTK_SELECTION_SINGLE, 1);
+
+  for (guint i = 0; language_ids [i]; i++)
+    {
+      g_autofree gchar *keywords = NULL;
+      g_autofree gchar *folded = NULL;
+      IdePreferencesLanguageRow *row;
+      GtkSourceLanguage *language;
+      const gchar *name;
+      const gchar *section;
+
+      if (dzl_str_equal0 (language_ids [i], "def"))
+        continue;
+
+      language = gtk_source_language_manager_get_language (manager, language_ids [i]);
+      name = gtk_source_language_get_name (language);
+      section = gtk_source_language_get_section (language);
+
+      keywords = g_strdup_printf ("%s %s %s", name, section, language_ids [i]);
+      folded = g_utf8_casefold (keywords, -1);
+
+      row = g_object_new (IDE_TYPE_PREFERENCES_LANGUAGE_ROW,
+                          "id", language_ids [i],
+                          "keywords", folded,
+                          "title", name,
+                          "visible", TRUE,
+                          NULL);
+      dzl_preferences_add_custom (preferences, "languages", "languages", GTK_WIDGET (row), NULL, i);
+
+      if G_UNLIKELY (group == NULL)
+        group = gtk_widget_get_ancestor (GTK_WIDGET (row), DZL_TYPE_PREFERENCES_GROUP);
+    }
+
+  g_assert (group != NULL);
+
+  g_signal_connect_object (search,
+                           "changed",
+                           G_CALLBACK (language_search_changed),
+                           group,
+                           0);
+
+  flow = gtk_widget_get_ancestor (group, DZL_TYPE_COLUMN_LAYOUT);
+
+  g_assert (flow != NULL);
+
+  dzl_column_layout_set_max_columns (DZL_COLUMN_LAYOUT (flow), 1);
+
+  dzl_preferences_add_page (preferences, "languages.id", NULL, 0);
+
+  dzl_preferences_add_list_group (preferences, "languages.id", "basic", _("General"), GTK_SELECTION_NONE, 0);
+  dzl_preferences_add_switch (preferences, "languages.id", "basic", "org.gnome.builder.editor.language", 
"trim-trailing-whitespace", "/org/gnome/builder/editor/language/{id}/", NULL, _("Trim trailing whitespace"), 
_("Upon saving, trailing whitespace from modified lines will be trimmed."), NULL, 10);
+  dzl_preferences_add_switch (preferences, "languages.id", "basic", "org.gnome.builder.editor.language", 
"overwrite-braces", "/org/gnome/builder/editor/language/{id}/", NULL, _("Overwrite Braces"), _("Overwrite 
closing braces"), NULL, 20);
+  dzl_preferences_add_switch (preferences, "languages.id", "basic", "org.gnome.builder.editor.language", 
"insert-matching-brace", "/org/gnome/builder/editor/language/{id}/", NULL, _("Insert Matching Brace"), 
_("Insert matching character for { [ ( or \""), NULL, 20);
+  dzl_preferences_add_switch (preferences, "languages.id", "basic", "org.gnome.builder.editor.language", 
"insert-trailing-newline", "/org/gnome/builder/editor/language/{id}/", NULL, _("Insert Trailing Newline"), 
_("Ensure files end with a newline"), NULL, 30);
+
+  dzl_preferences_add_list_group (preferences, "languages.id", "margin", _("Margins"), GTK_SELECTION_NONE, 
0);
+  dzl_preferences_add_radio (preferences, "languages.id", "margin", "org.gnome.builder.editor.language", 
"show-right-margin", "/org/gnome/builder/editor/language/{id}/", NULL, _("Show right margin"), NULL, NULL, 0);
+  dzl_preferences_add_spin_button (preferences, "languages.id", "margin", 
"org.gnome.builder.editor.language", "right-margin-position", "/org/gnome/builder/editor/language/{id}/", 
_("Right margin position"), _("Position in spaces for the right margin"), NULL, 10);
+
+  dzl_preferences_add_list_group (preferences, "languages.id", "indentation", _("Indentation"), 
GTK_SELECTION_NONE, 100);
+  dzl_preferences_add_spin_button (preferences, "languages.id", "indentation", 
"org.gnome.builder.editor.language", "tab-width", "/org/gnome/builder/editor/language/{id}/", _("Tab width"), 
_("Width of a tab character in spaces"), NULL, 10);
+  dzl_preferences_add_radio (preferences, "languages.id", "indentation", 
"org.gnome.builder.editor.language", "insert-spaces-instead-of-tabs", 
"/org/gnome/builder/editor/language/{id}/", NULL, _("Insert spaces instead of tabs"), _("Prefer spaces over 
use of tabs"), NULL, 20);
+  dzl_preferences_add_radio (preferences, "languages.id", "indentation", 
"org.gnome.builder.editor.language", "auto-indent", "/org/gnome/builder/editor/language/{id}/", NULL, 
_("Automatically indent"), _("Indent source code as you type"), NULL, 30);
+
+  dzl_preferences_add_list_group (preferences, "languages.id", "spaces-style", _("Spacing"), 
GTK_SELECTION_NONE, 600);
+  dzl_preferences_add_radio (preferences, "languages.id", "spaces-style", 
"org.gnome.builder.editor.language", "spaces-style", "/org/gnome/builder/editor/language/{id}/", 
"\"before-left-paren\"", _("Space before opening parentheses"), NULL, NULL, 0);
+  dzl_preferences_add_radio (preferences, "languages.id", "spaces-style", 
"org.gnome.builder.editor.language", "spaces-style", "/org/gnome/builder/editor/language/{id}/", 
"\"before-left-bracket\"", _("Space before opening brackets"), NULL, NULL, 1);
+  dzl_preferences_add_radio (preferences, "languages.id", "spaces-style", 
"org.gnome.builder.editor.language", "spaces-style", "/org/gnome/builder/editor/language/{id}/", 
"\"before-left-brace\"", _("Space before opening braces"), NULL, NULL, 2);
+  dzl_preferences_add_radio (preferences, "languages.id", "spaces-style", 
"org.gnome.builder.editor.language", "spaces-style", "/org/gnome/builder/editor/language/{id}/", 
"\"before-left-angle\"", _("Space before opening angles"), NULL, NULL, 3);
+  dzl_preferences_add_radio (preferences, "languages.id", "spaces-style", 
"org.gnome.builder.editor.language", "spaces-style", "/org/gnome/builder/editor/language/{id}/", "\"colon\"", 
_("Prefer a space before colons"), NULL, NULL, 4);
+  dzl_preferences_add_radio (preferences, "languages.id", "spaces-style", 
"org.gnome.builder.editor.language", "spaces-style", "/org/gnome/builder/editor/language/{id}/", "\"comma\"", 
_("Prefer a space before commas"), NULL, NULL, 5);
+  dzl_preferences_add_radio (preferences, "languages.id", "spaces-style", 
"org.gnome.builder.editor.language", "spaces-style", "/org/gnome/builder/editor/language/{id}/", 
"\"semicolon\"", _("Prefer a space before semicolons"), NULL, NULL, 6);
+}
+
+static gboolean
+workers_output (GtkSpinButton *button)
+{
+  GtkAdjustment *adj = gtk_spin_button_get_adjustment (button);
+
+  if (gtk_adjustment_get_value (adj) == -1)
+    {
+      gtk_entry_set_text (GTK_ENTRY (button), _("Default"));
+      return TRUE;
+    }
+  else if (gtk_adjustment_get_value (adj) == 0)
+    {
+      gtk_entry_set_text (GTK_ENTRY (button), _("Number of CPU"));
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gint
+workers_input (GtkSpinButton *button,
+               gdouble       *new_value)
+{
+  const gchar *text = gtk_entry_get_text (GTK_ENTRY (button));
+
+  if (g_strcmp0 (text, _("Default")) == 0)
+    {
+      *new_value = -1;
+      return TRUE;
+    }
+  else if (g_strcmp0 (text, _("Number of CPU")) == 0)
+    {
+      *new_value = 0;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_preferences_builtin_register_build (DzlPreferences *preferences)
+{
+  GtkWidget *widget, *bin;
+  guint id;
+
+  dzl_preferences_add_page (preferences, "build", _("Build"), 500);
+
+  dzl_preferences_add_list_group (preferences, "build", "basic", _("General"), GTK_SELECTION_NONE, 0);
+  id = dzl_preferences_add_spin_button (preferences, "build", "basic", "org.gnome.builder.build", 
"parallel", "/org/gnome/builder/build/", _("Build Workers"), _("Number of parallel build workers"), NULL, 0);
+
+  bin = dzl_preferences_get_widget (preferences, id);
+  widget = dzl_preferences_spin_button_get_spin_button (DZL_PREFERENCES_SPIN_BUTTON (bin));
+  gtk_entry_set_width_chars (GTK_ENTRY (widget), 20);
+  g_signal_connect (widget, "input", G_CALLBACK (workers_input), NULL);
+  g_signal_connect (widget, "output", G_CALLBACK (workers_output), NULL);
+
+  dzl_preferences_add_switch (preferences, "build", "basic", "org.gnome.builder", "clear-cache-at-startup", 
NULL, NULL, _("Clear build cache at startup"), _("Expired caches will be purged when Builder is started"), 
NULL, 10);
+
+  dzl_preferences_add_list_group (preferences, "build", "network", _("Network"), GTK_SELECTION_NONE, 100);
+  dzl_preferences_add_switch (preferences, "build", "network", "org.gnome.builder.build", 
"allow-network-when-metered", NULL, NULL, _("Allow downloads over metered connections"), _("Allow the use of 
metered network connections when automatically downloading dependencies"), NULL, 10);
+}
+
+static void
+ide_preferences_builtin_register_projects (DzlPreferences *preferences)
+{
+  dzl_preferences_add_page (preferences, "projects", _("Projects"), 450);
+
+  dzl_preferences_add_list_group (preferences, "projects", "directory", _("Workspace"), GTK_SELECTION_NONE, 
0);
+  dzl_preferences_add_file_chooser (preferences, "projects", "directory", "org.gnome.builder", 
"projects-directory", NULL, _("Projects directory"), _("A place for all your projects"), 
GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, NULL, 0);
+  dzl_preferences_add_switch (preferences, "projects", "directory", "org.gnome.builder", 
"restore-previous-files", NULL, NULL, _("Restore previously opened files"), _("Open previously opened files 
when loading a project"), NULL, 10);
+}
+
+#if 0
+static void
+author_changed_cb (DzlPreferencesEntry *entry,
+                   const gchar         *text,
+                   IdeVcsConfig        *conf)
+{
+  GValue value = G_VALUE_INIT;
+
+  g_assert (DZL_IS_PREFERENCES_ENTRY (entry));
+  g_assert (text != NULL);
+  g_assert (IDE_IS_VCS_CONFIG (conf));
+
+  g_value_init (&value, G_TYPE_STRING);
+  g_value_set_string (&value, text);
+
+  ide_vcs_config_set_config (conf, IDE_VCS_CONFIG_FULL_NAME, &value);
+
+  g_value_unset (&value);
+}
+
+static void
+email_changed_cb (DzlPreferencesEntry *entry,
+                  const gchar         *text,
+                  IdeVcsConfig        *conf)
+{
+  GValue value = G_VALUE_INIT;
+
+  g_assert (DZL_IS_PREFERENCES_ENTRY (entry));
+  g_assert (text != NULL);
+  g_assert (IDE_IS_VCS_CONFIG (conf));
+
+  g_value_init (&value, G_TYPE_STRING);
+  g_value_set_string (&value, text);
+
+  ide_vcs_config_set_config (conf, IDE_VCS_CONFIG_EMAIL, &value);
+
+  g_value_unset (&value);
+}
+
+static void
+vcs_configs_foreach_cb (PeasExtensionSet *set,
+                        PeasPluginInfo   *plugin_info,
+                        PeasExtension    *exten,
+                        gpointer          user_data)
+{
+  DzlPreferences *preferences = user_data;
+  IdeVcsConfig *conf = (IdeVcsConfig *)exten;
+  GValue value = G_VALUE_INIT;
+  GtkWidget *fullname;
+  GtkWidget *email;
+  GtkSizeGroup *size_group;
+  g_autofree gchar *key = NULL;
+  g_autofree gchar *author_name = NULL;
+  g_autofree gchar *author_email = NULL;
+  const gchar *name;
+  const gchar *id;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+  g_assert (IDE_IS_VCS_CONFIG (conf));
+
+  name = peas_plugin_info_get_name (plugin_info);
+  id = peas_plugin_info_get_module_name (plugin_info);
+  key = g_strdup_printf ("%s-config", id);
+
+  g_object_set_data_full (G_OBJECT (preferences), key, g_object_ref (conf), g_object_unref);
+
+  g_value_init (&value, G_TYPE_STRING);
+
+  ide_vcs_config_get_config (conf, IDE_VCS_CONFIG_FULL_NAME, &value);
+  author_name = g_strdup (g_value_get_string (&value));
+
+  g_value_reset (&value);
+
+  ide_vcs_config_get_config (conf, IDE_VCS_CONFIG_EMAIL, &value);
+  author_email = g_strdup (g_value_get_string (&value));
+
+  g_value_unset (&value);
+
+  fullname = g_object_new (DZL_TYPE_PREFERENCES_ENTRY,
+                           "text", dzl_str_empty0 (author_name) ? "" : author_name,
+                           "title", "Author",
+                           "visible", TRUE,
+                           NULL);
+
+  g_signal_connect_object (fullname,
+                           "changed",
+                           G_CALLBACK (author_changed_cb),
+                           conf,
+                           0);
+
+  email = g_object_new (DZL_TYPE_PREFERENCES_ENTRY,
+                        "text", dzl_str_empty0 (author_email) ? "" : author_email,
+                        "title", "Email",
+                        "visible", TRUE,
+                        NULL);
+
+  g_signal_connect_object (email,
+                           "changed",
+                           G_CALLBACK (email_changed_cb),
+                           conf,
+                           0);
+
+  dzl_preferences_add_list_group (preferences, "vcs", id, name, GTK_SELECTION_NONE, 0);
+  dzl_preferences_add_custom (preferences, "vcs", id, fullname, NULL, 0);
+  dzl_preferences_add_custom (preferences, "vcs", id, email, NULL, 0);
+
+  size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+  gtk_size_group_add_widget (size_group, dzl_preferences_entry_get_title_widget (DZL_PREFERENCES_ENTRY 
(fullname)));
+  gtk_size_group_add_widget (size_group, dzl_preferences_entry_get_title_widget (DZL_PREFERENCES_ENTRY 
(email)));
+  g_clear_object (&size_group);
+}
+
+static void
+ide_preferences_builtin_register_vcs (DzlPreferences *preferences)
+{
+  PeasEngine *engine;
+  PeasExtensionSet *extensions;
+
+  dzl_preferences_add_page (preferences, "vcs", _("Version Control"), 600);
+
+  engine = peas_engine_get_default ();
+  extensions = peas_extension_set_new (engine, IDE_TYPE_VCS_CONFIG, NULL);
+  peas_extension_set_foreach (extensions, vcs_configs_foreach_cb, preferences);
+  g_clear_object (&extensions);
+}
+#endif
+
+static void
+ide_preferences_builtin_register_sdks (DzlPreferences *preferences)
+{
+  /* only the page goes here, plugins will fill in the details */
+  dzl_preferences_add_page (preferences, "sdk", _("SDKs"), 550);
+}
+
+void
+_ide_preferences_builtin_register (DzlPreferences *preferences)
+{
+  ide_preferences_builtin_register_appearance (preferences);
+  ide_preferences_builtin_register_editor (preferences);
+  ide_preferences_builtin_register_languages (preferences);
+  ide_preferences_builtin_register_code_insight (preferences);
+  ide_preferences_builtin_register_completion (preferences);
+  ide_preferences_builtin_register_snippets (preferences);
+  ide_preferences_builtin_register_keyboard (preferences);
+  ide_preferences_builtin_register_plugins (preferences);
+  ide_preferences_builtin_register_build (preferences);
+  ide_preferences_builtin_register_projects (preferences);
+  //ide_preferences_builtin_register_vcs (preferences);
+  ide_preferences_builtin_register_sdks (preferences);
+}
diff --git a/src/libide/gui/ide-preferences-language-row-private.h 
b/src/libide/gui/ide-preferences-language-row-private.h
new file mode 100644
index 000000000..e2ba0dbcf
--- /dev/null
+++ b/src/libide/gui/ide-preferences-language-row-private.h
@@ -0,0 +1,31 @@
+/* ide-preferences-language-row.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <dazzle.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PREFERENCES_LANGUAGE_ROW (ide_preferences_language_row_get_type())
+
+G_DECLARE_FINAL_TYPE (IdePreferencesLanguageRow, ide_preferences_language_row, IDE, 
PREFERENCES_LANGUAGE_ROW, DzlPreferencesBin)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-language-row.c b/src/libide/gui/ide-preferences-language-row.c
new file mode 100644
index 000000000..b8844d8dc
--- /dev/null
+++ b/src/libide/gui/ide-preferences-language-row.c
@@ -0,0 +1,171 @@
+/* ide-preferences-language-row.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-preferences-language-row"
+
+#include "config.h"
+
+#include "ide-preferences-language-row-private.h"
+
+struct _IdePreferencesLanguageRow
+{
+  DzlPreferencesBin parent_instance;
+  gchar *id;
+  GtkLabel *title;
+};
+
+G_DEFINE_TYPE (IdePreferencesLanguageRow, ide_preferences_language_row, DZL_TYPE_PREFERENCES_BIN)
+
+enum {
+  PROP_0,
+  PROP_ID,
+  PROP_TITLE,
+  N_PROPS
+};
+
+enum {
+  ACTIVATE,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_preferences_language_row_activate (IdePreferencesLanguageRow *self)
+{
+  g_autoptr(GHashTable) map = NULL;
+  GtkWidget *preferences;
+
+  g_assert (IDE_IS_PREFERENCES_LANGUAGE_ROW (self));
+
+  if (self->id == NULL)
+    return;
+
+  preferences = gtk_widget_get_ancestor (GTK_WIDGET (self), DZL_TYPE_PREFERENCES);
+  if (preferences == NULL)
+    return;
+
+  map = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free);
+  g_hash_table_insert (map, (gchar *)"{id}", g_strdup (self->id));
+  dzl_preferences_set_page (DZL_PREFERENCES (preferences), "languages.id", map);
+}
+
+static void
+ide_preferences_language_row_get_property (GObject    *object,
+                                           guint       prop_id,
+                                           GValue     *value,
+                                           GParamSpec *pspec)
+{
+  IdePreferencesLanguageRow *self = IDE_PREFERENCES_LANGUAGE_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      g_value_set_string (value, self->id);
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, gtk_label_get_label (self->title));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_preferences_language_row_set_property (GObject      *object,
+                                           guint         prop_id,
+                                           const GValue *value,
+                                           GParamSpec   *pspec)
+{
+  IdePreferencesLanguageRow *self = IDE_PREFERENCES_LANGUAGE_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_ID:
+      self->id = g_value_dup_string (value);
+      break;
+
+    case PROP_TITLE:
+      gtk_label_set_label (self->title, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_preferences_language_row_finalize (GObject *object)
+{
+  IdePreferencesLanguageRow *self = (IdePreferencesLanguageRow *)object;
+
+  g_clear_pointer (&self->id, g_free);
+
+  G_OBJECT_CLASS (ide_preferences_language_row_parent_class)->finalize (object);
+}
+
+static void
+ide_preferences_language_row_class_init (IdePreferencesLanguageRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_preferences_language_row_finalize;
+  object_class->get_property = ide_preferences_language_row_get_property;
+  object_class->set_property = ide_preferences_language_row_set_property;
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "Id",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [ACTIVATE] =
+    g_signal_new_class_handler ("activate",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_FIRST,
+                                G_CALLBACK (ide_preferences_language_row_activate),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE, 0);
+
+  widget_class->activate_signal = signals [ACTIVATE];
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-preferences-language-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdePreferencesLanguageRow, title);
+}
+
+static void
+ide_preferences_language_row_init (IdePreferencesLanguageRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/libide/preferences/ide-preferences-language-row.ui 
b/src/libide/gui/ide-preferences-language-row.ui
similarity index 100%
rename from src/libide/preferences/ide-preferences-language-row.ui
rename to src/libide/gui/ide-preferences-language-row.ui
diff --git a/src/libide/gui/ide-preferences-surface.c b/src/libide/gui/ide-preferences-surface.c
new file mode 100644
index 000000000..e132b43c1
--- /dev/null
+++ b/src/libide/gui/ide-preferences-surface.c
@@ -0,0 +1,136 @@
+/* ide-preferences-surface.c
+ *
+ * Copyright 2015-2019 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 "ide-preferences-surface"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+
+#include "ide-preferences-addin.h"
+#include "ide-preferences-builtin-private.h"
+#include "ide-preferences-surface.h"
+#include "ide-surface.h"
+
+struct _IdePreferencesSurface
+{
+  IdeSurface          parent_instance;
+  DzlPreferencesView *view;
+  PeasExtensionSet   *extensions;
+};
+
+G_DEFINE_TYPE (IdePreferencesSurface, ide_preferences_surface, IDE_TYPE_SURFACE)
+
+static void
+ide_preferences_surface_addin_added_cb (PeasExtensionSet *set,
+                                        PeasPluginInfo   *plugin_info,
+                                        PeasExtension    *extension,
+                                        gpointer          user_data)
+{
+  IdePreferencesSurface *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_PREFERENCES_ADDIN (extension));
+  g_assert (IDE_IS_PREFERENCES_SURFACE (self));
+
+  ide_preferences_addin_load (IDE_PREFERENCES_ADDIN (extension), DZL_PREFERENCES (self->view));
+  dzl_preferences_view_reapply_filter (self->view);
+}
+
+static void
+ide_preferences_surface_addin_removed_cb (PeasExtensionSet *set,
+                                          PeasPluginInfo   *plugin_info,
+                                          PeasExtension    *extension,
+                                          gpointer          user_data)
+{
+  IdePreferencesSurface *self = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_PREFERENCES_ADDIN (extension));
+  g_assert (IDE_IS_PREFERENCES_SURFACE (self));
+
+  ide_preferences_addin_unload (IDE_PREFERENCES_ADDIN (extension), DZL_PREFERENCES (self->view));
+  dzl_preferences_view_reapply_filter (self->view);
+}
+
+static void
+ide_preferences_surface_destroy (GtkWidget *widget)
+{
+  IdePreferencesSurface *self = (IdePreferencesSurface *)widget;
+
+  g_clear_object (&self->extensions);
+
+  GTK_WIDGET_CLASS (ide_preferences_surface_parent_class)->destroy (widget);
+}
+
+static void
+ide_preferences_surface_constructed (GObject *object)
+{
+  IdePreferencesSurface *self = (IdePreferencesSurface *)object;
+
+  G_OBJECT_CLASS (ide_preferences_surface_parent_class)->constructed (object);
+
+  _ide_preferences_builtin_register (DZL_PREFERENCES (self->view));
+
+  self->extensions = peas_extension_set_new (peas_engine_get_default (),
+                                             IDE_TYPE_PREFERENCES_ADDIN,
+                                             NULL);
+
+  g_signal_connect (self->extensions,
+                    "extension-added",
+                    G_CALLBACK (ide_preferences_surface_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->extensions,
+                    "extension-removed",
+                    G_CALLBACK (ide_preferences_surface_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->extensions,
+                              ide_preferences_surface_addin_added_cb,
+                              self);
+}
+
+static void
+ide_preferences_surface_class_init (IdePreferencesSurfaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = ide_preferences_surface_constructed;
+
+  widget_class->destroy = ide_preferences_surface_destroy;
+}
+
+static void
+ide_preferences_surface_init (IdePreferencesSurface *self)
+{
+  ide_surface_set_icon_name (IDE_SURFACE (self), "preferences-system-symbolic");
+  gtk_widget_set_name (GTK_WIDGET (self), "preferences");
+
+  self->view = g_object_new (DZL_TYPE_PREFERENCES_VIEW,
+                             "visible", TRUE,
+                             NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->view));
+}
diff --git a/src/libide/gui/ide-preferences-surface.h b/src/libide/gui/ide-preferences-surface.h
new file mode 100644
index 000000000..5657af084
--- /dev/null
+++ b/src/libide/gui/ide-preferences-surface.h
@@ -0,0 +1,36 @@
+/* ide-preferences-surface.h
+ *
+ * Copyright 2015-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include "ide-surface.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PREFERENCES_SURFACE (ide_preferences_surface_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdePreferencesSurface, ide_preferences_surface, IDE, PREFERENCES_SURFACE, IdeSurface)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-window.c b/src/libide/gui/ide-preferences-window.c
new file mode 100644
index 000000000..450d5c457
--- /dev/null
+++ b/src/libide/gui/ide-preferences-window.c
@@ -0,0 +1,46 @@
+/* ide-preferences-window.c
+ *
+ * Copyright 2017-2019 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 "ide-preferences-window"
+
+#include "config.h"
+
+#include "ide-preferences-window.h"
+
+struct _IdePreferencesWindow
+{
+  DzlApplicationWindow parent_window;
+};
+
+G_DEFINE_TYPE (IdePreferencesWindow, ide_preferences_window, DZL_TYPE_APPLICATION_WINDOW)
+
+static void
+ide_preferences_window_class_init (IdePreferencesWindowClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-preferences-window.ui");
+}
+
+static void
+ide_preferences_window_init (IdePreferencesWindow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/libide/gui/ide-preferences-window.h b/src/libide/gui/ide-preferences-window.h
new file mode 100644
index 000000000..dc7fd2752
--- /dev/null
+++ b/src/libide/gui/ide-preferences-window.h
@@ -0,0 +1,33 @@
+/* ide-preferences-window.h
+ *
+ * Copyright 2017-2019 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 <dazzle.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PREFERENCES_WINDOW (ide_preferences_window_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdePreferencesWindow, ide_preferences_window, IDE, PREFERENCES_WINDOW, 
DzlApplicationWindow)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-preferences-window.ui b/src/libide/gui/ide-preferences-window.ui
new file mode 100644
index 000000000..a02207197
--- /dev/null
+++ b/src/libide/gui/ide-preferences-window.ui
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdePreferencesWindow" parent="DzlApplicationWindow">
+    <child type="titlebar">
+      <object class="IdeHeaderBar" id="header_bar">
+        <property name="title" translatable="yes">Preferences</property>
+        <property name="show-close-button">true</property>
+        <property name="visible">true</property>
+      </object>
+    </child>
+    <child>
+      <object class="IdePreferencesSurface" id="surface">
+        <property name="visible">true</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-primary-workspace-actions.c b/src/libide/gui/ide-primary-workspace-actions.c
new file mode 100644
index 000000000..e90976f4e
--- /dev/null
+++ b/src/libide/gui/ide-primary-workspace-actions.c
@@ -0,0 +1,109 @@
+/* ide-primary-workspace-actions.c
+ *
+ * Copyright 2018-2019 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 "ide-primary-workspace-actions"
+
+#include "config.h"
+
+#include <libide-foundry.h>
+#include <libpeas/peas.h>
+
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-primary-workspace.h"
+
+static void
+update_dependencies_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  IdeDependencyUpdater *updater = (IdeDependencyUpdater *)object;
+  g_autoptr(IdePrimaryWorkspace) self = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeContext *context;
+
+  g_assert (IDE_IS_DEPENDENCY_UPDATER (updater));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+
+  if (!ide_dependency_updater_update_finish (updater, result, &error))
+    ide_context_warning (context, "%s", error->message);
+
+  ide_object_destroy (IDE_OBJECT (updater));
+}
+
+static void
+ide_primary_workspace_actions_update_dependencies_cb (PeasExtensionSet *set,
+                                                      PeasPluginInfo   *plugin_info,
+                                                      PeasExtension    *exten,
+                                                      gpointer          user_data)
+{
+  IdeDependencyUpdater *updater = (IdeDependencyUpdater *)exten;
+  IdePrimaryWorkspace *self = user_data;
+  IdeContext *context;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_DEPENDENCY_UPDATER (updater));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  ide_object_append (IDE_OBJECT (context), IDE_OBJECT (updater));
+
+  ide_dependency_updater_update_async (updater,
+                                       NULL,
+                                       update_dependencies_cb,
+                                       g_object_ref (self));
+}
+
+static void
+ide_primary_workspace_actions_update_dependencies (GSimpleAction *action,
+                                                   GVariant      *param,
+                                                   gpointer       user_data)
+{
+  IdePrimaryWorkspace *self = user_data;
+  g_autoptr(PeasExtensionSet) set = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  set = peas_extension_set_new (peas_engine_get_default (),
+                                IDE_TYPE_DEPENDENCY_UPDATER,
+                                NULL);
+  peas_extension_set_foreach (set, ide_primary_workspace_actions_update_dependencies_cb, self);
+}
+
+static const GActionEntry actions[] = {
+  { "update-dependencies", ide_primary_workspace_actions_update_dependencies },
+};
+
+void
+_ide_primary_workspace_init_actions (IdePrimaryWorkspace *self)
+{
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (self),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+}
diff --git a/src/libide/gui/ide-primary-workspace.c b/src/libide/gui/ide-primary-workspace.c
new file mode 100644
index 000000000..a88b3d9fd
--- /dev/null
+++ b/src/libide/gui/ide-primary-workspace.c
@@ -0,0 +1,141 @@
+/* ide-primary-workspace.c
+ *
+ * Copyright 2018-2019 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 "ide-primary-workspace"
+
+#include "config.h"
+
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-header-bar.h"
+#include "ide-omni-bar.h"
+#include "ide-primary-workspace.h"
+#include "ide-run-button.h"
+#include "ide-search-entry.h"
+#include "ide-surface.h"
+#include "ide-window-settings-private.h"
+
+/**
+ * SECTION:ide-primary-workspace
+ * @title: IdePrimaryWorkspace
+ * @short_description: The primary IDE window
+ *
+ * The primary workspace is the main workspace window for the user. This is the
+ * "IDE experience" workspace. It is generally created by the workbench when
+ * opening a project (unless another workspace type has been requested).
+ *
+ * See ide_workbench_open_async() for how to select another workspace type
+ * when opening a project.
+ *
+ * Returns: (transfer full): an #IdePrimaryWorkspace
+ *
+ * Since: 3.32
+ */
+
+struct _IdePrimaryWorkspace
+{
+  IdeWorkspace   parent_instance;
+
+  /* Template widgets */
+  IdeHeaderBar   *header_bar;
+  DzlMenuButton  *surface_menu_button;
+  IdeRunButton   *run_button;
+  IdeSearchEntry *search_entry;
+  GtkLabel       *project_title;
+};
+
+G_DEFINE_TYPE (IdePrimaryWorkspace, ide_primary_workspace, IDE_TYPE_WORKSPACE)
+
+static void
+ide_primary_workspace_context_set (IdeWorkspace *workspace,
+                                   IdeContext   *context)
+{
+  IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)workspace;
+  IdeProjectInfo *project_info;
+  IdeWorkbench *workbench;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  IDE_WORKSPACE_CLASS (ide_primary_workspace_parent_class)->context_set (workspace, context);
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+  project_info = ide_workbench_get_project_info (workbench);
+
+  if (project_info)
+    g_object_bind_property (project_info, "name", self->project_title, "label",
+                            G_BINDING_SYNC_CREATE);
+}
+
+static void
+ide_primary_workspace_surface_set (IdeWorkspace *workspace,
+                                   IdeSurface   *surface)
+{
+  IdePrimaryWorkspace *self = (IdePrimaryWorkspace *)workspace;
+
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (self));
+  g_assert (!surface || IDE_IS_SURFACE (surface));
+
+  if (DZL_IS_DOCK_ITEM (surface))
+    {
+      g_autofree gchar *icon_name = NULL;
+
+      icon_name = dzl_dock_item_get_icon_name (DZL_DOCK_ITEM (surface));
+      g_object_set (self->surface_menu_button,
+                    "icon-name", icon_name,
+                    NULL);
+    }
+
+  IDE_WORKSPACE_CLASS (ide_primary_workspace_parent_class)->surface_set (workspace, surface);
+}
+
+static void
+ide_primary_workspace_class_init (IdePrimaryWorkspaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdeWorkspaceClass *workspace_class = IDE_WORKSPACE_CLASS (klass);
+
+  ide_workspace_class_set_kind (workspace_class, "primary");
+
+  workspace_class->surface_set = ide_primary_workspace_surface_set;
+  workspace_class->context_set = ide_primary_workspace_context_set;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-primary-workspace.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, header_bar);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, project_title);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, run_button);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, search_entry);
+  gtk_widget_class_bind_template_child (widget_class, IdePrimaryWorkspace, surface_menu_button);
+
+  g_type_ensure (IDE_TYPE_HEADER_BAR);
+  g_type_ensure (IDE_TYPE_OMNI_BAR);
+  g_type_ensure (IDE_TYPE_RUN_BUTTON);
+  g_type_ensure (IDE_TYPE_SEARCH_ENTRY);
+}
+
+static void
+ide_primary_workspace_init (IdePrimaryWorkspace *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  _ide_primary_workspace_init_actions (self);
+  _ide_window_settings_register (GTK_WINDOW (self));
+}
diff --git a/src/libide/gui/ide-primary-workspace.h b/src/libide/gui/ide-primary-workspace.h
new file mode 100644
index 000000000..a20da72eb
--- /dev/null
+++ b/src/libide/gui/ide-primary-workspace.h
@@ -0,0 +1,38 @@
+/* ide-primary-workspace.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include "ide-application.h"
+#include "ide-surface.h"
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PRIMARY_WORKSPACE (ide_primary_workspace_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdePrimaryWorkspace, ide_primary_workspace, IDE, PRIMARY_WORKSPACE, IdeWorkspace)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-primary-workspace.ui b/src/libide/gui/ide-primary-workspace.ui
new file mode 100644
index 000000000..a374869b0
--- /dev/null
+++ b/src/libide/gui/ide-primary-workspace.ui
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdePrimaryWorkspace" parent="IdeWorkspace">
+    <child type="titlebar">
+      <object class="IdeHeaderBar" id="header_bar">
+        <property name="menu-id">ide-primary-workspace-menu</property>
+        <property name="show-close-button">true</property>
+        <property name="show-fullscreen-button">true</property>
+        <property name="visible">true</property>
+        <child type="left">
+          <object class="IdeSurfacesButton" id="surface_menu_button">
+            <property name="focus-on-click">false</property>
+            <property name="menu-id">ide-primary-workspace-surfaces-menu</property>
+            <property name="show-accels">true</property>
+            <property name="show-arrow">true</property>
+            <property name="show-icons">true</property>
+            <!-- disable transitions since they'll cause jitter with the
+                 whole surface changing below it. -->
+            <property name="transitions-enabled">false</property>
+            <property name="has-tooltip">true</property>
+            <property name="tooltip-text" translatable="yes">Change workspace surface</property>
+          </object>
+        </child>
+        <child type="title">
+          <object class="IdeOmniBar" id="omni_bar">
+            <property name="halign">center</property>
+            <property name="hexpand">false</property>
+            <property name="hexpand-set">true</property>
+            <property name="visible">true</property>
+            <child type="placeholder">
+              <object class="GtkLabel" id="project_title">
+                <property name="visible">true</property>
+                <property name="xalign">0.0</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child type="right-of-center">
+          <object class="IdeRunButton" id="run_button">
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child type="right">
+          <object class="IdeNotificationsButton" id="notifications_button">
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child type="right">
+          <object class="IdeSearchEntry" id="search_entry">
+            <property name="margin-start">6</property>
+            <property name="margin-end">6</property>
+            <property name="primary-icon-name">edit-find-symbolic</property>
+            <property name="placeholder-text" translatable="yes">Press Ctrl+. to search</property>
+            <property name="width-chars">5</property>
+            <property name="max-width-chars">24</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-run-button.c b/src/libide/gui/ide-run-button.c
new file mode 100644
index 000000000..6b69341ba
--- /dev/null
+++ b/src/libide/gui/ide-run-button.c
@@ -0,0 +1,200 @@
+/* ide-run-button.c
+ *
+ * Copyright 2016-2019 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 "ide-run-button"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-foundry.h>
+
+#include "ide-gui-global.h"
+#include "ide-run-button.h"
+
+#include "ide-run-manager-private.h"
+
+struct _IdeRunButton
+{
+  GtkBox                parent_instance;
+
+  GtkButton            *button;
+  GtkImage             *button_image;
+  DzlMenuButton        *menu_button;
+  GtkButton            *stop_button;
+  GtkShortcutsShortcut *run_shortcut;
+  GtkLabel             *run_tooltip_message;
+  DzlShortcutTooltip   *tooltip;
+};
+
+G_DEFINE_TYPE (IdeRunButton, ide_run_button, GTK_TYPE_BOX)
+
+static void
+ide_run_button_handler_set (IdeRunButton  *self,
+                            GParamSpec    *pspec,
+                            IdeRunManager *run_manager)
+{
+  const GList *list;
+  const GList *iter;
+  const gchar *handler;
+
+  g_assert (IDE_IS_RUN_BUTTON (self));
+  g_assert (IDE_IS_RUN_MANAGER (run_manager));
+
+  handler = ide_run_manager_get_handler (run_manager);
+  list = _ide_run_manager_get_handlers (run_manager);
+
+  for (iter = list; iter; iter = iter->next)
+    {
+      const IdeRunHandlerInfo *info = iter->data;
+
+      if (g_strcmp0 (info->id, handler) == 0)
+        {
+          g_object_set (self->button_image,
+                        "icon-name", info->icon_name,
+                        NULL);
+          break;
+        }
+    }
+}
+
+static void
+ide_run_button_load (IdeRunButton *self,
+                     IdeContext   *context)
+{
+  IdeRunManager *run_manager;
+
+  g_assert (IDE_IS_RUN_BUTTON (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  run_manager = ide_run_manager_from_context (context);
+
+  g_object_bind_property (run_manager, "busy", self->button, "visible",
+                          G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN);
+  g_object_bind_property (run_manager, "busy", self->stop_button, "visible",
+                          G_BINDING_SYNC_CREATE);
+
+  g_signal_connect_object (run_manager,
+                           "notify::handler",
+                           G_CALLBACK (ide_run_button_handler_set),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_run_button_handler_set (self, NULL, run_manager);
+}
+
+static void
+ide_run_button_context_set (GtkWidget  *widget,
+                            IdeContext *context)
+{
+  IdeRunButton *self = (IdeRunButton *)widget;
+
+  g_assert (IDE_IS_RUN_BUTTON (self));
+  g_assert (!context || IDE_IS_CONTEXT (context));
+
+  if (context != NULL)
+    ide_run_button_load (self, context);
+}
+
+static gboolean
+ide_run_button_query_tooltip (IdeRunButton *self,
+                              gint          x,
+                              gint          y,
+                              gboolean      keyboard_tooltip,
+                              GtkTooltip   *tooltip,
+                              GtkButton    *button)
+{
+  IdeRunManager *run_manager;
+  const GList *list;
+  const GList *iter;
+  const gchar *handler;
+  IdeContext *context;
+
+  g_assert (IDE_IS_RUN_BUTTON (self));
+  g_assert (GTK_IS_TOOLTIP (tooltip));
+  g_assert (GTK_IS_BUTTON (button));
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  run_manager = ide_run_manager_from_context (context);
+  handler = ide_run_manager_get_handler (run_manager);
+  list = _ide_run_manager_get_handlers (run_manager);
+
+  for (iter = list; iter; iter = iter->next)
+    {
+      const IdeRunHandlerInfo *info = iter->data;
+
+      if (g_strcmp0 (info->id, handler) == 0)
+        {
+          gboolean enabled;
+          /* Figure out if the run action is enabled. If it
+           * is not, then we should inform the user that
+           * the project cannot be run yet because the
+           * build pipeline is not yet configured. */
+          g_action_group_query_action (G_ACTION_GROUP (run_manager),
+                                       "run",
+                                       &enabled,
+                                       NULL,
+                                       NULL,
+                                       NULL,
+                                       NULL);
+
+          if (!enabled)
+            {
+              gtk_tooltip_set_custom (tooltip, GTK_WIDGET (self->run_tooltip_message));
+              return TRUE;
+            }
+
+          /* The shortcut tooltip will set this up after us */
+          dzl_shortcut_tooltip_set_accel (self->tooltip, info->accel);
+          dzl_shortcut_tooltip_set_title (self->tooltip, info->title);
+        }
+    }
+
+  return FALSE;
+}
+
+static void
+ide_run_button_class_init (IdeRunButtonClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-run-button.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, button);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, button_image);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, menu_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, run_shortcut);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, stop_button);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, run_tooltip_message);
+  gtk_widget_class_bind_template_child (widget_class, IdeRunButton, tooltip);
+}
+
+static void
+ide_run_button_init (IdeRunButton *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->button,
+                           "query-tooltip",
+                           G_CALLBACK (ide_run_button_query_tooltip),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  ide_widget_set_context_handler (self, ide_run_button_context_set);
+}
diff --git a/src/libide/gui/ide-run-button.h b/src/libide/gui/ide-run-button.h
new file mode 100644
index 000000000..f758c3adc
--- /dev/null
+++ b/src/libide/gui/ide-run-button.h
@@ -0,0 +1,33 @@
+/* ide-run-button.h
+ *
+ * Copyright 2016-2019 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_RUN_BUTTON (ide_run_button_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeRunButton, ide_run_button, IDE, RUN_BUTTON, GtkBox)
+
+GtkWidget *ide_run_button_new (void);
+
+G_END_DECLS
diff --git a/src/libide/runner/ide-run-button.ui b/src/libide/gui/ide-run-button.ui
similarity index 100%
rename from src/libide/runner/ide-run-button.ui
rename to src/libide/gui/ide-run-button.ui
diff --git a/src/libide/gui/ide-search-entry.c b/src/libide/gui/ide-search-entry.c
new file mode 100644
index 000000000..8d8088ca4
--- /dev/null
+++ b/src/libide/gui/ide-search-entry.c
@@ -0,0 +1,294 @@
+/* ide-search-entry.c
+ *
+ * Copyright 2017-2019 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 "ide-search-entry"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-core.h>
+#include <libide-search.h>
+
+#include "ide-gui-global.h"
+#include "ide-search-entry.h"
+#include "ide-workbench.h"
+
+#define DEFAULT_SEARCH_MAX 25
+#define I_ g_intern_string
+
+struct _IdeSearchEntry
+{
+  DzlSuggestionEntry parent_instance;
+  guint              max_results;
+};
+
+G_DEFINE_TYPE (IdeSearchEntry, ide_search_entry, DZL_TYPE_SUGGESTION_ENTRY)
+
+enum {
+  PROP_0,
+  PROP_MAX_RESULTS,
+  N_PROPS
+};
+
+enum {
+  UNFOCUS,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+search_popover_position_func (DzlSuggestionEntry *entry,
+                              GdkRectangle       *area,
+                              gboolean           *is_absolute,
+                              gpointer            user_data)
+{
+  gint new_width;
+
+  g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
+  g_assert (area != NULL);
+  g_assert (is_absolute != NULL);
+  g_assert (user_data == NULL);
+
+#define RIGHT_MARGIN 6
+
+  /* We want the search area to be the right 2/5ths of the window, with a bit
+   * of margin on the popover.
+   */
+
+  dzl_suggestion_entry_window_position_func (entry, area, is_absolute, NULL);
+
+  new_width = (area->width * 2 / 5);
+  area->x += area->width - new_width;
+  area->width = new_width - RIGHT_MARGIN;
+  area->y -= 3;
+
+#undef RIGHT_MARGIN
+}
+
+static void
+ide_search_entry_search_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeSearchEngine *engine = (IdeSearchEngine *)object;
+  g_autoptr(IdeSearchEntry) self = user_data;
+  g_autoptr(GListModel) suggestions = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_SEARCH_ENTRY (self));
+  g_assert (IDE_IS_SEARCH_ENGINE (engine));
+
+  suggestions = ide_search_engine_search_finish (engine, result, &error);
+
+  if (error != NULL)
+    {
+      /* TODO: Elevate to workbench message once we have that capability */
+      g_warning ("%s", error->message);
+      return;
+    }
+
+  g_assert (suggestions != NULL);
+  g_assert (G_IS_LIST_MODEL (suggestions));
+  g_assert (g_type_is_a (g_list_model_get_item_type (suggestions), DZL_TYPE_SUGGESTION));
+
+  dzl_suggestion_entry_set_model (DZL_SUGGESTION_ENTRY (self), suggestions);
+}
+
+static void
+ide_search_entry_changed (IdeSearchEntry *self)
+{
+  IdeSearchEngine *engine;
+  IdeWorkbench *workbench;
+  const gchar *typed_text;
+
+  g_assert (IDE_IS_SEARCH_ENTRY (self));
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+  engine = ide_workbench_get_search_engine (workbench);
+  typed_text = dzl_suggestion_entry_get_typed_text (DZL_SUGGESTION_ENTRY (self));
+
+  if (dzl_str_empty0 (typed_text))
+    dzl_suggestion_entry_set_model (DZL_SUGGESTION_ENTRY (self), NULL);
+  else
+    ide_search_engine_search_async (engine,
+                                    typed_text,
+                                    self->max_results,
+                                    NULL,
+                                    ide_search_entry_search_cb,
+                                    g_object_ref (self));
+}
+
+static void
+suggestion_activated (DzlSuggestionEntry *entry,
+                      DzlSuggestion      *suggestion)
+{
+  g_assert (IDE_IS_SEARCH_ENTRY (entry));
+  g_assert (IDE_IS_SEARCH_RESULT (suggestion));
+
+  /* TODO: Get last focus from workspace */
+  ide_search_result_activate (IDE_SEARCH_RESULT (suggestion), GTK_WIDGET (entry));
+
+  /* Chain up to properly clear entry buffer */
+  if (DZL_SUGGESTION_ENTRY_CLASS (ide_search_entry_parent_class)->suggestion_activated)
+    DZL_SUGGESTION_ENTRY_CLASS (ide_search_entry_parent_class)->suggestion_activated (entry, suggestion);
+}
+
+static void
+ide_search_entry_unfocus (IdeSearchEntry *self)
+{
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_SEARCH_ENTRY (self));
+
+  g_signal_emit_by_name (self, "hide-suggestions");
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+  gtk_widget_grab_focus (toplevel);
+  gtk_entry_set_text (GTK_ENTRY (self), "");
+}
+
+static void
+ide_search_entry_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  IdeSearchEntry *self = IDE_SEARCH_ENTRY (object);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_RESULTS:
+      g_value_set_uint (value, self->max_results);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_search_entry_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  IdeSearchEntry *self = IDE_SEARCH_ENTRY (object);
+
+  switch (prop_id)
+    {
+    case PROP_MAX_RESULTS:
+      self->max_results = g_value_get_uint (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static DzlShortcutEntry shortcuts[] = {
+  { "org.gnome.builder.workspace.global-search",
+    0, NULL,
+    NC_("shortcut window", "Workspace shortcuts"),
+    NC_("shortcut window", "Search"),
+    NC_("shortcut window", "Focus to the global search entry") },
+};
+
+static void
+ide_search_entry_init_shortcuts (IdeSearchEntry *self)
+{
+  DzlShortcutController *controller;
+
+  g_assert (IDE_IS_SEARCH_ENTRY (self));
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_callback (controller,
+                                                I_("org.gnome.builder.workspace.global-search"),
+                                                "<Primary>period",
+                                                DZL_SHORTCUT_PHASE_CAPTURE | DZL_SHORTCUT_PHASE_GLOBAL,
+                                                (GtkCallback)gtk_widget_grab_focus,
+                                                NULL,
+                                                NULL);
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             shortcuts,
+                                             G_N_ELEMENTS (shortcuts),
+                                             GETTEXT_PACKAGE);
+}
+
+static void
+ide_search_entry_class_init (IdeSearchEntryClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  DzlSuggestionEntryClass *suggestion_entry_class = DZL_SUGGESTION_ENTRY_CLASS (klass);
+  GtkBindingSet *bindings;
+
+  object_class->get_property = ide_search_entry_get_property;
+  object_class->set_property = ide_search_entry_set_property;
+
+  suggestion_entry_class->suggestion_activated = suggestion_activated;
+
+  properties [PROP_MAX_RESULTS] =
+    g_param_spec_uint ("max-results",
+                       "Max Results",
+                       "Maximum number of search results to display",
+                       1,
+                       1000,
+                       DEFAULT_SEARCH_MAX,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [UNFOCUS] =
+    g_signal_new_class_handler ("unfocus",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (ide_search_entry_unfocus),
+                                NULL, NULL, NULL, G_TYPE_NONE, 0);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-search-entry.ui");
+
+  bindings = gtk_binding_set_by_class (klass);
+  gtk_binding_entry_add_signal (bindings, GDK_KEY_Escape, 0, "unfocus", 0);
+}
+
+static void
+ide_search_entry_init (IdeSearchEntry *self)
+{
+  self->max_results = DEFAULT_SEARCH_MAX;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "global-search");
+
+  g_signal_connect (self,
+                    "changed",
+                    G_CALLBACK (ide_search_entry_changed),
+                    NULL);
+
+  dzl_suggestion_entry_set_position_func (DZL_SUGGESTION_ENTRY (self),
+                                          search_popover_position_func,
+                                          NULL,
+                                          NULL);
+
+  ide_search_entry_init_shortcuts (self);
+}
diff --git a/src/libide/gui/ide-search-entry.h b/src/libide/gui/ide-search-entry.h
new file mode 100644
index 000000000..f1bd206ea
--- /dev/null
+++ b/src/libide/gui/ide-search-entry.h
@@ -0,0 +1,39 @@
+/* ide-search-entry.h
+ *
+ * Copyright 2015-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SEARCH_ENTRY (ide_search_entry_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeSearchEntry, ide_search_entry, IDE, SEARCH_ENTRY, DzlSuggestionEntry)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_search_entry_new (void);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-search-entry.ui b/src/libide/gui/ide-search-entry.ui
new file mode 100644
index 000000000..f5e623ca1
--- /dev/null
+++ b/src/libide/gui/ide-search-entry.ui
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeSearchEntry" parent="DzlSuggestionEntry">
+    <child internal-child="popover">
+      <object class="DzlSuggestionPopover">
+        <property name="title-ellipsize">middle</property>
+        <property name="subtitle-ellipsize">end</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/gui/ide-session-addin.c b/src/libide/gui/ide-session-addin.c
new file mode 100644
index 000000000..576e91023
--- /dev/null
+++ b/src/libide/gui/ide-session-addin.c
@@ -0,0 +1,172 @@
+/* ide-session-addin.c
+ *
+ * Copyright 2018-2019 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 "ide-session-addin"
+
+#include "config.h"
+
+#include "ide-session-addin.h"
+
+G_DEFINE_INTERFACE (IdeSessionAddin, ide_session_addin, IDE_TYPE_OBJECT)
+
+static void
+ide_session_addin_real_save_async (IdeSessionAddin     *self,
+                                   IdeWorkbench         *workbench,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_session_addin_real_save_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Save not supported");
+}
+
+static GVariant *
+ide_session_addin_real_save_finish (IdeSessionAddin  *self,
+                                    GAsyncResult     *result,
+                                    GError          **error)
+{
+  return g_task_propagate_pointer (G_TASK (result), error);
+}
+
+static void
+ide_session_addin_real_restore_async (IdeSessionAddin     *self,
+                                      IdeWorkbench         *workbench,
+                                      GVariant            *state,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_session_addin_real_restore_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Restore not supported");
+}
+
+static gboolean
+ide_session_addin_real_restore_finish (IdeSessionAddin  *self,
+                                       GAsyncResult     *result,
+                                       GError          **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_session_addin_default_init (IdeSessionAddinInterface *iface)
+{
+  iface->save_async = ide_session_addin_real_save_async;
+  iface->save_finish = ide_session_addin_real_save_finish;
+  iface->restore_async = ide_session_addin_real_restore_async;
+  iface->restore_finish = ide_session_addin_real_restore_finish;
+}
+
+/**
+ * ide_session_addin_save_async:
+ * @self: a #IdeSessionAddin
+ * @workbench: an #IdeWorkbench
+ * @cancellable: (nullable): A #GCancellable or %NULL
+ * @callback: callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronous request to save state about the session.
+ *
+ * The resulting state will be provided when restoring the addin
+ * at a future time.
+ *
+ * Since: 3.30
+ */
+void
+ide_session_addin_save_async (IdeSessionAddin     *self,
+                              IdeWorkbench        *workbench,
+                              GCancellable        *cancellable,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_SESSION_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_SESSION_ADDIN_GET_IFACE (self)->save_async (self, workbench, cancellable, callback, user_data);
+}
+
+/**
+ * ide_session_addin_save_finish:
+ * @self: a #IdeSessionAddin
+ *
+ * Completes an asynchronous request to save session state.
+ *
+ * The resulting #GVariant will be used to restore state at a future time.
+ *
+ * Returns: (transfer full) (nullable): a #GVariant or %NULL.
+ *
+ * Since: 3.30
+ */
+GVariant *
+ide_session_addin_save_finish (IdeSessionAddin  *self,
+                               GAsyncResult     *result,
+                               GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_SESSION_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_SESSION_ADDIN_GET_IFACE (self)->save_finish (self, result, error);
+}
+
+/**
+ * ide_session_addin_restore_async:
+ * @self: a #IdeSessionAddin
+ * @workbench: an #IdeWorkbench
+ * @state: a #GVariant of previous state
+ * @cancellable: (nullable): A #GCancellable or %NULL
+ * @callback: callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronous request to restore session state by the addin.
+ *
+ * Since: 3.30
+ */
+void
+ide_session_addin_restore_async (IdeSessionAddin     *self,
+                                 IdeWorkbench        *workbench,
+                                 GVariant            *state,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_SESSION_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_SESSION_ADDIN_GET_IFACE (self)->restore_async (self, workbench, state, cancellable, callback, 
user_data);
+}
+
+gboolean
+ide_session_addin_restore_finish (IdeSessionAddin  *self,
+                                  GAsyncResult     *result,
+                                  GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_SESSION_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_SESSION_ADDIN_GET_IFACE (self)->restore_finish (self, result, error);
+}
diff --git a/src/libide/gui/ide-session-addin.h b/src/libide/gui/ide-session-addin.h
new file mode 100644
index 000000000..14425f5e3
--- /dev/null
+++ b/src/libide/gui/ide-session-addin.h
@@ -0,0 +1,83 @@
+/* ide-session-addin.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-workbench.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SESSION_ADDIN (ide_session_addin_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeSessionAddin, ide_session_addin, IDE, SESSION_ADDIN, IdeObject)
+
+struct _IdeSessionAddinInterface
+{
+  GTypeInterface parent;
+
+  void      (*save_async)     (IdeSessionAddin      *self,
+                               IdeWorkbench         *workbench,
+                               GCancellable         *cancellable,
+                               GAsyncReadyCallback   callback,
+                               gpointer              user_data);
+  GVariant *(*save_finish)    (IdeSessionAddin      *self,
+                               GAsyncResult         *result,
+                               GError              **error);
+  void      (*restore_async)  (IdeSessionAddin      *self,
+                               IdeWorkbench         *workbench,
+                               GVariant             *state,
+                               GCancellable         *cancellable,
+                               GAsyncReadyCallback   callback,
+                               gpointer              user_data);
+  gboolean  (*restore_finish) (IdeSessionAddin      *self,
+                               GAsyncResult         *result,
+                               GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void      ide_session_addin_save_async     (IdeSessionAddin      *self,
+                                            IdeWorkbench         *workbench,
+                                            GCancellable         *cancellable,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GVariant *ide_session_addin_save_finish    (IdeSessionAddin      *self,
+                                            GAsyncResult         *result,
+                                            GError              **error);
+IDE_AVAILABLE_IN_3_32
+void      ide_session_addin_restore_async  (IdeSessionAddin      *self,
+                                            IdeWorkbench         *workbench,
+                                            GVariant             *state,
+                                            GCancellable         *cancellable,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean  ide_session_addin_restore_finish (IdeSessionAddin      *self,
+                                            GAsyncResult         *result,
+                                            GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-session-private.h b/src/libide/gui/ide-session-private.h
new file mode 100644
index 000000000..7a7f343dc
--- /dev/null
+++ b/src/libide/gui/ide-session-private.h
@@ -0,0 +1,51 @@
+/* ide-session-private.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+#include "ide-workbench.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SESSION (ide_session_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeSession, ide_session, IDE, SESSION, IdeObject)
+
+IdeSession *ide_session_new            (void);
+void        ide_session_restore_async  (IdeSession           *self,
+                                        IdeWorkbench         *workbench,
+                                        GCancellable         *cancellable,
+                                        GAsyncReadyCallback   callback,
+                                        gpointer              user_data);
+gboolean    ide_session_restore_finish (IdeSession           *self,
+                                        GAsyncResult         *result,
+                                        GError              **error);
+void        ide_session_save_async     (IdeSession           *self,
+                                        IdeWorkbench         *workbench,
+                                        GCancellable         *cancellable,
+                                        GAsyncReadyCallback   callback,
+                                        gpointer              user_data);
+gboolean    ide_session_save_finish    (IdeSession           *self,
+                                        GAsyncResult         *result,
+                                        GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-session.c b/src/libide/gui/ide-session.c
new file mode 100644
index 000000000..29d5fe81d
--- /dev/null
+++ b/src/libide/gui/ide-session.c
@@ -0,0 +1,518 @@
+/* ide-session.c
+ *
+ * Copyright 2018-2019 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 "ide-session"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+
+#include "ide-session-addin.h"
+#include "ide-session-private.h"
+
+struct _IdeSession
+{
+  IdeObject               parent_instance;
+  IdeExtensionSetAdapter *addins;
+};
+
+typedef struct
+{
+  GPtrArray    *addins;
+  GVariantDict  dict;
+  gint          active;
+  guint         dict_needs_clear : 1;
+} Save;
+
+typedef struct
+{
+  IdeWorkbench *workbench;
+  GPtrArray    *addins;
+  GVariant     *state;
+  gint          active;
+} Restore;
+
+G_DEFINE_TYPE (IdeSession, ide_session, IDE_TYPE_OBJECT)
+
+static void
+restore_free (Restore *r)
+{
+  g_assert (r != NULL);
+
+  g_clear_pointer (&r->addins, g_ptr_array_unref);
+  g_clear_pointer (&r->state, g_variant_unref);
+  g_clear_object (&r->workbench);
+  g_slice_free (Restore, r);
+}
+
+static void
+save_free (Save *s)
+{
+  g_assert (s != NULL);
+  g_assert (s->active == 0);
+
+  if (s->dict_needs_clear)
+    g_variant_dict_clear (&s->dict);
+
+  g_clear_pointer (&s->addins, g_ptr_array_unref);
+  g_slice_free (Save, s);
+}
+
+static void
+collect_addins_cb (IdeExtensionSetAdapter *set,
+                   PeasPluginInfo         *plugin_info,
+                   PeasExtension          *exten,
+                   gpointer                user_data)
+{
+  GPtrArray *ar = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_SESSION_ADDIN (exten));
+  g_assert (ar != NULL);
+
+  g_ptr_array_add (ar, g_object_ref (exten));
+}
+
+static void
+ide_session_destroy (IdeObject *object)
+{
+  IdeSession *self = (IdeSession *)object;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SESSION (self));
+
+  ide_clear_and_destroy_object (&self->addins);
+
+  IDE_OBJECT_CLASS (ide_session_parent_class)->destroy (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_session_parent_set (IdeObject *object,
+                        IdeObject *parent)
+{
+  IdeSession *self = (IdeSession *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SESSION (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (parent == NULL)
+    return;
+
+  self->addins = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_SESSION_ADDIN,
+                                                NULL, NULL);
+}
+
+static void
+ide_session_class_init (IdeSessionClass *klass)
+{
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  i_object_class->destroy = ide_session_destroy;
+  i_object_class->parent_set = ide_session_parent_set;
+}
+
+static void
+ide_session_init (IdeSession *self)
+{
+}
+
+static void
+ide_session_restore_addin_restore_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  IdeSessionAddin *addin = (IdeSessionAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  Restore *r;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SESSION_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  r = ide_task_get_task_data (task);
+
+  g_assert (r != NULL);
+  g_assert (r->addins != NULL);
+  g_assert (r->active > 0);
+  g_assert (r->state != NULL);
+
+  if (!ide_session_addin_restore_finish (addin, result, &error))
+    g_warning ("%s: %s", G_OBJECT_TYPE_NAME (addin), error->message);
+
+  r->active--;
+
+  if (r->active == 0)
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_session_restore_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_autoptr(GBytes) bytes = NULL;
+  GCancellable *cancellable;
+  Restore *r;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  r = ide_task_get_task_data (task);
+  cancellable = ide_task_get_cancellable (task);
+
+  g_assert (r != NULL);
+  g_assert (r->addins != NULL);
+  g_assert (r->active > 0);
+  g_assert (r->state == NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!(bytes = g_file_load_bytes_finish (file, result, NULL, &error)))
+    {
+      if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+        ide_task_return_boolean (task, TRUE);
+      else
+        ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (g_bytes_get_size (bytes) == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  r->state = g_variant_new_from_bytes (G_VARIANT_TYPE_VARDICT, bytes, FALSE);
+
+  if (r->state == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_DATA,
+                                 "Failed to decode session state");
+      IDE_EXIT;
+    }
+
+  g_assert (r->addins != NULL);
+  g_assert (r->addins->len > 0);
+
+  for (guint i = 0; i < r->addins->len; i++)
+    {
+      IdeSessionAddin *addin = g_ptr_array_index (r->addins, i);
+      g_autoptr(GVariant) state = NULL;
+
+      g_assert (IDE_IS_SESSION_ADDIN (addin));
+
+      state = g_variant_lookup_value (r->state,
+                                      G_OBJECT_TYPE_NAME (addin),
+                                      NULL);
+
+      ide_session_addin_restore_async (addin,
+                                       r->workbench,
+                                       state,
+                                       cancellable,
+                                       ide_session_restore_addin_restore_cb,
+                                       g_object_ref (task));
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_session_restore_async:
+ * @self: an #IdeSession
+ * @workbench: an #IdeWorkbench
+ * @cancellable: (nullable): a #GCancellbale or %NULL
+ * @callback: the callback to execute upon completion
+ * @user_data: user data for callback
+ *
+ * This function will asynchronously restore the state of the project to
+ * the point it was last saved (typically upon shutdown). This includes
+ * open documents and editor splits to the degree possible.
+ *
+ * Since: 3.30
+ */
+void
+ide_session_restore_async (IdeSession          *self,
+                           IdeWorkbench        *workbench,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) file = NULL;
+  IdeContext *context;
+  Restore *r;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SESSION (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_session_restore_async);
+
+  r = g_slice_new0 (Restore);
+  r->workbench = g_object_ref (workbench);
+  r->addins = g_ptr_array_new_with_free_func (g_object_unref);
+  ide_extension_set_adapter_foreach (self->addins, collect_addins_cb, r->addins);
+  r->active = r->addins->len;
+  ide_task_set_task_data (task, r, restore_free);
+
+  if (r->active == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  file = ide_context_cache_file (context, "session.gvariant", NULL);
+
+  g_file_load_bytes_async (file,
+                           cancellable,
+                           ide_session_restore_load_cb,
+                           g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_session_restore_finish (IdeSession    *self,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_SESSION (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_session_save_cb (GObject      *object,
+                     GAsyncResult *result,
+                     gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!g_file_replace_contents_finish (file, result, NULL, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_session_save_addin_save_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeSessionAddin *addin = (IdeSessionAddin *)object;
+  g_autoptr(GVariant) variant = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeSession *self;
+  Save *s;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SESSION_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  s = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_SESSION (self));
+  g_assert (s != NULL);
+  g_assert (s->addins != NULL);
+  g_assert (s->active > 0);
+
+  variant = ide_session_addin_save_finish (addin, result, &error);
+
+  if (error != NULL)
+    g_warning ("%s: %s", G_OBJECT_TYPE_NAME (addin), error->message);
+
+  if (variant != NULL)
+    {
+      g_assert (!g_variant_is_floating (variant));
+
+      s->dict_needs_clear = TRUE;
+      g_variant_dict_insert_value (&s->dict, G_OBJECT_TYPE_NAME (addin), variant);
+    }
+
+  s->active--;
+
+  if (s->active == 0)
+    {
+      g_autoptr(GVariant) state = NULL;
+      g_autoptr(GBytes) bytes = NULL;
+      g_autoptr(GFile) file = NULL;
+      GCancellable *cancellable;
+      IdeContext *context;
+
+      s->dict_needs_clear = FALSE;
+
+      state = g_variant_take_ref (g_variant_dict_end (&s->dict));
+      bytes = g_variant_get_data_as_bytes (state);
+
+      cancellable = ide_task_get_cancellable (task);
+      context = ide_object_get_context (IDE_OBJECT (self));
+      file = ide_context_cache_file (context, "session.gvariant", NULL);
+
+      if (ide_task_return_error_if_cancelled (task))
+        IDE_EXIT;
+
+      g_file_replace_contents_bytes_async (file,
+                                           bytes,
+                                           NULL,
+                                           FALSE,
+                                           G_FILE_CREATE_NONE,
+                                           cancellable,
+                                           ide_session_save_cb,
+                                           g_steal_pointer (&task));
+    }
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_session_save_async:
+ * @self: an #IdeSession
+ * @workbench: an #IdeWorkbench
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * This function will request that various components save their active state
+ * so that the project may be restored to the current layout when the project
+ * is re-opened at a later time.
+ *
+ * Since: 3.30
+ */
+void
+ide_session_save_async (IdeSession          *self,
+                        IdeWorkbench        *workbench,
+                        GCancellable        *cancellable,
+                        GAsyncReadyCallback  callback,
+                        gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  Save *s;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SESSION (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_session_save_async);
+
+  s = g_slice_new0 (Save);
+  s->addins = g_ptr_array_new_with_free_func (g_object_unref);
+  ide_extension_set_adapter_foreach (self->addins, collect_addins_cb, s->addins);
+  s->active = s->addins->len;
+  g_variant_dict_init (&s->dict, NULL);
+  s->dict_needs_clear = TRUE;
+  ide_task_set_task_data (task, s, save_free);
+
+  if (s->active == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  for (guint i = 0; i < s->addins->len; i++)
+    {
+      IdeSessionAddin *addin = g_ptr_array_index (s->addins, i);
+
+      ide_session_addin_save_async (addin,
+                                    workbench,
+                                    cancellable,
+                                    ide_session_save_addin_save_cb,
+                                    g_object_ref (task));
+    }
+
+  g_assert (s != NULL);
+  g_assert (s->active > 0);
+  g_assert (s->addins->len == s->active);
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_session_save_finish (IdeSession    *self,
+                         GAsyncResult  *result,
+                         GError       **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_SESSION (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+IdeSession *
+ide_session_new (void)
+{
+  return g_object_new (IDE_TYPE_SESSION, NULL);
+}
diff --git a/src/libide/gui/ide-shortcut-label-private.h b/src/libide/gui/ide-shortcut-label-private.h
new file mode 100644
index 000000000..5d27f230f
--- /dev/null
+++ b/src/libide/gui/ide-shortcut-label-private.h
@@ -0,0 +1,45 @@
+/* ide-shortcut-label-private.h
+ *
+ * Copyright 2017-2019 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUT_LABEL (ide_shortcut_label_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutLabel, ide_shortcut_label, IDE, SHORTCUT_LABEL, GtkBox)
+
+GtkWidget   *ide_shortcut_label_new             (void);
+const gchar *ide_shortcut_label_get_accel       (IdeShortcutLabel *self);
+void         ide_shortcut_label_set_accel       (IdeShortcutLabel *self,
+                                                 const gchar      *accel);
+const gchar *ide_shortcut_label_get_action      (IdeShortcutLabel *self);
+void         ide_shortcut_label_set_action      (IdeShortcutLabel *self,
+                                                 const gchar      *action);
+const gchar *ide_shortcut_label_get_command     (IdeShortcutLabel *self);
+void         ide_shortcut_label_set_command     (IdeShortcutLabel *self,
+                                                 const gchar      *command);
+const gchar *ide_shortcut_label_get_title       (IdeShortcutLabel *self);
+void         ide_shortcut_label_set_title       (IdeShortcutLabel *self,
+                                                 const gchar      *title);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-shortcut-label.c b/src/libide/gui/ide-shortcut-label.c
new file mode 100644
index 000000000..d561f5d9c
--- /dev/null
+++ b/src/libide/gui/ide-shortcut-label.c
@@ -0,0 +1,271 @@
+/* ide-shortcut-label.c
+ *
+ * Copyright 2017-2019 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 "ide-shortcut-label"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-shortcut-label-private.h"
+
+struct _IdeShortcutLabel
+{
+  GtkBox       parent_instance;
+
+  GtkLabel    *accel_label;
+  GtkLabel    *title;
+
+  const gchar *accel;
+  const gchar *action;
+  const gchar *command;
+};
+
+enum {
+  PROP_0,
+  PROP_ACCEL,
+  PROP_ACTION,
+  PROP_COMMAND,
+  PROP_TITLE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeShortcutLabel, ide_shortcut_label, GTK_TYPE_BOX)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_shortcut_label_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeShortcutLabel *self = IDE_SHORTCUT_LABEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCEL:
+      g_value_set_static_string (value, ide_shortcut_label_get_accel (self));
+      break;
+
+    case PROP_ACTION:
+      g_value_set_static_string (value, ide_shortcut_label_get_action (self));
+      break;
+
+    case PROP_COMMAND:
+      g_value_set_static_string (value, ide_shortcut_label_get_command (self));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, ide_shortcut_label_get_title (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_label_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeShortcutLabel *self = IDE_SHORTCUT_LABEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_ACCEL:
+      ide_shortcut_label_set_accel (self, g_value_get_string (value));
+      break;
+
+    case PROP_ACTION:
+      ide_shortcut_label_set_action (self, g_value_get_string (value));
+      break;
+
+    case PROP_COMMAND:
+      ide_shortcut_label_set_command (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_shortcut_label_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_shortcut_label_class_init (IdeShortcutLabelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_shortcut_label_get_property;
+  object_class->set_property = ide_shortcut_label_set_property;
+
+  properties [PROP_ACTION] =
+    g_param_spec_string ("action",
+                         "Action",
+                         "Action",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ACCEL] =
+    g_param_spec_string ("accel",
+                         "Accel",
+                         "The accel label to override the discovered accel",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_COMMAND] =
+    g_param_spec_string ("command",
+                         "Command",
+                         "Command",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "Title",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_shortcut_label_init (IdeShortcutLabel *self)
+{
+  self->title = g_object_new (GTK_TYPE_LABEL,
+                              "visible", TRUE,
+                              "xalign", 0.0f,
+                              NULL);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->title), "dim-label");
+  gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (self->title),
+                                     "fill", TRUE,
+                                     "pack-type", GTK_PACK_START,
+                                     NULL);
+
+  self->accel_label = g_object_new (GTK_TYPE_LABEL,
+                                    "visible", TRUE,
+                                    "xalign", 1.0f,
+                                    NULL);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->accel_label), "dim-label");
+  gtk_container_add_with_properties (GTK_CONTAINER (self), GTK_WIDGET (self->accel_label),
+                                     "fill", TRUE,
+                                     "pack-type", GTK_PACK_END,
+                                     NULL);
+}
+
+GtkWidget *
+ide_shortcut_label_new (void)
+{
+  return g_object_new (IDE_TYPE_SHORTCUT_LABEL, NULL);
+}
+
+const gchar *
+ide_shortcut_label_get_accel (IdeShortcutLabel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_LABEL (self), NULL);
+
+  return self->accel;
+}
+
+const gchar *
+ide_shortcut_label_get_action (IdeShortcutLabel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_LABEL (self), NULL);
+
+  return self->action;
+}
+
+const gchar *
+ide_shortcut_label_get_command (IdeShortcutLabel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_LABEL (self), NULL);
+
+  return self->command;
+}
+
+const gchar *
+ide_shortcut_label_get_title (IdeShortcutLabel *self)
+{
+  g_return_val_if_fail (IDE_IS_SHORTCUT_LABEL (self), NULL);
+
+  return gtk_label_get_label (self->title);
+}
+
+void
+ide_shortcut_label_set_accel (IdeShortcutLabel *self,
+                              const gchar      *accel)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_LABEL (self));
+
+  accel = g_intern_string (accel);
+
+  if (accel != self->accel)
+    {
+      self->accel = accel;
+      gtk_label_set_label (self->accel_label, accel);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACCEL]);
+    }
+}
+
+void
+ide_shortcut_label_set_action (IdeShortcutLabel *self,
+                               const gchar      *action)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_LABEL (self));
+
+  action = g_intern_string (action);
+
+  if (action != self->action)
+    {
+      self->action = action;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ACTION]);
+    }
+}
+
+void
+ide_shortcut_label_set_command (IdeShortcutLabel *self,
+                                const gchar      *command)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_LABEL (self));
+
+  command = g_intern_string (command);
+
+  if (command != self->command)
+    {
+      self->command = command;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_COMMAND]);
+    }
+}
+
+void
+ide_shortcut_label_set_title (IdeShortcutLabel *self,
+                              const gchar      *title)
+{
+  g_return_if_fail (IDE_IS_SHORTCUT_LABEL (self));
+
+  gtk_label_set_label (self->title, title);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+}
diff --git a/src/libide/gui/ide-shortcuts-window-private.h b/src/libide/gui/ide-shortcuts-window-private.h
new file mode 100644
index 000000000..5ce88ce59
--- /dev/null
+++ b/src/libide/gui/ide-shortcuts-window-private.h
@@ -0,0 +1,31 @@
+/* ide-shortcuts-window.h
+ *
+ * Copyright 2015-2019 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SHORTCUTS_WINDOW (ide_shortcuts_window_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeShortcutsWindow, ide_shortcuts_window, IDE, SHORTCUTS_WINDOW, GtkShortcutsWindow)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-shortcuts-window.c b/src/libide/gui/ide-shortcuts-window.c
new file mode 100644
index 000000000..5f7dc3494
--- /dev/null
+++ b/src/libide/gui/ide-shortcuts-window.c
@@ -0,0 +1,48 @@
+/* ide-shortcuts-window.c
+ *
+ * Copyright 2015-2019 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 "ide-shortcuts-window"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-shortcuts-window-private.h"
+
+struct _IdeShortcutsWindow
+{
+  GtkShortcutsWindow parent_instance;
+};
+
+G_DEFINE_TYPE (IdeShortcutsWindow, ide_shortcuts_window, GTK_TYPE_SHORTCUTS_WINDOW)
+
+static void
+ide_shortcuts_window_class_init (IdeShortcutsWindowClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-gui/ui/ide-shortcuts-window.ui");
+}
+
+static void
+ide_shortcuts_window_init (IdeShortcutsWindow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/libide/keybindings/ide-shortcuts-window.ui b/src/libide/gui/ide-shortcuts-window.ui
similarity index 100%
rename from src/libide/keybindings/ide-shortcuts-window.ui
rename to src/libide/gui/ide-shortcuts-window.ui
diff --git a/src/libide/gui/ide-surface.c b/src/libide/gui/ide-surface.c
new file mode 100644
index 000000000..43224679c
--- /dev/null
+++ b/src/libide/gui/ide-surface.c
@@ -0,0 +1,259 @@
+/* ide-surface.c
+ *
+ * Copyright 2018-2019 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 "ide-surface"
+
+#include "config.h"
+
+#include "ide-gui-private.h"
+#include "ide-surface.h"
+
+typedef struct
+{
+  gchar *icon_name;
+  gchar *title;
+} IdeSurfacePrivate;
+
+enum {
+  PROP_0,
+  PROP_ICON_NAME,
+  PROP_TITLE,
+  N_PROPS
+};
+
+static void dock_item_iface_init (DzlDockItemInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeSurface, ide_surface, DZL_TYPE_DOCK_BIN,
+                         G_ADD_PRIVATE (IdeSurface)
+                         G_IMPLEMENT_INTERFACE (DZL_TYPE_DOCK_ITEM, dock_item_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_surface_finalize (GObject *object)
+{
+  IdeSurface *self = (IdeSurface *)object;
+  IdeSurfacePrivate *priv = ide_surface_get_instance_private (self);
+
+  g_clear_pointer (&priv->icon_name, g_free);
+  g_clear_pointer (&priv->title, g_free);
+
+  G_OBJECT_CLASS (ide_surface_parent_class)->finalize (object);
+}
+
+static void
+ide_surface_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  IdeSurface *self = IDE_SURFACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_ICON_NAME:
+      g_value_set_string (value, dzl_dock_item_get_icon_name (DZL_DOCK_ITEM (self)));
+      break;
+
+    case PROP_TITLE:
+      g_value_set_string (value, dzl_dock_item_get_title (DZL_DOCK_ITEM (self)));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_surface_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  IdeSurface *self = IDE_SURFACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_ICON_NAME:
+      ide_surface_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_TITLE:
+      ide_surface_set_title (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_surface_class_init (IdeSurfaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_surface_finalize;
+  object_class->get_property = ide_surface_get_property;
+  object_class->set_property = ide_surface_set_property;
+
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The icon name for the surface",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TITLE] =
+    g_param_spec_string ("title",
+                         "Title",
+                         "The title for the surface, if any",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_surface_init (IdeSurface *self)
+{
+}
+
+/**
+ * ide_surface_new:
+ *
+ * Creates a new #IdeSurface.
+ *
+ * Surfaces contain the main window contents that are placed inside of an
+ * #IdeWorkspace (window). You may have multiple surfaces in a workspace,
+ * and the user can switch between them.
+ *
+ * Returns: (transfer full): an #IdeSurface or %NULL
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_surface_new (void)
+{
+  return g_object_new (IDE_TYPE_SURFACE, NULL);
+}
+
+void
+ide_surface_set_icon_name (IdeSurface  *self,
+                           const gchar *icon_name)
+{
+  IdeSurfacePrivate *priv = ide_surface_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SURFACE (self));
+
+  if (!ide_str_equal0 (priv->icon_name, icon_name))
+    {
+      g_free (priv->icon_name);
+      priv->icon_name = g_strdup (icon_name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON_NAME]);
+    }
+}
+
+void
+ide_surface_set_title (IdeSurface  *self,
+                       const gchar *title)
+{
+  IdeSurfacePrivate *priv = ide_surface_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SURFACE (self));
+
+  if (!ide_str_equal0 (priv->title, title))
+    {
+      g_free (priv->title);
+      priv->title = g_strdup (title);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TITLE]);
+    }
+}
+
+/**
+ * ide_surface_foreach_page:
+ * @self: a #IdeSurface
+ * @callback: (scope call): callback to execute for each page
+ * @user_data: closure data for @callback
+ *
+ * Calls @callback for every page found within the surface @self.
+ *
+ * Since: 3.32
+ */
+void
+ide_surface_foreach_page (IdeSurface  *self,
+                          GtkCallback  callback,
+                          gpointer     user_data)
+{
+  g_return_if_fail (IDE_IS_SURFACE (self));
+  g_return_if_fail (callback != NULL);
+
+  if (IDE_SURFACE_GET_CLASS (self)->foreach_page)
+    IDE_SURFACE_GET_CLASS (self)->foreach_page (self, callback, user_data);
+}
+
+static gchar *
+ide_surface_real_get_icon_name (DzlDockItem *item)
+{
+  IdeSurface *self = (IdeSurface *)item;
+  IdeSurfacePrivate *priv = ide_surface_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SURFACE (self), NULL);
+
+  return g_strdup (priv->icon_name);
+}
+
+static gchar *
+ide_surface_real_get_title (DzlDockItem *item)
+{
+  IdeSurface *self = (IdeSurface *)item;
+  IdeSurfacePrivate *priv = ide_surface_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SURFACE (self), NULL);
+
+  return g_strdup (priv->title);
+}
+
+static void
+dock_item_iface_init (DzlDockItemInterface *iface)
+{
+  iface->get_icon_name = ide_surface_real_get_icon_name;
+  iface->get_title = ide_surface_real_get_title;
+}
+
+gboolean
+ide_surface_agree_to_shutdown (IdeSurface *self)
+{
+  g_return_val_if_fail (IDE_IS_SURFACE (self), FALSE);
+
+  if (IDE_SURFACE_GET_CLASS (self)->agree_to_shutdown)
+    return IDE_SURFACE_GET_CLASS (self)->agree_to_shutdown (self);
+
+  return TRUE;
+}
+
+void
+_ide_surface_set_fullscreen (IdeSurface *self,
+                             gboolean    fullscreen)
+{
+  g_return_if_fail (IDE_IS_SURFACE (self));
+
+  if (IDE_SURFACE_GET_CLASS (self)->set_fullscreen)
+    IDE_SURFACE_GET_CLASS (self)->set_fullscreen (self, fullscreen);
+}
diff --git a/src/libide/gui/ide-surface.h b/src/libide/gui/ide-surface.h
new file mode 100644
index 000000000..2be97c69f
--- /dev/null
+++ b/src/libide/gui/ide-surface.h
@@ -0,0 +1,67 @@
+/* ide-surface.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SURFACE (ide_surface_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeSurface, ide_surface, IDE, SURFACE, DzlDockBin)
+
+struct _IdeSurfaceClass
+{
+  DzlDockBinClass parent_class;
+
+  void     (*foreach_page)        (IdeSurface  *self,
+                                   GtkCallback  callback,
+                                   gpointer     user_data);
+  gboolean (*agree_to_shutdown)   (IdeSurface  *self);
+  void     (*set_fullscreen)      (IdeSurface  *self,
+                                   gboolean     fullscreen);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_surface_new               (void);
+IDE_AVAILABLE_IN_3_32
+void       ide_surface_set_icon_name     (IdeSurface  *self,
+                                          const gchar *icon_name);
+IDE_AVAILABLE_IN_3_32
+void       ide_surface_set_title         (IdeSurface  *self,
+                                          const gchar *title);
+IDE_AVAILABLE_IN_3_32
+void       ide_surface_foreach_page      (IdeSurface  *self,
+                                          GtkCallback  callback,
+                                          gpointer     user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_surface_agree_to_shutdown (IdeSurface  *self);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-surfaces-button.c b/src/libide/gui/ide-surfaces-button.c
new file mode 100644
index 000000000..447ebf475
--- /dev/null
+++ b/src/libide/gui/ide-surfaces-button.c
@@ -0,0 +1,107 @@
+/* ide-surfaces-button.c
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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 "ide-surfaces-button"
+
+#include "config.h"
+
+#include <libide-core.h>
+
+#include "ide-surfaces-button.h"
+
+struct _IdeSurfacesButton
+{
+  DzlMenuButton parent_instance;
+};
+
+G_DEFINE_TYPE (IdeSurfacesButton, ide_surfaces_button, DZL_TYPE_MENU_BUTTON)
+
+static void
+ide_surfaces_button_items_changed_cb (IdeSurfacesButton *self,
+                                      guint              position,
+                                      guint              added,
+                                      guint              removed,
+                                      GMenuModel        *model)
+{
+  gboolean visible = FALSE;
+  guint n_items;
+  guint count = 0;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SURFACES_BUTTON (self));
+  g_assert (G_IS_MENU_MODEL (model));
+
+  /* We either have multiple sections, or a single section with
+   * possibly multiple children. Any of these means visible.
+   */
+
+  n_items = g_menu_model_get_n_items (model);
+  visible = n_items > 1;
+
+  for (guint i = 0; !visible && i < n_items; i++)
+    {
+      g_autoptr(GMenuLinkIter) iter = g_menu_model_iterate_item_links (model, i);
+
+      while (g_menu_link_iter_next (iter))
+        {
+          g_autoptr(GMenuModel) child = g_menu_link_iter_get_value (iter);
+          count += g_menu_model_get_n_items (child);
+        }
+
+      visible = count > 1;
+    }
+
+  gtk_widget_set_visible (GTK_WIDGET (self), visible);
+}
+
+static void
+ide_surfaces_button_notify_model (IdeSurfacesButton *self,
+                                  GParamSpec        *pspec,
+                                  gpointer           user_data)
+{
+  GMenuModel *model;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SURFACES_BUTTON (self));
+
+  if ((model = dzl_menu_button_get_model (DZL_MENU_BUTTON (self))))
+    {
+      g_signal_connect_object (model,
+                               "items-changed",
+                               G_CALLBACK (ide_surfaces_button_items_changed_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+      ide_surfaces_button_items_changed_cb (self, 0, 0, 0, model);
+    }
+}
+
+static void
+ide_surfaces_button_class_init (IdeSurfacesButtonClass *klass)
+{
+}
+
+static void
+ide_surfaces_button_init (IdeSurfacesButton *self)
+{
+  g_signal_connect (self,
+                    "notify::model",
+                    G_CALLBACK (ide_surfaces_button_notify_model),
+                    NULL);
+}
diff --git a/src/libide/gui/ide-surfaces-button.h b/src/libide/gui/ide-surfaces-button.h
new file mode 100644
index 000000000..d9efe9808
--- /dev/null
+++ b/src/libide/gui/ide-surfaces-button.h
@@ -0,0 +1,37 @@
+/* ide-surfaces-button.h
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <dazzle.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SURFACES_BUTTON (ide_surfaces_button_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeSurfacesButton, ide_surfaces_button, IDE, SURFACES_BUTTON, DzlMenuButton)
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-tagged-entry.c b/src/libide/gui/ide-tagged-entry.c
new file mode 100644
index 000000000..e719bb0bf
--- /dev/null
+++ b/src/libide/gui/ide-tagged-entry.c
@@ -0,0 +1,1244 @@
+/*
+ * Copyright 2011 Red Hat, Inc.
+ * Copyright 2013 Ignacio Casal Quinteiro
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 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 Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Author: Cosimo Cecchi <cosimoc redhat com>
+ *
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "config.h"
+
+#include <math.h>
+
+#include "ide-tagged-entry.h"
+
+#define BUTTON_INTERNAL_SPACING 6
+
+struct _IdeTaggedEntryTagPrivate {
+  IdeTaggedEntry *entry;
+  GdkWindow *window;
+  PangoLayout *layout;
+
+  gchar *label;
+  gchar *style;
+  gboolean has_close_button;
+
+  cairo_surface_t *close_surface;
+  GtkStateFlags last_button_state;
+};
+
+struct _IdeTaggedEntryPrivate {
+  GList *tags;
+
+  IdeTaggedEntryTag *in_child;
+  gboolean in_child_button;
+  gboolean in_child_active;
+  gboolean in_child_button_active;
+  gboolean button_visible;
+};
+
+enum {
+  SIGNAL_TAG_CLICKED,
+  SIGNAL_TAG_BUTTON_CLICKED,
+  LAST_SIGNAL
+};
+
+enum {
+  PROP_0,
+  PROP_TAG_BUTTON_VISIBLE,
+  NUM_PROPERTIES
+};
+
+enum {
+  PROP_TAG_0,
+  PROP_TAG_LABEL,
+  PROP_TAG_HAS_CLOSE_BUTTON,
+  PROP_TAG_STYLE,
+  NUM_TAG_PROPERTIES
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTaggedEntry, ide_tagged_entry, GTK_TYPE_SEARCH_ENTRY)
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTaggedEntryTag, ide_tagged_entry_tag, G_TYPE_OBJECT)
+
+static guint signals[LAST_SIGNAL] = { 0, };
+static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
+static GParamSpec *tag_properties[NUM_TAG_PROPERTIES] = { NULL, };
+
+static void ide_tagged_entry_get_text_area_size (GtkEntry *entry,
+                                                gint *x,
+                                                gint *y,
+                                                gint *width,
+                                                gint *height);
+static gint ide_tagged_entry_tag_get_width (IdeTaggedEntryTag *tag,
+                                           IdeTaggedEntry *entry);
+static GtkStyleContext * ide_tagged_entry_tag_get_context (IdeTaggedEntryTag *tag,
+                                                          IdeTaggedEntry *entry);
+
+static void
+ide_tagged_entry_tag_get_margin (IdeTaggedEntryTag *tag,
+                                IdeTaggedEntry *entry,
+                                GtkBorder *margin)
+{
+  GtkStyleContext *context;
+
+  context = ide_tagged_entry_tag_get_context (tag, entry);
+  gtk_style_context_set_state (context, GTK_STATE_FLAG_NORMAL);
+  gtk_style_context_get_margin (context,
+                                gtk_style_context_get_state (context),
+                                margin);
+  gtk_style_context_restore (context);
+}
+
+static void
+ide_tagged_entry_tag_ensure_close_surface (IdeTaggedEntryTag *tag,
+                                          GtkStyleContext *context)
+{
+  GtkIconInfo *info;
+  GdkPixbuf *pixbuf;
+  gint icon_size;
+  gint scale_factor;
+
+  if (tag->priv->close_surface != NULL)
+    return;
+
+  gtk_icon_size_lookup (GTK_ICON_SIZE_MENU,
+                        &icon_size, NULL);
+  scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (tag->priv->entry));
+
+  info = gtk_icon_theme_lookup_icon_for_scale (gtk_icon_theme_get_default (),
+                                               "window-close-symbolic",
+                                               icon_size, scale_factor,
+                                               GTK_ICON_LOOKUP_GENERIC_FALLBACK);
+
+  /* FIXME: we need a fallback icon in case the icon is not found */
+  pixbuf = gtk_icon_info_load_symbolic_for_context (info, context, NULL, NULL);
+  tag->priv->close_surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale_factor, tag->priv->window);
+
+  g_object_unref (info);
+  g_object_unref (pixbuf);
+}
+
+static gint
+ide_tagged_entry_tag_panel_get_height (IdeTaggedEntryTag *tag,
+                                      IdeTaggedEntry *entry)
+{
+  GtkWidget *widget = GTK_WIDGET (entry);
+  gint height, req_height;
+  GtkRequisition requisition;
+  GtkAllocation allocation;
+  GtkBorder margin;
+
+  gtk_widget_get_allocation (widget, &allocation);
+  gtk_widget_get_preferred_size (widget, &requisition, NULL);
+  ide_tagged_entry_tag_get_margin (tag, entry, &margin);
+
+  /* the tag panel height is the whole entry height, minus the tag margins */
+  req_height = requisition.height - gtk_widget_get_margin_top (widget) - gtk_widget_get_margin_bottom 
(widget);
+  height = MIN (req_height, allocation.height) - margin.top - margin.bottom;
+
+  return height;
+}
+
+static void
+ide_tagged_entry_tag_panel_get_position (IdeTaggedEntry *self,
+                                        gint *x_out,
+                                        gint *y_out)
+{
+  GtkWidget *widget = GTK_WIDGET (self);
+  gint text_x, text_y, text_width, text_height, req_height;
+  GtkAllocation allocation;
+  GtkRequisition requisition;
+
+  gtk_widget_get_allocation (widget, &allocation);
+  gtk_widget_get_preferred_size (widget, &requisition, NULL);
+  req_height = requisition.height - gtk_widget_get_margin_top (widget) - gtk_widget_get_margin_bottom 
(widget);
+
+  ide_tagged_entry_get_text_area_size (GTK_ENTRY (self), &text_x, &text_y, &text_width, &text_height);
+
+  /* allocate the panel immediately after the text area */
+  if (x_out)
+    *x_out = allocation.x + text_x + text_width;
+  if (y_out)
+    *y_out = allocation.y + (gint) floor ((allocation.height - req_height) / 2);
+}
+
+static gint
+ide_tagged_entry_tag_panel_get_width (IdeTaggedEntry *self)
+{
+  IdeTaggedEntryTag *tag;
+  gint width;
+  GList *l;
+
+  width = 0;
+
+  for (l = self->priv->tags; l != NULL; l = l->next)
+    {
+      tag = l->data;
+      width += ide_tagged_entry_tag_get_width (tag, self);
+    }
+
+  return width;
+}
+
+static void
+ide_tagged_entry_tag_ensure_layout (IdeTaggedEntryTag *tag,
+                                   IdeTaggedEntry *entry)
+{
+  if (tag->priv->layout != NULL)
+    return;
+
+  tag->priv->layout = pango_layout_new (gtk_widget_get_pango_context (GTK_WIDGET (entry)));
+  pango_layout_set_text (tag->priv->layout, tag->priv->label, -1);
+}
+
+static GtkStateFlags
+ide_tagged_entry_tag_get_state (IdeTaggedEntryTag *tag,
+                               IdeTaggedEntry *entry)
+{
+  GtkStateFlags state = GTK_STATE_FLAG_NORMAL;
+
+  if (entry->priv->in_child == tag)
+    state |= GTK_STATE_FLAG_PRELIGHT;
+
+  if (entry->priv->in_child_active)
+    state |= GTK_STATE_FLAG_ACTIVE;
+
+  return state;
+}
+
+static GtkStateFlags
+ide_tagged_entry_tag_get_button_state (IdeTaggedEntryTag *tag,
+                                      IdeTaggedEntry *entry)
+{
+  GtkStateFlags state = GTK_STATE_FLAG_NORMAL;
+
+  if (entry->priv->in_child == tag)
+    {
+      if (entry->priv->in_child_button_active)
+        state |= GTK_STATE_FLAG_ACTIVE;
+
+      else if (entry->priv->in_child_button)
+        state |= GTK_STATE_FLAG_PRELIGHT;
+    }
+
+  return state;
+}
+
+static GtkStyleContext *
+ide_tagged_entry_tag_get_context (IdeTaggedEntryTag *tag,
+                                 IdeTaggedEntry    *entry)
+{
+  GtkWidget *widget = GTK_WIDGET (entry);
+  GtkStyleContext *retval;
+  GList *l, *list;
+
+  retval = gtk_widget_get_style_context (widget);
+  gtk_style_context_save (retval);
+
+  list = gtk_style_context_list_classes (retval);
+  for (l = list; l; l = l->next)
+    gtk_style_context_remove_class (retval, l->data);
+  g_list_free (list);
+  gtk_style_context_add_class (retval, tag->priv->style);
+
+  return retval;
+}
+
+static gint
+ide_tagged_entry_tag_get_width (IdeTaggedEntryTag *tag,
+                               IdeTaggedEntry *entry)
+{
+  GtkBorder button_padding, button_border, button_margin;
+  GtkStyleContext *context;
+  GtkStateFlags state;
+  gint layout_width;
+  gint button_width;
+  gint scale_factor;
+
+  ide_tagged_entry_tag_ensure_layout (tag, entry);
+  pango_layout_get_pixel_size (tag->priv->layout, &layout_width, NULL);
+
+  context = ide_tagged_entry_tag_get_context (tag, entry);
+  state = ide_tagged_entry_tag_get_state (tag, entry);
+
+  gtk_style_context_set_state (context, state);
+  gtk_style_context_get_padding (context,
+                                 gtk_style_context_get_state (context),
+                                 &button_padding);
+  gtk_style_context_get_border (context,
+                                gtk_style_context_get_state (context),
+                                &button_border);
+  gtk_style_context_get_margin (context,
+                                gtk_style_context_get_state (context),
+                                &button_margin);
+
+  ide_tagged_entry_tag_ensure_close_surface (tag, context);
+
+  gtk_style_context_restore (context);
+
+  button_width = 0;
+  if (entry->priv->button_visible && tag->priv->has_close_button)
+    {
+      scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (entry));
+      button_width = cairo_image_surface_get_width (tag->priv->close_surface) / scale_factor +
+        BUTTON_INTERNAL_SPACING;
+    }
+
+  return layout_width + button_padding.left + button_padding.right +
+    button_border.left + button_border.right +
+    button_margin.left + button_margin.right +
+    button_width;
+}
+
+static void
+ide_tagged_entry_tag_get_size (IdeTaggedEntryTag *tag,
+                              IdeTaggedEntry *entry,
+                              gint *width_out,
+                              gint *height_out)
+{
+  gint width, panel_height;
+
+  width = ide_tagged_entry_tag_get_width (tag, entry);
+  panel_height = ide_tagged_entry_tag_panel_get_height (tag, entry);
+
+  if (width_out)
+    *width_out = width;
+  if (height_out)
+    *height_out = panel_height;
+}
+
+static void
+ide_tagged_entry_tag_get_relative_allocations (IdeTaggedEntryTag *tag,
+                                              IdeTaggedEntry *entry,
+                                              GtkStyleContext *context,
+                                              GtkAllocation *background_allocation_out,
+                                              GtkAllocation *layout_allocation_out,
+                                              GtkAllocation *button_allocation_out)
+{
+  GtkAllocation background_allocation, layout_allocation, button_allocation;
+  gint width, height, x, y, pix_width, pix_height;
+  gint layout_width, layout_height;
+  gint scale_factor;
+  GtkBorder padding, border;
+  GtkStateFlags state;
+
+  width = gdk_window_get_width (tag->priv->window);
+  height = gdk_window_get_height (tag->priv->window);
+  scale_factor = gdk_window_get_scale_factor (tag->priv->window);
+
+  state = ide_tagged_entry_tag_get_state (tag, entry);
+  gtk_style_context_save (context);
+  gtk_style_context_set_state (context, state);
+  gtk_style_context_get_margin (context,
+                                gtk_style_context_get_state (context),
+                                &padding);
+  gtk_style_context_restore (context);
+
+  width -= padding.left + padding.right;
+  height -= padding.top + padding.bottom;
+  x = padding.left;
+  y = padding.top;
+
+  background_allocation.x = x;
+  background_allocation.y = y;
+  background_allocation.width = width;
+  background_allocation.height = height;
+
+  layout_allocation = button_allocation = background_allocation;
+
+  gtk_style_context_save (context);
+  gtk_style_context_set_state (context, state);
+  gtk_style_context_get_padding (context,
+                                 gtk_style_context_get_state (context),
+                                 &padding);
+  gtk_style_context_get_border (context,
+                                gtk_style_context_get_state (context),
+                                &border);
+  gtk_style_context_restore (context);
+
+  ide_tagged_entry_tag_ensure_layout (tag, entry);
+  pango_layout_get_pixel_size (tag->priv->layout, &layout_width, &layout_height);
+
+  layout_allocation.x += border.left + padding.left;
+  layout_allocation.y += (layout_allocation.height - layout_height) / 2;
+
+  if (entry->priv->button_visible && tag->priv->has_close_button)
+    {
+      pix_width = cairo_image_surface_get_width (tag->priv->close_surface) / scale_factor;
+      pix_height = cairo_image_surface_get_height (tag->priv->close_surface) / scale_factor;
+    }
+  else
+    {
+      pix_width = 0;
+      pix_height = 0;
+    }
+
+  button_allocation.x += width - pix_width - border.right - padding.right;
+  button_allocation.y += (height - pix_height) / 2;
+  button_allocation.width = pix_width;
+  button_allocation.height = pix_height;
+
+  if (background_allocation_out)
+    *background_allocation_out = background_allocation;
+  if (layout_allocation_out)
+    *layout_allocation_out = layout_allocation;
+  if (button_allocation_out)
+    *button_allocation_out = button_allocation;
+}
+
+static gboolean
+ide_tagged_entry_tag_event_is_button (IdeTaggedEntryTag *tag,
+                                     IdeTaggedEntry *entry,
+                                     gdouble event_x,
+                                     gdouble event_y)
+{
+  GtkAllocation button_allocation;
+  GtkStyleContext *context;
+
+  if (!entry->priv->button_visible || !tag->priv->has_close_button)
+    return FALSE;
+
+  context = ide_tagged_entry_tag_get_context (tag, entry);
+  ide_tagged_entry_tag_get_relative_allocations (tag, entry, context, NULL, NULL, &button_allocation);
+
+  gtk_style_context_restore (context);
+
+  /* see if the event falls into the button allocation */
+  if ((event_x >= button_allocation.x &&
+       event_x <= button_allocation.x + button_allocation.width) &&
+      (event_y >= button_allocation.y &&
+       event_y <= button_allocation.y + button_allocation.height))
+    return TRUE;
+
+  return FALSE;
+}
+
+gboolean
+ide_tagged_entry_tag_get_area (IdeTaggedEntryTag      *tag,
+                              cairo_rectangle_int_t *rect)
+{
+  GtkStyleContext *context;
+  GtkAllocation background_allocation;
+  int window_x, window_y;
+  GtkAllocation alloc;
+
+  g_return_val_if_fail (IDE_IS_TAGGED_ENTRY_TAG (tag), FALSE);
+  g_return_val_if_fail (rect != NULL, FALSE);
+
+  gdk_window_get_position (tag->priv->window, &window_x, &window_y);
+  gtk_widget_get_allocation (GTK_WIDGET (tag->priv->entry), &alloc);
+  context = ide_tagged_entry_tag_get_context (tag, tag->priv->entry);
+  ide_tagged_entry_tag_get_relative_allocations (tag, tag->priv->entry, context,
+                                                &background_allocation,
+                                                NULL, NULL);
+  gtk_style_context_restore (context);
+
+  rect->x = window_x - alloc.x + background_allocation.x;
+  rect->y = window_y - alloc.y + background_allocation.y;
+  rect->width = background_allocation.width;
+  rect->height = background_allocation.height;
+
+  return TRUE;
+}
+
+static void
+ide_tagged_entry_tag_draw (IdeTaggedEntryTag *tag,
+                          cairo_t *cr,
+                          IdeTaggedEntry *entry)
+{
+  GtkStyleContext *context;
+  GtkStateFlags state;
+  GtkAllocation background_allocation, layout_allocation, button_allocation;
+
+  context = ide_tagged_entry_tag_get_context (tag, entry);
+  ide_tagged_entry_tag_get_relative_allocations (tag, entry, context,
+                                                &background_allocation,
+                                                &layout_allocation,
+                                                &button_allocation);
+
+  cairo_save (cr);
+  gtk_cairo_transform_to_window (cr, GTK_WIDGET (entry), tag->priv->window);
+
+  gtk_style_context_save (context);
+
+  state = ide_tagged_entry_tag_get_state (tag, entry);
+  gtk_style_context_set_state (context, state);
+  gtk_render_background (context, cr,
+                         background_allocation.x, background_allocation.y,
+                         background_allocation.width, background_allocation.height);
+  gtk_render_frame (context, cr,
+                    background_allocation.x, background_allocation.y,
+                    background_allocation.width, background_allocation.height);
+
+  gtk_render_layout (context, cr,
+                     layout_allocation.x, layout_allocation.y,
+                     tag->priv->layout);
+
+  gtk_style_context_restore (context);
+
+  if (!entry->priv->button_visible || !tag->priv->has_close_button)
+    goto done;
+
+  gtk_style_context_add_class (context, GTK_STYLE_CLASS_BUTTON);
+  state = ide_tagged_entry_tag_get_button_state (tag, entry);
+  gtk_style_context_set_state (context, state);
+
+  /* if the state changed since last time we draw the pixbuf,
+   * clear and redraw it.
+   */
+  if (state != tag->priv->last_button_state)
+    {
+      g_clear_pointer (&tag->priv->close_surface, cairo_surface_destroy);
+      ide_tagged_entry_tag_ensure_close_surface (tag, context);
+
+      tag->priv->last_button_state = state;
+    }
+
+  gtk_render_background (context, cr,
+                         button_allocation.x, button_allocation.y,
+                         button_allocation.width, button_allocation.height);
+  gtk_render_frame (context, cr,
+                         button_allocation.x, button_allocation.y,
+                         button_allocation.width, button_allocation.height);
+
+  gtk_render_icon_surface (context, cr,
+                           tag->priv->close_surface,
+                           button_allocation.x, button_allocation.y);
+
+done:
+  gtk_style_context_restore (context);
+
+  cairo_restore (cr);
+}
+
+static void
+ide_tagged_entry_tag_unrealize (IdeTaggedEntryTag *tag)
+{
+  if (tag->priv->window == NULL)
+    return;
+
+  gdk_window_set_user_data (tag->priv->window, NULL);
+  gdk_window_destroy (tag->priv->window);
+  tag->priv->window = NULL;
+}
+
+static void
+ide_tagged_entry_tag_realize (IdeTaggedEntryTag *tag,
+                             IdeTaggedEntry *entry)
+{
+  GtkWidget *widget = GTK_WIDGET (entry);
+  GdkWindowAttr attributes;
+  gint attributes_mask;
+  gint tag_width, tag_height;
+
+  if (tag->priv->window != NULL)
+    return;
+
+  attributes.window_type = GDK_WINDOW_CHILD;
+  attributes.wclass = GDK_INPUT_ONLY;
+  attributes.event_mask = gtk_widget_get_events (widget);
+  attributes.event_mask |= GDK_BUTTON_PRESS_MASK
+    | GDK_BUTTON_RELEASE_MASK | GDK_LEAVE_NOTIFY_MASK | GDK_ENTER_NOTIFY_MASK
+    | GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK;
+
+  ide_tagged_entry_tag_get_size (tag, entry, &tag_width, &tag_height);
+  attributes.x = 0;
+  attributes.y = 0;
+  attributes.width = tag_width;
+  attributes.height = tag_height;
+
+  attributes_mask = GDK_WA_X | GDK_WA_Y;
+
+  tag->priv->window = gdk_window_new (gtk_widget_get_window (widget),
+                                &attributes, attributes_mask);
+  gdk_window_set_user_data (tag->priv->window, widget);
+}
+
+static gboolean
+ide_tagged_entry_draw (GtkWidget *widget,
+                      cairo_t *cr)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  IdeTaggedEntryTag *tag;
+  GList *l;
+
+  GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->draw (widget, cr);
+
+  for (l = self->priv->tags; l != NULL; l = l->next)
+    {
+      tag = l->data;
+      ide_tagged_entry_tag_draw (tag, cr, self);
+    }
+
+  return FALSE;
+}
+
+static void
+ide_tagged_entry_map (GtkWidget *widget)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  IdeTaggedEntryTag *tag;
+  GList *l;
+
+  if (gtk_widget_get_realized (widget) && !gtk_widget_get_mapped (widget))
+    {
+      GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->map (widget);
+
+      for (l = self->priv->tags; l != NULL; l = l->next)
+        {
+          tag = l->data;
+          gdk_window_show (tag->priv->window);
+        }
+    }
+}
+
+static void
+ide_tagged_entry_unmap (GtkWidget *widget)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  IdeTaggedEntryTag *tag;
+  GList *l;
+
+  if (gtk_widget_get_mapped (widget))
+    {
+      for (l = self->priv->tags; l != NULL; l = l->next)
+        {
+          tag = l->data;
+          gdk_window_hide (tag->priv->window);
+        }
+
+      GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->unmap (widget);
+    }
+}
+
+static void
+ide_tagged_entry_realize (GtkWidget *widget)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  IdeTaggedEntryTag *tag;
+  GList *l;
+
+  GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->realize (widget);
+
+  for (l = self->priv->tags; l != NULL; l = l->next)
+    {
+      tag = l->data;
+      ide_tagged_entry_tag_realize (tag, self);
+    }
+}
+
+static void
+ide_tagged_entry_unrealize (GtkWidget *widget)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  IdeTaggedEntryTag *tag;
+  GList *l;
+
+  GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->unrealize (widget);
+
+  for (l = self->priv->tags; l != NULL; l = l->next)
+    {
+      tag = l->data;
+      ide_tagged_entry_tag_unrealize (tag);
+    }
+}
+
+static void
+ide_tagged_entry_get_text_area_size (GtkEntry *entry,
+                                    gint *x,
+                                    gint *y,
+                                    gint *width,
+                                    gint *height)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (entry);
+  gint tag_panel_width;
+
+  GTK_ENTRY_CLASS (ide_tagged_entry_parent_class)->get_text_area_size (entry, x, y, width, height);
+
+  tag_panel_width = ide_tagged_entry_tag_panel_get_width (self);
+
+  if (width)
+    *width -= tag_panel_width;
+}
+
+static void
+ide_tagged_entry_size_allocate (GtkWidget *widget,
+                               GtkAllocation *allocation)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  gint x, y, width, height;
+  IdeTaggedEntryTag *tag;
+  GList *l;
+
+  gtk_widget_set_allocation (widget, allocation);
+  GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->size_allocate (widget, allocation);
+
+  if (gtk_widget_get_realized (widget))
+    {
+      ide_tagged_entry_tag_panel_get_position (self, &x, &y);
+
+      for (l = self->priv->tags; l != NULL; l = l->next)
+        {
+          GtkBorder margin;
+
+          tag = l->data;
+          ide_tagged_entry_tag_get_size (tag, self, &width, &height);
+          ide_tagged_entry_tag_get_margin (tag, self, &margin);
+          gdk_window_move_resize (tag->priv->window, x, y + margin.top, width, height);
+
+          x += width;
+        }
+
+      gtk_widget_queue_draw (widget);
+    }
+}
+
+static void
+ide_tagged_entry_get_preferred_width (GtkWidget *widget,
+                                     gint *minimum,
+                                     gint *natural)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  gint tag_panel_width;
+
+  GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->get_preferred_width (widget, minimum, natural);
+
+  tag_panel_width = ide_tagged_entry_tag_panel_get_width (self);
+
+  if (minimum)
+    *minimum += tag_panel_width;
+  if (natural)
+    *natural += tag_panel_width;
+}
+
+static void
+ide_tagged_entry_finalize (GObject *obj)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (obj);
+
+  if (self->priv->tags != NULL)
+    {
+      g_list_free_full (self->priv->tags, g_object_unref);
+      self->priv->tags = NULL;
+    }
+
+  G_OBJECT_CLASS (ide_tagged_entry_parent_class)->finalize (obj);
+}
+
+static IdeTaggedEntryTag *
+ide_tagged_entry_find_tag_by_window (IdeTaggedEntry *self,
+                                    GdkWindow *window)
+{
+  IdeTaggedEntryTag *tag = NULL, *elem;
+  GList *l;
+
+  for (l = self->priv->tags; l != NULL; l = l->next)
+    {
+      elem = l->data;
+      if (elem->priv->window == window)
+        {
+          tag = elem;
+          break;
+        }
+    }
+
+  return tag;
+}
+
+static gint
+ide_tagged_entry_enter_notify (GtkWidget        *widget,
+                              GdkEventCrossing *event)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  IdeTaggedEntryTag *tag;
+
+  tag = ide_tagged_entry_find_tag_by_window (self, event->window);
+
+  if (tag != NULL)
+    {
+      self->priv->in_child = tag;
+      gtk_widget_queue_draw (widget);
+    }
+
+  return GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->enter_notify_event (widget, event);
+}
+
+static gint
+ide_tagged_entry_leave_notify (GtkWidget        *widget,
+                              GdkEventCrossing *event)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+
+  if (self->priv->in_child != NULL)
+    {
+      self->priv->in_child = NULL;
+      gtk_widget_queue_draw (widget);
+    }
+
+  return GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->leave_notify_event (widget, event);
+}
+
+static gint
+ide_tagged_entry_motion_notify (GtkWidget      *widget,
+                               GdkEventMotion *event)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  IdeTaggedEntryTag *tag;
+
+  tag = ide_tagged_entry_find_tag_by_window (self, event->window);
+
+  if (tag != NULL)
+    {
+      gdk_event_request_motions (event);
+
+      self->priv->in_child = tag;
+      self->priv->in_child_button = ide_tagged_entry_tag_event_is_button (tag, self, event->x, event->y);
+      gtk_widget_queue_draw (widget);
+
+      return FALSE;
+    }
+
+  return GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->motion_notify_event (widget, event);
+}
+
+static gboolean
+ide_tagged_entry_button_release_event (GtkWidget *widget,
+                                      GdkEventButton *event)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  IdeTaggedEntryTag *tag;
+
+  tag = ide_tagged_entry_find_tag_by_window (self, event->window);
+
+  if (tag != NULL)
+    {
+      self->priv->in_child_active = FALSE;
+
+      if (ide_tagged_entry_tag_event_is_button (tag, self, event->x, event->y))
+        {
+          self->priv->in_child_button_active = FALSE;
+          g_signal_emit (self, signals[SIGNAL_TAG_BUTTON_CLICKED], 0, tag);
+        }
+      else
+        {
+          g_signal_emit (self, signals[SIGNAL_TAG_CLICKED], 0, tag);
+        }
+
+      gtk_widget_queue_draw (widget);
+
+      return TRUE;
+    }
+
+  return GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->button_release_event (widget, event);
+}
+
+static gboolean
+ide_tagged_entry_button_press_event (GtkWidget *widget,
+                                    GdkEventButton *event)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (widget);
+  IdeTaggedEntryTag *tag;
+
+  tag = ide_tagged_entry_find_tag_by_window (self, event->window);
+
+  if (tag != NULL)
+    {
+      if (ide_tagged_entry_tag_event_is_button (tag, self, event->x, event->y))
+        self->priv->in_child_button_active = TRUE;
+      else
+        self->priv->in_child_active = TRUE;
+
+      gtk_widget_queue_draw (widget);
+
+      return TRUE;
+    }
+
+  return GTK_WIDGET_CLASS (ide_tagged_entry_parent_class)->button_press_event (widget, event);
+}
+
+static void
+ide_tagged_entry_init (IdeTaggedEntry *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, IDE_TYPE_TAGGED_ENTRY, IdeTaggedEntryPrivate);
+  self->priv->button_visible = TRUE;
+}
+
+static void
+ide_tagged_entry_get_property (GObject      *object,
+                              guint         property_id,
+                              GValue       *value,
+                              GParamSpec   *pspec)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (object);
+
+  switch (property_id)
+    {
+      case PROP_TAG_BUTTON_VISIBLE:
+        g_value_set_boolean (value, ide_tagged_entry_get_tag_button_visible (self));
+        break;
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void
+ide_tagged_entry_set_property (GObject      *object,
+                              guint         property_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeTaggedEntry *self = IDE_TAGGED_ENTRY (object);
+
+  switch (property_id)
+    {
+      case PROP_TAG_BUTTON_VISIBLE:
+        ide_tagged_entry_set_tag_button_visible (self, g_value_get_boolean (value));
+        break;
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void
+ide_tagged_entry_class_init (IdeTaggedEntryClass *klass)
+{
+  GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass);
+  GtkEntryClass *eclass = GTK_ENTRY_CLASS (klass);
+  GObjectClass *oclass = G_OBJECT_CLASS (klass);
+
+  oclass->finalize = ide_tagged_entry_finalize;
+  oclass->set_property = ide_tagged_entry_set_property;
+  oclass->get_property = ide_tagged_entry_get_property;
+
+  wclass->realize = ide_tagged_entry_realize;
+  wclass->unrealize = ide_tagged_entry_unrealize;
+  wclass->map = ide_tagged_entry_map;
+  wclass->unmap = ide_tagged_entry_unmap;
+  wclass->size_allocate = ide_tagged_entry_size_allocate;
+  wclass->get_preferred_width = ide_tagged_entry_get_preferred_width;
+  wclass->draw = ide_tagged_entry_draw;
+  wclass->enter_notify_event = ide_tagged_entry_enter_notify;
+  wclass->leave_notify_event = ide_tagged_entry_leave_notify;
+  wclass->motion_notify_event = ide_tagged_entry_motion_notify;
+  wclass->button_press_event = ide_tagged_entry_button_press_event;
+  wclass->button_release_event = ide_tagged_entry_button_release_event;
+
+  eclass->get_text_area_size = ide_tagged_entry_get_text_area_size;
+
+  signals[SIGNAL_TAG_CLICKED] =
+    g_signal_new ("tag-clicked",
+                  IDE_TYPE_TAGGED_ENTRY,
+                  G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1, IDE_TYPE_TAGGED_ENTRY_TAG);
+  signals[SIGNAL_TAG_BUTTON_CLICKED] =
+    g_signal_new ("tag-button-clicked",
+                  IDE_TYPE_TAGGED_ENTRY,
+                  G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1, IDE_TYPE_TAGGED_ENTRY_TAG);
+
+  properties[PROP_TAG_BUTTON_VISIBLE] =
+    g_param_spec_boolean ("tag-close-visible", "Tag close icon visibility",
+                          "Whether the close button should be shown in tags.", TRUE,
+                          G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (oclass, NUM_PROPERTIES, properties);
+}
+
+static void
+ide_tagged_entry_tag_init (IdeTaggedEntryTag *self)
+{
+  IdeTaggedEntryTagPrivate *priv;
+
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, IDE_TYPE_TAGGED_ENTRY_TAG, IdeTaggedEntryTagPrivate);
+  priv = self->priv;
+
+  priv->last_button_state = GTK_STATE_FLAG_NORMAL;
+}
+
+static void
+ide_tagged_entry_tag_finalize (GObject *obj)
+{
+  IdeTaggedEntryTag *tag = IDE_TAGGED_ENTRY_TAG (obj);
+  IdeTaggedEntryTagPrivate *priv = tag->priv;
+
+  if (priv->window != NULL)
+    ide_tagged_entry_tag_unrealize (tag);
+
+  g_clear_object (&priv->layout);
+  g_clear_pointer (&priv->close_surface, cairo_surface_destroy);
+  g_free (priv->label);
+  g_free (priv->style);
+
+  G_OBJECT_CLASS (ide_tagged_entry_tag_parent_class)->finalize (obj);
+}
+
+static void
+ide_tagged_entry_tag_get_property (GObject      *object,
+                                  guint         property_id,
+                                  GValue       *value,
+                                  GParamSpec   *pspec)
+{
+  IdeTaggedEntryTag *self = IDE_TAGGED_ENTRY_TAG (object);
+
+  switch (property_id)
+    {
+      case PROP_TAG_LABEL:
+        g_value_set_string (value, ide_tagged_entry_tag_get_label (self));
+        break;
+      case PROP_TAG_HAS_CLOSE_BUTTON:
+        g_value_set_boolean (value, ide_tagged_entry_tag_get_has_close_button (self));
+        break;
+      case PROP_TAG_STYLE:
+        g_value_set_string (value, ide_tagged_entry_tag_get_style (self));
+        break;
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void
+ide_tagged_entry_tag_set_property (GObject      *object,
+                                  guint         property_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  IdeTaggedEntryTag *self = IDE_TAGGED_ENTRY_TAG (object);
+
+  switch (property_id)
+    {
+      case PROP_TAG_LABEL:
+        ide_tagged_entry_tag_set_label (self, g_value_get_string (value));
+        break;
+      case PROP_TAG_HAS_CLOSE_BUTTON:
+        ide_tagged_entry_tag_set_has_close_button (self, g_value_get_boolean (value));
+        break;
+      case PROP_TAG_STYLE:
+        ide_tagged_entry_tag_set_style (self, g_value_get_string (value));
+        break;
+      default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void
+ide_tagged_entry_tag_class_init (IdeTaggedEntryTagClass *klass)
+{
+  GObjectClass *oclass = G_OBJECT_CLASS (klass);
+
+  oclass->finalize = ide_tagged_entry_tag_finalize;
+  oclass->set_property = ide_tagged_entry_tag_set_property;
+  oclass->get_property = ide_tagged_entry_tag_get_property;
+
+  tag_properties[PROP_TAG_LABEL] =
+    g_param_spec_string ("label", "Label",
+                         "Text to show on the tag.", NULL,
+                         G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  tag_properties[PROP_TAG_HAS_CLOSE_BUTTON] =
+    g_param_spec_boolean ("has-close-button", "Tag has a close button",
+                          "Whether the tag has a close button.", TRUE,
+                          G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  tag_properties[PROP_TAG_STYLE] =
+    g_param_spec_string ("style", "Style",
+                         "Style of the tag.", "entry-tag",
+                         G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (oclass, NUM_TAG_PROPERTIES, tag_properties);
+}
+
+IdeTaggedEntry *
+ide_tagged_entry_new (void)
+{
+  return g_object_new (IDE_TYPE_TAGGED_ENTRY, NULL);
+}
+
+gboolean
+ide_tagged_entry_insert_tag (IdeTaggedEntry    *self,
+                            IdeTaggedEntryTag *tag,
+                            gint              position)
+{
+  if (g_list_find (self->priv->tags, tag) != NULL)
+    return FALSE;
+
+  tag->priv->entry = self;
+
+  self->priv->tags = g_list_insert (self->priv->tags, g_object_ref (tag), position);
+
+  if (gtk_widget_get_realized (GTK_WIDGET (self)))
+    ide_tagged_entry_tag_realize (tag, self);
+
+  if (gtk_widget_get_mapped (GTK_WIDGET (self)))
+    gdk_window_show_unraised (tag->priv->window);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+
+  return TRUE;
+}
+
+gboolean
+ide_tagged_entry_add_tag (IdeTaggedEntry    *self,
+                         IdeTaggedEntryTag *tag)
+{
+  return ide_tagged_entry_insert_tag (self, tag, -1);
+}
+
+gboolean
+ide_tagged_entry_remove_tag (IdeTaggedEntry    *self,
+                            IdeTaggedEntryTag *tag)
+{
+  if (!g_list_find (self->priv->tags, tag))
+    return FALSE;
+
+  ide_tagged_entry_tag_unrealize (tag);
+
+  self->priv->tags = g_list_remove (self->priv->tags, tag);
+  g_object_unref (tag);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+
+  return TRUE;
+}
+
+IdeTaggedEntryTag *
+ide_tagged_entry_tag_new (const gchar *label)
+{
+  return g_object_new (IDE_TYPE_TAGGED_ENTRY_TAG, "label", label, NULL);
+}
+
+void
+ide_tagged_entry_tag_set_label (IdeTaggedEntryTag *tag,
+                               const gchar *label)
+{
+  IdeTaggedEntryTagPrivate *priv;
+
+  g_return_if_fail (IDE_IS_TAGGED_ENTRY_TAG (tag));
+
+  priv = tag->priv;
+
+  if (g_strcmp0 (priv->label, label) != 0)
+    {
+      GtkWidget *entry;
+
+      g_free (priv->label);
+      priv->label = g_strdup (label);
+      g_clear_object (&priv->layout);
+
+      entry = GTK_WIDGET (tag->priv->entry);
+      if (entry)
+        gtk_widget_queue_resize (entry);
+    }
+}
+
+const gchar *
+ide_tagged_entry_tag_get_label (IdeTaggedEntryTag *tag)
+{
+  g_return_val_if_fail (IDE_IS_TAGGED_ENTRY_TAG (tag), NULL);
+
+  return tag->priv->label;
+}
+
+void
+ide_tagged_entry_tag_set_has_close_button (IdeTaggedEntryTag *tag,
+                                          gboolean has_close_button)
+{
+  IdeTaggedEntryTagPrivate *priv;
+
+  g_return_if_fail (IDE_IS_TAGGED_ENTRY_TAG (tag));
+
+  priv = tag->priv;
+
+  has_close_button = has_close_button != FALSE;
+  if (priv->has_close_button != has_close_button)
+    {
+      GtkWidget *entry;
+
+      priv->has_close_button = has_close_button;
+      g_clear_object (&priv->layout);
+
+      entry = GTK_WIDGET (priv->entry);
+      if (entry)
+        gtk_widget_queue_resize (entry);
+    }
+}
+
+gboolean
+ide_tagged_entry_tag_get_has_close_button (IdeTaggedEntryTag *tag)
+{
+  g_return_val_if_fail (IDE_IS_TAGGED_ENTRY_TAG (tag), FALSE);
+
+  return tag->priv->has_close_button;
+}
+
+void
+ide_tagged_entry_tag_set_style (IdeTaggedEntryTag *tag,
+                               const gchar *style)
+{
+  IdeTaggedEntryTagPrivate *priv;
+
+  g_return_if_fail (IDE_IS_TAGGED_ENTRY_TAG (tag));
+
+  priv = tag->priv;
+
+  if (g_strcmp0 (priv->style, style) != 0)
+    {
+      GtkWidget *entry;
+
+      g_free (priv->style);
+      priv->style = g_strdup (style);
+      g_clear_object (&priv->layout);
+
+      entry = GTK_WIDGET (tag->priv->entry);
+      if (entry)
+        gtk_widget_queue_resize (entry);
+    }
+}
+
+const gchar *
+ide_tagged_entry_tag_get_style (IdeTaggedEntryTag *tag)
+{
+  g_return_val_if_fail (IDE_IS_TAGGED_ENTRY_TAG (tag), NULL);
+
+  return tag->priv->style;
+}
+
+void
+ide_tagged_entry_set_tag_button_visible (IdeTaggedEntry *self,
+                                        gboolean       visible)
+{
+  g_return_if_fail (IDE_IS_TAGGED_ENTRY (self));
+
+  if (self->priv->button_visible == visible)
+    return;
+
+  self->priv->button_visible = visible;
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TAG_BUTTON_VISIBLE]);
+}
+
+gboolean
+ide_tagged_entry_get_tag_button_visible (IdeTaggedEntry *self)
+{
+  g_return_val_if_fail (IDE_IS_TAGGED_ENTRY (self), FALSE);
+
+  return self->priv->button_visible;
+}
diff --git a/src/libide/gui/ide-tagged-entry.h b/src/libide/gui/ide-tagged-entry.h
new file mode 100644
index 000000000..261985d2d
--- /dev/null
+++ b/src/libide/gui/ide-tagged-entry.h
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2011 Red Hat, Inc.
+ * Copyright 2013 Ignacio Casal Quinteiro
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2 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 Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * Author: Cosimo Cecchi <cosimoc redhat com>
+ *
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef __IDE_TAGGED_ENTRY_H__
+#define __IDE_TAGGED_ENTRY_H__
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TAGGED_ENTRY ide_tagged_entry_get_type()
+#define IDE_TAGGED_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), IDE_TYPE_TAGGED_ENTRY, IdeTaggedEntry))
+#define IDE_TAGGED_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), IDE_TYPE_TAGGED_ENTRY, 
IdeTaggedEntryClass))
+#define IDE_IS_TAGGED_ENTRY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), IDE_TYPE_TAGGED_ENTRY))
+#define IDE_IS_TAGGED_ENTRY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), IDE_TYPE_TAGGED_ENTRY))
+#define IDE_TAGGED_ENTRY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), IDE_TYPE_TAGGED_ENTRY, 
IdeTaggedEntryClass))
+
+typedef struct _IdeTaggedEntry IdeTaggedEntry;
+typedef struct _IdeTaggedEntryClass IdeTaggedEntryClass;
+typedef struct _IdeTaggedEntryPrivate IdeTaggedEntryPrivate;
+
+typedef struct _IdeTaggedEntryTag IdeTaggedEntryTag;
+typedef struct _IdeTaggedEntryTagClass IdeTaggedEntryTagClass;
+typedef struct _IdeTaggedEntryTagPrivate IdeTaggedEntryTagPrivate;
+
+struct _IdeTaggedEntry
+{
+  GtkSearchEntry parent;
+
+  IdeTaggedEntryPrivate *priv;
+};
+
+struct _IdeTaggedEntryClass
+{
+  GtkSearchEntryClass parent_class;
+};
+
+#define IDE_TYPE_TAGGED_ENTRY_TAG ide_tagged_entry_tag_get_type()
+#define IDE_TAGGED_ENTRY_TAG(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), IDE_TYPE_TAGGED_ENTRY_TAG, 
IdeTaggedEntryTag))
+#define IDE_TAGGED_ENTRY_TAG_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), IDE_TYPE_TAGGED_ENTRY_TAG, 
IdeTaggedEntryTagClass))
+#define IDE_IS_TAGGED_ENTRY_TAG(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), IDE_TYPE_TAGGED_ENTRY_TAG))
+#define IDE_IS_TAGGED_ENTRY_TAG_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), IDE_TYPE_TAGGED_ENTRY_TAG))
+#define IDE_TAGGED_ENTRY_TAG_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), IDE_TYPE_TAGGED_ENTRY_TAG, 
IdeTaggedEntryTagClass))
+
+struct _IdeTaggedEntryTag
+{
+  GObject parent;
+
+  IdeTaggedEntryTagPrivate *priv;
+};
+
+struct _IdeTaggedEntryTagClass
+{
+  GObjectClass parent_class;
+};
+
+IDE_AVAILABLE_IN_3_32
+GType ide_tagged_entry_get_type (void) G_GNUC_CONST;
+
+IDE_AVAILABLE_IN_3_32
+IdeTaggedEntry *ide_tagged_entry_new (void);
+
+IDE_AVAILABLE_IN_3_32
+void     ide_tagged_entry_set_tag_button_visible (IdeTaggedEntry *self,
+                                                 gboolean       visible);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tagged_entry_get_tag_button_visible (IdeTaggedEntry *self);
+
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tagged_entry_insert_tag (IdeTaggedEntry    *self,
+                                     IdeTaggedEntryTag *tag,
+                                     gint              position);
+
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tagged_entry_add_tag (IdeTaggedEntry    *self,
+                                  IdeTaggedEntryTag *tag);
+
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tagged_entry_remove_tag (IdeTaggedEntry *self,
+                                     IdeTaggedEntryTag *tag);
+
+IDE_AVAILABLE_IN_3_32
+GType ide_tagged_entry_tag_get_type (void) G_GNUC_CONST;
+
+IDE_AVAILABLE_IN_3_32
+IdeTaggedEntryTag *ide_tagged_entry_tag_new (const gchar *label);
+
+IDE_AVAILABLE_IN_3_32
+void ide_tagged_entry_tag_set_label (IdeTaggedEntryTag *tag,
+                                    const gchar *label);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_tagged_entry_tag_get_label (IdeTaggedEntryTag *tag);
+
+IDE_AVAILABLE_IN_3_32
+void ide_tagged_entry_tag_set_has_close_button (IdeTaggedEntryTag *tag,
+                                               gboolean has_close_button);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tagged_entry_tag_get_has_close_button (IdeTaggedEntryTag *tag);
+
+IDE_AVAILABLE_IN_3_32
+void ide_tagged_entry_tag_set_style (IdeTaggedEntryTag *tag,
+                                    const gchar *style);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_tagged_entry_tag_get_style (IdeTaggedEntryTag *tag);
+
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tagged_entry_tag_get_area (IdeTaggedEntryTag      *tag,
+                                       cairo_rectangle_int_t *rect);
+
+G_END_DECLS
+
+#endif /* __IDE_TAGGED_ENTRY_H__ */
diff --git a/src/libide/gui/ide-transfer-button.c b/src/libide/gui/ide-transfer-button.c
new file mode 100644
index 000000000..e977587da
--- /dev/null
+++ b/src/libide/gui/ide-transfer-button.c
@@ -0,0 +1,247 @@
+/* ide-transfer-button.c
+ *
+ * Copyright 2017-2019 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 "ide-transfer-button"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-gui-global.h"
+#include "ide-transfer-button.h"
+
+typedef struct
+{
+  IdeTransfer  *transfer;
+  GCancellable *cancellable;
+} IdeTransferButtonPrivate;
+
+enum {
+  PROP_0,
+  PROP_TRANSFER,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTransferButton, ide_transfer_button, DZL_TYPE_PROGRESS_BUTTON)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+notify_progress_cb (IdeTransferButton *self,
+                    GParamSpec        *pspec,
+                    IdeTransfer       *transfer)
+{
+  gdouble progress;
+
+  g_assert (IDE_IS_TRANSFER_BUTTON (self));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_TRANSFER (transfer));
+
+  progress = ide_transfer_get_progress (transfer);
+
+  dzl_progress_button_set_progress (DZL_PROGRESS_BUTTON (self), progress * 100.0);
+}
+
+static void
+notify_active_cb (IdeTransferButton *self,
+                  GParamSpec        *pspec,
+                  IdeTransfer       *transfer)
+{
+  g_assert (IDE_IS_TRANSFER_BUTTON (self));
+  g_assert (IDE_IS_TRANSFER (transfer));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self), !ide_transfer_get_active (transfer));
+}
+
+static void
+ide_transfer_button_set_transfer (IdeTransferButton *self,
+                                  IdeTransfer       *transfer)
+{
+  IdeTransferButtonPrivate *priv = ide_transfer_button_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER_BUTTON (self));
+  g_assert (!transfer || IDE_IS_TRANSFER (transfer));
+
+  if (transfer != priv->transfer)
+    {
+      if (priv->transfer != NULL)
+        {
+          g_signal_handlers_disconnect_by_func (priv->transfer, notify_progress_cb, self);
+          g_signal_handlers_disconnect_by_func (priv->transfer, notify_active_cb, self);
+          g_clear_object (&priv->transfer);
+          gtk_widget_hide (GTK_WIDGET (self));
+        }
+
+      if (transfer != NULL)
+        {
+          priv->transfer = g_object_ref (transfer);
+          g_signal_connect_object (priv->transfer,
+                                   "notify::active",
+                                   G_CALLBACK (notify_active_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+          g_signal_connect_object (priv->transfer,
+                                   "notify::progress",
+                                   G_CALLBACK (notify_progress_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+          notify_active_cb (self, NULL, priv->transfer);
+          gtk_widget_show (GTK_WIDGET (self));
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_TRANSFER]);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_transfer_button_execute_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeTransferManager *transfer_manager = (IdeTransferManager *)object;
+  g_autoptr(IdeTransferButton) self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER_BUTTON (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TRANSFER_MANAGER (transfer_manager));
+
+  ide_transfer_manager_execute_finish (transfer_manager, result, NULL);
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE);
+  dzl_progress_button_set_show_progress (DZL_PROGRESS_BUTTON (self), FALSE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_transfer_button_clicked (GtkButton *button)
+{
+  IdeTransferButton *self = (IdeTransferButton *)button;
+  IdeTransferButtonPrivate *priv = ide_transfer_button_get_instance_private (self);
+  IdeTransferManager *transfer_manager;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER_BUTTON (self));
+
+  if (priv->transfer == NULL)
+    return;
+
+  dzl_progress_button_set_show_progress (DZL_PROGRESS_BUTTON (self), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE);
+
+  transfer_manager = ide_transfer_manager_get_default ();
+
+  /* TODO: Cancellable state */
+  g_clear_object (&priv->cancellable);
+  priv->cancellable = g_cancellable_new ();
+
+  ide_transfer_manager_execute_async (transfer_manager,
+                                      priv->transfer,
+                                      priv->cancellable,
+                                      ide_transfer_button_execute_cb,
+                                      g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+ide_transfer_button_finalize (GObject *object)
+{
+  IdeTransferButton *self = (IdeTransferButton *)object;
+  IdeTransferButtonPrivate *priv = ide_transfer_button_get_instance_private (self);
+
+  g_clear_object (&priv->cancellable);
+  g_clear_object (&priv->transfer);
+
+  G_OBJECT_CLASS (ide_transfer_button_parent_class)->finalize (object);
+}
+
+static void
+ide_transfer_button_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  IdeTransferButton *self = (IdeTransferButton *)object;
+  IdeTransferButtonPrivate *priv = ide_transfer_button_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_TRANSFER:
+      g_value_set_object (value, priv->transfer);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_transfer_button_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  IdeTransferButton *self = (IdeTransferButton *)object;
+
+  switch (prop_id)
+    {
+    case PROP_TRANSFER:
+      ide_transfer_button_set_transfer (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_transfer_button_class_init (IdeTransferButtonClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkButtonClass *button_class = GTK_BUTTON_CLASS (klass);
+
+  object_class->finalize = ide_transfer_button_finalize;
+  object_class->get_property = ide_transfer_button_get_property;
+  object_class->set_property = ide_transfer_button_set_property;
+
+  button_class->clicked = ide_transfer_button_clicked;
+
+  properties [PROP_TRANSFER] =
+    g_param_spec_object ("transfer",
+                         "Transfer",
+                         "Transfer",
+                         IDE_TYPE_TRANSFER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_transfer_button_init (IdeTransferButton *self)
+{
+}
diff --git a/src/libide/gui/ide-transfer-button.h b/src/libide/gui/ide-transfer-button.h
new file mode 100644
index 000000000..b208d4c3d
--- /dev/null
+++ b/src/libide/gui/ide-transfer-button.h
@@ -0,0 +1,48 @@
+/* ide-transfer-button.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TRANSFER_BUTTON (ide_transfer_button_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTransferButton, ide_transfer_button, IDE, TRANSFER_BUTTON, DzlProgressButton)
+
+struct _IdeTransferButtonClass
+{
+  DzlProgressButtonClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget *ide_transfer_button_new (IdeTransfer *transfer);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-transient-sidebar.c b/src/libide/gui/ide-transient-sidebar.c
new file mode 100644
index 000000000..2fa15b032
--- /dev/null
+++ b/src/libide/gui/ide-transient-sidebar.c
@@ -0,0 +1,355 @@
+/* ide-transient-sidebar.c
+ *
+ * Copyright 2017-2019 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 "ide-transient-sidebar"
+
+#include "config.h"
+
+#include "ide-frame.h"
+#include "ide-grid.h"
+#include "ide-transient-sidebar.h"
+
+typedef struct
+{
+  DzlSignalGroup *toplevel_signals;
+  GWeakRef        page_ref;
+  gint            hold_count;
+} IdeTransientSidebarPrivate;
+
+static void ide_transient_sidebar_page_destroyed (IdeTransientSidebar *self,
+                                                  IdePage             *page);
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTransientSidebar, ide_transient_sidebar, IDE_TYPE_PANEL)
+
+static gboolean
+has_page_related_focus (IdeTransientSidebar *self)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+  g_autoptr(IdePage) page = NULL;
+  GtkWidget *focus_page;
+  GtkWidget *toplevel;
+  GtkWidget *focus;
+  GtkWidget *grid;
+
+  g_assert (IDE_IS_TRANSIENT_SIDEBAR (self));
+
+  /* If there is no page, then nothing more to do */
+  page = g_weak_ref_get (&priv->page_ref);
+  if (page == NULL)
+    return FALSE;
+
+  /* We need the toplevel to get the current focus */
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+  if (!GTK_IS_WINDOW (toplevel))
+    return FALSE;
+
+  /* Synthesize succes when there is no focus, this can happen inbetween
+   * various state transitions.
+   */
+  focus = gtk_window_get_focus (GTK_WINDOW (toplevel));
+  if (focus == NULL)
+    return TRUE;
+
+  /* If focus is inside this widget, then we don't want to hide */
+  if (gtk_widget_is_ancestor (focus, GTK_WIDGET (self)))
+    return TRUE;
+
+  /* If focus is in the page, then we definitely don't want to hide */
+  if (gtk_widget_is_ancestor (focus, GTK_WIDGET (page)))
+    return TRUE;
+
+  /* If the focus has entered another page, then we can release. */
+  focus_page = gtk_widget_get_ancestor (focus, IDE_TYPE_PAGE);
+  if (focus_page && focus_page != GTK_WIDGET (page))
+    return FALSE;
+
+  /* If we found ourselves a grid, and it has no pages in it, we shall
+   * expect that there are no more pages to apply.
+   */
+  grid = gtk_widget_get_ancestor (focus, IDE_TYPE_GRID);
+  if (grid != NULL &&
+      ide_grid_count_pages (IDE_GRID (grid)) == 0)
+    return FALSE;
+
+  /* Focus hasn't landed anywhere that indicates to us that the
+   * page definitely isn't visible anymore, so we can just keep
+   * the panel visible for now.
+   */
+
+  return TRUE;
+}
+
+static void
+set_visible (IdeTransientSidebar *self,
+             gboolean             visible)
+{
+  const gchar *prop_name;
+  GtkPositionType pos;
+  GtkWidget *bin;
+
+  g_assert (IDE_IS_TRANSIENT_SIDEBAR (self));
+
+  if (!(bin = gtk_widget_get_ancestor (GTK_WIDGET (self), DZL_TYPE_DOCK_BIN)))
+    {
+      g_warning ("Failed to locate DzlDockBin for transition");
+      return;
+    }
+
+  gtk_container_child_get (GTK_CONTAINER (bin), GTK_WIDGET (self),
+                           "position", &pos,
+                           NULL);
+
+  switch (pos)
+    {
+    case GTK_POS_TOP:
+      prop_name = "top-visible";
+      break;
+
+    case GTK_POS_BOTTOM:
+      prop_name = "bottom-visible";
+      break;
+
+    case GTK_POS_LEFT:
+      prop_name = "left-visible";
+      break;
+
+    case GTK_POS_RIGHT:
+      prop_name = "right-visible";
+      break;
+
+    default:
+      g_return_if_reached ();
+    }
+
+  g_object_set (bin, prop_name, visible, NULL);
+}
+
+static void
+ide_transient_sidebar_after_set_focus (IdeTransientSidebar *self,
+                                       GtkWidget           *focus,
+                                       GtkWindow           *toplevel)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+
+  g_assert (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_assert (!toplevel || GTK_IS_WINDOW (toplevel));
+  g_assert (priv->hold_count >= 0);
+
+  if (priv->hold_count > 0)
+    return;
+
+  /*
+   * If we are currently visible, then check to see if the focus has gone
+   * somewhere outside the panel or the page. If so, we need to dismiss
+   * the panel.
+   *
+   * We try to be tolerant of sibling focus on such things like the stack
+   * header.
+   */
+  if (gtk_widget_get_visible (GTK_WIDGET (self)))
+    {
+      if (!has_page_related_focus (self))
+        {
+          g_autoptr(GtkWidget) old_page = g_weak_ref_get (&priv->page_ref);
+
+          if (old_page != NULL)
+            g_signal_handlers_disconnect_by_func (old_page,
+                                                  G_CALLBACK (ide_transient_sidebar_page_destroyed),
+                                                  self);
+
+          set_visible (self, FALSE);
+          g_weak_ref_set (&priv->page_ref, NULL);
+        }
+    }
+}
+
+static void
+ide_transient_sidebar_page_destroyed (IdeTransientSidebar *self,
+                                      IdePage             *page)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+
+  g_assert (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_assert (IDE_IS_PAGE (page));
+
+  g_signal_handlers_disconnect_by_func (page,
+                                        G_CALLBACK (ide_transient_sidebar_page_destroyed),
+                                        self);
+
+  g_weak_ref_set (&priv->page_ref, NULL);
+
+  ide_transient_sidebar_after_set_focus (self, NULL, NULL);
+}
+
+static void
+ide_transient_sidebar_hierarchy_changed (GtkWidget *widget,
+                                         GtkWidget *old_toplevel)
+{
+  IdeTransientSidebar *self = (IdeTransientSidebar *)widget;
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_assert (!old_toplevel || GTK_IS_WIDGET (old_toplevel));
+
+  toplevel = gtk_widget_get_toplevel (widget);
+  if (!GTK_IS_WINDOW (toplevel))
+    toplevel = NULL;
+
+  dzl_signal_group_set_target (priv->toplevel_signals, toplevel);
+}
+
+static void
+ide_transient_sidebar_finalize (GObject *object)
+{
+  IdeTransientSidebar *self = (IdeTransientSidebar *)object;
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+
+  g_clear_object (&priv->toplevel_signals);
+  g_weak_ref_clear (&priv->page_ref);
+
+  G_OBJECT_CLASS (ide_transient_sidebar_parent_class)->finalize (object);
+}
+
+static void
+ide_transient_sidebar_class_init (IdeTransientSidebarClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_transient_sidebar_finalize;
+
+  widget_class->hierarchy_changed = ide_transient_sidebar_hierarchy_changed;
+}
+
+static void
+ide_transient_sidebar_init (IdeTransientSidebar *self)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+  GtkWidget *paned;
+  GtkWidget *stack;
+
+  g_weak_ref_init (&priv->page_ref, NULL);
+
+  priv->toplevel_signals = dzl_signal_group_new (GTK_TYPE_WINDOW);
+
+  dzl_signal_group_connect_data (priv->toplevel_signals,
+                                 "set-focus",
+                                 G_CALLBACK (ide_transient_sidebar_after_set_focus),
+                                 self, NULL,
+                                 G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+
+  if (NULL != (paned = gtk_bin_get_child (GTK_BIN (self))) &&
+      DZL_IS_MULTI_PANED (paned) &&
+      NULL != (stack = dzl_multi_paned_get_nth_child (DZL_MULTI_PANED (paned), 0)) &&
+      DZL_IS_DOCK_STACK (stack))
+    {
+      GtkWidget *tab_strip;
+
+      /* We want to hide the tab strip in the stack for the transient bar */
+      tab_strip = dzl_gtk_widget_find_child_typed (stack, DZL_TYPE_TAB_STRIP);
+      if (tab_strip != NULL)
+        gtk_widget_hide (tab_strip);
+    }
+}
+
+/**
+ * ide_transient_sidebar_set_page:
+ * @self: a #IdeTransientSidebar
+ * @page: (nullable): An #IdePage or %NULL
+ *
+ * Sets the page for which the panel is transient for. When focus leaves the
+ * sidebar or the page, the panel will be dismissed.
+ *
+ * Since: 3.32
+ */
+void
+ide_transient_sidebar_set_page (IdeTransientSidebar *self,
+                                IdePage             *page)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+  g_autoptr(GtkWidget) old_page = NULL;
+
+  g_return_if_fail (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_return_if_fail (!page || IDE_IS_PAGE (page));
+
+  old_page = g_weak_ref_get (&priv->page_ref);
+  if (old_page != NULL)
+    g_signal_handlers_disconnect_by_func (old_page,
+                                          G_CALLBACK (ide_transient_sidebar_page_destroyed),
+                                          self);
+
+  if (page != NULL)
+    g_signal_connect_object (page,
+                             "destroy",
+                             G_CALLBACK (ide_transient_sidebar_page_destroyed),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+  g_weak_ref_set (&priv->page_ref, page);
+}
+
+void
+ide_transient_sidebar_set_panel (IdeTransientSidebar *self,
+                                 GtkWidget           *panel)
+{
+  GtkWidget *stack;
+
+  g_return_if_fail (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (panel));
+
+  stack = gtk_widget_get_parent (GTK_WIDGET (panel));
+
+  if (GTK_IS_STACK (stack))
+    gtk_stack_set_visible_child (GTK_STACK (stack), panel);
+  else
+    g_warning ("Failed to locate stack containing panel");
+}
+
+void
+ide_transient_sidebar_lock (IdeTransientSidebar *self)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_return_if_fail (priv->hold_count >= 0);
+
+  priv->hold_count++;
+
+  if (!dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (self)))
+    set_visible (self, TRUE);
+}
+
+void
+ide_transient_sidebar_unlock (IdeTransientSidebar *self)
+{
+  IdeTransientSidebarPrivate *priv = ide_transient_sidebar_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TRANSIENT_SIDEBAR (self));
+  g_return_if_fail (priv->hold_count > 0);
+
+  priv->hold_count--;
+
+  if (priv->hold_count == 0)
+    {
+      if (dzl_dock_revealer_get_reveal_child (DZL_DOCK_REVEALER (self)))
+        set_visible (self, FALSE);
+    }
+}
diff --git a/src/libide/gui/ide-transient-sidebar.h b/src/libide/gui/ide-transient-sidebar.h
new file mode 100644
index 000000000..0e27c0525
--- /dev/null
+++ b/src/libide/gui/ide-transient-sidebar.h
@@ -0,0 +1,58 @@
+/* ide-transient-sidebar.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-panel.h"
+#include "ide-page.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TRANSIENT_SIDEBAR (ide_transient_sidebar_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTransientSidebar, ide_transient_sidebar, IDE, TRANSIENT_SIDEBAR, IdePanel)
+
+struct _IdeTransientSidebarClass
+{
+  IdePanelClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_transient_sidebar_set_panel (IdeTransientSidebar *self,
+                                      GtkWidget           *panel);
+IDE_AVAILABLE_IN_3_32
+void ide_transient_sidebar_set_page  (IdeTransientSidebar *self,
+                                      IdePage             *page);
+IDE_AVAILABLE_IN_3_32
+void ide_transient_sidebar_lock      (IdeTransientSidebar *self);
+IDE_AVAILABLE_IN_3_32
+void ide_transient_sidebar_unlock    (IdeTransientSidebar *self);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-window-settings-private.h b/src/libide/gui/ide-window-settings-private.h
new file mode 100644
index 000000000..e8fecd2b4
--- /dev/null
+++ b/src/libide/gui/ide-window-settings-private.h
@@ -0,0 +1,29 @@
+/* ide-window-settings.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+void _ide_window_settings_register (GtkWindow *window);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-window-settings.c b/src/libide/gui/ide-window-settings.c
new file mode 100644
index 000000000..dc2701c48
--- /dev/null
+++ b/src/libide/gui/ide-window-settings.c
@@ -0,0 +1,165 @@
+/* ide-window-settings.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-window-settings"
+
+#include "config.h"
+
+#include "ide-window-settings-private.h"
+
+#define GB_WINDOW_MIN_WIDTH  1280
+#define GB_WINDOW_MIN_HEIGHT 720
+#define SAVE_TIMEOUT_SECS    1
+
+static GSettings *settings;
+
+static gboolean
+ide_window_settings__window_save_settings_cb (gpointer data)
+{
+  GtkWindow *window = data;
+
+  g_assert (GTK_IS_WINDOW (window));
+  g_assert (G_IS_SETTINGS (settings));
+
+  if (gtk_widget_get_realized (GTK_WIDGET (window)) &&
+      gtk_widget_get_visible (GTK_WIDGET (window)))
+    {
+      GdkRectangle geom;
+      gboolean maximized;
+
+      g_object_set_data (G_OBJECT (window), "SETTINGS_HANDLER_ID", NULL);
+
+      gtk_window_get_size (window, &geom.width, &geom.height);
+      gtk_window_get_position (window, &geom.x, &geom.y);
+      maximized = gtk_window_is_maximized (window);
+
+      g_settings_set (settings, "window-size", "(ii)", geom.width, geom.height);
+      g_settings_set (settings, "window-position", "(ii)", geom.x, geom.y);
+      g_settings_set_boolean (settings, "window-maximized", maximized);
+    }
+
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+ide_window_settings__window_configure_event (GtkWindow         *window,
+                                             GdkEventConfigure *event)
+{
+  guint handler;
+
+  g_assert (GTK_IS_WINDOW (window));
+  g_assert (event != NULL);
+  g_assert (G_IS_SETTINGS (settings));
+
+  handler = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (window), "SETTINGS_HANDLER_ID"));
+
+  if (handler == 0)
+    {
+      handler = g_timeout_add_seconds (SAVE_TIMEOUT_SECS,
+                                       ide_window_settings__window_save_settings_cb,
+                                       window);
+      g_object_set_data (G_OBJECT (window), "SETTINGS_HANDLER_ID", GINT_TO_POINTER (handler));
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_window_settings__window_realize (GtkWindow *window)
+{
+  GdkRectangle geom = { 0 };
+  gboolean maximized = FALSE;
+
+  g_assert (GTK_IS_WINDOW (window));
+  g_assert (G_IS_SETTINGS (settings));
+
+  g_settings_get (settings, "window-position", "(ii)", &geom.x, &geom.y);
+  g_settings_get (settings, "window-size", "(ii)", &geom.width, &geom.height);
+  g_settings_get (settings, "window-maximized", "b", &maximized);
+
+  geom.width = MAX (geom.width, GB_WINDOW_MIN_WIDTH);
+  geom.height = MAX (geom.height, GB_WINDOW_MIN_HEIGHT);
+  gtk_window_set_default_size (window, geom.width, geom.height);
+
+  gtk_window_move (window, geom.x, geom.y);
+
+  if (maximized)
+    gtk_window_maximize (window);
+}
+
+static void
+ide_window_settings__window_destroy (GtkWindow *window)
+{
+  guint handler;
+
+  g_assert (GTK_IS_WINDOW (window));
+  g_assert (G_IS_SETTINGS (settings));
+
+  handler = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (window), "SETTINGS_HANDLER_ID"));
+
+  if (handler != 0)
+    {
+      g_source_remove (handler);
+      g_object_set_data (G_OBJECT (window), "SETTINGS_HANDLER_ID", NULL);
+    }
+
+  g_signal_handlers_disconnect_by_func (window,
+                                        G_CALLBACK (ide_window_settings__window_configure_event),
+                                        NULL);
+
+  g_signal_handlers_disconnect_by_func (window,
+                                        G_CALLBACK (ide_window_settings__window_destroy),
+                                        NULL);
+
+  g_signal_handlers_disconnect_by_func (window,
+                                        G_CALLBACK (ide_window_settings__window_realize),
+                                        NULL);
+
+  g_object_unref (settings);
+}
+
+void
+_ide_window_settings_register (GtkWindow *window)
+{
+  if (settings == NULL)
+    {
+      settings = g_settings_new ("org.gnome.builder");
+      g_object_add_weak_pointer (G_OBJECT (settings), (gpointer *)&settings);
+    }
+  else
+    {
+      g_object_ref (settings);
+    }
+
+  g_signal_connect (window,
+                    "configure-event",
+                    G_CALLBACK (ide_window_settings__window_configure_event),
+                    NULL);
+
+  g_signal_connect (window,
+                    "destroy",
+                    G_CALLBACK (ide_window_settings__window_destroy),
+                    NULL);
+
+  g_signal_connect (window,
+                    "realize",
+                    G_CALLBACK (ide_window_settings__window_realize),
+                    NULL);
+}
diff --git a/src/libide/gui/ide-workbench-addin.c b/src/libide/gui/ide-workbench-addin.c
new file mode 100644
index 000000000..3876fe118
--- /dev/null
+++ b/src/libide/gui/ide-workbench-addin.c
@@ -0,0 +1,402 @@
+/* ide-workbench-addin.c
+ *
+ * Copyright 2018-2019 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 "ide-workbench-addin"
+
+#include "config.h"
+
+#include "ide-workbench-addin.h"
+
+G_DEFINE_INTERFACE (IdeWorkbenchAddin, ide_workbench_addin, G_TYPE_OBJECT)
+
+static void ide_workbench_addin_real_open_at_async (IdeWorkbenchAddin   *self,
+                                                    GFile               *file,
+                                                    const gchar         *hint,
+                                                    gint                 at_line,
+                                                    gint                 at_line_offset,
+                                                    IdeBufferOpenFlags   flags,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+static void ide_workbench_addin_real_open_async    (IdeWorkbenchAddin   *self,
+                                                    GFile               *file,
+                                                    const gchar         *hint,
+                                                    IdeBufferOpenFlags   flags,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data);
+
+static void
+ide_workbench_addin_real_load_project_async (IdeWorkbenchAddin   *self,
+                                             IdeProjectInfo      *project_info,
+                                             GCancellable        *cancellable,
+                                             GAsyncReadyCallback  callback,
+                                             gpointer             user_data)
+{
+  ide_task_report_new_error (self, callback, user_data,
+                             ide_workbench_addin_real_load_project_async,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Loading projects is not supported");
+}
+
+static gboolean
+ide_workbench_addin_real_load_project_finish (IdeWorkbenchAddin  *self,
+                                              GAsyncResult       *result,
+                                              GError            **error)
+{
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_workbench_addin_real_unload_project_async (IdeWorkbenchAddin   *self,
+                                               IdeProjectInfo      *project_info,
+                                               GCancellable        *cancellable,
+                                               GAsyncReadyCallback  callback,
+                                               gpointer             user_data)
+{
+  ide_task_report_new_error (self, callback, user_data,
+                             ide_workbench_addin_real_unload_project_async,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "Unloading projects is not supported");
+}
+
+static gboolean
+ide_workbench_addin_real_unload_project_finish (IdeWorkbenchAddin  *self,
+                                                GAsyncResult       *result,
+                                                GError            **error)
+{
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_workbench_addin_real_open_async (IdeWorkbenchAddin   *self,
+                                     GFile               *file,
+                                     const gchar         *hint,
+                                     IdeBufferOpenFlags   flags,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  IdeWorkbenchAddinInterface *iface;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  iface = IDE_WORKBENCH_ADDIN_GET_IFACE (self);
+
+  if (iface->open_at_async == (gpointer)ide_workbench_addin_real_open_at_async)
+    {
+      ide_task_report_new_error (self, callback, user_data,
+                                 ide_workbench_addin_real_open_async,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Opening files is not supported");
+      return;
+    }
+
+  iface->open_at_async (self, file, hint, -1, -1, flags, cancellable, callback, user_data);
+}
+
+static void
+ide_workbench_addin_real_open_at_async (IdeWorkbenchAddin   *self,
+                                        GFile               *file,
+                                        const gchar         *hint,
+                                        gint                 at_line,
+                                        gint                 at_line_offset,
+                                        IdeBufferOpenFlags   flags,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  IdeWorkbenchAddinInterface *iface;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  iface = IDE_WORKBENCH_ADDIN_GET_IFACE (self);
+
+  if (iface->open_async == (gpointer)ide_workbench_addin_real_open_async)
+    {
+      ide_task_report_new_error (self, callback, user_data,
+                                 ide_workbench_addin_real_open_at_async,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Opening files is not supported");
+      return;
+    }
+
+  iface->open_async (self, file, hint, flags, cancellable, callback, user_data);
+}
+
+static gboolean
+ide_workbench_addin_real_open_finish (IdeWorkbenchAddin  *self,
+                                      GAsyncResult       *result,
+                                      GError            **error)
+{
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_workbench_addin_default_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load_project_async = ide_workbench_addin_real_load_project_async;
+  iface->load_project_finish = ide_workbench_addin_real_load_project_finish;
+  iface->unload_project_async = ide_workbench_addin_real_unload_project_async;
+  iface->unload_project_finish = ide_workbench_addin_real_unload_project_finish;
+  iface->open_async = ide_workbench_addin_real_open_async;
+  iface->open_at_async = ide_workbench_addin_real_open_at_async;
+  iface->open_finish = ide_workbench_addin_real_open_finish;
+}
+
+void
+ide_workbench_addin_load (IdeWorkbenchAddin *self,
+                          IdeWorkbench      *workbench)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->load)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->load (self, workbench);
+}
+
+void
+ide_workbench_addin_unload (IdeWorkbenchAddin *self,
+                            IdeWorkbench      *workbench)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKBENCH (workbench));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->unload)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->unload (self, workbench);
+}
+
+void
+ide_workbench_addin_load_project_async (IdeWorkbenchAddin   *self,
+                                        IdeProjectInfo      *project_info,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->load_project_async (self,
+                                                            project_info,
+                                                            cancellable,
+                                                            callback,
+                                                            user_data);
+}
+
+gboolean
+ide_workbench_addin_load_project_finish (IdeWorkbenchAddin  *self,
+                                         GAsyncResult       *result,
+                                         GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_WORKBENCH_ADDIN_GET_IFACE (self)->load_project_finish (self, result, error);
+}
+
+void
+ide_workbench_addin_unload_project_async (IdeWorkbenchAddin   *self,
+                                          IdeProjectInfo      *project_info,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->unload_project_async (self,
+                                                              project_info,
+                                                              cancellable,
+                                                              callback,
+                                                              user_data);
+}
+
+gboolean
+ide_workbench_addin_unload_project_finish (IdeWorkbenchAddin  *self,
+                                           GAsyncResult       *result,
+                                           GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_WORKBENCH_ADDIN_GET_IFACE (self)->unload_project_finish (self, result, error);
+}
+
+void
+ide_workbench_addin_workspace_added (IdeWorkbenchAddin *self,
+                                     IdeWorkspace      *workspace)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->workspace_added)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->workspace_added (self, workspace);
+}
+
+void
+ide_workbench_addin_workspace_removed (IdeWorkbenchAddin *self,
+                                       IdeWorkspace      *workspace)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->workspace_removed)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->workspace_removed (self, workspace);
+}
+
+gboolean
+ide_workbench_addin_can_open (IdeWorkbenchAddin *self,
+                              GFile             *file,
+                              const gchar       *content_type,
+                              gint              *priority)
+{
+  gint real_priority;
+
+  g_return_val_if_fail (IDE_IS_WORKBENCH_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+
+  if (priority == NULL)
+    priority = &real_priority;
+  else
+    *priority = 0;
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->can_open)
+    return IDE_WORKBENCH_ADDIN_GET_IFACE (self)->can_open (self, file, content_type, priority);
+
+  return FALSE;
+}
+
+void
+ide_workbench_addin_open_async (IdeWorkbenchAddin   *self,
+                                GFile               *file,
+                                const gchar         *content_type,
+                                IdeBufferOpenFlags   flags,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->open_async (self,
+                                                    file,
+                                                    content_type,
+                                                    flags,
+                                                    cancellable,
+                                                    callback,
+                                                    user_data);
+}
+
+void
+ide_workbench_addin_open_at_async (IdeWorkbenchAddin   *self,
+                                   GFile               *file,
+                                   const gchar         *content_type,
+                                   gint                 at_line,
+                                   gint                 at_line_offset,
+                                   IdeBufferOpenFlags   flags,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_WORKBENCH_ADDIN_GET_IFACE (self)->open_at_async (self,
+                                                       file,
+                                                       content_type,
+                                                       at_line,
+                                                       at_line_offset,
+                                                       flags,
+                                                       cancellable,
+                                                       callback,
+                                                       user_data);
+}
+
+gboolean
+ide_workbench_addin_open_finish (IdeWorkbenchAddin  *self,
+                                 GAsyncResult       *result,
+                                 GError            **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_WORKBENCH_ADDIN_GET_IFACE (self)->open_finish (self, result, error);
+}
+
+/**
+ * ide_workbench_addin_vcs_changed:
+ * @self: a #IdeWorkbenchAddin
+ * @vcs: (nullable): an #IdeVcs
+ *
+ * This function notifies an #IdeWorkbenchAddin that the version control
+ * system has changed. This happens when ide_workbench_set_vcs() is called
+ * or after an addin is loaded.
+ *
+ * This is helpful for plugins that want to react to VCS changes such as
+ * changing branches, or tracking commits.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_addin_vcs_changed (IdeWorkbenchAddin *self,
+                                 IdeVcs            *vcs)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_VCS (vcs));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->vcs_changed)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->vcs_changed (self, vcs);
+}
+
+/**
+ * ide_workbench_addin_project_loaded:
+ * @self: an #IdeWorkbenchAddin
+ * @project_info: an #IdeProjectInfo
+ *
+ * This function is called after the project has been loaded.
+ *
+ * It is useful for situations where you do not need to influence the
+ * project loading, but do need to perform operations after it has
+ * completed.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_addin_project_loaded (IdeWorkbenchAddin *self,
+                                    IdeProjectInfo    *project_info)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH_ADDIN (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+
+  if (IDE_WORKBENCH_ADDIN_GET_IFACE (self)->project_loaded)
+    IDE_WORKBENCH_ADDIN_GET_IFACE (self)->project_loaded (self, project_info);
+}
diff --git a/src/libide/gui/ide-workbench-addin.h b/src/libide/gui/ide-workbench-addin.h
new file mode 100644
index 000000000..ec7f3db3f
--- /dev/null
+++ b/src/libide/gui/ide-workbench-addin.h
@@ -0,0 +1,159 @@
+/* ide-workbench-addin.h
+ *
+ * Copyright 2018-2019 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 "ide-workbench.h"
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKBENCH_ADDIN (ide_workbench_addin_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeWorkbenchAddin, ide_workbench_addin, IDE, WORKBENCH_ADDIN, GObject)
+
+struct _IdeWorkbenchAddinInterface
+{
+  GTypeInterface parent;
+
+  void     (*load)                  (IdeWorkbenchAddin     *self,
+                                     IdeWorkbench          *workbench);
+  void     (*unload)                (IdeWorkbenchAddin     *self,
+                                     IdeWorkbench          *workbench);
+  void     (*load_project_async)    (IdeWorkbenchAddin     *self,
+                                     IdeProjectInfo        *project_info,
+                                     GCancellable          *cancellable,
+                                     GAsyncReadyCallback    callback,
+                                     gpointer               user_data);
+  gboolean (*load_project_finish)   (IdeWorkbenchAddin     *self,
+                                     GAsyncResult          *result,
+                                     GError               **error);
+  void     (*unload_project_async)  (IdeWorkbenchAddin     *self,
+                                     IdeProjectInfo        *project_info,
+                                     GCancellable          *cancellable,
+                                     GAsyncReadyCallback    callback,
+                                     gpointer               user_data);
+  gboolean (*unload_project_finish) (IdeWorkbenchAddin     *self,
+                                     GAsyncResult          *result,
+                                     GError               **error);
+  void     (*project_loaded)        (IdeWorkbenchAddin     *self,
+                                     IdeProjectInfo        *project_info);
+  void     (*workspace_added)       (IdeWorkbenchAddin     *self,
+                                     IdeWorkspace          *workspace);
+  void     (*workspace_removed)     (IdeWorkbenchAddin     *self,
+                                     IdeWorkspace          *workspace);
+  gboolean (*can_open)              (IdeWorkbenchAddin     *self,
+                                     GFile                 *file,
+                                     const gchar           *content_type,
+                                     gint                  *priority);
+  void     (*open_async)            (IdeWorkbenchAddin     *self,
+                                     GFile                 *file,
+                                     const gchar           *content_type,
+                                     IdeBufferOpenFlags     flags,
+                                     GCancellable          *cancellable,
+                                     GAsyncReadyCallback    callback,
+                                     gpointer               user_data);
+  void     (*open_at_async)         (IdeWorkbenchAddin     *self,
+                                     GFile                 *file,
+                                     const gchar           *content_type,
+                                     gint                   at_line,
+                                     gint                   at_line_offset,
+                                     IdeBufferOpenFlags     flags,
+                                     GCancellable          *cancellable,
+                                     GAsyncReadyCallback    callback,
+                                     gpointer               user_data);
+  gboolean (*open_finish)           (IdeWorkbenchAddin     *self,
+                                     GAsyncResult          *result,
+                                     GError               **error);
+  void     (*vcs_changed)           (IdeWorkbenchAddin     *self,
+                                     IdeVcs                *vcs);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_load                  (IdeWorkbenchAddin    *self,
+                                                    IdeWorkbench         *workbench);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_unload                (IdeWorkbenchAddin    *self,
+                                                    IdeWorkbench         *workbench);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_load_project_async    (IdeWorkbenchAddin    *self,
+                                                    IdeProjectInfo       *project_info,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_workbench_addin_load_project_finish   (IdeWorkbenchAddin    *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_unload_project_async  (IdeWorkbenchAddin    *self,
+                                                    IdeProjectInfo       *project_info,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_workbench_addin_unload_project_finish (IdeWorkbenchAddin    *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_project_loaded        (IdeWorkbenchAddin    *self,
+                                                    IdeProjectInfo       *project_info);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_workspace_added       (IdeWorkbenchAddin    *self,
+                                                    IdeWorkspace         *workspace);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_workspace_removed     (IdeWorkbenchAddin    *self,
+                                                    IdeWorkspace         *workspace);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_workbench_addin_can_open              (IdeWorkbenchAddin    *self,
+                                                    GFile                *file,
+                                                    const gchar          *content_type,
+                                                    gint                 *priority);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_open_async            (IdeWorkbenchAddin    *self,
+                                                    GFile                *file,
+                                                    const gchar          *content_type,
+                                                    IdeBufferOpenFlags    flags,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_open_at_async         (IdeWorkbenchAddin    *self,
+                                                    GFile                *file,
+                                                    const gchar          *content_type,
+                                                    gint                  at_line,
+                                                    gint                  at_line_offset,
+                                                    IdeBufferOpenFlags    flags,
+                                                    GCancellable         *cancellable,
+                                                    GAsyncReadyCallback   callback,
+                                                    gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_workbench_addin_open_finish           (IdeWorkbenchAddin    *self,
+                                                    GAsyncResult         *result,
+                                                    GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_workbench_addin_vcs_changed           (IdeWorkbenchAddin    *self,
+                                                    IdeVcs               *vcs);
+IDE_AVAILABLE_IN_3_32
+IdeWorkbenchAddin *ide_workbench_addin_find_by_module_name (IdeWorkbench *workbench,
+                                                            const gchar  *module_name);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-workbench.c b/src/libide/gui/ide-workbench.c
new file mode 100644
index 000000000..ddd963959
--- /dev/null
+++ b/src/libide/gui/ide-workbench.c
@@ -0,0 +1,2299 @@
+/* ide-workbench.c
+ *
+ * Copyright 2014-2019 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 "ide-workbench"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-debugger.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+
+#include "ide-context-private.h"
+#include "ide-foundry-init.h"
+#include "ide-thread-private.h"
+
+#include "ide-application.h"
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-primary-workspace.h"
+#include "ide-session-private.h"
+#include "ide-workbench.h"
+#include "ide-workbench-addin.h"
+#include "ide-workspace.h"
+
+/**
+ * SECTION:ide-workbench
+ * @title: IdeWorkbench
+ * @short_description: window group for all windows within a project
+ *
+ * The #IdeWorkbench is a #GtkWindowGroup containing the #IdeContext (root
+ * data-structure for a project) and all of the windows associated with the
+ * project.
+ *
+ * Usually, windows within the #IdeWorkbench are an #IdeWorkspace. They can
+ * react to changes in the #IdeContext or its descendants to represent the
+ * project and it's state.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeWorkbench
+{
+  GtkWindowGroup    parent_instance;
+
+  /* MRU of workspaces, link embedded in workspace */
+  GQueue            mru_queue;
+
+  /* Owned references */
+  PeasExtensionSet *addins;
+  GCancellable     *cancellable;
+  IdeContext       *context;
+  IdeBuildSystem   *build_system;
+  IdeProjectInfo   *project_info;
+  IdeVcs           *vcs;
+  IdeVcsMonitor    *vcs_monitor;
+  IdeSearchEngine  *search_engine;
+  IdeSession       *session;
+
+  /* Various flags */
+  guint             unloaded : 1;
+};
+
+typedef struct
+{
+  GPtrArray          *addins;
+  IdeWorkbenchAddin  *preferred;
+  GFile              *file;
+  gchar              *hint;
+  gchar              *content_type;
+  IdeBufferOpenFlags  flags;
+  gint                at_line;
+  gint                at_line_offset;
+} Open;
+
+typedef struct
+{
+  IdeProjectInfo *project_info;
+  GPtrArray      *addins;
+  GType           workspace_type;
+  gint64          present_time;
+} LoadProject;
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_VCS,
+  N_PROPS
+};
+
+static void ide_workbench_action_close       (IdeWorkbench *self,
+                                              GVariant     *param);
+static void ide_workbench_action_open        (IdeWorkbench *self,
+                                              GVariant     *param);
+static void ide_workbench_action_dump_tasks  (IdeWorkbench *self,
+                                              GVariant     *param);
+static void ide_workbench_action_object_tree (IdeWorkbench *self,
+                                              GVariant     *param);
+static void ide_workbench_action_inspector   (IdeWorkbench *self,
+                                              GVariant     *param);
+
+
+DZL_DEFINE_ACTION_GROUP (IdeWorkbench, ide_workbench, {
+  { "close", ide_workbench_action_close },
+  { "open", ide_workbench_action_open },
+  { "-inspector", ide_workbench_action_inspector },
+  { "-object-tree", ide_workbench_action_object_tree },
+  { "-dump-tasks", ide_workbench_action_dump_tasks },
+})
+
+G_DEFINE_TYPE_WITH_CODE (IdeWorkbench, ide_workbench, GTK_TYPE_WINDOW_GROUP,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
+                                                ide_workbench_init_action_group))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+load_project_free (LoadProject *lp)
+{
+  g_clear_object (&lp->project_info);
+  g_clear_pointer (&lp->addins, g_ptr_array_unref);
+  g_slice_free (LoadProject, lp);
+}
+
+static void
+open_free (Open *o)
+{
+  g_clear_pointer (&o->addins, g_ptr_array_unref);
+  g_clear_object (&o->preferred);
+  g_clear_object (&o->file);
+  g_clear_pointer (&o->hint, g_free);
+  g_clear_pointer (&o->content_type, g_free);
+  g_slice_free (Open, o);
+}
+
+static gboolean
+ignore_error (GError *error)
+{
+  return g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
+         g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED);
+}
+
+static void
+ide_workbench_set_context (IdeWorkbench *self,
+                           IdeContext   *context)
+{
+  g_autoptr(IdeContext) new_context = NULL;
+  g_autoptr(IdeBufferManager) bufmgr = NULL;
+  IdeBuildSystem *build_system;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!context || IDE_IS_CONTEXT (context));
+
+  if (context == NULL)
+    context = new_context = ide_context_new ();
+
+  g_set_object (&self->context, context);
+
+  /* Make sure we have access to buffer manager early */
+  bufmgr = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_BUFFER_MANAGER);
+
+  /* And use a fallback build system if one is not already available */
+  if ((build_system = ide_context_peek_child_typed (context, IDE_TYPE_BUILD_SYSTEM)))
+    self->build_system = g_object_ref (build_system);
+  else
+    self->build_system = ide_object_ensure_child_typed (IDE_OBJECT (context), 
IDE_TYPE_FALLBACK_BUILD_SYSTEM);
+
+  /* Setup session monitor for future use */
+  self->session = ide_session_new ();
+  ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (self->session));
+}
+
+static void
+ide_workbench_addin_added_workspace_cb (IdeWorkspace      *workspace,
+                                        IdeWorkbenchAddin *addin)
+{
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+
+  ide_workbench_addin_workspace_added (addin, workspace);
+}
+
+static void
+ide_workbench_addin_removed_workspace_cb (IdeWorkspace      *workspace,
+                                          IdeWorkbenchAddin *addin)
+{
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+
+  ide_workbench_addin_workspace_removed (addin, workspace);
+}
+
+static void
+ide_workbench_addin_added_cb (PeasExtensionSet *set,
+                              PeasPluginInfo   *plugin_info,
+                              PeasExtension    *exten,
+                              gpointer          user_data)
+{
+  IdeWorkbench *self = user_data;
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  ide_workbench_addin_load (addin, self);
+
+  /* Notify of the VCS system up-front */
+  if (self->vcs != NULL)
+    ide_workbench_addin_vcs_changed (addin, self->vcs);
+
+  /*
+   * If we already loaded a project, then give the plugin a
+   * chance to handle that, even if it is delayed a bit.
+   */
+
+  if (self->project_info != NULL)
+    ide_workbench_addin_load_project_async (addin, self->project_info, NULL, NULL, NULL);
+
+  ide_workbench_foreach_workspace (self,
+                                   (GtkCallback)ide_workbench_addin_added_workspace_cb,
+                                   addin);
+}
+
+static void
+ide_workbench_addin_removed_cb (PeasExtensionSet *set,
+                                PeasPluginInfo   *plugin_info,
+                                PeasExtension    *exten,
+                                gpointer          user_data)
+{
+  IdeWorkbench *self = user_data;
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  /* Notify of workspace removals so addins don't need to manually
+   * track them for cleanup.
+   */
+  ide_workbench_foreach_workspace (self,
+                                   (GtkCallback)ide_workbench_addin_removed_workspace_cb,
+                                   addin);
+
+  ide_workbench_addin_unload (addin, self);
+}
+
+static void
+ide_workbench_notify_context_title (IdeWorkbench *self,
+                                    GParamSpec   *pspec,
+                                    IdeContext   *context)
+{
+  g_autofree gchar *formatted = NULL;
+  g_autofree gchar *title = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  title = ide_context_dup_title (context);
+  formatted = g_strdup_printf (_("Builder — %s"), title);
+  ide_workbench_foreach_workspace (self,
+                                   (GtkCallback)gtk_window_set_title,
+                                   formatted);
+}
+
+static void
+ide_workbench_notify_context_workdir (IdeWorkbench *self,
+                                      GParamSpec   *pspec,
+                                      IdeContext   *context)
+{
+  g_autoptr(GFile) workdir = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  workdir = ide_context_ref_workdir (context);
+  ide_vcs_monitor_set_root (self->vcs_monitor, workdir);
+}
+
+static void
+ide_workbench_constructed (GObject *object)
+{
+  IdeWorkbench *self = (IdeWorkbench *)object;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  if (self->context == NULL)
+    self->context = ide_context_new ();
+
+  g_signal_connect_object (self->context,
+                           "notify::title",
+                           G_CALLBACK (ide_workbench_notify_context_title),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->context,
+                           "notify::workdir",
+                           G_CALLBACK (ide_workbench_notify_context_workdir),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  G_OBJECT_CLASS (ide_workbench_parent_class)->constructed (object);
+
+  self->vcs_monitor = g_object_new (IDE_TYPE_VCS_MONITOR, NULL);
+  ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (self->vcs_monitor));
+
+  self->addins = peas_extension_set_new (peas_engine_get_default (),
+                                         IDE_TYPE_WORKBENCH_ADDIN,
+                                         NULL);
+
+  g_signal_connect (self->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_workbench_addin_added_cb),
+                    self);
+
+  g_signal_connect (self->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_workbench_addin_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_workbench_addin_added_cb,
+                              self);
+}
+
+static void
+ide_workbench_finalize (GObject *object)
+{
+  IdeWorkbench *self = (IdeWorkbench *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_object (&self->build_system);
+  g_clear_object (&self->vcs);
+  g_clear_object (&self->search_engine);
+  g_clear_object (&self->session);
+  g_clear_object (&self->project_info);
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->context);
+
+  G_OBJECT_CLASS (ide_workbench_parent_class)->finalize (object);
+}
+
+static void
+ide_workbench_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  IdeWorkbench *self = IDE_WORKBENCH (object);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, ide_workbench_get_context (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_workbench_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeWorkbench *self = IDE_WORKBENCH (object);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      ide_workbench_set_context (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_workbench_class_init (IdeWorkbenchClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_workbench_constructed;
+  object_class->finalize = ide_workbench_finalize;
+  object_class->get_property = ide_workbench_get_property;
+  object_class->set_property = ide_workbench_set_property;
+
+  /**
+   * IdeWorkbench:context:
+   *
+   * The "context" property is the #IdeContext for the project.
+   *
+   * The #IdeContext is the root #IdeObject used in the tree of
+   * objects representing the project and the workings of the IDE.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The IdeContext for the workbench",
+                         IDE_TYPE_CONTEXT,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeWorkbench:vcs:
+   *
+   * The "vcs" property contains an #IdeVcs that represents the version control
+   * system that is currently loaded for the project.
+   *
+   * The #IdeVcs is registered by an #IdeWorkbenchAddin when loading a project.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_VCS] =
+    g_param_spec_object ("vcs",
+                         "Vcs",
+                         "The version control system, if any",
+                         IDE_TYPE_VCS,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_workbench_init (IdeWorkbench *self)
+{
+}
+
+static void
+collect_addins_cb (PeasExtensionSet *set,
+                   PeasPluginInfo   *plugin_info,
+                   PeasExtension    *exten,
+                   gpointer          user_data)
+{
+  GPtrArray *ar = user_data;
+  g_ptr_array_add (ar, g_object_ref (exten));
+}
+
+static GPtrArray *
+ide_workbench_collect_addins (IdeWorkbench *self)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  ar = g_ptr_array_new_with_free_func (g_object_unref);
+  if (self->addins != NULL)
+    peas_extension_set_foreach (self->addins, collect_addins_cb, ar);
+  return g_steal_pointer (&ar);
+}
+
+static IdeWorkbenchAddin *
+ide_workbench_find_addin (IdeWorkbench *self,
+                          const gchar  *hint)
+{
+  PeasEngine *engine;
+  PeasPluginInfo *plugin_info;
+  PeasExtension *exten = NULL;
+
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+  g_return_val_if_fail (hint != NULL, NULL);
+
+  engine = peas_engine_get_default ();
+
+  if ((plugin_info = peas_engine_get_plugin_info (engine, hint)))
+    exten = peas_extension_set_get_extension (self->addins, plugin_info);
+
+  return exten ? g_object_ref (IDE_WORKBENCH_ADDIN (exten)) : NULL;
+}
+
+/**
+ * ide_workbench_new:
+ *
+ * Creates a new #IdeWorkbench.
+ *
+ * This does not create any windows, you'll need to request that a workspace
+ * be created based on the kind of workspace you want to display to the user.
+ *
+ * Returns: an #IdeWorkbench
+ *
+ * Since: 3.32
+ */
+IdeWorkbench *
+ide_workbench_new (void)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+
+  return g_object_new (IDE_TYPE_WORKBENCH, NULL);
+}
+
+/**
+ * ide_workbench_new_for_context:
+ *
+ * Creates a new #IdeWorkbench using @context for the #IdeWorkbench:context.
+ *
+ * Returns: (transfer full): an #IdeWorkbench
+ *
+ * Since: 3.32
+ */
+IdeWorkbench *
+ide_workbench_new_for_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return g_object_new (IDE_TYPE_CONTEXT,
+                       "visible", TRUE,
+                       NULL);
+}
+
+/**
+ * ide_workbench_get_context:
+ * @self: an #IdeWorkbench
+ *
+ * Gets the #IdeContext for the workbench.
+ *
+ * Returns: (transfer none): an #IdeContext
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_workbench_get_context (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->context;
+}
+
+/**
+ * ide_workbench_from_widget:
+ * @widget: a #GtkWidget
+ *
+ * Finds the #IdeWorkbench associated with a widget.
+ *
+ * Returns: (nullable) (transfer none): an #IdeWorkbench or %NULL
+ *
+ * Since: 3.32
+ */
+IdeWorkbench *
+ide_workbench_from_widget (GtkWidget *widget)
+{
+  GtkWindowGroup *group;
+  GtkWidget *toplevel;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+  /*
+   * The workbench is a window group, and the workspaces belong to us. So we
+   * just need to get the toplevel window group property, and cast.
+   */
+
+  if ((toplevel = gtk_widget_get_toplevel (widget)) &&
+      GTK_IS_WINDOW (toplevel) &&
+      (group = gtk_window_get_group (GTK_WINDOW (toplevel))) &&
+      IDE_IS_WORKBENCH (group))
+    return IDE_WORKBENCH (group);
+
+  return NULL;
+}
+
+/**
+ * ide_workbench_foreach_workspace:
+ * @self: an #IdeWorkbench
+ * @callback: (scope call): a #GtkCallback to call for each #IdeWorkspace
+ * @user_data: user data for @callback
+ *
+ * Iterates the available workspaces in the workbench. Workspaces are iterated
+ * in most-recently-used order.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_foreach_workspace (IdeWorkbench *self,
+                                 GtkCallback   callback,
+                                 gpointer      user_data)
+{
+  GList *copy;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (callback != NULL);
+
+  /* Copy for re-entrancy safety */
+  copy = g_list_copy (self->mru_queue.head);
+
+  for (const GList *iter = copy; iter; iter = iter->next)
+    {
+      IdeWorkspace *workspace = iter->data;
+      g_assert (IDE_IS_WORKSPACE (workspace));
+      callback (iter->data, user_data);
+    }
+
+  g_list_free (copy);
+}
+
+/**
+ * ide_workbench_foreach_page:
+ * @self: a #IdeWorkbench
+ * @callback: (scope call): a callback to execute for each page
+ * @user_data: closure data for @callback
+ *
+ * Calls @callback for every page loaded in the workbench, by iterating
+ * workspaces in order of most-recently-used.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_foreach_page (IdeWorkbench *self,
+                            GtkCallback   callback,
+                            gpointer      user_data)
+{
+  GList *copy;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (callback != NULL);
+
+  /* Make a copy to be safe against auto-cleanup removals */
+  copy = g_list_copy (self->mru_queue.head);
+  for (const GList *iter = copy; iter; iter = iter->next)
+    {
+      IdeWorkspace *workspace = iter->data;
+      g_assert (IDE_IS_WORKSPACE (workspace));
+      ide_workspace_foreach_page (workspace, callback, user_data);
+    }
+  g_list_free (copy);
+}
+
+static void
+ide_workbench_workspace_has_toplevel_focus_cb (IdeWorkbench *self,
+                                               GParamSpec   *pspec,
+                                               IdeWorkspace *workspace)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (gtk_window_get_group (GTK_WINDOW (workspace)) == GTK_WINDOW_GROUP (self));
+
+  if (gtk_window_has_toplevel_focus (GTK_WINDOW (workspace)))
+    {
+      GList *mru_link = _ide_workspace_get_mru_link (workspace);
+
+      g_queue_unlink (&self->mru_queue, mru_link);
+
+      g_assert (mru_link->prev == NULL);
+      g_assert (mru_link->next == NULL);
+      g_assert (mru_link->data == (gpointer)workspace);
+
+      g_queue_push_head_link (&self->mru_queue, mru_link);
+    }
+}
+
+static void
+insert_action_groups_foreach_cb (IdeWorkspace *workspace,
+                                 gpointer      user_data)
+{
+  IdeWorkbench *self = user_data;
+  struct {
+    const gchar *name;
+    GType        child_type;
+  } groups[] = {
+    { "config-manager", IDE_TYPE_CONFIGURATION_MANAGER },
+    { "build-manager", IDE_TYPE_BUILD_MANAGER },
+    { "device-manager", IDE_TYPE_DEVICE_MANAGER },
+    { "run-manager", IDE_TYPE_RUN_MANAGER },
+    { "test-manager", IDE_TYPE_TEST_MANAGER },
+  };
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  for (guint i = 0; i < G_N_ELEMENTS (groups); i++)
+    {
+      IdeObject *child;
+
+      if ((child = ide_context_peek_child_typed (self->context, groups[i].child_type)))
+        gtk_widget_insert_action_group (GTK_WIDGET (workspace),
+                                        groups[i].name,
+                                        G_ACTION_GROUP (child));
+    }
+}
+
+/**
+ * ide_workbench_add_workspace:
+ * @self: an #IdeWorkbench
+ * @workspace: an #IdeWorkspace
+ *
+ * Adds @workspace to @workbench.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_add_workspace (IdeWorkbench *self,
+                             IdeWorkspace *workspace)
+{
+  g_autoptr(GPtrArray) addins = NULL;
+  g_autofree gchar *title = NULL;
+  g_autofree gchar *formatted = NULL;
+  GList *mru_link;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  /* Now add the window to the workspace (which takes no reference, as the
+   * window will take a reference back to us.
+   */
+  if (gtk_window_get_group (GTK_WINDOW (workspace)) != GTK_WINDOW_GROUP (self))
+    gtk_window_group_add_window (GTK_WINDOW_GROUP (self), GTK_WINDOW (workspace));
+
+  g_assert (gtk_window_has_group (GTK_WINDOW (workspace)));
+  g_assert (gtk_window_get_group (GTK_WINDOW (workspace)) == GTK_WINDOW_GROUP (self));
+
+  /* Now place the workspace into our MRU tracking */
+  mru_link = _ide_workspace_get_mru_link (workspace);
+
+  if (gtk_window_has_toplevel_focus (GTK_WINDOW (workspace)))
+    g_queue_push_head_link (&self->mru_queue, mru_link);
+  else
+    g_queue_push_tail_link (&self->mru_queue, mru_link);
+
+  /* Update the context for the workspace, even if we're not loaded,
+   * this IdeContext will be updated later.
+   */
+  _ide_workspace_set_context (workspace, self->context);
+
+  /* This causes the workspace to get an additional reference to the group
+   * (which already happens from GtkWindow:group), but IdeWorkspace will
+   * remove itself in IdeWorkspace.destroy.
+   */
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace),
+                                  "workbench",
+                                  G_ACTION_GROUP (self));
+
+  /* Give the workspace access to all the action groups of the context that
+   * might be useful for them to access (debug-manager, run-manager, etc).
+   */
+  if (self->project_info != NULL)
+    insert_action_groups_foreach_cb (workspace, self);
+
+  /* Track toplevel focus changes to maintain a most-recently-used queue. */
+  g_signal_connect_object (workspace,
+                           "notify::has-toplevel-focus",
+                           G_CALLBACK (ide_workbench_workspace_has_toplevel_focus_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* Notify all the addins about the new workspace. */
+  if ((addins = ide_workbench_collect_addins (self)))
+    {
+      for (guint i = 0; i < addins->len; i++)
+        {
+          IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
+          ide_workbench_addin_workspace_added (addin, workspace);
+        }
+    }
+
+  title = ide_context_dup_title (self->context);
+  formatted = g_strdup_printf (_("Builder — %s"), title);
+  gtk_window_set_title (GTK_WINDOW (workspace), formatted);
+}
+
+/**
+ * ide_workbench_remove_workspace:
+ * @self: an #IdeWorkbench
+ * @workspace: an #IdeWorkspace
+ *
+ * Removes @workspace from @workbench.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_remove_workspace (IdeWorkbench *self,
+                                IdeWorkspace *workspace)
+{
+  g_autoptr(GPtrArray) addins = NULL;
+  GList *list;
+  GList *mru_link;
+  guint count = 0;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  /* Stop tracking MRU changes */
+  mru_link = _ide_workspace_get_mru_link (workspace);
+  g_queue_unlink (&self->mru_queue, mru_link);
+  g_signal_handlers_disconnect_by_func (workspace,
+                                        G_CALLBACK (ide_workbench_workspace_has_toplevel_focus_cb),
+                                        self);
+
+  /* Notify all the addins about losing the workspace. */
+  if ((addins = ide_workbench_collect_addins (self)))
+    {
+      for (guint i = 0; i < addins->len; i++)
+        {
+          IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
+          ide_workbench_addin_workspace_removed (addin, workspace);
+        }
+    }
+
+  /* Clear our action group (which drops an additional back-reference) */
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "workbench", NULL);
+
+  /* Only cleanup the group if it hasn't already been removed */
+  if (gtk_window_has_group (GTK_WINDOW (workspace)))
+    gtk_window_group_remove_window (GTK_WINDOW_GROUP (self), GTK_WINDOW (workspace));
+
+  /*
+   * If this is our last workspace being closed, then we want to
+   * try to cleanup the workbench and shut things down.
+   */
+
+  list = gtk_window_group_list_windows (GTK_WINDOW_GROUP (self));
+  for (const GList *iter = list; iter; iter = iter->next)
+    {
+      GtkWindow *window = iter->data;
+
+      if (IDE_IS_WORKSPACE (window) && workspace != IDE_WORKSPACE (window))
+        count++;
+    }
+  g_list_free (list);
+
+  /*
+   * If there are no more workspaces left, then we will want to also
+   * unload the workbench opportunistically, so that the application
+   * can exit cleanly.
+   */
+  if (count == 0 && self->unloaded == FALSE)
+    ide_workbench_unload_async (self, NULL, NULL, NULL);
+}
+
+/**
+ * ide_workbench_focus_workspace:
+ * @self: an #IdeWorkbench
+ * @workspace: an #IdeWorkspace
+ *
+ * Requests that @workspace be raised in the windows of @self, and
+ * displayed to the user.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_focus_workspace (IdeWorkbench *self,
+                               IdeWorkspace *workspace)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  ide_gtk_window_present (GTK_WINDOW (workspace));
+}
+
+static void
+ide_workbench_project_loaded_foreach_cb (PeasExtensionSet *set,
+                                         PeasPluginInfo   *plugin_info,
+                                         PeasExtension    *exten,
+                                         gpointer          user_data)
+{
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
+  IdeWorkbench *self = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_PROJECT_INFO (self->project_info));
+
+  ide_workbench_addin_project_loaded (addin, self->project_info);
+}
+
+static void
+ide_workbench_session_restore_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  IdeSession *session = (IdeSession *)object;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SESSION (session));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (!ide_session_restore_finish (session, result, &error))
+    g_warning ("%s", error->message);
+}
+
+static void
+ide_workbench_load_project_completed (IdeWorkbench *self,
+                                      IdeTask      *task)
+{
+  LoadProject *lp;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_TASK (task));
+
+  lp = ide_task_get_task_data (task);
+
+  g_assert (lp != NULL);
+  g_assert (lp->addins != NULL);
+  g_assert (lp->addins->len == 0);
+
+  if (lp->workspace_type != G_TYPE_INVALID)
+    {
+      IdeWorkspace *workspace;
+
+      workspace = g_object_new (lp->workspace_type,
+                                "application", IDE_APPLICATION_DEFAULT,
+                                NULL);
+      ide_workbench_add_workspace (self, IDE_WORKSPACE (workspace));
+      gtk_window_present_with_time (GTK_WINDOW (workspace), lp->present_time);
+    }
+
+  /* Give workspaces access to the various GActionGroups */
+  ide_workbench_foreach_workspace (self,
+                                   (GtkCallback)insert_action_groups_foreach_cb,
+                                   self);
+
+  /* Notify addins that projects have loaded */
+  peas_extension_set_foreach (self->addins,
+                              ide_workbench_project_loaded_foreach_cb,
+                              self);
+
+  /* And now restore the user session, but don't block our task for
+   * it since the greeter is waiting on us.
+   */
+  ide_session_restore_async (self->session,
+                             self,
+                             ide_task_get_cancellable (task),
+                             ide_workbench_session_restore_cb,
+                             NULL);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_workbench_load_project_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeWorkbench *self;
+  LoadProject *lp;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  lp = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (lp != NULL);
+  g_assert (IDE_IS_PROJECT_INFO (lp->project_info));
+  g_assert (lp->addins != NULL);
+  g_assert (lp->addins->len > 0);
+
+  if (!ide_workbench_addin_load_project_finish (addin, result, &error))
+    {
+      if (!ignore_error (error))
+        g_warning ("%s addin failed to load project: %s",
+                   G_OBJECT_TYPE_NAME (addin), error->message);
+    }
+
+  g_ptr_array_remove (lp->addins, addin);
+
+  if (lp->addins->len == 0)
+    ide_workbench_load_project_completed (self, task);
+}
+
+static void
+ide_workbench_init_foundry_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeWorkbench *self;
+  GCancellable *cancellable;
+  LoadProject *lp;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!_ide_foundry_init_finish (result, &error))
+    g_critical ("Failed to initialize foundry: %s", error->message);
+
+  cancellable = ide_task_get_cancellable (task);
+  self = ide_task_get_source_object (task);
+  lp = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (lp != NULL);
+  g_assert (lp->addins != NULL);
+  g_assert (IDE_IS_PROJECT_INFO (lp->project_info));
+
+  /* Now, we need to notify all of the workbench addins that we're
+   * opening the project. Once they have all completed, we'll create the
+   * new workspace window and attach it. That saves us the work of
+   * rendering various frames of the during the intensive load process.
+   */
+
+
+  for (guint i = 0; i < lp->addins->len; i++)
+    {
+      IdeWorkbenchAddin *addin = g_ptr_array_index (lp->addins, i);
+
+      ide_workbench_addin_load_project_async (addin,
+                                              lp->project_info,
+                                              cancellable,
+                                              ide_workbench_load_project_cb,
+                                              g_object_ref (task));
+    }
+
+  if (lp->addins->len == 0)
+    ide_workbench_load_project_completed (self, task);
+}
+
+/**
+ * ide_workbench_load_project_async:
+ * @self: a #IdeWorkbench
+ * @project_info: an #IdeProjectInfo describing the project to open
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: (nullable): a #GAsyncReadyCallback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * Requests that a project be opened in the workbench.
+ *
+ * @project_info should contain enough information to discover and load the
+ * project. Depending on the various fields of the #IdeProjectInfo,
+ * different plugins may become active as part of loading the project.
+ *
+ * Note that this may only be called once for an #IdeWorkbench. If you need
+ * to open a second project, you need to create and register a second
+ * workbench first, and then open using that secondary workbench.
+ *
+ * @callback should call ide_workbench_load_project_finish() to obtain the
+ * result of the open request.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_load_project_async (IdeWorkbench        *self,
+                                  IdeProjectInfo      *project_info,
+                                  GType                workspace_type,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(GPtrArray) addins = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) parent = NULL;
+  g_autofree gchar *name = NULL;
+  const gchar *project_id;
+  LoadProject *lp;
+  GFile *directory;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (IDE_IS_PROJECT_INFO (project_info));
+  g_return_if_fail (workspace_type != IDE_TYPE_WORKSPACE);
+  g_return_if_fail (workspace_type == G_TYPE_INVALID ||
+                    g_type_is_a (workspace_type, IDE_TYPE_WORKSPACE));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (self->unloaded == FALSE);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_workbench_load_project_async);
+
+  if (self->project_info != NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "Cannot load project, a project is already loaded");
+      IDE_EXIT;
+    }
+
+  _ide_context_set_has_project (self->context);
+
+  g_set_object (&self->project_info, project_info);
+
+  /* Update context project-id based on project-info */
+  if ((project_id = ide_project_info_get_id (project_info)))
+    {
+      g_autofree gchar *generated = ide_create_project_id (project_id);
+      ide_context_set_project_id (self->context, generated);
+    }
+
+  /*
+   * Track the directory root based on project info. If we didn't get a
+   * directory set, then take the parent of the project file.
+   */
+
+  if ((directory = ide_project_info_get_directory (project_info)))
+    {
+      ide_context_set_workdir (self->context, directory);
+    }
+  else
+    {
+      GFile *file = ide_project_info_get_file (project_info);
+
+      if (g_file_query_file_type (file, G_FILE_COPY_NOFOLLOW_SYMLINKS, NULL) == G_FILE_TYPE_DIRECTORY)
+        {
+          ide_context_set_workdir (self->context, file);
+          directory = file;
+        }
+      else
+        {
+          ide_context_set_workdir (self->context, (parent = g_file_get_parent (file)));
+          directory = parent;
+        }
+
+      ide_project_info_set_directory (project_info, directory);
+    }
+
+  g_assert (G_IS_FILE (directory));
+
+  name = g_file_get_basename (directory);
+  ide_context_set_title (self->context, name);
+
+  /* If there has not been a project name set, make the default matching
+   * the directory name. A plugin may update the name with more information
+   * based on .doap files, etc.
+   */
+  if (!ide_project_info_get_name (project_info))
+    ide_project_info_set_name (project_info, name);
+
+  /* Setup some information we're going to need later on when loading the
+   * individual workbench addins (and then creating the workspace).
+   */
+  lp = g_slice_new0 (LoadProject);
+  lp->project_info = g_object_ref (project_info);
+  /* HACK: Workaround for lack of last event time */
+  lp->present_time = g_get_monotonic_time () / 1000L;
+  lp->addins = ide_workbench_collect_addins (self);
+  lp->workspace_type = workspace_type;
+  ide_task_set_task_data (task, lp, load_project_free);
+
+  /*
+   * Before we load any addins, we want to register the Foundry subsystems
+   * such as the device manager, diagnostics engine, configurations, etc.
+   * This makes sure that we have some basics setup before addins load.
+   */
+  _ide_foundry_init_async (self->context,
+                           cancellable,
+                           ide_workbench_init_foundry_cb,
+                           g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_workbench_load_project_finish:
+ * @self: a #IdeWorkbench
+ *
+ * Completes an asynchronous request to open a project using
+ * ide_workbench_load_project_async().
+ *
+ * Returns: %TRUE if the project was successfully opened; otherwise %FALSE
+ *   and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_workbench_load_project_finish (IdeWorkbench  *self,
+                                   GAsyncResult  *result,
+                                   GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+print_object_tree (IdeObject *object,
+                   gpointer   depthptr)
+{
+  gint depth = GPOINTER_TO_INT (depthptr);
+  g_autofree gchar *space = g_strnfill (depth * 2, ' ');
+  g_autofree gchar *info = ide_object_repr (object);
+
+  g_print ("%s%s\n", space, info);
+  ide_object_foreach (object,
+                      (GFunc)print_object_tree,
+                      GINT_TO_POINTER (depth + 1));
+}
+
+static void
+ide_workbench_action_object_tree (IdeWorkbench *self,
+                                  GVariant     *param)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  print_object_tree (IDE_OBJECT (self->context), NULL);
+}
+
+static void
+ide_workbench_action_dump_tasks (IdeWorkbench *self,
+                                 GVariant     *param)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+
+  _ide_dump_tasks ();
+}
+
+static void
+ide_workbench_action_inspector (IdeWorkbench *self,
+                                GVariant     *param)
+{
+  gtk_window_set_interactive_debugging (TRUE);
+}
+
+static void
+ide_workbench_action_close (IdeWorkbench *self,
+                            GVariant     *param)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (param == NULL);
+
+  if (self->unloaded == FALSE)
+    ide_workbench_unload_async (self, NULL, NULL, NULL);
+}
+
+static void
+ide_workbench_action_open (IdeWorkbench *self,
+                           GVariant     *param)
+{
+  GtkFileChooserNative *chooser;
+  IdeWorkspace *workspace;
+  gint ret;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (param == NULL);
+
+  workspace = ide_workbench_get_current_workspace (self);
+
+  chooser = gtk_file_chooser_native_new (_("Open File…"),
+                                         GTK_WINDOW (workspace),
+                                         GTK_FILE_CHOOSER_ACTION_OPEN,
+                                         _("Open"),
+                                         _("Cancel"));
+  gtk_native_dialog_set_modal (GTK_NATIVE_DIALOG (chooser), FALSE);
+  gtk_file_chooser_set_local_only (GTK_FILE_CHOOSER (chooser), FALSE);
+  gtk_file_chooser_set_select_multiple (GTK_FILE_CHOOSER (chooser), TRUE);
+
+  ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (chooser));
+
+  if (ret == GTK_RESPONSE_ACCEPT)
+    {
+      g_autoslist(GFile) files = gtk_file_chooser_get_files (GTK_FILE_CHOOSER (chooser));
+
+      for (const GSList *iter = files; iter; iter = iter->next)
+        {
+          GFile *file = iter->data;
+
+          g_assert (G_IS_FILE (file));
+
+          ide_workbench_open_async (self, file, NULL, 0, NULL, NULL, NULL);
+        }
+    }
+
+  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (chooser));
+}
+
+/**
+ * ide_workbench_get_search_engine:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the search engine for the workbench, if any.
+ *
+ * Returns: (transfer none): an #IdeSearchEngine
+ *
+ * Since: 3.32
+ */
+IdeSearchEngine *
+ide_workbench_get_search_engine (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+  g_return_val_if_fail (self->context != NULL, NULL);
+
+  if (self->search_engine == NULL)
+      self->search_engine = ide_object_ensure_child_typed (IDE_OBJECT (self->context),
+                                                           IDE_TYPE_SEARCH_ENGINE);
+
+  return self->search_engine;
+}
+
+/**
+ * ide_workbench_get_project_info:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the #IdeProjectInfo for the workbench, if a project has been or is
+ * currently, loading.
+ *
+ * Returns: (transfer none) (nullable): an #IdeProjectInfo or %NULL
+ *
+ * Since: 3.32
+ */
+IdeProjectInfo *
+ide_workbench_get_project_info (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->project_info;
+}
+
+static void
+ide_workbench_unload_project_completed (IdeWorkbench *self,
+                                        IdeTask      *task)
+{
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (IDE_IS_TASK (task));
+
+  g_clear_object (&self->addins);
+  ide_workbench_foreach_workspace (self, (GtkCallback)gtk_widget_destroy, NULL);
+
+  if (self->context != NULL)
+    {
+      ide_object_destroy (IDE_OBJECT (self->context));
+      g_clear_object (&self->context);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_workbench_unload_project_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeWorkbench *self;
+  GPtrArray *addins;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  addins = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (addins != NULL);
+  g_assert (addins->len > 0);
+
+  if (!ide_workbench_addin_unload_project_finish (addin, result, &error))
+    {
+      if (!ignore_error (error))
+        g_warning ("%s failed to unload project: %s",
+                   G_OBJECT_TYPE_NAME (addin), error->message);
+    }
+
+  g_ptr_array_remove (addins, addin);
+
+  if (addins->len == 0)
+    ide_workbench_unload_project_completed (self, task);
+}
+
+static void
+ide_workbench_session_save_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  IdeSession *session = (IdeSession *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeWorkbench *self;
+  GPtrArray *addins;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SESSION (session));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  /* Not much we can display to the user, as we're tearing widgets down */
+  if (!ide_session_save_finish (session, result, &error))
+    g_warning ("%s", error->message);
+
+  /* Now we can request that each of our addins unload the project. */
+
+  self = ide_task_get_source_object (task);
+  addins = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (addins != NULL);
+
+  if (addins->len == 0)
+    {
+      ide_workbench_unload_project_completed (self, task);
+      return;
+    }
+
+  for (guint i = 0; i < addins->len; i++)
+    {
+      IdeWorkbenchAddin *addin = g_ptr_array_index (addins, i);
+
+      ide_workbench_addin_unload_project_async (addin,
+                                                self->project_info,
+                                                ide_task_get_cancellable (task),
+                                                ide_workbench_unload_project_cb,
+                                                g_object_ref (task));
+    }
+}
+
+/**
+ * ide_workbench_unload_async:
+ * @self: an #IdeWorkbench
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously unloads the workbench.
+ *
+ * All #IdeWorkspace windows will be closed after calling this
+ * function.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_unload_async (IdeWorkbench        *self,
+                            GCancellable        *cancellable,
+                            GAsyncReadyCallback  callback,
+                            gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) addins = NULL;
+  GApplication *app;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_workbench_unload_async);
+
+  if (self->unloaded)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  self->unloaded = TRUE;
+
+  /* Keep the GApplication alive for the lifetime of the task */
+  app = g_application_get_default ();
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (g_application_release),
+                           app,
+                           G_CONNECT_SWAPPED);
+  g_application_hold (app);
+
+  /*
+   * Remove our workbench from the application, so that no new
+   * open-file requests can keep us alive while we're shutting
+   * down.
+   */
+
+  ide_application_remove_workbench (IDE_APPLICATION (app), self);
+
+  /* If we haven't loaded a project, then there is nothing to
+   * do right now, just let ide_workbench_addin_unload() be called
+   * when the workbench disposes.
+   */
+  if (self->project_info == NULL)
+    {
+      ide_workbench_unload_project_completed (self, task);
+      return;
+    }
+
+  addins = ide_workbench_collect_addins (self);
+  ide_task_set_task_data (task, g_ptr_array_ref (addins), g_ptr_array_unref);
+
+  /* First unload the session while we are stable */
+  ide_session_save_async (self->session,
+                          self,
+                          cancellable,
+                          ide_workbench_session_save_cb,
+                          g_steal_pointer (&task));
+
+}
+
+/**
+ * ide_workbench_unload_finish:
+ * @self: an #IdeWorkbench
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+
+ * Completes a request to unload the workbench.
+ *
+ * Returns: %TRUE if the workbench was unloaded successfully,
+ *   otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_workbench_unload_finish (IdeWorkbench *self,
+                             GAsyncResult *result,
+                             GError **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_workbench_open_all_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  IdeWorkbench *self = (IdeWorkbench *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  gint *n_active;
+
+  g_assert (IDE_IS_WORKBENCH (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_workbench_open_finish (self, result, &error))
+    g_message ("Failed to open file: %s", error->message);
+
+  n_active = ide_task_get_task_data (task);
+  g_assert (n_active != NULL);
+
+  (*n_active)--;
+
+  if (*n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+/**
+ * ide_workbench_open_all_async:
+ * @self: an #IdeWorkbench
+ * @files: (array length=n_files): an array of #GFile
+ * @n_files: number of #GFiles contained in @files
+ * @hint: (nullable): an optional hint about what addin to use
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Requests that the workbench open all of the #GFile denoted by @files.
+ *
+ * If @hint is provided, that will be used to determine what workbench
+ * addin to use when opening the file. The @hint name should match the
+ * module name of the plugin.
+ *
+ * Call ide_workbench_open_finish() from @callback to complete this
+ * operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_open_all_async (IdeWorkbench         *self,
+                              GFile               **files,
+                              guint                 n_files,
+                              const gchar          *hint,
+                              GCancellable         *cancellable,
+                              GAsyncReadyCallback   callback,
+                              gpointer              user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) ar = NULL;
+  gint *n_active;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_workbench_open_all_async);
+
+  if (n_files == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  ar = g_ptr_array_new_full (n_files, g_object_unref);
+  for (guint i = 0; i < n_files; i++)
+    g_ptr_array_add (ar, g_object_ref (files[i]));
+
+  n_active = g_new0 (gint, 1);
+  *n_active = ar->len;
+  ide_task_set_task_data (task, n_active, g_free);
+
+  for (guint i = 0; i < ar->len; i++)
+    {
+      GFile *file = g_ptr_array_index (ar, i);
+
+      ide_workbench_open_async (self,
+                                file,
+                                hint,
+                                IDE_BUFFER_OPEN_FLAGS_NONE,
+                                cancellable,
+                                ide_workbench_open_all_cb,
+                                g_object_ref (task));
+    }
+}
+
+/**
+ * ide_workbench_open_async:
+ * @self: an #IdeWorkbench
+ * @file: a #GFile
+ * @hint: (nullable): an optional hint about what addin to use
+ * @flags: optional flags when opening the file
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Requests that the workbench open @file.
+ *
+ * If @hint is provided, that will be used to determine what workbench
+ * addin to use when opening the file. The @hint name should match the
+ * module name of the plugin.
+ *
+ * @flags may be ignored by some backends.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_open_async (IdeWorkbench        *self,
+                          GFile               *file,
+                          const gchar         *hint,
+                          IdeBufferOpenFlags   flags,
+                          GCancellable        *cancellable,
+                          GAsyncReadyCallback  callback,
+                          gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_workbench_open_at_async (self,
+                               file,
+                               hint,
+                               -1,
+                               -1,
+                               flags,
+                               cancellable,
+                               callback,
+                               user_data);
+}
+
+static void
+ide_workbench_open_cb (GObject      *object,
+                       GAsyncResult *result,
+                       gpointer      user_data)
+{
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)object;
+  IdeWorkbenchAddin *next;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GCancellable *cancellable;
+  Open *o;
+
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  cancellable = ide_task_get_cancellable (task);
+  o = ide_task_get_task_data (task);
+
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (o != NULL);
+  g_assert (o->addins != NULL);
+  g_assert (o->addins->len > 0);
+
+  if (ide_workbench_addin_open_finish (addin, result, &error))
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  g_debug ("%s did not open the file, trying next.",
+           G_OBJECT_TYPE_NAME (addin));
+
+  g_ptr_array_remove (o->addins, addin);
+
+  /*
+   * We failed to open the file, try the next addin that is
+   * left which said it supported the content-type.
+   */
+
+  if (o->addins->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "Failed to locate addin supporting file");
+      return;
+    }
+
+  next = g_ptr_array_index (o->addins, 0);
+
+  ide_workbench_addin_open_at_async (next,
+                                     o->file,
+                                     o->content_type,
+                                     o->at_line,
+                                     o->at_line_offset,
+                                     o->flags,
+                                     cancellable,
+                                     ide_workbench_open_cb,
+                                     g_steal_pointer (&task));
+}
+
+static gint
+sort_by_priority (gconstpointer a,
+                  gconstpointer b,
+                  gpointer      user_data)
+{
+  IdeWorkbenchAddin *addin_a = *(IdeWorkbenchAddin **)a;
+  IdeWorkbenchAddin *addin_b = *(IdeWorkbenchAddin **)b;
+  Open *o = user_data;
+  gint prio_a = 0;
+  gint prio_b = 0;
+
+  if (!ide_workbench_addin_can_open (addin_a, o->file, o->content_type, &prio_a))
+    return 1;
+
+  if (!ide_workbench_addin_can_open (addin_b, o->file, o->content_type, &prio_b))
+    return -1;
+
+  return prio_a - prio_b;
+}
+
+static void
+ide_workbench_open_query_info_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GFileInfo) info = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeWorkbenchAddin *first;
+  GCancellable *cancellable;
+  Open *o;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  cancellable = ide_task_get_cancellable (task);
+  o = ide_task_get_task_data (task);
+
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (o != NULL);
+  g_assert (o->addins != NULL);
+  g_assert (o->addins->len > 0);
+
+  if ((info = g_file_query_info_finish (file, result, &error)))
+    o->content_type = g_strdup (g_file_info_get_content_type (info));
+
+  /* Remove unsupported addins while iterating backwards so that
+   * we can preserve the ordering of the array as we go.
+   */
+  for (guint i = o->addins->len; i > 0; i--)
+    {
+      IdeWorkbenchAddin *addin = g_ptr_array_index (o->addins, i - 1);
+      gint prio = G_MAXINT;
+
+      if (!ide_workbench_addin_can_open (addin, o->file, o->content_type, &prio))
+        {
+          g_ptr_array_remove_index_fast (o->addins, i - 1);
+          if (o->preferred == addin)
+            g_clear_object (&o->preferred);
+        }
+    }
+
+  if (o->addins->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "No addins can open the file");
+      return;
+    }
+
+  /*
+   * Now sort the addins by priority, so that we can attempt to load them
+   * in the preferred ordering.
+   */
+  g_ptr_array_sort_with_data (o->addins, sort_by_priority, o);
+
+  /*
+   * Ensure that we place the preferred at the head of the array, so
+   * that it gets preference over default priorities.
+   */
+  if (o->preferred != NULL)
+    {
+      g_ptr_array_insert (o->addins, 0, g_object_ref (o->preferred));
+
+      for (guint i = 1; i < o->addins->len; i++)
+        {
+          if (g_ptr_array_index (o->addins, i) == (gpointer)o->preferred)
+            {
+              g_ptr_array_remove_index (o->addins, i);
+              break;
+            }
+        }
+    }
+
+  /* Now start requesting that addins attempt to load the file. */
+
+  first = g_ptr_array_index (o->addins, 0);
+
+  ide_workbench_addin_open_at_async (first,
+                                     o->file,
+                                     o->content_type,
+                                     o->at_line,
+                                     o->at_line_offset,
+                                     o->flags,
+                                     cancellable,
+                                     ide_workbench_open_cb,
+                                     g_steal_pointer (&task));
+}
+
+/**
+ * ide_workbench_open_at_async:
+ * @self: an #IdeWorkbench
+ * @file: a #GFile
+ * @hint: (nullable): an optional hint about what addin to use
+ * @at_line: the line number to open at, or -1 to ignore
+ * @at_line_offset: the line offset to open at, or -1 to ignore
+ * @flags: optional #IdeBufferOpenFlags
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Like ide_workbench_open_async(), this allows opening a file
+ * within the workbench. However, it also allows specifying a
+ * line and column offset within the file to focus. Usually, this
+ * only makes sense for files that can be opened in an editor.
+ *
+ * @at_line and @at_line_offset may be < 0 to ignore the parameters.
+ *
+ * @flags may be ignored by some backends
+ *
+ * Use ide_workbench_open_finish() to receive teh result of this
+ * asynchronous operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_open_at_async (IdeWorkbench        *self,
+                             GFile               *file,
+                             const gchar         *hint,
+                             gint                 at_line,
+                             gint                 at_line_offset,
+                             IdeBufferOpenFlags   flags,
+                             GCancellable        *cancellable,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) addins = NULL;
+  Open *o;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (self->unloaded == FALSE);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* Canonicalize parameters */
+  if (at_line < 0)
+    at_line = -1;
+  if (at_line_offset < 0)
+    at_line_offset = -1;
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_workbench_open_at_async);
+
+  /*
+   * Make sure we might have an addin to load after discovering
+   * the files content-type.
+   */
+  if (!(addins = ide_workbench_collect_addins (self)) || addins->len == 0)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "No addins could open the file");
+      return;
+    }
+
+  o = g_slice_new0 (Open);
+  o->addins = g_ptr_array_ref (addins);
+  if (hint != NULL)
+    o->preferred = ide_workbench_find_addin (self, hint);
+  o->file = g_object_ref (file);
+  o->hint = g_strdup (hint);
+  o->flags = flags;
+  o->at_line = at_line;
+  o->at_line_offset = at_line_offset;
+  ide_task_set_task_data (task, o, open_free);
+
+  g_file_query_info_async (file,
+                           G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
+                           G_FILE_QUERY_INFO_NONE,
+                           G_PRIORITY_DEFAULT,
+                           cancellable,
+                           ide_workbench_open_query_info_cb,
+                           g_steal_pointer (&task));
+}
+
+/**
+ * ide_workbench_open_finish:
+ * @self: an #IdeWorkbench
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes a request to open a file using either
+ * ide_workbench_open_async() or ide_workbench_open_at_async().
+ *
+ * Returns: %TRUE if the file was successfully opened; otherwise
+ *   %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_workbench_open_finish (IdeWorkbench  *self,
+                           GAsyncResult  *result,
+                           GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_workbench_get_current_workspace:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the most recently focused workspace, which may be used to
+ * deliver events such as opening new pages.
+ *
+ * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL
+ *
+ * Since: 3.32
+ */
+IdeWorkspace *
+ide_workbench_get_current_workspace (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  if (self->mru_queue.length > 0)
+    return IDE_WORKSPACE (self->mru_queue.head->data);
+
+  return NULL;
+}
+
+/**
+ * ide_workbench_activate:
+ * @self: a #IdeWorkbench
+ *
+ * This function will attempt to raise the most recently focused workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_activate (IdeWorkbench *self)
+{
+  IdeWorkspace *workspace;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+
+  if ((workspace = ide_workbench_get_current_workspace (self)))
+    ide_workbench_focus_workspace (self, workspace);
+}
+
+static void
+ide_workbench_propagate_vcs_cb (PeasExtensionSet *set,
+                                PeasPluginInfo   *plugin_info,
+                                PeasExtension    *exten,
+                                gpointer          user_data)
+{
+  IdeWorkbenchAddin *addin = (IdeWorkbenchAddin *)exten;
+  IdeVcs *vcs = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (!vcs || IDE_IS_VCS (vcs));
+
+  ide_workbench_addin_vcs_changed (addin, vcs);
+}
+
+/**
+ * ide_workbench_get_vcs:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the #IdeVcs that has been loaded for the workbench, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeVcs or %NULL
+ *
+ * Since: 3.32
+ */
+IdeVcs *
+ide_workbench_get_vcs (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->vcs;
+}
+
+/**
+ * ide_workbench_get_vcs_monitor:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the #IdeVcsMonitor for the workbench, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeVcsMonitor or %NULL
+ *
+ * Since: 3.32
+ */
+IdeVcsMonitor *
+ide_workbench_get_vcs_monitor (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->vcs_monitor;
+}
+
+static void
+remove_non_matching_vcs_cb (IdeObject *child,
+                            IdeVcs    *vcs)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_OBJECT (child));
+  g_assert (IDE_IS_VCS (vcs));
+
+  if (IDE_IS_VCS (child) && IDE_VCS (child) != vcs)
+    ide_object_destroy (child);
+}
+
+/**
+ * ide_workbench_set_vcs:
+ * @self: a #IdeWorkbench
+ * @vcs: (nullable): an #IdeVcs
+ *
+ * Sets the #IdeVcs for the workbench.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_set_vcs (IdeWorkbench *self,
+                       IdeVcs       *vcs)
+{
+  g_autoptr(IdeVcs) local_vcs = NULL;
+  g_autoptr(GFile) local_workdir = NULL;
+  g_autoptr(GFile) workdir = NULL;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!vcs || IDE_IS_VCS (vcs));
+
+  if (vcs == self->vcs)
+    return;
+
+  if (vcs == NULL)
+    {
+      local_workdir = ide_context_ref_workdir (self->context);
+      vcs = local_vcs = IDE_VCS (ide_directory_vcs_new (local_workdir));
+    }
+
+  g_set_object (&self->vcs, vcs);
+  ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (vcs));
+  ide_object_foreach (IDE_OBJECT (self->context),
+                      (GFunc)remove_non_matching_vcs_cb,
+                      vcs);
+
+  if ((workdir = ide_vcs_get_workdir (vcs)))
+    ide_context_set_workdir (self->context, workdir);
+
+  ide_vcs_monitor_set_vcs (self->vcs_monitor, self->vcs);
+
+  peas_extension_set_foreach (self->addins,
+                              ide_workbench_propagate_vcs_cb,
+                              self->vcs);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VCS]);
+}
+
+/**
+ * ide_workbench_get_build_system:
+ * @self: a #IdeWorkbench
+ *
+ * Gets the #IdeBuildSystem for the workbench, if any.
+ *
+ * Returns: (transfer none) (nullable): an #IdeBuildSystem or %NULL
+ *
+ * Since: 3.32
+ */
+IdeBuildSystem *
+ide_workbench_get_build_system (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+
+  return self->build_system;
+}
+
+static void
+remove_non_matching_build_systems_cb (IdeObject      *child,
+                                      IdeBuildSystem *build_system)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_OBJECT (child));
+  g_assert (IDE_IS_BUILD_SYSTEM (build_system));
+
+  if (IDE_IS_BUILD_SYSTEM (child) && IDE_BUILD_SYSTEM (child) != build_system)
+    ide_object_destroy (child);
+}
+
+/**
+ * ide_workbench_set_build_system:
+ * @self: a #IdeWorkbench
+ * @build_system: (nullable): an #IdeBuildSystem or %NULL
+ *
+ * Sets the #IdeBuildSystem for the workbench.
+ *
+ * If @build_system is %NULL, then a fallback build system will be used
+ * instead. It does not provide building capabilities, but allows for some
+ * components that require a build system to continue functioning.
+ *
+ * Since: 3.32
+ */
+void
+ide_workbench_set_build_system (IdeWorkbench   *self,
+                                IdeBuildSystem *build_system)
+{
+  g_autoptr(IdeBuildSystem) local_build_system = NULL;
+  IdeBuildManager *build_manager;
+
+  g_return_if_fail (IDE_IS_WORKBENCH (self));
+  g_return_if_fail (!build_system || IDE_IS_BUILD_SYSTEM (build_system));
+
+  if (build_system == self->build_system)
+    return;
+
+  /* We want there to always be a build system available so that various
+   * plugins don't need lots of extra code to handle the %NULL case. So
+   * if @build_system is %NULL, then we'll create a fallback build system
+   * and assign that instead.
+   */
+
+  if (build_system == NULL)
+    build_system = local_build_system = ide_fallback_build_system_new ();
+
+  /* We want to add our new build system before removing the old build
+   * system to ensure there is always an #IdeBuildSystem child of the
+   * IdeContext.
+   */
+  g_set_object (&self->build_system, build_system);
+  ide_object_append (IDE_OBJECT (self->context), IDE_OBJECT (build_system));
+
+  /* Now remove any previous build-system from the context */
+  ide_object_foreach (IDE_OBJECT (self->context),
+                      (GFunc)remove_non_matching_build_systems_cb,
+                      build_system);
+
+  /* Ask the build-manager to setup a new pipeline */
+  if ((build_manager = ide_context_peek_child_typed (self->context, IDE_TYPE_BUILD_MANAGER)))
+    ide_build_manager_invalidate (build_manager);
+}
+
+/**
+ * ide_workbench_get_workspace_by_type:
+ * @self: a #IdeWorkbench
+ * @type: a #GType of a subclass of #IdeWorkspace
+ *
+ * Gets the most-recently-used workspace that matches @type.
+ *
+ * Returns: (transfer none) (nullable): an #IdeWorkspace or %NULL
+ *
+ * Since: 3.32
+ */
+IdeWorkspace *
+ide_workbench_get_workspace_by_type (IdeWorkbench *self,
+                                     GType         type)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), NULL);
+  g_return_val_if_fail (g_type_is_a (type, IDE_TYPE_WORKSPACE), NULL);
+
+  for (const GList *iter = self->mru_queue.head; iter; iter = iter->next)
+    {
+      if (G_TYPE_CHECK_INSTANCE_TYPE (iter->data, type))
+        return IDE_WORKSPACE (iter->data);
+    }
+
+  return NULL;
+}
+
+gboolean
+_ide_workbench_is_last_workspace (IdeWorkbench *self,
+                                  IdeWorkspace *workspace)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
+
+  return self->mru_queue.length == 1 &&
+         g_queue_peek_head (&self->mru_queue) == (gpointer)workspace;
+}
+
+/**
+ * ide_workbench_has_project:
+ * @self: a #IdeWorkbench
+ *
+ * Returns %TRUE if a project is loaded (or currently loading) in the
+ * workbench.
+ *
+ * Returns: %TRUE if the workbench has a project
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_workbench_has_project (IdeWorkbench *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (self), FALSE);
+
+  return self->project_info != NULL;
+}
+
+/**
+ * ide_workbench_addin_find_by_module_name:
+ * @workbench: an #IdeWorkbench
+ * @module_name: the name of the addin module
+ *
+ * Finds the addin (if any) matching the plugin's @module_name.
+ *
+ * Returns: (transfer none) (nullable): an #IdeWorkbenchAddin or %NULL
+ *
+ * Since: 3.32
+ */
+IdeWorkbenchAddin *
+ide_workbench_addin_find_by_module_name (IdeWorkbench *workbench,
+                                         const gchar  *module_name)
+{
+  PeasPluginInfo *plugin_info;
+  PeasExtension *ret = NULL;
+  PeasEngine *engine;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKBENCH (workbench), NULL);
+  g_return_val_if_fail (module_name != NULL, NULL);
+
+  if (workbench->addins == NULL)
+    return NULL;
+
+  engine = peas_engine_get_default ();
+
+  if ((plugin_info = peas_engine_get_plugin_info (engine, module_name)))
+    ret = peas_extension_set_get_extension (workbench->addins, plugin_info);
+
+  return IDE_WORKBENCH_ADDIN (ret);
+}
diff --git a/src/libide/gui/ide-workbench.h b/src/libide/gui/ide-workbench.h
new file mode 100644
index 000000000..b3a0ae7cd
--- /dev/null
+++ b/src/libide/gui/ide-workbench.h
@@ -0,0 +1,144 @@
+/* ide-workbench.h
+ *
+ * Copyright 2014-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <libide-foundry.h>
+#include <libide-projects.h>
+#include <libide-search.h>
+#include <libide-vcs.h>
+
+#include "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKBENCH (ide_workbench_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeWorkbench, ide_workbench, IDE, WORKBENCH, GtkWindowGroup)
+
+IDE_AVAILABLE_IN_3_32
+IdeWorkbench    *ide_workbench_new                   (void);
+IDE_AVAILABLE_IN_3_32
+IdeWorkbench    *ide_workbench_new_for_context       (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_activate              (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeProjectInfo  *ide_workbench_get_project_info      (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_workbench_has_project           (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext      *ide_workbench_get_context           (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeWorkspace    *ide_workbench_get_current_workspace (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeWorkspace    *ide_workbench_get_workspace_by_type (IdeWorkbench         *self,
+                                                      GType                 type);
+IDE_AVAILABLE_IN_3_32
+IdeSearchEngine *ide_workbench_get_search_engine     (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeWorkbench    *ide_workbench_from_widget           (GtkWidget            *widget);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_add_workspace         (IdeWorkbench         *self,
+                                                      IdeWorkspace         *workspace);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_remove_workspace      (IdeWorkbench         *self,
+                                                      IdeWorkspace         *workspace);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_focus_workspace       (IdeWorkbench         *self,
+                                                      IdeWorkspace         *workspace);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_foreach_workspace     (IdeWorkbench         *self,
+                                                      GtkCallback           callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_foreach_page          (IdeWorkbench         *self,
+                                                      GtkCallback           callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_load_project_async    (IdeWorkbench         *self,
+                                                      IdeProjectInfo       *project_info,
+                                                      GType                 workspace_type,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_workbench_load_project_finish   (IdeWorkbench         *self,
+                                                      GAsyncResult         *result,
+                                                      GError              **error);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_unload_async          (IdeWorkbench         *self,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_workbench_unload_finish         (IdeWorkbench         *self,
+                                                      GAsyncResult         *result,
+                                                      GError              **error);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_open_async            (IdeWorkbench         *self,
+                                                      GFile                *file,
+                                                      const gchar          *hint,
+                                                      IdeBufferOpenFlags    flags,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_open_at_async         (IdeWorkbench         *self,
+                                                      GFile                *file,
+                                                      const gchar          *hint,
+                                                      gint                  at_line,
+                                                      gint                  at_line_offset,
+                                                      IdeBufferOpenFlags    flags,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_open_all_async        (IdeWorkbench         *self,
+                                                      GFile               **files,
+                                                      guint                 n_files,
+                                                      const gchar          *hint,
+                                                      GCancellable         *cancellable,
+                                                      GAsyncReadyCallback   callback,
+                                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean         ide_workbench_open_finish           (IdeWorkbench         *self,
+                                                      GAsyncResult         *result,
+                                                      GError              **error);
+IDE_AVAILABLE_IN_3_32
+IdeVcs          *ide_workbench_get_vcs               (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_set_vcs               (IdeWorkbench         *self,
+                                                      IdeVcs               *vcs);
+IDE_AVAILABLE_IN_3_32
+IdeVcsMonitor   *ide_workbench_get_vcs_monitor       (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+IdeBuildSystem  *ide_workbench_get_build_system      (IdeWorkbench         *self);
+IDE_AVAILABLE_IN_3_32
+void             ide_workbench_set_build_system      (IdeWorkbench         *self,
+                                                      IdeBuildSystem       *build_system);
+
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-worker-manager.c b/src/libide/gui/ide-worker-manager.c
new file mode 100644
index 000000000..aef90a2dc
--- /dev/null
+++ b/src/libide/gui/ide-worker-manager.c
@@ -0,0 +1,299 @@
+/* ide-worker-manager.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-worker-manager"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <gio/gio.h>
+#include <gio/gunixsocketaddress.h>
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "ide-worker-process.h"
+#include "ide-worker-manager.h"
+
+struct _IdeWorkerManager
+{
+  GObject      parent_instance;
+
+  GDBusServer *dbus_server;
+  GHashTable  *plugin_name_to_worker;
+};
+
+G_DEFINE_TYPE (IdeWorkerManager, ide_worker_manager, G_TYPE_OBJECT)
+
+DZL_DEFINE_COUNTER (instances, "IdeWorkerManager", "Instances", "Number of IdeWorkerManager instances")
+
+static gboolean
+ide_worker_manager_new_connection_cb (IdeWorkerManager *self,
+                                      GDBusConnection  *connection,
+                                      GDBusServer      *server)
+{
+  GCredentials *credentials;
+  GHashTableIter iter;
+  gpointer key, value;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_WORKER_MANAGER (self));
+  g_assert (G_IS_DBUS_CONNECTION (connection));
+  g_assert (G_IS_DBUS_SERVER (server));
+
+  g_dbus_connection_set_exit_on_close (connection, FALSE);
+
+  credentials = g_dbus_connection_get_peer_credentials (connection);
+  if ((credentials == NULL) || (-1 == g_credentials_get_unix_pid (credentials, NULL)))
+    IDE_RETURN (FALSE);
+
+  g_hash_table_iter_init (&iter, self->plugin_name_to_worker);
+
+  while (g_hash_table_iter_next (&iter, &key, &value))
+    {
+      IdeWorkerProcess *process = value;
+
+      if (ide_worker_process_matches_credentials (process, credentials))
+        {
+          ide_worker_process_set_connection (process, connection);
+          IDE_RETURN (TRUE);
+        }
+    }
+
+  IDE_RETURN (FALSE);
+}
+
+static void
+ide_worker_manager_constructed (GObject *object)
+{
+  IdeWorkerManager *self = (IdeWorkerManager *)object;
+  g_autofree gchar *guid = NULL;
+  g_autofree gchar *address = NULL;
+  GError *error = NULL;
+
+  g_assert (IDE_IS_WORKER_MANAGER (self));
+
+  G_OBJECT_CLASS (ide_worker_manager_parent_class)->constructed (object);
+
+  if (g_unix_socket_address_abstract_names_supported ())
+    {
+      address = g_strdup_printf ("unix:abstract=/tmp/gnome-builder-%u", (int)getpid ());
+    }
+  else
+    {
+      g_autofree gchar *tmpdir = NULL;
+
+      tmpdir = g_dir_make_tmp ("gnome-builder-worker-XXXXXX", NULL);
+
+      if (tmpdir == NULL)
+        {
+          g_error ("Failed to determine temporary directory for DBus.");
+          exit (EXIT_FAILURE);
+        }
+
+      address = g_strdup_printf ("unix:tmpdir=%s", tmpdir);
+    }
+
+  guid = g_dbus_generate_guid ();
+
+  self->dbus_server = g_dbus_server_new_sync (address,
+                                              G_DBUS_SERVER_FLAGS_NONE,
+                                              guid,
+                                              NULL,
+                                              NULL,
+                                              &error);
+
+  if (error != NULL)
+    {
+      g_error ("%s", error->message);
+      exit (EXIT_FAILURE);
+    }
+
+  g_signal_connect_object (self->dbus_server,
+                           "new-connection",
+                           G_CALLBACK (ide_worker_manager_new_connection_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  IDE_TRACE_MSG ("GDBusServer listening at %s", address);
+
+  g_dbus_server_start (self->dbus_server);
+
+  g_assert (g_dbus_server_is_active (self->dbus_server));
+}
+
+static void
+ide_worker_manager_force_exit_worker (gpointer instance)
+{
+  IdeWorkerProcess *process = instance;
+
+  g_assert (IDE_IS_WORKER_PROCESS (process));
+
+  ide_worker_process_quit (process);
+  g_object_unref (process);
+}
+
+static void
+ide_worker_manager_finalize (GObject *object)
+{
+  IdeWorkerManager *self = (IdeWorkerManager *)object;
+
+  if (self->dbus_server != NULL)
+    g_dbus_server_stop (self->dbus_server);
+
+  g_clear_pointer (&self->plugin_name_to_worker, g_hash_table_unref);
+  g_clear_object (&self->dbus_server);
+
+  G_OBJECT_CLASS (ide_worker_manager_parent_class)->finalize (object);
+
+  DZL_COUNTER_DEC (instances);
+}
+
+static void
+ide_worker_manager_class_init (IdeWorkerManagerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = ide_worker_manager_constructed;
+  object_class->finalize = ide_worker_manager_finalize;
+}
+
+static void
+ide_worker_manager_init (IdeWorkerManager *self)
+{
+  DZL_COUNTER_INC (instances);
+
+  self->plugin_name_to_worker =
+    g_hash_table_new_full (g_str_hash,
+                           g_str_equal,
+                           g_free,
+                           ide_worker_manager_force_exit_worker);
+}
+
+static IdeWorkerProcess *
+ide_worker_manager_get_worker_process (IdeWorkerManager *self,
+                                       const gchar      *plugin_name)
+{
+  IdeWorkerProcess *worker_process;
+
+  g_assert (IDE_IS_WORKER_MANAGER (self));
+  g_assert (plugin_name != NULL);
+
+  if (!self->plugin_name_to_worker || !self->dbus_server)
+    return NULL;
+
+  worker_process = g_hash_table_lookup (self->plugin_name_to_worker, plugin_name);
+
+  if (worker_process == NULL)
+    {
+      g_autofree gchar *address = NULL;
+
+      address = g_strdup_printf ("%s,guid=%s",
+                                 g_dbus_server_get_client_address (self->dbus_server),
+                                 g_dbus_server_get_guid (self->dbus_server));
+
+      worker_process = ide_worker_process_new ("gnome-builder", plugin_name, address);
+      g_hash_table_insert (self->plugin_name_to_worker, g_strdup (plugin_name), worker_process);
+      ide_worker_process_run (worker_process);
+    }
+
+  return worker_process;
+}
+
+static void
+ide_worker_manager_get_worker_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  IdeWorkerProcess *worker_process = (IdeWorkerProcess *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GDBusProxy *proxy;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_WORKER_PROCESS (worker_process));
+  g_assert (IDE_IS_TASK (task));
+
+  proxy = ide_worker_process_get_proxy_finish (worker_process, result, &error);
+
+  if (proxy == NULL)
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task, proxy, g_object_unref);
+
+  IDE_EXIT;
+}
+
+void
+ide_worker_manager_get_worker_async (IdeWorkerManager    *self,
+                                     const gchar         *plugin_name,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  IdeWorkerProcess *worker_process;
+  IdeTask *task;
+
+  g_return_if_fail (IDE_IS_WORKER_MANAGER (self));
+  g_return_if_fail (plugin_name != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  worker_process = ide_worker_manager_get_worker_process (self, plugin_name);
+  ide_worker_process_get_proxy_async (worker_process,
+                                      cancellable,
+                                      ide_worker_manager_get_worker_cb,
+                                      task);
+}
+
+GDBusProxy *
+ide_worker_manager_get_worker_finish (IdeWorkerManager  *self,
+                                      GAsyncResult      *result,
+                                      GError           **error)
+{
+  IdeTask *task = (IdeTask *)result;
+
+  g_return_val_if_fail (IDE_IS_WORKER_MANAGER (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (task), NULL);
+
+  return ide_task_propagate_pointer (task, error);
+}
+
+IdeWorkerManager *
+ide_worker_manager_new (void)
+{
+  return g_object_new (IDE_TYPE_WORKER_MANAGER, NULL);
+}
+
+void
+ide_worker_manager_shutdown (IdeWorkerManager *self)
+{
+  g_return_if_fail (IDE_IS_WORKER_MANAGER (self));
+
+  if (self->dbus_server != NULL)
+    g_dbus_server_stop (self->dbus_server);
+
+  g_clear_pointer (&self->plugin_name_to_worker, g_hash_table_unref);
+  g_clear_object (&self->dbus_server);
+}
diff --git a/src/libide/gui/ide-worker-manager.h b/src/libide/gui/ide-worker-manager.h
new file mode 100644
index 000000000..89640d622
--- /dev/null
+++ b/src/libide/gui/ide-worker-manager.h
@@ -0,0 +1,42 @@
+/* ide-worker-manager.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 IDE_TYPE_WORKER_MANAGER (ide_worker_manager_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeWorkerManager, ide_worker_manager, IDE, WORKER_MANAGER, GObject)
+
+IdeWorkerManager *ide_worker_manager_new               (void);
+void              ide_worker_manager_shutdown          (IdeWorkerManager     *self);
+void              ide_worker_manager_get_worker_async  (IdeWorkerManager     *self,
+                                                        const gchar          *plugin_name,
+                                                        GCancellable         *cancellable,
+                                                        GAsyncReadyCallback   callback,
+                                                        gpointer              user_data);
+GDBusProxy       *ide_worker_manager_get_worker_finish (IdeWorkerManager     *self,
+                                                        GAsyncResult         *result,
+                                                        GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-worker-process.c b/src/libide/gui/ide-worker-process.c
new file mode 100644
index 000000000..01f56efdf
--- /dev/null
+++ b/src/libide/gui/ide-worker-process.c
@@ -0,0 +1,475 @@
+/* ide-worker-process.c
+ *
+ * Copyright 2015-2019 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 "ide-worker-process"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libpeas/peas.h>
+#include <libide-threading.h>
+
+#include "ide-worker-process.h"
+#include "ide-worker.h"
+
+struct _IdeWorkerProcess
+{
+  GObject          parent_instance;
+
+  gchar           *argv0;
+  gchar           *dbus_address;
+  gchar           *plugin_name;
+  GSubprocess     *subprocess;
+  GDBusConnection *connection;
+  GPtrArray       *tasks;
+  IdeWorker       *worker;
+
+  guint            quit : 1;
+};
+
+G_DEFINE_TYPE (IdeWorkerProcess, ide_worker_process, G_TYPE_OBJECT)
+
+DZL_DEFINE_COUNTER (instances, "IdeWorkerProcess", "Instances", "Number of IdeWorkerProcess instances")
+
+enum {
+  PROP_0,
+  PROP_ARGV0,
+  PROP_PLUGIN_NAME,
+  PROP_DBUS_ADDRESS,
+  LAST_PROP
+};
+
+static GParamSpec *gParamSpecs [LAST_PROP];
+
+static void ide_worker_process_respawn (IdeWorkerProcess *self);
+
+IdeWorkerProcess *
+ide_worker_process_new (const gchar *argv0,
+                        const gchar *plugin_name,
+                        const gchar *dbus_address)
+{
+  IdeWorkerProcess *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (argv0 != NULL, NULL);
+  g_return_val_if_fail (plugin_name != NULL, NULL);
+  g_return_val_if_fail (dbus_address != NULL, NULL);
+
+  ret = g_object_new (IDE_TYPE_WORKER_PROCESS,
+                      "argv0", argv0,
+                      "plugin-name", plugin_name,
+                      "dbus-address", dbus_address,
+                      NULL);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_worker_process_wait_check_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  GSubprocess *subprocess = (GSubprocess *)object;
+  g_autoptr(IdeWorkerProcess) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_SUBPROCESS (subprocess));
+  g_assert (IDE_IS_WORKER_PROCESS (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (!g_subprocess_wait_check_finish (subprocess, result, &error))
+    {
+      if (!self->quit)
+        g_warning ("%s", error->message);
+    }
+
+  g_clear_object (&self->subprocess);
+
+  if (!self->quit)
+    ide_worker_process_respawn (self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_worker_process_respawn (IdeWorkerProcess *self)
+{
+  g_autoptr(GSubprocessLauncher) launcher = NULL;
+  g_autoptr(GSubprocess) subprocess = NULL;
+  g_autofree gchar *plugin = NULL;
+  g_autofree gchar *dbus_address = NULL;
+  g_autoptr(GString) verbosearg = NULL;
+  GError *error = NULL;
+  GPtrArray *args;
+  gint verbosity;
+  gint i;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_WORKER_PROCESS (self));
+  g_assert (self->subprocess == NULL);
+
+  plugin = g_strdup_printf ("--plugin=%s", self->plugin_name);
+  dbus_address = g_strdup_printf ("--dbus-address=%s", self->dbus_address);
+
+  verbosearg = g_string_new ("-");
+  verbosity = ide_log_get_verbosity ();
+  for (i = 0; i < verbosity; i++)
+    g_string_append_c (verbosearg, 'v');
+
+  launcher = g_subprocess_launcher_new (G_SUBPROCESS_FLAGS_NONE);
+  args = g_ptr_array_new ();
+  g_ptr_array_add (args, self->argv0); /* gnome-builder */
+  g_ptr_array_add (args,(gchar *) "--type=worker");
+  g_ptr_array_add (args, plugin); /* --plugin= */
+  g_ptr_array_add (args, dbus_address); /* --dbus-address= */
+  g_ptr_array_add (args, verbosity > 0 ? verbosearg->str : NULL);
+  g_ptr_array_add (args, NULL);
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    g_autofree gchar *str = NULL;
+    str = g_strjoinv (" ", (gchar **)args->pdata);
+    IDE_TRACE_MSG ("Launching '%s'", str);
+  }
+#endif
+
+  subprocess = g_subprocess_launcher_spawnv (launcher,
+                                             (const gchar * const *)args->pdata,
+                                             &error);
+
+  g_ptr_array_free (args, TRUE);
+
+  if (subprocess == NULL)
+    {
+      g_warning ("Failed to spawn %s", error->message);
+      g_clear_error (&error);
+      IDE_EXIT;
+    }
+
+  self->subprocess = g_object_ref (subprocess);
+
+  g_subprocess_wait_check_async (subprocess,
+                                 NULL,
+                                 ide_worker_process_wait_check_cb,
+                                 g_object_ref (self));
+
+  if (self->worker == NULL)
+    {
+      PeasEngine *engine;
+      PeasExtension *exten;
+      PeasPluginInfo *plugin_info;
+
+      engine = peas_engine_get_default ();
+      plugin_info = peas_engine_get_plugin_info (engine, self->plugin_name);
+
+      if (plugin_info != NULL)
+        {
+          exten = peas_engine_create_extension (engine, plugin_info, IDE_TYPE_WORKER, NULL);
+          self->worker = IDE_WORKER (exten);
+        }
+    }
+
+  IDE_EXIT;
+}
+
+void
+ide_worker_process_run (IdeWorkerProcess *self)
+{
+  g_return_if_fail (IDE_IS_WORKER_PROCESS (self));
+  g_return_if_fail (self->subprocess == NULL);
+
+  ide_worker_process_respawn (self);
+}
+
+void
+ide_worker_process_quit (IdeWorkerProcess *self)
+{
+  g_return_if_fail (IDE_IS_WORKER_PROCESS (self));
+
+  self->quit = TRUE;
+
+  if (self->subprocess != NULL)
+    {
+      g_autoptr(GSubprocess) subprocess = g_steal_pointer (&self->subprocess);
+
+      g_subprocess_force_exit (subprocess);
+    }
+}
+
+static void
+ide_worker_process_dispose (GObject *object)
+{
+  IdeWorkerProcess *self = (IdeWorkerProcess *)object;
+
+  if (self->subprocess != NULL)
+    ide_worker_process_quit (self);
+
+  G_OBJECT_CLASS (ide_worker_process_parent_class)->dispose (object);
+}
+
+static void
+ide_worker_process_finalize (GObject *object)
+{
+  IdeWorkerProcess *self = (IdeWorkerProcess *)object;
+
+  g_clear_pointer (&self->argv0, g_free);
+  g_clear_pointer (&self->plugin_name, g_free);
+  g_clear_pointer (&self->dbus_address, g_free);
+  g_clear_pointer (&self->tasks, g_ptr_array_unref);
+  g_clear_object (&self->connection);
+  g_clear_object (&self->subprocess);
+  g_clear_object (&self->worker);
+
+  G_OBJECT_CLASS (ide_worker_process_parent_class)->finalize (object);
+
+  DZL_COUNTER_DEC (instances);
+}
+
+static void
+ide_worker_process_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdeWorkerProcess *self = IDE_WORKER_PROCESS (object);
+
+  switch (prop_id)
+    {
+    case PROP_ARGV0:
+      g_value_set_string (value, self->argv0);
+      break;
+
+    case PROP_PLUGIN_NAME:
+      g_value_set_string (value, self->plugin_name);
+      break;
+
+    case PROP_DBUS_ADDRESS:
+      g_value_set_string (value, self->dbus_address);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_worker_process_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdeWorkerProcess *self = IDE_WORKER_PROCESS (object);
+
+  switch (prop_id)
+    {
+    case PROP_ARGV0:
+      self->argv0 = g_value_dup_string (value);
+      break;
+
+    case PROP_PLUGIN_NAME:
+      self->plugin_name = g_value_dup_string (value);
+      break;
+
+    case PROP_DBUS_ADDRESS:
+      self->dbus_address = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_worker_process_class_init (IdeWorkerProcessClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_worker_process_dispose;
+  object_class->finalize = ide_worker_process_finalize;
+  object_class->get_property = ide_worker_process_get_property;
+  object_class->set_property = ide_worker_process_set_property;
+
+  gParamSpecs [PROP_ARGV0] =
+    g_param_spec_string ("argv0",
+                         "Argv0",
+                         "Argv0",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_PLUGIN_NAME] =
+    g_param_spec_string ("plugin-name",
+                         "plugin-name",
+                         "plugin-name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  gParamSpecs [PROP_DBUS_ADDRESS] =
+    g_param_spec_string ("dbus-address",
+                         "dbus-address",
+                         "dbus-address",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, gParamSpecs);
+}
+
+static void
+ide_worker_process_init (IdeWorkerProcess *self)
+{
+  DZL_COUNTER_INC (instances);
+}
+
+gboolean
+ide_worker_process_matches_credentials (IdeWorkerProcess *self,
+                                        GCredentials     *credentials)
+{
+  g_autofree gchar *str = NULL;
+  const gchar *identifier;
+  pid_t pid;
+
+  g_return_val_if_fail (IDE_IS_WORKER_PROCESS (self), FALSE);
+  g_return_val_if_fail (G_IS_CREDENTIALS (credentials), FALSE);
+
+  if ((self->subprocess != NULL) &&
+      (identifier = g_subprocess_get_identifier (self->subprocess)) &&
+      (pid = g_credentials_get_unix_pid (credentials, NULL)) != -1)
+    {
+      str = g_strdup_printf ("%d", (int)pid);
+      if (g_strcmp0 (identifier, str) == 0)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_worker_process_create_proxy_for_task (IdeWorkerProcess *self,
+                                          IdeTask          *task)
+{
+  GDBusProxy *proxy;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_WORKER_PROCESS (self));
+  g_assert (IDE_IS_TASK (task));
+
+  if (self->worker == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_PROXY_FAILED,
+                                 "Failed to create IdeWorker instance.");
+      IDE_EXIT;
+    }
+
+  proxy = ide_worker_create_proxy (self->worker, self->connection, &error);
+
+  if (proxy == NULL)
+    {
+      if (error == NULL)
+        error = g_error_new_literal (G_IO_ERROR,
+                                     G_IO_ERROR_PROXY_FAILED,
+                                     "IdeWorker returned NULL and did not set an error.");
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  ide_task_return_pointer (task, proxy, g_object_unref);
+
+  IDE_EXIT;
+}
+
+void
+ide_worker_process_set_connection (IdeWorkerProcess *self,
+                                   GDBusConnection  *connection)
+{
+  g_return_if_fail (IDE_IS_WORKER_PROCESS (self));
+  g_return_if_fail (G_IS_DBUS_CONNECTION (connection));
+
+  if (g_set_object (&self->connection, connection))
+    {
+      if (self->tasks != NULL)
+        {
+          g_autoptr(GPtrArray) ar = NULL;
+          guint i;
+
+          ar = self->tasks;
+          self->tasks = NULL;
+
+          for (i = 0; i < ar->len; i++)
+            {
+              IdeTask *task = g_ptr_array_index (ar, i);
+              ide_worker_process_create_proxy_for_task (self, task);
+            }
+        }
+    }
+}
+
+void
+ide_worker_process_get_proxy_async (IdeWorkerProcess    *self,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_WORKER_PROCESS (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+
+  if (self->connection != NULL)
+    {
+      ide_worker_process_create_proxy_for_task (self, task);
+      IDE_EXIT;
+    }
+
+  if (self->tasks == NULL)
+    self->tasks = g_ptr_array_new_with_free_func (g_object_unref);
+
+  g_ptr_array_add (self->tasks, g_object_ref (task));
+
+  IDE_EXIT;
+}
+
+GDBusProxy *
+ide_worker_process_get_proxy_finish (IdeWorkerProcess  *self,
+                                     GAsyncResult      *result,
+                                     GError           **error)
+{
+  IdeTask *task = (IdeTask *)result;
+  GDBusProxy *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_WORKER_PROCESS (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (task), NULL);
+
+  ret = ide_task_propagate_pointer (task, error);
+
+  IDE_RETURN (ret);
+}
diff --git a/src/libide/gui/ide-worker-process.h b/src/libide/gui/ide-worker-process.h
new file mode 100644
index 000000000..ec79be523
--- /dev/null
+++ b/src/libide/gui/ide-worker-process.h
@@ -0,0 +1,50 @@
+/* ide-worker-process.h
+ *
+ * Copyright 2015-2019 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 IDE_TYPE_WORKER_PROCESS (ide_worker_process_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeWorkerProcess, ide_worker_process, IDE, WORKER_PROCESS, GObject)
+
+IdeWorkerProcess *ide_worker_process_new                 (const gchar          *argv0,
+                                                          const gchar          *type,
+                                                          const gchar          *dbus_address);
+void              ide_worker_process_run                 (IdeWorkerProcess     *self);
+void              ide_worker_process_quit                (IdeWorkerProcess     *self);
+gpointer          ide_worker_process_create_proxy        (IdeWorkerProcess     *self,
+                                                          GError              **error);
+gboolean          ide_worker_process_matches_credentials (IdeWorkerProcess     *self,
+                                                          GCredentials         *credentials);
+void              ide_worker_process_set_connection      (IdeWorkerProcess     *self,
+                                                          GDBusConnection      *connection);
+void              ide_worker_process_get_proxy_async     (IdeWorkerProcess     *self,
+                                                          GCancellable         *cancellable,
+                                                          GAsyncReadyCallback   callback,
+                                                          gpointer              user_data);
+GDBusProxy       *ide_worker_process_get_proxy_finish    (IdeWorkerProcess     *self,
+                                                          GAsyncResult         *result,
+                                                          GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-worker.c b/src/libide/gui/ide-worker.c
new file mode 100644
index 000000000..a01b64787
--- /dev/null
+++ b/src/libide/gui/ide-worker.c
@@ -0,0 +1,68 @@
+/* ide-worker.c
+ *
+ * Copyright 2015-2019 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 "ide-worker"
+
+#include "config.h"
+
+#include <libide-core.h>
+
+#include "ide-worker.h"
+
+G_DEFINE_INTERFACE (IdeWorker, ide_worker, G_TYPE_OBJECT)
+
+static void
+ide_worker_default_init (IdeWorkerInterface *iface)
+{
+}
+
+void
+ide_worker_register_service (IdeWorker       *self,
+                             GDBusConnection *connection)
+{
+  g_return_if_fail (IDE_IS_WORKER (self));
+  g_return_if_fail (G_IS_DBUS_CONNECTION (connection));
+
+  IDE_WORKER_GET_IFACE (self)->register_service (self, connection);
+}
+
+/**
+ * ide_worker_create_proxy:
+ * @self: An #IdeWorker.
+ * @connection: a #GDBusConnection connected to the worker process.
+ * @error: (allow-none): a location for a #GError, or %NULL.
+ *
+ * Creates a new proxy to be connected to the subprocess peer on the other
+ * end of @connection.
+ *
+ * Returns: (transfer full): a #GDBusProxy or %NULL.
+ *
+ * Since: 3.32
+ */
+GDBusProxy *
+ide_worker_create_proxy (IdeWorker        *self,
+                         GDBusConnection  *connection,
+                         GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_WORKER (self), NULL);
+  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
+
+  return IDE_WORKER_GET_IFACE (self)->create_proxy (self, connection, error);
+}
diff --git a/src/libide/gui/ide-worker.h b/src/libide/gui/ide-worker.h
new file mode 100644
index 000000000..0de20edd9
--- /dev/null
+++ b/src/libide/gui/ide-worker.h
@@ -0,0 +1,51 @@
+/* ide-worker.h
+ *
+ * Copyright 2015-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKER (ide_worker_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeWorker, ide_worker, IDE, WORKER, GObject)
+
+struct _IdeWorkerInterface
+{
+  GTypeInterface parent;
+
+  GDBusProxy *(*create_proxy)     (IdeWorker        *self,
+                                   GDBusConnection  *connection,
+                                   GError          **error);
+  void        (*register_service) (IdeWorker        *self,
+                                   GDBusConnection  *connection);
+};
+
+IDE_AVAILABLE_IN_3_32
+GDBusProxy *ide_worker_create_proxy     (IdeWorker        *self,
+                                         GDBusConnection  *connection,
+                                         GError          **error);
+IDE_AVAILABLE_IN_3_32
+void        ide_worker_register_service (IdeWorker        *self,
+                                         GDBusConnection  *connection);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-workspace-actions.c b/src/libide/gui/ide-workspace-actions.c
new file mode 100644
index 000000000..1257cd6ad
--- /dev/null
+++ b/src/libide/gui/ide-workspace-actions.c
@@ -0,0 +1,92 @@
+/* ide-workspace-actions.c
+ *
+ * Copyright 2018-2019 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 "ide-workspace-actions"
+
+#include "config.h"
+
+#include "ide-gui-private.h"
+
+static void
+ide_workspace_actions_close (GSimpleAction *action,
+                             GVariant      *param,
+                             gpointer       user_data)
+{
+  IdeWorkspace *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  gtk_window_close (GTK_WINDOW (self));
+}
+
+static void
+ide_workspace_actions_show_menu (GSimpleAction *action,
+                                 GVariant      *param,
+                                 gpointer       user_data)
+{
+  IdeWorkspace *self = user_data;
+  GtkWidget *titlebar;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  titlebar = gtk_window_get_titlebar (GTK_WINDOW (self));
+  if (GTK_IS_STACK (titlebar))
+    titlebar = gtk_stack_get_visible_child (GTK_STACK (titlebar));
+
+  if (IDE_IS_HEADER_BAR (titlebar))
+    _ide_header_bar_show_menu (IDE_HEADER_BAR (titlebar));
+}
+
+static void
+ide_workspace_actions_surface (GSimpleAction *action,
+                               GVariant      *param,
+                               gpointer       user_data)
+{
+  IdeWorkspace *self = user_data;
+  const gchar *surface;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  surface = g_variant_get_string (param, NULL);
+
+  ide_workspace_set_visible_surface_name (self, surface);
+}
+
+static const GActionEntry actions[] = {
+  { "show-menu", ide_workspace_actions_show_menu },
+  { "surface", ide_workspace_actions_surface, "s" },
+  { "close", ide_workspace_actions_close },
+};
+
+void
+_ide_workspace_init_actions (IdeWorkspace *self)
+{
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (self),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+}
diff --git a/src/libide/gui/ide-workspace-addin.c b/src/libide/gui/ide-workspace-addin.c
new file mode 100644
index 000000000..c44fee4ca
--- /dev/null
+++ b/src/libide/gui/ide-workspace-addin.c
@@ -0,0 +1,118 @@
+/* ide-workspace-addin.c
+ *
+ * Copyright 2018-2019 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 "ide-workspace-addin"
+
+#include "config.h"
+
+#include "ide-workspace.h"
+#include "ide-workspace-addin.h"
+
+/**
+ * SECTION:ide-workspace-addin
+ * @title: IdeWorkspaceAddin
+ * @short_description: Extend the #IdeWorkspace windows
+ *
+ * The #IdeWorkspaceAddin is created with each #IdeWorkspace, allowing
+ * plugins a chance to modify each window that is created.
+ *
+ * If you set `X-Workspace-Kind=primary` in your `.plugin` file, your
+ * addin will only be loaded in the primary workspace. You may specify
+ * multiple workspace kinds such as `primary` or `secondary` separated
+ * by a comma such as `primary,secondary;`.
+ *
+ * Since: 3.32
+ */
+
+G_DEFINE_INTERFACE (IdeWorkspaceAddin, ide_workspace_addin, G_TYPE_OBJECT)
+
+static void
+ide_workspace_addin_default_init (IdeWorkspaceAddinInterface *iface)
+{
+}
+
+/**
+ * ide_workspace_addin_load:
+ * @self: a #IdeWorkspaceAddin
+ *
+ * Lods the #IdeWorkspaceAddin.
+ *
+ * This is a good place to modify the workspace from your addin.
+ * Remember to unmodify the workspace in ide_workspace_addin_unload().
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_addin_load (IdeWorkspaceAddin *self,
+                          IdeWorkspace      *workspace)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKSPACE_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  if (IDE_WORKSPACE_ADDIN_GET_IFACE (self)->load)
+    IDE_WORKSPACE_ADDIN_GET_IFACE (self)->load (self, workspace);
+}
+
+/**
+ * ide_workspace_addin_unload:
+ * @self: a #IdeWorkspaceAddin
+ *
+ * Unloads the #IdeWorkspaceAddin.
+ *
+ * This is a good place to unmodify the workspace from anything you
+ * did in ide_workspace_addin_load().
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_addin_unload (IdeWorkspaceAddin *self,
+                            IdeWorkspace      *workspace)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKSPACE_ADDIN (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+
+  if (IDE_WORKSPACE_ADDIN_GET_IFACE (self)->unload)
+    IDE_WORKSPACE_ADDIN_GET_IFACE (self)->unload (self, workspace);
+}
+
+/**
+ * ide_workspace_addin_surface_set:
+ * @self: an #IdeWorkspaceAddin
+ * @surface: (nullable): an #IdeSurface or %NULL
+ *
+ * This function is called to notify the addin of the current surface.
+ * It may be set to %NULL before unloading the addin to allow addins
+ * to do surface change state handling and cleanup in one function.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_addin_surface_set (IdeWorkspaceAddin *self,
+                                 IdeSurface        *surface)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_WORKSPACE_ADDIN (self));
+  g_return_if_fail (!surface || IDE_IS_SURFACE (surface));
+
+  if (IDE_WORKSPACE_ADDIN_GET_IFACE (self)->surface_set)
+    IDE_WORKSPACE_ADDIN_GET_IFACE (self)->surface_set (self, surface);
+}
diff --git a/src/libide/gui/ide-workspace-addin.h b/src/libide/gui/ide-workspace-addin.h
new file mode 100644
index 000000000..8bf602c88
--- /dev/null
+++ b/src/libide/gui/ide-workspace-addin.h
@@ -0,0 +1,54 @@
+/* ide-workspace-addin.h
+ *
+ * Copyright 2018-2019 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 "ide-workspace.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKSPACE_ADDIN (ide_workspace_addin_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeWorkspaceAddin, ide_workspace_addin, IDE, WORKSPACE_ADDIN, GObject)
+
+struct _IdeWorkspaceAddinInterface
+{
+  GTypeInterface parent_iface;
+
+  void (*load)        (IdeWorkspaceAddin *self,
+                       IdeWorkspace      *workspace);
+  void (*unload)      (IdeWorkspaceAddin *self,
+                       IdeWorkspace      *workspace);
+  void (*surface_set) (IdeWorkspaceAddin *self,
+                       IdeSurface        *surface);
+};
+
+IDE_AVAILABLE_IN_3_32
+void ide_workspace_addin_load        (IdeWorkspaceAddin *self,
+                                      IdeWorkspace      *workspace);
+IDE_AVAILABLE_IN_3_32
+void ide_workspace_addin_unload      (IdeWorkspaceAddin *self,
+                                      IdeWorkspace      *workspace);
+IDE_AVAILABLE_IN_3_32
+void ide_workspace_addin_surface_set (IdeWorkspaceAddin *self,
+                                      IdeSurface        *surface);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-workspace.c b/src/libide/gui/ide-workspace.c
new file mode 100644
index 000000000..7db4e9a03
--- /dev/null
+++ b/src/libide/gui/ide-workspace.c
@@ -0,0 +1,971 @@
+/* ide-workspace.c
+ *
+ * Copyright 2014-2019 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 "ide-workspace"
+
+#include "config.h"
+
+#include <libide-plugins.h>
+
+#include "ide-gui-global.h"
+#include "ide-gui-private.h"
+#include "ide-workspace.h"
+#include "ide-workspace-addin.h"
+
+#define MUX_ACTIONS_KEY "IDE_WORKSPACE_MUX_ACTIONS"
+
+typedef struct
+{
+  /* Used as a link in IdeWorkbench's GQueue to track the most-recently-used
+   * workspaces based on recent focus.
+   */
+  GList mru_link;
+
+  /* This cancellable auto-cancels when the window is destroyed using
+   * ::delete-event() so that async operations can be made to auto-cancel.
+   */
+  GCancellable *cancellable;
+
+  /* The context for our workbench. It may not have a project loaded until
+   * the ide_workbench_load_project_async() workflow has been called, but it
+   * is usable without a project (albeit restricted).
+   */
+  IdeContext *context;
+
+  /* Our addins for the workspace window, that are limited by the "kind" of
+   * workspace that is loaded. Plugin files can specify X-Workspace-Kind to
+   * limit the plugin to specific type(s) of workspace.
+   */
+  IdeExtensionSetAdapter *addins;
+
+  /* We use an overlay as our top-most child so that plugins can potentially
+   * render any widget a layer above the UI.
+   */
+  GtkOverlay *overlay;
+
+  /* All workspaces are comprised of a series of "surfaces". However there may
+   * only ever be a single surface in a workspace (such as the editor workspace
+   * which is dedicated for editing).
+   */
+  GtkStack *surfaces;
+
+  /* The event box ensures that we can have events that will be used by the
+   * fullscreen overlay so that it gets delivery of crossing events.
+   */
+  GtkEventBox *event_box;
+
+  /* A MRU that is updated as pages are focused. It allows us to move through
+   * the pages in the order they've been most-recently focused.
+   */
+  GQueue page_mru;
+} IdeWorkspacePrivate;
+
+typedef struct
+{
+  GtkCallback callback;
+  gpointer    user_data;
+} ForeachPage;
+
+enum {
+  SURFACE_SET,
+  N_SIGNALS
+};
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_VISIBLE_SURFACE,
+  N_PROPS
+};
+
+static void buildable_iface_init (GtkBuildableIface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdeWorkspace, ide_workspace, DZL_TYPE_APPLICATION_WINDOW,
+                                  G_ADD_PRIVATE (IdeWorkspace)
+                                  G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, buildable_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_workspace_addin_added_cb (IdeExtensionSetAdapter *set,
+                              PeasPluginInfo         *plugin_info,
+                              PeasExtension          *exten,
+                              gpointer                user_data)
+{
+  IdeWorkspaceAddin *addin = (IdeWorkspaceAddin *)exten;
+  IdeWorkspace *self = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKSPACE_ADDIN (addin));
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  g_debug ("Loading workspace addin from module %s",
+           peas_plugin_info_get_module_name (plugin_info));
+
+  ide_workspace_addin_load (addin, self);
+}
+
+static void
+ide_workspace_addin_removed_cb (IdeExtensionSetAdapter *set,
+                                PeasPluginInfo         *plugin_info,
+                                PeasExtension          *exten,
+                                gpointer                user_data)
+{
+  IdeWorkspaceAddin *addin = (IdeWorkspaceAddin *)exten;
+  IdeWorkspace *self = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKSPACE_ADDIN (addin));
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  g_debug ("Unloading workspace addin from module %s",
+           peas_plugin_info_get_module_name (plugin_info));
+
+  ide_workspace_addin_surface_set (addin, NULL);
+  ide_workspace_addin_unload (addin, self);
+}
+
+static void
+ide_workspace_real_context_set (IdeWorkspace *self,
+                                IdeContext   *context)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  priv->addins = ide_extension_set_adapter_new (NULL,
+                                                NULL,
+                                                IDE_TYPE_WORKSPACE_ADDIN,
+                                                "Workspace-Kind",
+                                                IDE_WORKSPACE_GET_CLASS (self)->kind);
+
+  g_signal_connect (priv->addins,
+                    "extension-added",
+                    G_CALLBACK (ide_workspace_addin_added_cb),
+                    self);
+
+  g_signal_connect (priv->addins,
+                    "extension-removed",
+                    G_CALLBACK (ide_workspace_addin_removed_cb),
+                    self);
+
+  ide_extension_set_adapter_foreach (priv->addins,
+                                     ide_workspace_addin_added_cb,
+                                     self);
+}
+
+static void
+ide_workspace_addin_surface_set_cb (IdeExtensionSetAdapter *set,
+                                    PeasPluginInfo         *plugin_info,
+                                    PeasExtension          *exten,
+                                    gpointer                user_data)
+{
+  IdeWorkspaceAddin *addin = (IdeWorkspaceAddin *)exten;
+  IdeSurface *surface = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_WORKSPACE_ADDIN (addin));
+  g_assert (!surface || IDE_IS_SURFACE (surface));
+
+  ide_workspace_addin_surface_set (addin, surface);
+}
+
+static void
+ide_workspace_real_surface_set (IdeWorkspace *self,
+                                IdeSurface   *surface)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (!surface || IDE_IS_SURFACE (surface));
+
+  if (priv->addins != NULL)
+    ide_extension_set_adapter_foreach (priv->addins,
+                                       ide_workspace_addin_surface_set_cb,
+                                       surface);
+}
+
+/**
+ * ide_workspace_foreach_surface:
+ * @self: a #IdeWorkspace
+ * @callback: (scope call): a #GtkCallback to execute for every surface
+ * @user_data: user data for @callback
+ *
+ * Calls callback for every #IdeSurface based #GtkWidget that is registered
+ * in the workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_foreach_surface (IdeWorkspace *self,
+                               GtkCallback   callback,
+                               gpointer      user_data)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (callback != NULL);
+
+  gtk_container_foreach (GTK_CONTAINER (priv->surfaces), callback, user_data);
+}
+
+static void
+ide_workspace_agree_to_shutdown_cb (GtkWidget *widget,
+                                    gpointer   user_data)
+{
+  gboolean *blocked = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_SURFACE (widget));
+  g_assert (blocked != NULL);
+
+  *blocked |= !ide_surface_agree_to_shutdown (IDE_SURFACE (widget));
+}
+
+static gboolean
+ide_workspace_agree_to_shutdown (IdeWorkspace *self)
+{
+  gboolean blocked = FALSE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  ide_workspace_foreach_surface (self,
+                                 ide_workspace_agree_to_shutdown_cb,
+                                 &blocked);
+
+  return !blocked;
+}
+
+static gboolean
+ide_workspace_delete_event (GtkWidget   *widget,
+                            GdkEventAny *any)
+{
+  IdeWorkspace *self = (IdeWorkspace *)widget;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  IdeWorkbench *workbench;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (any != NULL);
+
+  /* TODO:
+   *
+   * If there are any active transfers, we want to ask the user if they
+   * are sure they want to exit and risk losing them. We can allow them
+   * to be completed in the background.
+   *
+   * Note that we only want to do this on the final workspace window.
+   */
+
+  if (!ide_workspace_agree_to_shutdown (self))
+    return GDK_EVENT_STOP;
+
+  g_cancellable_cancel (priv->cancellable);
+
+  workbench = ide_widget_get_workbench (widget);
+
+  if (ide_workbench_has_project (workbench) &&
+      _ide_workbench_is_last_workspace (workbench, self))
+    {
+      gtk_widget_hide (GTK_WIDGET (self));
+      ide_workbench_unload_async (workbench, NULL, NULL, NULL);
+      return GDK_EVENT_STOP;
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_workspace_notify_surface_cb (IdeWorkspace *self,
+                                 GParamSpec   *pspec,
+                                 GtkStack     *surfaces)
+{
+  GtkWidget *visible_child;
+  IdeHeaderBar *header_bar;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (GTK_IS_STACK (surfaces));
+
+  visible_child = gtk_stack_get_visible_child (surfaces);
+  if (!IDE_IS_SURFACE (visible_child))
+    visible_child = NULL;
+
+  if (visible_child != NULL)
+    gtk_widget_grab_focus (visible_child);
+
+  if ((header_bar = ide_workspace_get_header_bar (self)))
+    {
+      if (visible_child != NULL)
+        dzl_gtk_widget_mux_action_groups (GTK_WIDGET (header_bar), visible_child, MUX_ACTIONS_KEY);
+      else
+        dzl_gtk_widget_mux_action_groups (GTK_WIDGET (header_bar), NULL, MUX_ACTIONS_KEY);
+    }
+
+  g_signal_emit (self, signals [SURFACE_SET], 0, visible_child);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VISIBLE_SURFACE]);
+}
+
+static void
+ide_workspace_destroy (GtkWidget *widget)
+{
+  IdeWorkspace *self = (IdeWorkspace *)widget;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  GtkWindowGroup *group;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  ide_clear_and_destroy_object (&priv->addins);
+
+  group = gtk_window_get_group (GTK_WINDOW (self));
+  if (IDE_IS_WORKBENCH (group))
+    ide_workbench_remove_workspace (IDE_WORKBENCH (group), self);
+
+  GTK_WIDGET_CLASS (ide_workspace_parent_class)->destroy (widget);
+}
+
+/**
+ * ide_workspace_class_set_kind:
+ * @klass: a #IdeWorkspaceClass
+ *
+ * Sets the shorthand name for the kind of workspace. This is used to limit
+ * what #IdeWorkspaceAddin may load within the workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_class_set_kind (IdeWorkspaceClass *klass,
+                              const gchar       *kind)
+{
+  g_return_if_fail (IDE_IS_WORKSPACE_CLASS (klass));
+
+  klass->kind = g_intern_string (kind);
+}
+
+
+static void
+ide_workspace_foreach_page_cb (GtkWidget *widget,
+                               gpointer   user_data)
+{
+  ForeachPage *state = user_data;
+
+  if (IDE_IS_SURFACE (widget))
+    ide_surface_foreach_page (IDE_SURFACE (widget), state->callback, state->user_data);
+}
+
+static void
+ide_workspace_real_foreach_page (IdeWorkspace *self,
+                                 GtkCallback   callback,
+                                 gpointer      user_data)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  ForeachPage state = { callback, user_data };
+
+  g_assert (IDE_IS_WORKSPACE (self));
+  g_assert (callback != NULL);
+
+  gtk_container_foreach (GTK_CONTAINER (priv->surfaces),
+                         ide_workspace_foreach_page_cb,
+                         &state);
+}
+
+static void
+ide_workspace_set_surface_fullscreen_cb (GtkWidget *widget,
+                                         gpointer   user_data)
+{
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (IDE_IS_SURFACE (widget))
+    _ide_surface_set_fullscreen (IDE_SURFACE (widget), !!user_data);
+}
+
+static void
+ide_workspace_real_set_fullscreen (DzlApplicationWindow *window,
+                                   gboolean              fullscreen)
+{
+  IdeWorkspace *self = (IdeWorkspace *)window;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  DZL_APPLICATION_WINDOW_CLASS (ide_workspace_parent_class)->set_fullscreen (window, fullscreen);
+
+  gtk_container_foreach (GTK_CONTAINER (priv->surfaces),
+                         ide_workspace_set_surface_fullscreen_cb,
+                         GUINT_TO_POINTER (fullscreen));
+}
+
+static void
+ide_workspace_grab_focus (GtkWidget *widget)
+{
+  IdeWorkspace *self = (IdeWorkspace *)widget;
+  IdeSurface *surface;
+
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  if ((surface = ide_workspace_get_visible_surface (self)))
+    gtk_widget_grab_focus (GTK_WIDGET (surface));
+}
+
+static void
+ide_workspace_finalize (GObject *object)
+{
+  IdeWorkspace *self = (IdeWorkspace *)object;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_clear_object (&priv->context);
+  g_clear_object (&priv->cancellable);
+
+  G_OBJECT_CLASS (ide_workspace_parent_class)->finalize (object);
+}
+
+static void
+ide_workspace_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  IdeWorkspace *self = IDE_WORKSPACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, ide_workspace_get_context (self));
+      break;
+
+    case PROP_VISIBLE_SURFACE:
+      g_value_set_object (value, ide_workspace_get_visible_surface (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_workspace_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeWorkspace *self = IDE_WORKSPACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_VISIBLE_SURFACE:
+      ide_workspace_set_visible_surface (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_workspace_class_init (IdeWorkspaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  DzlApplicationWindowClass *window_class = DZL_APPLICATION_WINDOW_CLASS (klass);
+
+  object_class->finalize = ide_workspace_finalize;
+  object_class->get_property = ide_workspace_get_property;
+  object_class->set_property = ide_workspace_set_property;
+
+  widget_class->destroy = ide_workspace_destroy;
+  widget_class->delete_event = ide_workspace_delete_event;
+  widget_class->grab_focus = ide_workspace_grab_focus;
+
+  window_class->set_fullscreen = ide_workspace_real_set_fullscreen;
+
+  klass->foreach_page = ide_workspace_real_foreach_page;
+  klass->context_set = ide_workspace_real_context_set;
+  klass->surface_set = ide_workspace_real_surface_set;
+
+  /**
+   * IdeWorkspace:context:
+   *
+   * The "context" property is the #IdeContext for the workspace. This is set
+   * when the workspace joins a workbench.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The IdeContext for the workspace, inherited from workbench",
+                         IDE_TYPE_CONTEXT,
+                         (G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeWorkspace:visible-surface:
+   *
+   * The "visible-surface" property contains the currently foremost surface
+   * in the workspaces stack of surfaces. Usually, this is the editor surface,
+   * but may be other surfaces such as build preferences, profiler, etc.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_VISIBLE_SURFACE] =
+    g_param_spec_object ("visible-surface",
+                         "Visible Surface",
+                         "The currently visible surface",
+                         IDE_TYPE_SURFACE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeWorkspace::surface-set:
+   * @self: an #IdeWorkspace
+   * @surface: (nullable): an #IdeSurface
+   *
+   * The "surface-set" signal is emitted when the current surface changes
+   * within the workspace.
+   *
+   * Since: 3.32
+   */
+  signals [SURFACE_SET] =
+    g_signal_new ("surface-set",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeWorkspaceClass, surface_set),
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_SURFACE);
+  g_signal_set_va_marshaller (signals [SURFACE_SET],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/libide-gui/ui/ide-workspace.ui");
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWorkspace, event_box);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWorkspace, overlay);
+  gtk_widget_class_bind_template_child_private (widget_class, IdeWorkspace, surfaces);
+}
+
+static void
+ide_workspace_init (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  g_autofree gchar *app_id = NULL;
+
+  priv->mru_link.data = self;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (priv->surfaces,
+                           "notify::visible-child",
+                           G_CALLBACK (ide_workspace_notify_surface_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* Add org-gnome-Builder style CSS identifier */
+  app_id = g_strdelimit (g_strdup (ide_get_application_id ()), ".", '-');
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self), app_id);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self), "workspace");
+
+  /* Add events for motion controller of fullscreen titlebar */
+  gtk_widget_add_events (GTK_WIDGET (priv->event_box),
+                         (GDK_POINTER_MOTION_MASK |
+                          GDK_ENTER_NOTIFY_MASK |
+                          GDK_LEAVE_NOTIFY_MASK));
+
+  /* Initialize GActions for workspace */
+  _ide_workspace_init_actions (self);
+}
+
+GList *
+_ide_workspace_get_mru_link (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (IDE_IS_WORKSPACE (self));
+
+  return &priv->mru_link;
+}
+
+/**
+ * ide_workspace_get_context:
+ *
+ * Gets the #IdeContext for the #IdeWorkspace, which is set when the
+ * workspace joins an #IdeWorkbench.
+ *
+ * Returns: (transfer none) (nullable): an #IdeContext or %NULL
+ *
+ * Since: 3.32
+ */
+IdeContext *
+ide_workspace_get_context (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  return priv->context;
+}
+
+void
+_ide_workspace_set_context (IdeWorkspace *self,
+                            IdeContext   *context)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+  g_return_if_fail (priv->context == NULL);
+
+  if (g_set_object (&priv->context, context))
+    {
+      if (IDE_WORKSPACE_GET_CLASS (self)->context_set)
+        IDE_WORKSPACE_GET_CLASS (self)->context_set (self, context);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]);
+    }
+}
+
+/**
+ * ide_workspace_get_cancellable:
+ * @self: a #IdeWorkspace
+ *
+ * Gets a cancellable for a window. This is useful when you want operations
+ * to be cancelled if a window is closed.
+ *
+ * Returns: (transfer none): a #GCancellable
+ *
+ * Since: 3.32
+ */
+GCancellable *
+ide_workspace_get_cancellable (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  if (priv->cancellable == NULL)
+    priv->cancellable = g_cancellable_new ();
+
+  return priv->cancellable;
+}
+
+/**
+ * ide_workspace_foreach_page:
+ * @self: a #IdeWorkspace
+ * @callback: (scope call): a callback to execute for each view
+ * @user_data: closure data for @callback
+ *
+ * Calls @callback for each #IdePage found within the workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_foreach_page (IdeWorkspace *self,
+                            GtkCallback   callback,
+                            gpointer      user_data)
+{
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (callback != NULL);
+
+  if (IDE_WORKSPACE_GET_CLASS (self)->foreach_page)
+    IDE_WORKSPACE_GET_CLASS (self)->foreach_page (self, callback, user_data);
+}
+
+/**
+ * ide_workspace_get_header_bar:
+ * @self: a #IdeWorkspace
+ *
+ * Gets the headerbar for the workspace, if it is an #IdeHeaderBar.
+ * Also works around Gtk giving back a GtkStack for the header bar.
+ *
+ * Returns: (nullable) (transfer none): an #IdeHeaderBar or %NULL
+ *
+ * Since: 3.32
+ */
+IdeHeaderBar *
+ide_workspace_get_header_bar (IdeWorkspace *self)
+{
+  GtkWidget *titlebar;
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  if ((titlebar = gtk_window_get_titlebar (GTK_WINDOW (self))))
+    {
+      if (GTK_IS_STACK (titlebar))
+        titlebar = gtk_stack_get_visible_child (GTK_STACK (titlebar));
+
+      if (IDE_IS_HEADER_BAR (titlebar))
+        return IDE_HEADER_BAR (titlebar);
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_workspace_add_surface:
+ * @self: a #IdeWorkspace
+ *
+ * Adds a new #IdeSurface to the workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_add_surface (IdeWorkspace *self,
+                           IdeSurface   *surface)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  g_autofree gchar *title = NULL;
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_SURFACE (surface));
+
+  if (DZL_IS_DOCK_ITEM (surface))
+    title = dzl_dock_item_get_title (DZL_DOCK_ITEM (surface));
+
+  gtk_container_add_with_properties (GTK_CONTAINER (priv->surfaces), GTK_WIDGET (surface),
+                                     "name", gtk_widget_get_name (GTK_WIDGET (surface)),
+                                     "title", title,
+                                     NULL);
+}
+
+/**
+ * ide_workspace_set_visible_surface_name:
+ * @self: a #IdeWorkspace
+ * @visible_surface_name: the name of the #IdeSurface
+ *
+ * Sets the visible surface based on the name of the surface.  The name of the
+ * surface comes from gtk_widget_get_name(), which should be set when creating
+ * the surface using gtk_widget_set_name().
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_set_visible_surface_name (IdeWorkspace *self,
+                                        const gchar  *visible_surface_name)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (visible_surface_name != NULL);
+
+  gtk_stack_set_visible_child_name (priv->surfaces, visible_surface_name);
+}
+
+/**
+ * ide_workspace_get_visible_surface:
+ * @self: a #IdeWorkspace
+ *
+ * Gets the currently visible #IdeSurface, or %NULL
+ *
+ * Returns: (transfer none) (nullable): an #IdeSurface or %NULL
+ *
+ * Since: 3.32
+ */
+IdeSurface *
+ide_workspace_get_visible_surface (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  GtkWidget *child;
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  child = gtk_stack_get_visible_child (priv->surfaces);
+  if (!IDE_IS_SURFACE (child))
+    child = NULL;
+
+  return IDE_SURFACE (child);
+}
+
+/**
+ * ide_workspace_set_visible_surface:
+ * @self: a #IdeWorkspace
+ * @surface: an #IdeSurface
+ *
+ * Sets the #IdeWorkspace:visible-surface property which is the currently
+ * visible #IdeSurface in the workspace.
+ *
+ * Since: 3.32
+ */
+void
+ide_workspace_set_visible_surface (IdeWorkspace *self,
+                                   IdeSurface   *surface)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (IDE_IS_SURFACE (surface));
+
+  gtk_stack_set_visible_child (priv->surfaces, GTK_WIDGET (surface));
+}
+
+/**
+ * ide_workspace_get_surface_by_name:
+ * @self: a #IdeWorkspace
+ * @name: the name of the surface
+ *
+ * Locates an #IdeSurface that has been added to the workspace by the name
+ * that was registered for the widget using gtk_widget_set_name().
+ *
+ * Returns: (transfer none) (nullable): an #IdeSurface or %NULL
+ *
+ * Since: 3.32
+ */
+IdeSurface *
+ide_workspace_get_surface_by_name (IdeWorkspace *self,
+                                   const gchar  *name)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+  GtkWidget *child;
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+  g_return_val_if_fail (name != NULL, NULL);
+
+  child = gtk_stack_get_child_by_name (priv->surfaces, name);
+
+  return IDE_IS_SURFACE (child) ? IDE_SURFACE (child) : NULL;
+}
+
+static GObject *
+ide_workspace_get_internal_child (GtkBuildable *buildable,
+                                  GtkBuilder   *builder,
+                                  const gchar  *child_name)
+{
+  IdeWorkspace *self = (IdeWorkspace *)buildable;
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_assert (GTK_IS_BUILDABLE (buildable));
+  g_assert (GTK_IS_BUILDER (builder));
+  g_assert (child_name != NULL);
+
+  if (ide_str_equal0 (child_name, "surfaces"))
+    return G_OBJECT (priv->surfaces);
+
+  return NULL;
+}
+
+static void
+buildable_iface_init (GtkBuildableIface *iface)
+{
+  iface->get_internal_child = ide_workspace_get_internal_child;
+}
+
+/**
+ * ide_workspace_get_overlay:
+ * @self: a #IdeWorkspace
+ *
+ * Gets a #GtkOverlay that contains all of the primary contents of the window
+ * (everything except the headerbar). This can be used by plugins to draw
+ * above the workspace contents.
+ *
+ * Returns: (transfer none): a #GtkOverlay
+ *
+ * Since: 3.32
+ */
+GtkOverlay *
+ide_workspace_get_overlay (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  return priv->overlay;
+}
+
+/**
+ * ide_workspace_get_most_recent_page:
+ * @self: a #IdeWorkspace
+ *
+ * Gets the most recently focused #IdePage.
+ *
+ * Returns: (transfer none) (nullable): an #IdePage or %NULL
+ *
+ * Since: 3.32
+ */
+IdePage *
+ide_workspace_get_most_recent_page (IdeWorkspace *self)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_WORKSPACE (self), NULL);
+
+  if (priv->page_mru.head != NULL)
+    return IDE_PAGE (priv->page_mru.head->data);
+
+  return NULL;
+}
+
+void
+_ide_workspace_add_page_mru (IdeWorkspace *self,
+                             GList        *mru_link)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (mru_link != NULL);
+  g_return_if_fail (mru_link->prev == NULL);
+  g_return_if_fail (mru_link->next == NULL);
+  g_return_if_fail (IDE_IS_PAGE (mru_link->data));
+
+  g_debug ("Adding %s to page MRU",
+           G_OBJECT_TYPE_NAME (mru_link->data));
+
+  g_queue_push_head_link (&priv->page_mru, mru_link);
+}
+
+void
+_ide_workspace_remove_page_mru (IdeWorkspace *self,
+                                GList        *mru_link)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (mru_link != NULL);
+  g_return_if_fail (IDE_IS_PAGE (mru_link->data));
+
+  g_debug ("Removing %s from page MRU",
+           G_OBJECT_TYPE_NAME (mru_link->data));
+
+  g_queue_unlink (&priv->page_mru, mru_link);
+}
+
+void
+_ide_workspace_move_front_page_mru (IdeWorkspace *self,
+                                    GList        *mru_link)
+{
+  IdeWorkspacePrivate *priv = ide_workspace_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_WORKSPACE (self));
+  g_return_if_fail (mru_link != NULL);
+  g_return_if_fail (IDE_IS_PAGE (mru_link->data));
+
+  if (mru_link == priv->page_mru.head)
+    return;
+
+  g_debug ("Moving %s to front of page MRU",
+           G_OBJECT_TYPE_NAME (mru_link->data));
+
+  g_queue_unlink (&priv->page_mru, mru_link);
+  g_queue_push_head_link (&priv->page_mru, mru_link);
+}
diff --git a/src/libide/gui/ide-workspace.h b/src/libide/gui/ide-workspace.h
new file mode 100644
index 000000000..9e899c9d0
--- /dev/null
+++ b/src/libide/gui/ide-workspace.h
@@ -0,0 +1,96 @@
+/* ide-workspace.h
+ *
+ * Copyright 2014-2019 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
+
+#if !defined (IDE_GUI_INSIDE) && !defined (IDE_GUI_COMPILATION)
+# error "Only <libide-gui.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <libide-core.h>
+#include <libide-projects.h>
+
+#include "ide-header-bar.h"
+#include "ide-page.h"
+#include "ide-surface.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_WORKSPACE (ide_workspace_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeWorkspace, ide_workspace, IDE, WORKSPACE, DzlApplicationWindow)
+
+struct _IdeWorkspaceClass
+{
+  DzlApplicationWindowClass parent_class;
+
+  const gchar *kind;
+
+  void (*context_set)  (IdeWorkspace *self,
+                        IdeContext   *context);
+  void (*foreach_page) (IdeWorkspace *self,
+                        GtkCallback   callback,
+                        gpointer      user_data);
+  void (*surface_set)  (IdeWorkspace *self,
+                        IdeSurface   *surface);
+
+  /*< private >*/
+  gpointer _reserved[32];
+};
+
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_class_set_kind           (IdeWorkspaceClass *klass,
+                                                      const gchar       *kind);
+IDE_AVAILABLE_IN_3_32
+IdeHeaderBar *ide_workspace_get_header_bar           (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_3_32
+IdeContext   *ide_workspace_get_context              (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_3_32
+GCancellable *ide_workspace_get_cancellable          (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_foreach_page             (IdeWorkspace      *self,
+                                                      GtkCallback        callback,
+                                                      gpointer           user_data);
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_foreach_surface          (IdeWorkspace      *self,
+                                                      GtkCallback        callback,
+                                                      gpointer           user_data);
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_add_surface              (IdeWorkspace      *self,
+                                                      IdeSurface        *surface);
+IDE_AVAILABLE_IN_3_32
+IdeSurface   *ide_workspace_get_surface_by_name      (IdeWorkspace      *self,
+                                                      const gchar       *name);
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_set_visible_surface_name (IdeWorkspace      *self,
+                                                      const gchar       *visible_surface_name);
+IDE_AVAILABLE_IN_3_32
+IdeSurface   *ide_workspace_get_visible_surface      (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_workspace_set_visible_surface      (IdeWorkspace      *self,
+                                                      IdeSurface        *surface);
+IDE_AVAILABLE_IN_3_32
+GtkOverlay   *ide_workspace_get_overlay              (IdeWorkspace      *self);
+IDE_AVAILABLE_IN_3_32
+IdePage      *ide_workspace_get_most_recent_page     (IdeWorkspace      *self);
+
+G_END_DECLS
diff --git a/src/libide/gui/ide-workspace.ui b/src/libide/gui/ide-workspace.ui
new file mode 100644
index 000000000..6af729f22
--- /dev/null
+++ b/src/libide/gui/ide-workspace.ui
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeWorkspace" parent="DzlApplicationWindow">
+    <child>
+      <object class="GtkEventBox" id="event_box">
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkOverlay" id="overlay">
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkStack" id="surfaces">
+                <property name="homogeneous">false</property>
+                <property name="expand">true</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
+
diff --git a/src/libide/gui/libide-gui.gresource.xml b/src/libide/gui/libide-gui.gresource.xml
new file mode 100644
index 000000000..cec829c7c
--- /dev/null
+++ b/src/libide/gui/libide-gui.gresource.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/libide-gui">
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+  </gresource>
+  <gresource prefix="/org/gnome/libide-gui/ui">
+    <file preprocess="xml-stripblanks">ide-environment-editor-row.ui</file>
+    <file preprocess="xml-stripblanks">ide-frame-header.ui</file>
+    <file preprocess="xml-stripblanks">ide-frame.ui</file>
+    <file preprocess="xml-stripblanks">ide-header-bar.ui</file>
+    <file preprocess="xml-stripblanks">ide-notification-list-box-row.ui</file>
+    <file preprocess="xml-stripblanks">ide-notification-view.ui</file>
+    <file preprocess="xml-stripblanks">ide-notifications-button.ui</file>
+    <file preprocess="xml-stripblanks">ide-omni-bar.ui</file>
+    <file preprocess="xml-stripblanks">ide-panel.ui</file>
+    <file preprocess="xml-stripblanks">ide-preferences-language-row.ui</file>
+    <file preprocess="xml-stripblanks">ide-preferences-window.ui</file>
+    <file preprocess="xml-stripblanks">ide-primary-workspace.ui</file>
+    <file preprocess="xml-stripblanks">ide-run-button.ui</file>
+    <file preprocess="xml-stripblanks">ide-search-entry.ui</file>
+    <file preprocess="xml-stripblanks">ide-shortcuts-window.ui</file>
+    <file preprocess="xml-stripblanks">ide-workspace.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/gui/libide-gui.h b/src/libide/gui/libide-gui.h
new file mode 100644
index 000000000..258cd487e
--- /dev/null
+++ b/src/libide/gui/libide-gui.h
@@ -0,0 +1,70 @@
+/* ide-gui.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+#include <libide-core.h>
+#include <libide-io.h>
+#include <libide-projects.h>
+#include <libide-threading.h>
+
+#define IDE_GUI_INSIDE
+
+#include "ide-application.h"
+#include "ide-application-addin.h"
+#include "ide-cell-renderer-fancy.h"
+#include "ide-command.h"
+#include "ide-command-provider.h"
+#include "ide-config-view-addin.h"
+#include "ide-environment-editor.h"
+#include "ide-frame.h"
+#include "ide-frame-addin.h"
+#include "ide-frame-header.h"
+#include "ide-header-bar.h"
+#include "ide-fancy-tree-view.h"
+#include "ide-grid.h"
+#include "ide-grid-column.h"
+#include "ide-gui-global.h"
+#include "ide-header-bar.h"
+#include "ide-marked-view.h"
+#include "ide-notifications-button.h"
+#include "ide-omni-bar-addin.h"
+#include "ide-omni-bar.h"
+#include "ide-page.h"
+#include "ide-pane.h"
+#include "ide-panel.h"
+#include "ide-preferences-addin.h"
+#include "ide-preferences-surface.h"
+#include "ide-preferences-window.h"
+#include "ide-primary-workspace.h"
+#include "ide-search-entry.h"
+#include "ide-session-addin.h"
+#include "ide-surface.h"
+#include "ide-surfaces-button.h"
+#include "ide-tagged-entry.h"
+#include "ide-transfer-button.h"
+#include "ide-transient-sidebar.h"
+#include "ide-workbench.h"
+#include "ide-workbench-addin.h"
+#include "ide-workspace.h"
+#include "ide-workspace-addin.h"
+
+#undef IDE_GUI_INSIDE
diff --git a/src/libide/gui/meson.build b/src/libide/gui/meson.build
new file mode 100644
index 000000000..3c494df9a
--- /dev/null
+++ b/src/libide/gui/meson.build
@@ -0,0 +1,212 @@
+libide_gui_header_subdir = join_paths(libide_header_subdir, 'gui')
+libide_include_directories += include_directories('.')
+
+libide_gui_generated_headers = []
+
+#
+# Public API Headers
+#
+
+libide_gui_public_headers = [
+  'ide-application.h',
+  'ide-application-addin.h',
+  'ide-cell-renderer-fancy.h',
+  'ide-command.h',
+  'ide-command-provider.h',
+  'ide-config-view-addin.h',
+  'ide-environment-editor.h',
+  'ide-fancy-tree-view.h',
+  'ide-frame-addin.h',
+  'ide-frame-header.h',
+  'ide-frame.h',
+  'ide-grid-column.h',
+  'ide-grid.h',
+  'ide-gui-global.h',
+  'ide-header-bar.h',
+  'ide-marked-view.h',
+  'ide-notifications-button.h',
+  'ide-omni-bar-addin.h',
+  'ide-omni-bar.h',
+  'ide-page.h',
+  'ide-pane.h',
+  'ide-panel.h',
+  'ide-preferences-addin.h',
+  'ide-preferences-surface.h',
+  'ide-preferences-window.h',
+  'ide-primary-workspace.h',
+  'ide-search-entry.h',
+  'ide-session-addin.h',
+  'ide-surface.h',
+  'ide-surfaces-button.h',
+  'ide-tagged-entry.h',
+  'ide-transfer-button.h',
+  'ide-transient-sidebar.h',
+  'ide-worker.h',
+  'ide-workbench.h',
+  'ide-workbench-addin.h',
+  'ide-workspace.h',
+  'ide-workspace-addin.h',
+  'libide-gui.h',
+]
+
+install_headers(libide_gui_public_headers, subdir: libide_gui_header_subdir)
+
+#
+# Sources
+#
+
+libide_gui_private_headers = [
+  'gs-markdown-private.h',
+  'ide-application-private.h',
+  'ide-environment-editor-row.h',
+  'ide-frame-wrapper.h',
+  'ide-gui-private.h',
+  'ide-keybindings.h',
+  'ide-notification-list-box-row-private.h',
+  'ide-notifications-button-popover-private.h',
+  'ide-notification-stack-private.h',
+  'ide-notification-view-private.h',
+  'ide-preferences-builtin-private.h',
+  'ide-preferences-language-row-private.h',
+  'ide-run-button.h',
+  'ide-session-private.h',
+  'ide-window-settings-private.h',
+  'ide-shortcut-label-private.h',
+  'ide-shortcuts-window-private.h',
+  'ide-worker-manager.h',
+  'ide-worker-process.h',
+]
+
+libide_gui_private_sources = [
+  'gs-markdown.c',
+  'ide-application-actions.c',
+  'ide-application-color.c',
+  'ide-application-shortcuts.c',
+  'ide-application-plugins.c',
+  'ide-environment-editor-row.c',
+  'ide-frame-actions.c',
+  'ide-frame-shortcuts.c',
+  'ide-frame-wrapper.c',
+  'ide-grid-actions.c',
+  'ide-grid-column-actions.c',
+  'ide-header-bar-shortcuts.c',
+  'ide-keybindings.c',
+  'ide-notification-list-box-row.c',
+  'ide-notification-stack.c',
+  'ide-notification-view.c',
+  'ide-notifications-button-popover.c',
+  'ide-preferences-builtin.c',
+  'ide-preferences-language-row.c',
+  'ide-primary-workspace-actions.c',
+  'ide-run-button.c',
+  'ide-session.c',
+  'ide-shortcuts-window.c',
+  'ide-window-settings.c',
+  'ide-worker-manager.c',
+  'ide-worker-process.c',
+  'ide-workspace-actions.c',
+]
+
+libide_gui_public_sources = [
+  'ide-application.c',
+  'ide-application-addin.c',
+  'ide-application-command-line.c',
+  'ide-application-open.c',
+  'ide-cell-renderer-fancy.c',
+  'ide-command.c',
+  'ide-command-provider.c',
+  'ide-config-view-addin.c',
+  'ide-environment-editor.c',
+  'ide-fancy-tree-view.c',
+  'ide-frame-addin.c',
+  'ide-frame-header.c',
+  'ide-frame.c',
+  'ide-grid-column.c',
+  'ide-grid.c',
+  'ide-gui-global.c',
+  'ide-header-bar.c',
+  'ide-marked-view.c',
+  'ide-notifications-button.c',
+  'ide-omni-bar-addin.c',
+  'ide-omni-bar.c',
+  'ide-page.c',
+  'ide-pane.c',
+  'ide-panel.c',
+  'ide-primary-workspace.c',
+  'ide-preferences-addin.c',
+  'ide-preferences-surface.c',
+  'ide-preferences-window.c',
+  'ide-search-entry.c',
+  'ide-session-addin.c',
+  'ide-shortcut-label.c',
+  'ide-surface.c',
+  'ide-surfaces-button.c',
+  'ide-tagged-entry.c',
+  'ide-transient-sidebar.c',
+  'ide-transfer-button.c',
+  'ide-workbench.c',
+  'ide-workbench-addin.c',
+  'ide-workspace.c',
+  'ide-workspace-addin.c',
+  'ide-worker.c',
+]
+
+libide_gui_sources = libide_gui_public_sources + libide_gui_private_sources
+
+#
+# Generated Resource Files
+#
+
+libide_gui_resources = gnome.compile_resources(
+  'ide-gui-resources',
+  'libide-gui.gresource.xml',
+  c_name: 'ide_gui',
+)
+libide_gui_generated_headers += [libide_gui_resources[1]]
+libide_gui_sources += libide_gui_resources[0]
+
+
+#
+# Dependencies
+#
+
+libide_gui_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libgtksource_dep,
+  libdazzle_dep,
+  libpeas_dep,
+  libwebkit_dep,
+
+  libide_core_dep,
+  libide_io_dep,
+  libide_foundry_dep,
+  libide_debugger_dep,
+  libide_plugins_dep,
+  libide_projects_dep,
+  libide_search_dep,
+  libide_themes_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_gui = static_library('ide-gui-' + libide_api_version, libide_gui_sources,
+   dependencies: libide_gui_deps,
+         c_args: libide_args + release_args + ['-DIDE_GUI_COMPILATION'],
+)
+
+libide_gui_dep = declare_dependency(
+              sources: libide_gui_private_headers + libide_gui_generated_headers,
+         dependencies: libide_gui_deps,
+           link_whole: libide_gui,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_gui_public_sources)
+gnome_builder_public_headers += files(libide_gui_public_headers)
+gnome_builder_private_sources += files(libide_gui_private_sources)
+gnome_builder_private_headers += files(libide_gui_private_headers)
+gnome_builder_include_subdirs += libide_gui_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-gui.h', '-DIDE_GUI_COMPILATION']
diff --git a/src/libide/io/ide-content-type.c b/src/libide/io/ide-content-type.c
new file mode 100644
index 000000000..df2a88dc7
--- /dev/null
+++ b/src/libide/io/ide-content-type.c
@@ -0,0 +1,117 @@
+/* ide-content-type.c
+ *
+ * Copyright 2018-2019 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 "ide-glib"
+
+#include "config.h"
+
+#include "ide-content-type.h"
+
+/**
+ * ide_g_content_type_get_symbolic_icon:
+ *
+ * This function is simmilar to g_content_type_get_symbolic_icon() except that
+ * it takes our bundled icons into account to ensure that they are taken at a
+ * higher priority than the fallbacks from the current icon theme such as
+ * Adwaita.
+ *
+ * Returns: (transfer full) (nullable): A #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_g_content_type_get_symbolic_icon (const gchar *content_type)
+{
+  static GHashTable *bundled;
+  g_autoptr(GIcon) icon = NULL;
+
+  g_return_val_if_fail (content_type != NULL, NULL);
+
+  if (g_once_init_enter (&bundled))
+    {
+      GHashTable *table = g_hash_table_new (g_str_hash, g_str_equal);
+
+      /*
+       * This needs to be updated when we add icons for specific mime-types
+       * because of how icon theme loading works (and it wanting to use
+       * Adwaita generic icons before our hicolor specific icons.
+       */
+
+#define ADD_ICON(t, n, v) g_hash_table_insert (t, (gpointer)n, v ? (gpointer)v : (gpointer)n)
+      ADD_ICON (table, "application-x-php-symbolic", NULL);
+      ADD_ICON (table, "text-css-symbolic", NULL);
+      ADD_ICON (table, "text-html-symbolic", NULL);
+      ADD_ICON (table, "text-markdown-symbolic", NULL);
+      ADD_ICON (table, "text-rust-symbolic", NULL);
+      ADD_ICON (table, "text-sql-symbolic", NULL);
+      ADD_ICON (table, "text-x-authors-symbolic", NULL);
+      ADD_ICON (table, "text-x-changelog-symbolic", NULL);
+      ADD_ICON (table, "text-x-chdr-symbolic", NULL);
+      ADD_ICON (table, "text-x-copying-symbolic", NULL);
+      ADD_ICON (table, "text-x-cpp-symbolic", NULL);
+      ADD_ICON (table, "text-x-csrc-symbolic", NULL);
+      ADD_ICON (table, "text-x-javascript-symbolic", NULL);
+      ADD_ICON (table, "text-x-python-symbolic", NULL);
+      ADD_ICON (table, "text-x-python3-symbolic", "text-x-python-symbolic");
+      ADD_ICON (table, "text-x-readme-symbolic", NULL);
+      ADD_ICON (table, "text-x-ruby-symbolic", NULL);
+      ADD_ICON (table, "text-x-script-symbolic", NULL);
+      ADD_ICON (table, "text-x-vala-symbolic", NULL);
+      ADD_ICON (table, "text-xml-symbolic", NULL);
+#undef ADD_ICON
+
+      g_once_init_leave (&bundled, table);
+    }
+
+  /*
+   * Basically just steal the name if we get something that is not generic,
+   * because that is the only way we can somewhat ensure that we don't use
+   * the Adwaita fallback for generic when what we want is the *exact* match
+   * from our hicolor/ bundle.
+   */
+
+  icon = g_content_type_get_symbolic_icon (content_type);
+
+  if (G_IS_THEMED_ICON (icon))
+    {
+      const gchar * const *names = g_themed_icon_get_names (G_THEMED_ICON (icon));
+
+      if (names != NULL)
+        {
+          gboolean fallback = FALSE;
+
+          for (guint i = 0; names[i] != NULL; i++)
+            {
+              const gchar *replace = g_hash_table_lookup (bundled, names[i]);
+
+              if (replace != NULL)
+                return g_icon_new_for_string (replace, NULL);
+
+              fallback |= (g_str_equal (names[i], "text-plain") ||
+                           g_str_equal (names[i], "application-octet-stream"));
+            }
+
+          if (fallback)
+            return g_icon_new_for_string ("text-x-generic-symbolic", NULL);
+        }
+    }
+
+  return g_steal_pointer (&icon);
+}
diff --git a/src/libide/io/ide-content-type.h b/src/libide/io/ide-content-type.h
new file mode 100644
index 000000000..6304b69c1
--- /dev/null
+++ b/src/libide/io/ide-content-type.h
@@ -0,0 +1,31 @@
+/* ide-content-type.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+GIcon *ide_g_content_type_get_symbolic_icon (const gchar *content_type);
+
+G_END_DECLS
+
diff --git a/src/libide/io/ide-gfile.c b/src/libide/io/ide-gfile.c
new file mode 100644
index 000000000..22148bc9c
--- /dev/null
+++ b/src/libide/io/ide-gfile.c
@@ -0,0 +1,691 @@
+/* ide-gfile.c
+ *
+ * Copyright 2016-2019 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 "ide-gfile"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-gfile.h"
+
+static GPtrArray *g_ignored;
+G_LOCK_DEFINE_STATIC (ignored);
+
+static GPtrArray *
+get_ignored_locked (void)
+{
+  static const gchar *ignored_patterns[] = {
+    /* Ignore Gio temporary files */
+    ".goutputstream-*",
+    /* Ignore minified JS */
+    "*.min.js",
+    "*.min.js.*",
+  };
+
+  if (g_ignored == NULL)
+    {
+      g_ignored = g_ptr_array_new ();
+      for (guint i = 0; i < G_N_ELEMENTS (ignored_patterns); i++)
+        g_ptr_array_add (g_ignored, g_pattern_spec_new (ignored_patterns[i]));
+    }
+
+  return g_ignored;
+}
+
+/**
+ * ide_g_file_add_ignored_pattern:
+ * @pattern: a #GPatternSpec style glob pattern
+ *
+ * Adds a pattern that can be used to match ingored files. These are global
+ * to the application, so they should only include well-known ignored files
+ * such as those internal to a build system, or version control system, and
+ * similar.
+ *
+ * Since: 3.32
+ */
+void
+ide_g_file_add_ignored_pattern (const gchar *pattern)
+{
+  G_LOCK (ignored);
+  g_ptr_array_add (get_ignored_locked (), g_pattern_spec_new (pattern));
+  G_UNLOCK (ignored);
+}
+
+/**
+ * ide_path_is_ignored:
+ * @path: the path to the file
+ *
+ * Checks if @path should be ignored using the global file
+ * ignores registered with Builder.
+ *
+ * Returns: %TRUE if @path should be ignored, otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_path_is_ignored (const gchar *path)
+{
+  g_autofree gchar *name = NULL;
+  g_autofree gchar *reversed = NULL;
+  GPtrArray *ignored;
+  gsize len;
+  gboolean ret = FALSE;
+
+  name = g_path_get_basename (path);
+  len = strlen (name);
+  reversed = g_utf8_strreverse (name, len);
+
+  /* Ignore empty files for whatever reason */
+  if (ide_str_empty0 (name))
+    return TRUE;
+
+  /* Ignore builtin backup files by GIO */
+  if (name[len - 1] == '~')
+    return TRUE;
+
+  G_LOCK (ignored);
+
+  ignored = get_ignored_locked ();
+
+  for (guint i = 0; i < ignored->len; i++)
+    {
+      GPatternSpec *pattern_spec = g_ptr_array_index (ignored, i);
+
+      if (g_pattern_match (pattern_spec, len, name, reversed))
+        {
+          ret = TRUE;
+          break;
+        }
+    }
+
+  G_UNLOCK (ignored);
+
+  return ret;
+}
+
+/**
+ * ide_g_file_is_ignored:
+ * @file: a #GFile
+ *
+ * Checks if @file should be ignored using the internal ignore rules.  If you
+ * care about the version control system, see #IdeVcs and ide_vcs_is_ignored().
+ *
+ * Returns: %TRUE if @file should be ignored; otherwise %FALSE.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_g_file_is_ignored (GFile *file)
+{
+  g_autofree gchar *name = NULL;
+  g_autofree gchar *reversed = NULL;
+  GPtrArray *ignored;
+  gsize len;
+  gboolean ret = FALSE;
+
+  name = g_file_get_basename (file);
+  len = strlen (name);
+  reversed = g_utf8_strreverse (name, len);
+
+  /* Ignore empty files for whatever reason */
+  if (ide_str_empty0 (name))
+    return TRUE;
+
+  /* Ignore builtin backup files by GIO */
+  if (name[len - 1] == '~')
+    return TRUE;
+
+  G_LOCK (ignored);
+
+  ignored = get_ignored_locked ();
+
+  for (guint i = 0; i < ignored->len; i++)
+    {
+      GPatternSpec *pattern_spec = g_ptr_array_index (ignored, i);
+
+      if (g_pattern_match (pattern_spec, len, name, reversed))
+        {
+          ret = TRUE;
+          break;
+        }
+    }
+
+  G_UNLOCK (ignored);
+
+  return ret;
+}
+
+/**
+ * ide_g_file_get_uncanonical_relative_path:
+ * @file: a #GFile
+ * @other: a #GFile with a common ancestor to @file
+ *
+ * This function is similar to g_file_get_relative_path() except that
+ * @file and @other only need to have a shared common ancestor.
+ *
+ * This is useful if you must use a relative path instead of the absolute,
+ * canonical path.
+ *
+ * This is being implemented for use when communicating to GDB. When that
+ * becomes unnecessary, this should no longer be used.
+ *
+ * Returns: (nullable): A relative path, or %NULL if no common ancestor was
+ *   found for the relative path.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_g_file_get_uncanonical_relative_path (GFile *file,
+                                          GFile *other)
+{
+  g_autoptr(GFile) ancestor = NULL;
+  g_autoptr(GString) relatives = NULL;
+  g_autofree gchar *path = NULL;
+  g_autofree gchar *suffix = NULL;
+
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+  g_return_val_if_fail (G_IS_FILE (other), NULL);
+
+  /* Nothing for matching files */
+  if (file == other || g_file_equal (file, other))
+    return NULL;
+
+  /* Make sure we're working with files of the same type */
+  if (G_OBJECT_TYPE (file) != G_OBJECT_TYPE (other))
+    return NULL;
+
+  /* Already descendant, just give the actual path */
+  if (g_file_has_prefix (other, file))
+    return g_file_get_path (other);
+
+  relatives = g_string_new ("/");
+
+  /* Find the common ancestor */
+  ancestor = g_object_ref (file);
+  while (ancestor != NULL &&
+         !g_file_has_prefix (other, ancestor) &&
+         !g_file_equal (other, ancestor))
+    {
+      g_autoptr(GFile) parent = g_file_get_parent (ancestor);
+
+      /* We reached the root, nothing more to do */
+      if (g_file_equal (parent, ancestor))
+        return NULL;
+
+      g_string_append_len (relatives, "../", strlen ("../"));
+
+      g_clear_object (&ancestor);
+      ancestor = g_steal_pointer (&parent);
+    }
+
+  g_assert (G_IS_FILE (ancestor));
+  g_assert (g_file_has_prefix (other, ancestor));
+  g_assert (g_file_has_prefix (file, ancestor));
+
+  path = g_file_get_path (file);
+  suffix = g_file_get_relative_path (ancestor, other);
+
+  if (path == NULL)
+    path = g_strdup ("/");
+
+  if (suffix == NULL)
+    suffix = g_strdup ("/");
+
+  return g_build_filename (path, relatives->str, suffix, NULL);
+}
+
+typedef struct
+{
+  gchar *attributes;
+  GFileQueryInfoFlags flags;
+} GetChildren;
+
+static void
+ide_g_file_get_children_worker (IdeTask      *task,
+                                gpointer      source_object,
+                                gpointer      task_data,
+                                GCancellable *cancellable)
+{
+  g_autoptr(GFileEnumerator) enumerator = NULL;
+  g_autoptr(GPtrArray) children = NULL;
+  g_autoptr(GError) error = NULL;
+  GetChildren *gc = task_data;
+  GFile *dir = source_object;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (G_IS_FILE (dir));
+  g_assert (gc != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  children = g_ptr_array_new_with_free_func (g_object_unref);
+
+  enumerator = g_file_enumerate_children (dir,
+                                          gc->attributes,
+                                          gc->flags,
+                                          cancellable,
+                                          &error);
+
+  if (enumerator == NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  for (;;)
+    {
+      g_autoptr(GFileInfo) file_info = NULL;
+
+      file_info = g_file_enumerator_next_file (enumerator, cancellable, &error);
+
+      if (error != NULL)
+        {
+          ide_task_return_error (task, g_steal_pointer (&error));
+          return;
+        }
+
+      if (file_info == NULL)
+        break;
+
+      g_ptr_array_add (children, g_steal_pointer (&file_info));
+    }
+
+  g_file_enumerator_close (enumerator, NULL, NULL);
+
+  ide_task_return_pointer (task,
+                           g_steal_pointer (&children),
+                           (GDestroyNotify) g_ptr_array_unref);
+}
+
+static void
+get_children_free (gpointer data)
+{
+  GetChildren *gc = data;
+
+  g_free (gc->attributes);
+  g_slice_free (GetChildren, gc);
+}
+
+/**
+ * ide_g_file_get_children_async:
+ * @file: a #IdeGlib
+ * @attributes: attributes to retrieve
+ * @flags: flags for the query
+ * @io_priority: the io priority
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * This function is like g_file_enumerate_children_async() except that
+ * it returns a #GPtrArray of #GFileInfo instead of an enumerator.
+ *
+ * This can be convenient when you know you need all of the #GFileInfo
+ * accessable at once, or the size will be small.
+ *
+ * Since: 3.32
+ */
+void
+ide_g_file_get_children_async (GFile               *file,
+                               const gchar         *attributes,
+                               GFileQueryInfoFlags  flags,
+                               gint                 io_priority,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  GetChildren *gc;
+
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (attributes != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  gc = g_slice_new0 (GetChildren);
+  gc->attributes = g_strdup (attributes);
+  gc->flags = flags;
+
+  task = ide_task_new (file, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_g_file_get_children_async);
+  ide_task_set_priority (task, io_priority);
+  ide_task_set_task_data (task, gc, get_children_free);
+
+#ifdef DEVELOPMENT_BUILD
+  /* Useful for testing slow interactions on project-tree and such */
+  if (g_getenv ("IDE_G_FILE_DELAY"))
+    {
+      gboolean
+      delayed_run (gpointer data)
+      {
+        g_autoptr(IdeTask) subtask = data;
+        ide_task_run_in_thread (subtask, ide_g_file_get_children_worker);
+        return G_SOURCE_REMOVE;
+      }
+      g_timeout_add_seconds (1, delayed_run, g_object_ref (task));
+      return;
+    }
+#endif
+
+  ide_task_run_in_thread (task, ide_g_file_get_children_worker);
+}
+
+/**
+ * ide_g_file_get_children_finish:
+ * @file: a #GFile
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_g_file_get_children_async().
+ *
+ * Returns: (transfer full) (element-type Gio.FileInfo): A #GPtrArray
+ *   of #GFileInfo if successful, otherwise %NULL.
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_g_file_get_children_finish (GFile         *file,
+                                GAsyncResult  *result,
+                                GError       **error)
+{
+  GPtrArray *ret;
+
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+  g_return_val_if_fail (ide_task_is_valid (IDE_TASK (result), file), NULL);
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ret);
+}
+
+typedef struct
+{
+  GPatternSpec *spec;
+  guint         depth;
+} Find;
+
+static void
+find_free (Find *f)
+{
+  g_clear_pointer (&f->spec, g_pattern_spec_free);
+  g_slice_free (Find, f);
+}
+
+static void
+populate_descendants_matching (GFile        *file,
+                               GCancellable *cancellable,
+                               GPtrArray    *results,
+                               GPatternSpec *spec,
+                               guint         depth)
+{
+  g_autoptr(GFileEnumerator) enumerator = NULL;
+  g_autoptr(GPtrArray) children = NULL;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (results != NULL);
+  g_assert (spec != NULL);
+
+  if (depth == 0)
+    return;
+
+  enumerator = g_file_enumerate_children (file,
+                                          G_FILE_ATTRIBUTE_STANDARD_NAME","
+                                          G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK","
+                                          G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                                          G_FILE_QUERY_INFO_NONE,
+                                          cancellable,
+                                          NULL);
+
+  if (enumerator == NULL)
+    return;
+
+  for (;;)
+    {
+      g_autoptr(GFileInfo) info = g_file_enumerator_next_file (enumerator, cancellable, NULL);
+      const gchar *name;
+      GFileType file_type;
+
+      if (info == NULL)
+        break;
+
+      name = g_file_info_get_name (info);
+      file_type = g_file_info_get_file_type (info);
+
+      if (g_pattern_match_string (spec, name))
+        g_ptr_array_add (results, g_file_enumerator_get_child (enumerator, info));
+
+      if (!g_file_info_get_is_symlink (info) && file_type == G_FILE_TYPE_DIRECTORY)
+        {
+          if (children == NULL)
+            children = g_ptr_array_new_with_free_func (g_object_unref);
+          g_ptr_array_add (children, g_file_enumerator_get_child (enumerator, info));
+        }
+    }
+
+  g_file_enumerator_close (enumerator, cancellable, NULL);
+
+  if (children != NULL)
+    {
+      for (guint i = 0; i < children->len; i++)
+        {
+          GFile *child = g_ptr_array_index (children, i);
+
+          /* Don't recurse into known bad directories */
+          if (!ide_g_file_is_ignored (child))
+            populate_descendants_matching (child, cancellable, results, spec, depth - 1);
+        }
+    }
+}
+
+static void
+ide_g_file_find_worker (IdeTask      *task,
+                        gpointer      source_object,
+                        gpointer      task_data,
+                        GCancellable *cancellable)
+{
+  GFile *file = source_object;
+  Find *f = task_data;
+  g_autoptr(GPtrArray) ret = NULL;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (G_IS_FILE (file));
+  g_assert (f != NULL);
+  g_assert (f->spec != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ret = g_ptr_array_new_with_free_func (g_object_unref);
+  populate_descendants_matching (file, cancellable, ret, f->spec, f->depth);
+  ide_task_return_pointer (task, g_steal_pointer (&ret), (GDestroyNotify)g_ptr_array_unref);
+}
+
+/**
+ * ide_g_file_find_with_depth_async:
+ * @file: a #IdeGlib
+ * @pattern: the glob pattern to search for using GPatternSpec
+ * @max_depth: maximum tree depth to search
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Searches descendants of @file for files matching @pattern.
+ *
+ * Only up to @max_depth subdirectories will be searched. However, if
+ * @max_depth is zero, then all directories will be searched.
+ *
+ * You may only match on the filename, not the directory.
+ *
+ * Since: 3.32
+ */
+void
+ide_g_file_find_with_depth_async (GFile               *file,
+                                  const gchar         *pattern,
+                                  guint                depth,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  Find *f;
+
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (pattern != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (depth == 0)
+    depth = G_MAXUINT;
+
+  task = ide_task_new (file, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_g_file_find_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW + 100);
+
+  f = g_slice_new0 (Find);
+  f->spec = g_pattern_spec_new (pattern);
+  f->depth = depth;
+  ide_task_set_task_data (task, f, find_free);
+
+  if (f->spec == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVAL,
+                                 "Invalid pattern spec: %s",
+                                 pattern);
+      return;
+    }
+
+  ide_task_run_in_thread (task, ide_g_file_find_worker);
+}
+
+/**
+ * ide_g_file_find_async:
+ * @file: a #IdeGlib
+ * @pattern: the glob pattern to search for using GPatternSpec
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a callback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Searches descendants of @file for files matching @pattern.
+ *
+ * You may only match on the filename, not the directory.
+ *
+ * Since: 3.32
+ */
+void
+ide_g_file_find_async (GFile               *file,
+                       const gchar         *pattern,
+                       GCancellable        *cancellable,
+                       GAsyncReadyCallback  callback,
+                       gpointer             user_data)
+{
+  ide_g_file_find_with_depth_async (file, pattern, G_MAXUINT, cancellable, callback, user_data);
+}
+
+/**
+ * ide_g_file_find_finish:
+ * @file: a #GFile
+ * @result: a result provided to callback
+ * @error: a location for a #GError or %NULL
+ *
+ * Gets the files that were found which matched the pattern.
+ *
+ * Returns: (transfer full) (element-type Gio.File): A #GPtrArray of #GFile
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_g_file_find_finish (GFile         *file,
+                        GAsyncResult  *result,
+                        GError       **error)
+{
+  GPtrArray *ret;
+
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ret);
+}
+
+/**
+ * ide_g_host_file_get_contents:
+ * @path: the path on the host
+ * @contents: (out): a location for the contents
+ * @len: (out): a location for the size, not including trailing \0
+ * @error: location for a #GError, or %NULL
+ *
+ * This is similar to g_file_get_contents() but ensures that we get
+ * the file from the host, rather than our mount namespace.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_g_host_file_get_contents (const gchar  *path,
+                              gchar       **contents,
+                              gsize        *len,
+                              GError      **error)
+{
+  g_return_val_if_fail (path != NULL, FALSE);
+
+  if (contents != NULL)
+    *contents = NULL;
+
+  if (len != NULL)
+    *len = 0;
+
+  if (!ide_is_flatpak ())
+    return g_file_get_contents (path, contents, len, error);
+
+  {
+    g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+    g_autoptr(IdeSubprocess) subprocess = NULL;
+    g_autoptr(GBytes) stdout_buf = NULL;
+
+    launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE |
+                                            G_SUBPROCESS_FLAGS_STDERR_SILENCE);
+    ide_subprocess_launcher_set_run_on_host (launcher, TRUE);
+    ide_subprocess_launcher_push_argv (launcher, "cat");
+    ide_subprocess_launcher_push_argv (launcher, path);
+
+    if (!(subprocess = ide_subprocess_launcher_spawn (launcher, NULL, error)))
+      return FALSE;
+
+    if (!ide_subprocess_communicate (subprocess, NULL, NULL, &stdout_buf, NULL, error))
+      return FALSE;
+
+    if (len != NULL)
+      *len = g_bytes_get_size (stdout_buf);
+
+    if (contents != NULL)
+      {
+        const guint8 *data;
+        gsize n;
+
+        /* g_file_get_contents() gurantees a trailing null byte */
+        data = g_bytes_get_data (stdout_buf, &n);
+        *contents = g_malloc (n + 1);
+        memcpy (*contents, data, n);
+        (*contents)[n] = '\0';
+      }
+  }
+
+  return TRUE;
+}
diff --git a/src/libide/io/ide-gfile.h b/src/libide/io/ide-gfile.h
new file mode 100644
index 000000000..250fec772
--- /dev/null
+++ b/src/libide/io/ide-gfile.h
@@ -0,0 +1,75 @@
+/* ide-gfile.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_IO_INSIDE) && !defined (IDE_IO_COMPILATION)
+# error "Only <libide-io.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_path_is_ignored                      (const gchar          *path);
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_g_file_is_ignored                    (GFile                *file);
+IDE_AVAILABLE_IN_3_32
+void       ide_g_file_add_ignored_pattern           (const gchar          *pattern);
+IDE_AVAILABLE_IN_3_32
+gchar     *ide_g_file_get_uncanonical_relative_path (GFile                *file,
+                                                     GFile                *other);
+IDE_AVAILABLE_IN_3_32
+void       ide_g_file_find_with_depth_async         (GFile                *file,
+                                                     const gchar          *pattern,
+                                                     guint                 max_depth,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+void       ide_g_file_find_async                    (GFile                *file,
+                                                     const gchar          *pattern,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GPtrArray *ide_g_file_find_finish                   (GFile                *file,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+IDE_AVAILABLE_IN_3_32
+void       ide_g_file_get_children_async            (GFile                *file,
+                                                     const gchar          *attributes,
+                                                     GFileQueryInfoFlags   flags,
+                                                     gint                  io_priority,
+                                                     GCancellable         *cancellable,
+                                                     GAsyncReadyCallback   callback,
+                                                     gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GPtrArray *ide_g_file_get_children_finish           (GFile                *file,
+                                                     GAsyncResult         *result,
+                                                     GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean   ide_g_host_file_get_contents             (const gchar          *path,
+                                                     gchar               **contents,
+                                                     gsize                *len,
+                                                     GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/io/ide-line-reader.c b/src/libide/io/ide-line-reader.c
new file mode 100644
index 000000000..042531e79
--- /dev/null
+++ b/src/libide/io/ide-line-reader.c
@@ -0,0 +1,100 @@
+/* ide-line-reader.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-line-reader"
+
+#include "config.h"
+
+#include <string.h>
+
+#include "ide-line-reader.h"
+
+void
+ide_line_reader_init (IdeLineReader *reader,
+                      gchar         *contents,
+                      gssize         length)
+{
+  g_assert (reader);
+
+  if (length < 0)
+    length = strlen (contents);
+
+  if (contents != NULL)
+    {
+      reader->contents = contents;
+      reader->length = length;
+      reader->pos = 0;
+    }
+  else
+    {
+      reader->contents = NULL;
+      reader->length = 0;
+      reader->pos = 0;
+    }
+}
+
+/**
+ * ide_line_reader_next:
+ * @reader: the #IdeLineReader
+ * @length: a location for the length of the line in bytes.
+ *
+ * Moves forward to the beginning of the next line in the buffer. No changes to the buffer
+ * are made, and the result is a pointer within the string passed as @contents in
+ * ide_line_reader_init(). Since the line most likely will not be terminated with a NULL byte,
+ * you must provide @length to determine the length of the line.
+ *
+ * Returns: (transfer none): The beginning of the line within the buffer.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_line_reader_next (IdeLineReader *reader,
+                      gsize         *length)
+{
+  gchar *ret = NULL;
+
+  g_assert (reader);
+  g_assert (length != NULL);
+
+  if ((reader->contents == NULL) || (reader->pos >= reader->length))
+    {
+      *length = 0;
+      return NULL;
+    }
+
+  ret = &reader->contents [reader->pos];
+
+  for (; reader->pos < reader->length; reader->pos++)
+    {
+      if (reader->contents [reader->pos] == '\n')
+        {
+          *length = &reader->contents [reader->pos] - ret;
+          /* Ingore the \r in \r\n if provided */
+          if (*length > 0 && reader->pos > 0 && reader->contents [reader->pos - 1] == '\r')
+            (*length)--;
+          reader->pos++;
+          return ret;
+        }
+    }
+
+  *length = &reader->contents [reader->pos] - ret;
+
+  return ret;
+}
diff --git a/src/libide/io/ide-line-reader.h b/src/libide/io/ide-line-reader.h
new file mode 100644
index 000000000..8e2a275fb
--- /dev/null
+++ b/src/libide/io/ide-line-reader.h
@@ -0,0 +1,42 @@
+/* ide-line-reader.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-core.h>
+
+G_BEGIN_DECLS
+
+typedef struct
+{
+  gchar  *contents;
+  gsize   length;
+  gssize  pos;
+} IdeLineReader;
+
+IDE_AVAILABLE_IN_3_32
+void   ide_line_reader_init (IdeLineReader *reader,
+                             gchar         *contents,
+                             gssize         length);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_line_reader_next (IdeLineReader *reader,
+                             gsize         *length);
+
+G_END_DECLS
diff --git a/src/libide/io/ide-marked-content.c b/src/libide/io/ide-marked-content.c
new file mode 100644
index 000000000..19f5ebf15
--- /dev/null
+++ b/src/libide/io/ide-marked-content.c
@@ -0,0 +1,236 @@
+/* ide-marked-content.c
+ *
+ * Copyright 2018-2019 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 "ide-marked-content"
+
+#include "config.h"
+
+#include "ide-marked-content.h"
+
+#define IDE_MARKED_CONTENT_MAGIC 0x81124633
+
+struct _IdeMarkedContent
+{
+  guint          magic;
+  IdeMarkedKind  kind;
+  GBytes        *data;
+  volatile gint  ref_count;
+};
+
+G_DEFINE_BOXED_TYPE (IdeMarkedContent,
+                     ide_marked_content,
+                     ide_marked_content_ref,
+                     ide_marked_content_unref)
+
+/**
+ * ide_marked_content_new:
+ * @content: a #GBytes containing the markup
+ * @kind: an #IdeMakredKind describing the markup kind
+ *
+ * Creates a new #IdeMarkedContent using the bytes provided.
+ *
+ * Returns: (transfer full): an #IdeMarkedContent
+ *
+ * Since: 3.32
+ */
+IdeMarkedContent *
+ide_marked_content_new (GBytes        *content,
+                        IdeMarkedKind  kind)
+{
+  IdeMarkedContent *self;
+
+  g_return_val_if_fail (content != NULL, NULL);
+
+  self = g_slice_new0 (IdeMarkedContent);
+  self->magic = IDE_MARKED_CONTENT_MAGIC;
+  self->ref_count = 1;
+  self->data = g_bytes_ref (content);
+  self->kind = kind;
+
+  return self;
+}
+
+/**
+ * ide_marked_content_new_plaintext:
+ * @plaintext: (nullable): a string containing the plaintext
+ *
+ * Creates a new #IdeMarkedContent of type %IDE_MARKED_KIND_PLAINTEXT
+ * with the contents of @string.
+ *
+ * Returns: (transfer full): an #IdeMarkedContent
+ *
+ * Since: 3.32
+ */
+IdeMarkedContent *
+ide_marked_content_new_plaintext (const gchar *plaintext)
+{
+  if (plaintext == NULL)
+    plaintext = "";
+
+  return ide_marked_content_new_from_data (plaintext, -1, IDE_MARKED_KIND_PLAINTEXT);
+}
+
+/**
+ * ide_marked_content_new_from_data:
+ * @data: the data for the content
+ * @len: the length of the data, or -1 to strlen() @data
+ * @kind: the kind of markup
+ *
+ * Creates a new #IdeMarkedContent from the provided data.
+ *
+ * Returns: (transfer full): an #IdeMarkedContent
+ *
+ * Since: 3.32
+ */
+IdeMarkedContent *
+ide_marked_content_new_from_data (const gchar   *data,
+                                  gssize         len,
+                                  IdeMarkedKind  kind)
+{
+  g_autoptr(GBytes) bytes = NULL;
+
+  if (len < 0)
+    len = strlen (data);
+
+  bytes = g_bytes_new (data, len);
+
+  return ide_marked_content_new (bytes, kind);
+}
+
+/**
+ * ide_marked_content_ref:
+ * @self: an #IdeMarkedContent
+ *
+ * Increments the reference count of @self by one.
+ *
+ * When a #IdeMarkedContent reaches a reference count of zero, by using
+ * ide_marked_content_unref(), it will be freed.
+ *
+ * Returns: (transfer full): @self with the reference count incremented
+ *
+ * Since: 3.32
+ */
+IdeMarkedContent *
+ide_marked_content_ref (IdeMarkedContent *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->magic == IDE_MARKED_CONTENT_MAGIC, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  g_atomic_int_inc (&self->ref_count);
+
+  return self;
+}
+
+/**
+ * ide_marked_content_unref:
+ * @self: an #IdeMarkedContent
+ *
+ * Decrements the reference count of @self by one.
+ *
+ * When the reference count of @self reaches zero, it will be freed.
+ *
+ * Since: 3.32
+ */
+void
+ide_marked_content_unref (IdeMarkedContent *self)
+{
+  g_return_if_fail (self != NULL);
+  g_return_if_fail (self->magic == IDE_MARKED_CONTENT_MAGIC);
+  g_return_if_fail (self->ref_count > 0);
+
+  if (g_atomic_int_dec_and_test (&self->ref_count))
+    {
+      self->magic = 0;
+      self->kind = 0;
+      g_clear_pointer (&self->data, g_bytes_unref);
+      g_slice_free (IdeMarkedContent, self);
+    }
+}
+
+/**
+ * ide_marked_content_get_kind:
+ * @self: an #IdeMarkedContent
+ *
+ * Gets the kind of markup that @self contains.
+ *
+ * This is used to display the content appropriately.
+ *
+ * Returns:
+ *
+ * Since: 3.32
+ */
+IdeMarkedKind
+ide_marked_content_get_kind (IdeMarkedContent *self)
+{
+  g_return_val_if_fail (self != NULL, 0);
+  g_return_val_if_fail (self->magic == IDE_MARKED_CONTENT_MAGIC, 0);
+  g_return_val_if_fail (self->ref_count > 0, 0);
+
+  return self->kind;
+}
+
+/**
+ * ide_marked_content_get_bytes:
+ *
+ * Gets the bytes for the marked content.
+ *
+ * Returns: (transfer none): a #GBytes
+ *
+ * Since: 3.32
+ */
+GBytes *
+ide_marked_content_get_bytes (IdeMarkedContent *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->magic == IDE_MARKED_CONTENT_MAGIC, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  return self->data;
+}
+
+/**
+ * ide_marked_content_as_string:
+ * @self: a #IdeMarkedContent
+ *
+ * Gets the contents of the marked content as a newly allcoated C string.
+ *
+ * Returns: (nullable): a newly allocated string or %NULL
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_marked_content_as_string (IdeMarkedContent *self)
+{
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->magic == IDE_MARKED_CONTENT_MAGIC, NULL);
+  g_return_val_if_fail (self->ref_count > 0, NULL);
+
+  if (self->data != NULL)
+    {
+      const gchar *buf;
+      gsize len;
+
+      if ((buf = g_bytes_get_data (self->data, &len)))
+        return g_strndup (buf, len);
+    }
+
+  return NULL;
+}
diff --git a/src/libide/io/ide-marked-content.h b/src/libide/io/ide-marked-content.h
new file mode 100644
index 000000000..0a5ecdba9
--- /dev/null
+++ b/src/libide/io/ide-marked-content.h
@@ -0,0 +1,67 @@
+/* ide-marked-content.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_IO_INSIDE) && !defined (IDE_IO_COMPILATION)
+# error "Only <libide-io.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_MARKED_CONTENT (ide_marked_content_get_type())
+
+typedef struct _IdeMarkedContent IdeMarkedContent;
+
+typedef enum
+{
+  IDE_MARKED_KIND_PLAINTEXT = 0,
+  IDE_MARKED_KIND_MARKDOWN  = 1,
+  IDE_MARKED_KIND_HTML      = 2,
+  IDE_MARKED_KIND_PANGO     = 3,
+} IdeMarkedKind;
+
+IDE_AVAILABLE_IN_3_32
+GType             ide_marked_content_get_type      (void);
+IDE_AVAILABLE_IN_3_32
+IdeMarkedContent *ide_marked_content_new           (GBytes           *content,
+                                                    IdeMarkedKind     kind);
+IDE_AVAILABLE_IN_3_32
+IdeMarkedContent *ide_marked_content_new_plaintext (const gchar      *plaintext);
+IDE_AVAILABLE_IN_3_32
+IdeMarkedContent *ide_marked_content_new_from_data (const gchar      *data,
+                                                    gssize            len,
+                                                    IdeMarkedKind     kind);
+IDE_AVAILABLE_IN_3_32
+GBytes           *ide_marked_content_get_bytes     (IdeMarkedContent *self);
+IDE_AVAILABLE_IN_3_32
+IdeMarkedKind     ide_marked_content_get_kind      (IdeMarkedContent *self);
+IDE_AVAILABLE_IN_3_32
+gchar            *ide_marked_content_as_string     (IdeMarkedContent *self);
+IDE_AVAILABLE_IN_3_32
+IdeMarkedContent *ide_marked_content_ref           (IdeMarkedContent *self);
+IDE_AVAILABLE_IN_3_32
+void              ide_marked_content_unref         (IdeMarkedContent *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeMarkedContent, ide_marked_content_unref)
+
+G_END_DECLS
diff --git a/src/libide/io/ide-path.c b/src/libide/io/ide-path.c
new file mode 100644
index 000000000..51bd84211
--- /dev/null
+++ b/src/libide/io/ide-path.c
@@ -0,0 +1,97 @@
+/* ide-path.c
+ *
+ * Copyright 2018-2019 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 "ide-posix"
+
+#include "config.h"
+
+#include <string.h>
+#include <unistd.h>
+#include <wordexp.h>
+
+#include "ide-path.h"
+
+/**
+ * ide_path_expand:
+ *
+ * This function will expand various "shell-like" features of the provided
+ * path using the POSIX wordexp(3) function. Command substitution will
+ * not be enabled, but path features such as ~user will be expanded.
+ *
+ * Returns: (transfer full): A newly allocated string containing the
+ *   expansion. A copy of the input string upon failure to expand.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_path_expand (const gchar *path)
+{
+  wordexp_t state = { 0 };
+  gchar *ret = NULL;
+  int r;
+
+  if (path == NULL)
+    return NULL;
+
+  r = wordexp (path, &state, WRDE_NOCMD);
+  if (r == 0 && state.we_wordc > 0)
+    ret = g_strdup (state.we_wordv [0]);
+  wordfree (&state);
+
+  if (!g_path_is_absolute (ret))
+    {
+      g_autofree gchar *freeme = ret;
+
+      ret = g_build_filename (g_get_home_dir (), freeme, NULL);
+    }
+
+  return ret;
+}
+
+/**
+ * ide_path_collapse:
+ *
+ * This function will collapse a path that starts with the users home
+ * directory into a shorthand notation using ~/ for the home directory.
+ *
+ * If the path does not have the home directory as a prefix, it will
+ * simply return a copy of @path.
+ *
+ * Returns: (transfer full): A new path, possibly collapsed.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_path_collapse (const gchar *path)
+{
+  g_autofree gchar *expanded = NULL;
+
+  if (path == NULL)
+    return NULL;
+
+  expanded = ide_path_expand (path);
+
+  if (g_str_has_prefix (expanded, g_get_home_dir ()))
+    return g_build_filename ("~",
+                             expanded + strlen (g_get_home_dir ()),
+                             NULL);
+
+  return g_steal_pointer (&expanded);
+}
diff --git a/src/libide/io/ide-path.h b/src/libide/io/ide-path.h
new file mode 100644
index 000000000..3144bc637
--- /dev/null
+++ b/src/libide/io/ide-path.h
@@ -0,0 +1,36 @@
+/* ide-path.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_IO_INSIDE) && !defined (IDE_IO_COMPILATION)
+# error "Only <libide-io.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+gchar *ide_path_collapse (const gchar *path);
+IDE_AVAILABLE_IN_3_32
+gchar *ide_path_expand   (const gchar *path);
+
+G_END_DECLS
diff --git a/src/libide/io/ide-persistent-map-builder.c b/src/libide/io/ide-persistent-map-builder.c
new file mode 100644
index 000000000..7556d87f3
--- /dev/null
+++ b/src/libide/io/ide-persistent-map-builder.c
@@ -0,0 +1,361 @@
+/* ide-persistent-map-builder.c
+ *
+ * Copyright 2017 Anoop Chandu <anoopchandu96 gmail com>
+ * Copyright 2017-2019 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 "ide-persistent-map-builder"
+
+#include "config.h"
+
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-persistent-map-builder.h"
+
+typedef struct
+{
+  /* Array of keys. */
+  GByteArray *keys;
+
+  /* Hash table of keys to remove duplicate keys. */
+  GHashTable *keys_hash;
+
+  /* Array of values. */
+  GPtrArray *values;
+  /*
+   * Array of key value pairs. This is pair of offset of
+   * key in keys array and index of value in values array.
+   */
+  GArray *kvpairs;
+
+  /* Dictionary for metadata. */
+  GVariantDict *metadata;
+
+  /* Where to write the file */
+  GFile *destination;
+} BuildState;
+
+typedef struct
+{
+  guint32 key;
+  guint32 value;
+} KVPair;
+
+struct _IdePersistentMapBuilder
+{
+  GObject parent;
+
+  /*
+   * The build state lets us keep all the contents together, and then
+   * pass it to the worker thread so the main thread can no longer access
+   * the existing state.
+   */
+  BuildState *state;
+};
+
+
+G_STATIC_ASSERT (sizeof (KVPair) == 8);
+
+G_DEFINE_TYPE (IdePersistentMapBuilder, ide_persistent_map_builder, G_TYPE_OBJECT)
+
+static void
+build_state_free (gpointer data)
+{
+  BuildState *state = data;
+
+  g_clear_pointer (&state->keys, g_byte_array_unref);
+  g_clear_pointer (&state->keys_hash, g_hash_table_unref);
+  g_clear_pointer (&state->values, g_ptr_array_unref);
+  g_clear_pointer (&state->kvpairs, g_array_unref);
+  g_clear_pointer (&state->metadata, g_variant_dict_unref);
+  g_clear_object (&state->destination);
+  g_slice_free (BuildState, state);
+}
+
+void
+ide_persistent_map_builder_insert (IdePersistentMapBuilder *self,
+                                   const gchar             *key,
+                                   GVariant                *value,
+                                   gboolean                 replace)
+{
+  g_autoptr(GVariant) local_value = NULL;
+  guint32 value_index;
+
+  g_return_if_fail (IDE_IS_PERSISTENT_MAP_BUILDER (self));
+  g_return_if_fail (key != NULL);
+  g_return_if_fail (value != NULL);
+  g_return_if_fail (self->state != NULL);
+  g_return_if_fail (self->state->keys_hash != NULL);
+  g_return_if_fail (self->state->values != NULL);
+  g_return_if_fail (self->state->kvpairs != NULL);
+
+  local_value = g_variant_ref_sink (value);
+
+  if (0 != (value_index = GPOINTER_TO_UINT (g_hash_table_lookup (self->state->keys_hash, key))))
+    {
+      if (replace)
+        {
+          g_variant_unref (g_ptr_array_index (self->state->values, value_index - 1));
+          g_ptr_array_index (self->state->values, value_index - 1) = g_steal_pointer (&local_value);
+        }
+    }
+  else
+    {
+      KVPair kvpair;
+
+      kvpair.key = self->state->keys->len;
+      kvpair.value = self->state->values->len;
+
+      g_byte_array_append (self->state->keys, (const guchar *)key, strlen (key) + 1);
+      g_ptr_array_add (self->state->values, g_steal_pointer (&local_value));
+      g_array_append_val (self->state->kvpairs, kvpair);
+
+      /*
+       * Key in hashtable is the actual key in our map.
+       * Value in hash table will point to element in values array
+       * where actual value of key in our map is there.
+       */
+      g_hash_table_insert (self->state->keys_hash,
+                           g_strdup (key),
+                           GUINT_TO_POINTER (kvpair.value + 1));
+    }
+}
+
+void
+ide_persistent_map_builder_set_metadata_int64 (IdePersistentMapBuilder *self,
+                                               const gchar             *key,
+                                               gint64                   value)
+{
+  g_return_if_fail (IDE_IS_PERSISTENT_MAP_BUILDER (self));
+  g_return_if_fail (key != NULL);
+  g_return_if_fail (self->state != NULL);
+  g_return_if_fail (self->state->metadata != NULL);
+
+  g_variant_dict_insert (self->state->metadata, key, "x", value);
+}
+
+static gint
+compare_keys (KVPair      *a,
+              KVPair      *b,
+              const gchar *keys)
+{
+  return g_strcmp0 (keys + a->key, keys + b->key);
+}
+
+static void
+ide_persistent_map_builder_write_worker (IdeTask      *task,
+                                         gpointer      source_object,
+                                         gpointer      task_data,
+                                         GCancellable *cancellable)
+{
+  BuildState *state = task_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) data = NULL;
+  GVariantDict dict;
+  GVariant *keys;
+  GVariant *values;
+  GVariant *kvpairs;
+  GVariant *metadata;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_PERSISTENT_MAP_BUILDER (source_object));
+  g_assert (state != NULL);
+  g_assert (state->keys != NULL);
+  g_assert (state->keys_hash != NULL);
+  g_assert (state->values != NULL);
+  g_assert (state->kvpairs != NULL);
+  g_assert (state->metadata != NULL);
+  g_assert (G_IS_FILE (state->destination));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (ide_task_return_error_if_cancelled (task))
+    return;
+
+  if (state->keys->len == 0)
+    {
+      g_autofree gchar *path = g_file_get_path (state->destination);
+
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_DATA,
+                                 "No entries to write for \"%s\"",
+                                 path);
+      return;
+    }
+
+  g_variant_dict_init (&dict, NULL);
+
+  keys = g_variant_new_fixed_array (G_VARIANT_TYPE_BYTE,
+                                    state->keys->data,
+                                    state->keys->len,
+                                    sizeof (guint8));
+
+  values = g_variant_new_array (NULL,
+                                (GVariant * const *)(gpointer)state->values->pdata,
+                                state->values->len);
+
+  g_array_sort_with_data (state->kvpairs,
+                          (GCompareDataFunc)compare_keys,
+                          state->keys->data);
+
+  kvpairs = g_variant_new_fixed_array (G_VARIANT_TYPE ("(uu)"),
+                                       state->kvpairs->data,
+                                       state->kvpairs->len,
+                                       sizeof (KVPair));
+
+  metadata = g_variant_dict_end (state->metadata);
+
+  g_variant_dict_insert_value (&dict, "keys", keys);
+  g_variant_dict_insert_value (&dict, "values", values);
+  g_variant_dict_insert_value (&dict, "kvpairs", kvpairs);
+  g_variant_dict_insert_value (&dict, "metadata", metadata);
+  g_variant_dict_insert (&dict, "version", "i", 2);
+  g_variant_dict_insert (&dict, "byte-order", "i", G_BYTE_ORDER);
+
+  data = g_variant_take_ref (g_variant_dict_end (&dict));
+
+  if (ide_task_return_error_if_cancelled (task))
+    return;
+
+  if (g_file_replace_contents (state->destination,
+                               g_variant_get_data (data),
+                               g_variant_get_size (data),
+                               NULL,
+                               FALSE,
+                               G_FILE_CREATE_NONE,
+                               NULL,
+                               cancellable,
+                               &error))
+    ide_task_return_boolean (task, TRUE);
+  else
+    ide_task_return_error (task, g_steal_pointer (&error));
+}
+
+gboolean
+ide_persistent_map_builder_write (IdePersistentMapBuilder  *self,
+                                  GFile                    *destination,
+                                  gint                      io_priority,
+                                  GCancellable             *cancellable,
+                                  GError                  **error)
+{
+  g_autoptr(IdeTask) task = NULL;
+  BuildState *state;
+
+  g_return_val_if_fail (IDE_IS_PERSISTENT_MAP_BUILDER (self), FALSE);
+  g_return_val_if_fail (G_IS_FILE (destination), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+  g_return_val_if_fail (self->state != NULL, FALSE);
+  g_return_val_if_fail (self->state->destination == NULL, FALSE);
+
+  state = g_steal_pointer (&self->state);
+  state->destination = g_object_ref (destination);
+
+  task = ide_task_new (self, cancellable, NULL, NULL);
+  ide_task_set_source_tag (task, ide_persistent_map_builder_write);
+  ide_task_set_priority (task, io_priority);
+  ide_task_set_kind (task, IDE_TASK_KIND_INDEXER);
+  ide_persistent_map_builder_write_worker (task, self, state, cancellable);
+
+  build_state_free (state);
+
+  return ide_task_propagate_boolean (task, error);
+}
+
+void
+ide_persistent_map_builder_write_async (IdePersistentMapBuilder *self,
+                                        GFile                   *destination,
+                                        gint                     io_priority,
+                                        GCancellable            *cancellable,
+                                        GAsyncReadyCallback      callback,
+                                        gpointer                 user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_PERSISTENT_MAP_BUILDER (self));
+  g_return_if_fail (G_IS_FILE (destination));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (self->state != NULL);
+  g_return_if_fail (self->state->destination != NULL);
+
+  self->state->destination = g_object_ref (destination);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_priority (task, io_priority);
+  ide_task_set_source_tag (task, ide_persistent_map_builder_write_async);
+  ide_task_set_kind (task, IDE_TASK_KIND_INDEXER);
+  ide_task_set_task_data (task, g_steal_pointer (&self->state), build_state_free);
+  ide_task_run_in_thread (task, ide_persistent_map_builder_write_worker);
+}
+
+/**
+ * ide_persistent_map_builder_write_finish:
+ * @self: an #IdePersistentMapBuilder
+ * @result: a #GAsyncResult provided to callback
+ * @error: location for a #GError, or %NULL
+ *
+ * Returns: %TRUE if the while was written successfully; otherwise %FALSE
+ *   and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_persistent_map_builder_write_finish (IdePersistentMapBuilder  *self,
+                                         GAsyncResult             *result,
+                                         GError                  **error)
+{
+  g_return_val_if_fail (IDE_IS_PERSISTENT_MAP_BUILDER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_persistent_map_builder_finalize (GObject *object)
+{
+  IdePersistentMapBuilder *self = (IdePersistentMapBuilder *)object;
+
+  g_clear_pointer (&self->state, build_state_free);
+
+  G_OBJECT_CLASS (ide_persistent_map_builder_parent_class)->finalize (object);
+}
+
+static void
+ide_persistent_map_builder_init (IdePersistentMapBuilder *self)
+{
+  self->state = g_slice_new0 (BuildState);
+  self->state->keys = g_byte_array_new ();
+  self->state->keys_hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+  self->state->values = g_ptr_array_new_with_free_func ((GDestroyNotify)g_variant_unref);
+  self->state->kvpairs = g_array_new (FALSE, FALSE, sizeof (KVPair));
+  self->state->metadata = g_variant_dict_new (NULL);
+}
+
+static void
+ide_persistent_map_builder_class_init (IdePersistentMapBuilderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_persistent_map_builder_finalize;
+}
+
+IdePersistentMapBuilder *
+ide_persistent_map_builder_new (void)
+{
+  return g_object_new (IDE_TYPE_PERSISTENT_MAP_BUILDER, NULL);
+}
diff --git a/src/libide/io/ide-persistent-map-builder.h b/src/libide/io/ide-persistent-map-builder.h
new file mode 100644
index 000000000..19bbb84e0
--- /dev/null
+++ b/src/libide/io/ide-persistent-map-builder.h
@@ -0,0 +1,62 @@
+/* ide-persistent-map-builder.h
+ *
+ * Copyright 2017 Anoop Chandu <anoopchandu96 gmail com>
+ * Copyright 2017-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PERSISTENT_MAP_BUILDER (ide_persistent_map_builder_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdePersistentMapBuilder, ide_persistent_map_builder, IDE, PERSISTENT_MAP_BUILDER, 
GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdePersistentMapBuilder *ide_persistent_map_builder_new                (void);
+IDE_AVAILABLE_IN_3_32
+void                     ide_persistent_map_builder_insert             (IdePersistentMapBuilder  *self,
+                                                                        const gchar              *key,
+                                                                        GVariant                 *value,
+                                                                        gboolean                  replace);
+IDE_AVAILABLE_IN_3_32
+void                     ide_persistent_map_builder_set_metadata_int64 (IdePersistentMapBuilder  *self,
+                                                                        const gchar              *key,
+                                                                        gint64                    value);
+IDE_AVAILABLE_IN_3_32
+gboolean                 ide_persistent_map_builder_write              (IdePersistentMapBuilder  *self,
+                                                                        GFile                    
*destination,
+                                                                        gint                      
io_priority,
+                                                                        GCancellable             
*cancellable,
+                                                                        GError                  **error);
+IDE_AVAILABLE_IN_3_32
+void                     ide_persistent_map_builder_write_async        (IdePersistentMapBuilder  *self,
+                                                                        GFile                    
*destination,
+                                                                        gint                      
io_priority,
+                                                                        GCancellable             
*cancellable,
+                                                                        GAsyncReadyCallback       callback,
+                                                                        gpointer                  user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean                 ide_persistent_map_builder_write_finish       (IdePersistentMapBuilder  *self,
+                                                                        GAsyncResult             *result,
+                                                                        GError                  **error);
+
+G_END_DECLS
diff --git a/src/libide/io/ide-persistent-map.c b/src/libide/io/ide-persistent-map.c
new file mode 100644
index 000000000..4bf7bb219
--- /dev/null
+++ b/src/libide/io/ide-persistent-map.c
@@ -0,0 +1,360 @@
+/* ide-persistent-map.c
+ *
+ * Copyright 2017 Anoop Chandu <anoopchandu96 gmail com>
+ * Copyright 2017-2019 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 "ide-persistent-map"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-persistent-map.h"
+
+typedef struct
+{
+  guint32 key;
+  guint32 value;
+} KVPair;
+
+struct _IdePersistentMap
+{
+  GObject            parent;
+
+  GMappedFile       *mapped_file;
+
+  GVariant          *data;
+
+  GVariant          *keys_var;
+  const gchar       *keys;
+
+  GVariant          *values;
+
+  GVariant          *kvpairs_var;
+  const KVPair      *kvpairs;
+
+  GVariantDict      *metadata;
+
+  gsize              n_kvpairs;
+
+  gint32             byte_order;
+
+  guint              load_called : 1;
+  guint              loaded : 1;
+};
+
+G_STATIC_ASSERT (sizeof (KVPair) == 8);
+
+G_DEFINE_TYPE (IdePersistentMap, ide_persistent_map, G_TYPE_OBJECT)
+
+static void
+ide_persistent_map_load_file_worker (IdeTask      *task,
+                                     gpointer      source_object,
+                                     gpointer      task_data,
+                                     GCancellable *cancellable)
+{
+  IdePersistentMap *self = source_object;
+  GFile *file = task_data;
+  g_autofree gchar *path = NULL;
+  g_autoptr(GMappedFile) mapped_file = NULL;
+  g_autoptr(GVariant) data = NULL;
+  g_autoptr(GVariant) keys = NULL;
+  g_autoptr(GVariant) values = NULL;
+  g_autoptr(GVariant) metadata = NULL;
+  g_autoptr(GVariant) kvpairs = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariantDict) dict = NULL;
+  gint32 version;
+  gsize n_elements;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_PERSISTENT_MAP (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (self->loaded == FALSE);
+
+  self->loaded = TRUE;
+
+  if (!g_file_is_native (file) || NULL == (path = g_file_get_path (file)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_FILENAME,
+                                 "Index must be a local file");
+      return;
+    }
+
+  mapped_file = g_mapped_file_new (path, FALSE, &error);
+
+  if (mapped_file == NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  data = g_variant_new_from_data (G_VARIANT_TYPE_VARDICT,
+                                  g_mapped_file_get_contents (mapped_file),
+                                  g_mapped_file_get_length (mapped_file),
+                                  FALSE, NULL, NULL);
+
+  if (data == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVAL,
+                                 "Failed to parse GVariant");
+      return;
+    }
+
+  g_variant_take_ref (data);
+
+  dict = g_variant_dict_new (data);
+
+  if (!g_variant_dict_lookup (dict, "version", "i", &version) || version != 2)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVAL,
+                                 "Version mismatch in gvariant. Got %d, expected 1",
+                                 version);
+      return;
+    }
+
+  keys = g_variant_dict_lookup_value (dict, "keys", G_VARIANT_TYPE_ARRAY);
+  values = g_variant_dict_lookup_value (dict, "values", G_VARIANT_TYPE_ARRAY);
+  kvpairs = g_variant_dict_lookup_value (dict, "kvpairs", G_VARIANT_TYPE_ARRAY);
+  metadata = g_variant_dict_lookup_value (dict, "metadata", G_VARIANT_TYPE_VARDICT);
+
+  if (!g_variant_dict_lookup (dict, "byte-order", "i", &self->byte_order))
+    self->byte_order = G_BYTE_ORDER;
+
+  if (keys == NULL || values == NULL || kvpairs == NULL || metadata == NULL || !self->byte_order)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVAL,
+                                 "Invalid GVariant index");
+      return;
+    }
+
+  self->keys = g_variant_get_fixed_array (keys, &n_elements, sizeof (guint8));
+  self->kvpairs = g_variant_get_fixed_array (kvpairs, &self->n_kvpairs, sizeof (KVPair));
+
+  self->mapped_file = g_steal_pointer (&mapped_file);
+  self->data = g_steal_pointer (&data);
+  self->keys_var = g_steal_pointer (&keys);
+  self->values = g_steal_pointer (&values);
+  self->kvpairs_var = g_steal_pointer (&kvpairs);
+  self->metadata = g_variant_dict_new (metadata);
+
+  g_assert (!g_variant_is_floating (self->data));
+  g_assert (!g_variant_is_floating (self->keys_var));
+  g_assert (!g_variant_is_floating (self->values));
+  g_assert (!g_variant_is_floating (self->kvpairs_var));
+  g_assert (self->keys != NULL);
+  g_assert (self->kvpairs != NULL);
+  g_assert (self->metadata != NULL);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+gboolean
+ide_persistent_map_load_file (IdePersistentMap *self,
+                              GFile            *file,
+                              GCancellable     *cancellable,
+                              GError          **error)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_val_if_fail (IDE_IS_PERSISTENT_MAP (self), FALSE);
+  g_return_val_if_fail (self->load_called == FALSE, FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  self->load_called = TRUE;
+
+  task = ide_task_new (self, cancellable, NULL, NULL);
+  ide_task_set_source_tag (task, ide_persistent_map_load_file);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_kind (task, IDE_TASK_KIND_INDEXER);
+  ide_persistent_map_load_file_worker (task, self, file, cancellable);
+
+  return ide_task_propagate_boolean (task, error);
+}
+
+void
+ide_persistent_map_load_file_async (IdePersistentMap    *self,
+                                    GFile               *file,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_PERSISTENT_MAP (self));
+  g_return_if_fail (self->load_called == FALSE);
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  self->load_called = TRUE;
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_persistent_map_load_file_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_kind (task, IDE_TASK_KIND_INDEXER);
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
+  ide_task_run_in_thread (task, ide_persistent_map_load_file_worker);
+}
+
+/**
+ * ide_persistent_map_load_file_finish:
+ * @self: an #IdePersistentMap
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Returns: Whether file is loaded or not.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_persistent_map_load_file_finish (IdePersistentMap  *self,
+                                     GAsyncResult      *result,
+                                     GError           **error)
+{
+  g_return_val_if_fail (IDE_IS_PERSISTENT_MAP (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_persistent_map_lookup_value:
+ * @self: An #IdePersistentMap instance.
+ * @key: key to lookup value
+ *
+ * Returns: (transfer full) : value associalted with @key.
+ *
+ * Since: 3.32
+ */
+GVariant *
+ide_persistent_map_lookup_value (IdePersistentMap *self,
+                                 const gchar      *key)
+{
+  g_autoptr(GVariant) value = NULL;
+  gint64 l;
+  gint64 r;
+
+  g_return_val_if_fail (IDE_IS_PERSISTENT_MAP (self), NULL);
+  g_return_val_if_fail (self->loaded, NULL);
+  g_return_val_if_fail (self != NULL, NULL);
+  g_return_val_if_fail (self->kvpairs != NULL, NULL);
+  g_return_val_if_fail (self->keys != NULL, NULL);
+  g_return_val_if_fail (self->values != NULL, NULL);
+  g_return_val_if_fail (self->n_kvpairs < G_MAXINT64, NULL);
+
+  if (self->n_kvpairs == 0)
+    return NULL;
+
+  /* unsigned long to signed long */
+  r = (gint64)self->n_kvpairs - 1;
+  l = 0;
+
+  while (l <= r)
+    {
+      gint64 m;
+      gint32 k;
+      gint cmp;
+
+      m = (l + r) / 2;
+      g_assert (m >= 0);
+
+      k = self->kvpairs [m].key;
+      g_assert (k >= 0);
+
+      cmp = g_strcmp0 (key, &self->keys [k]);
+
+      if (cmp < 0)
+        r = m - 1;
+      else if (cmp > 0)
+        l = m + 1;
+      else
+        {
+          value = g_variant_get_child_value (self->values, self->kvpairs [m].value);
+          break;
+        }
+    }
+
+  if (value != NULL && self->byte_order != G_BYTE_ORDER)
+    return g_variant_byteswap (value);
+
+  return g_steal_pointer (&value);
+}
+
+gint64
+ide_persistent_map_builder_get_metadata_int64 (IdePersistentMap *self,
+                                               const gchar      *key)
+{
+  guint64 value = 0;
+
+  g_return_val_if_fail (IDE_IS_PERSISTENT_MAP (self), 0);
+  g_return_val_if_fail (key != NULL, 0);
+  g_return_val_if_fail (self->metadata != NULL, 0);
+
+  if (!g_variant_dict_lookup (self->metadata, key, "x", &value))
+    return 0;
+
+  return value;
+}
+
+static void
+ide_persistent_map_finalize (GObject *object)
+{
+  IdePersistentMap *self = (IdePersistentMap *)object;
+
+  self->keys = NULL;
+  self->kvpairs = NULL;
+
+  g_clear_pointer (&self->data, g_variant_unref);
+  g_clear_pointer (&self->keys_var, g_variant_unref);
+  g_clear_pointer (&self->values, g_variant_unref);
+  g_clear_pointer (&self->kvpairs_var, g_variant_unref);
+  g_clear_pointer (&self->metadata, g_variant_dict_unref);
+  g_clear_pointer (&self->mapped_file, g_mapped_file_unref);
+
+  G_OBJECT_CLASS (ide_persistent_map_parent_class)->finalize (object);
+}
+
+static void
+ide_persistent_map_init (IdePersistentMap *self)
+{
+}
+
+static void
+ide_persistent_map_class_init (IdePersistentMapClass *self)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (self);
+
+  object_class->finalize = ide_persistent_map_finalize;
+}
+
+IdePersistentMap *
+ide_persistent_map_new (void)
+{
+  return g_object_new (IDE_TYPE_PERSISTENT_MAP, NULL);
+}
diff --git a/src/libide/io/ide-persistent-map.h b/src/libide/io/ide-persistent-map.h
new file mode 100644
index 000000000..8589c9068
--- /dev/null
+++ b/src/libide/io/ide-persistent-map.h
@@ -0,0 +1,53 @@
+/* ide-persistent-map.h
+ *
+ * Copyright 2017 Anoop Chandu <anoopchandu96 gmail com>
+ * Copyright 2017-2019 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-core.h>
+
+#define IDE_TYPE_PERSISTENT_MAP (ide_persistent_map_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdePersistentMap, ide_persistent_map, IDE, PERSISTENT_MAP, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdePersistentMap *ide_persistent_map_new                        (void);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_persistent_map_load_file                  (IdePersistentMap     *self,
+                                                                 GFile                *file,
+                                                                 GCancellable         *cancellable,
+                                                                 GError              **error);
+IDE_AVAILABLE_IN_3_32
+void              ide_persistent_map_load_file_async            (IdePersistentMap     *self,
+                                                                 GFile                *file,
+                                                                 GCancellable         *cancellable,
+                                                                 GAsyncReadyCallback   callback,
+                                                                 gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_persistent_map_load_file_finish           (IdePersistentMap     *self,
+                                                                 GAsyncResult         *result,
+                                                                 GError              **error);
+IDE_AVAILABLE_IN_3_32
+GVariant         *ide_persistent_map_lookup_value               (IdePersistentMap     *self,
+                                                                 const gchar          *key);
+IDE_AVAILABLE_IN_3_32
+gint64            ide_persistent_map_builder_get_metadata_int64 (IdePersistentMap     *self,
+                                                                 const gchar          *key);
diff --git a/src/libide/io/ide-pkcon-transfer.c b/src/libide/io/ide-pkcon-transfer.c
new file mode 100644
index 000000000..76c819b17
--- /dev/null
+++ b/src/libide/io/ide-pkcon-transfer.c
@@ -0,0 +1,279 @@
+/* ide-pkcon-transfer.c
+ *
+ * Copyright 2017-2019 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 "ide-pkcon-transfer"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+
+#include "ide-pkcon-transfer.h"
+
+struct _IdePkconTransfer
+{
+  IdeTransfer   parent;
+  gchar       **packages;
+  gchar        *status;
+};
+
+enum {
+  PROP_0,
+  PROP_PACKAGES,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdePkconTransfer, ide_pkcon_transfer, IDE_TYPE_TRANSFER)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_pkcon_transfer_update_title (IdePkconTransfer *self)
+{
+  g_autofree gchar *title = NULL;
+  guint count;
+
+  g_assert (IDE_IS_PKCON_TRANSFER (self));
+
+  count = g_strv_length (self->packages);
+  title = g_strdup_printf (ngettext ("Installing %u package", "Installing %u packages", count), count);
+  ide_transfer_set_title (IDE_TRANSFER (self), title);
+}
+
+static void
+ide_pkcon_transfer_wait_check_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  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));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_pkcon_transfer_read_line_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  GDataInputStream *stream = (GDataInputStream *)object;
+  g_autoptr(IdePkconTransfer) self = user_data;
+  g_autofree gchar *line = NULL;
+  g_auto(GStrv) parts = NULL;
+  gsize len;
+
+  g_assert (G_IS_DATA_INPUT_STREAM (stream));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_PKCON_TRANSFER (self));
+
+  if (!(line = g_data_input_stream_read_line_finish_utf8 (stream, result, &len, NULL)))
+    return;
+
+  parts = g_strsplit (line, ":", 2);
+
+  if (parts[0]) g_strstrip (parts[0]);
+  if (parts[1]) g_strstrip (parts[1]);
+
+  if (g_strcmp0 (parts[0], "Status") == 0)
+    ide_transfer_set_status (IDE_TRANSFER (self), parts[1]);
+  else if (g_strcmp0 (parts[0], "Percentage") == 0 && parts[1])
+    ide_transfer_set_progress (IDE_TRANSFER (self), g_strtod (parts[1], NULL) / 100.0);
+
+  g_data_input_stream_read_line_async (stream,
+                                       G_PRIORITY_DEFAULT,
+                                       NULL,
+                                       ide_pkcon_transfer_read_line_cb,
+                                       g_object_ref (self));
+}
+
+static void
+ide_pkcon_transfer_execute_async (IdeTransfer         *transfer,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  IdePkconTransfer *self = (IdePkconTransfer *)transfer;
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(GDataInputStream) data_stream = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  GInputStream *stdout_stream;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER (transfer));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_pkcon_transfer_execute_async);
+
+  if (self->packages == NULL || !self->packages[0])
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE);
+  ide_subprocess_launcher_set_run_on_host (launcher, TRUE);
+  ide_subprocess_launcher_push_argv (launcher, "pkcon");
+  ide_subprocess_launcher_push_argv (launcher, "install");
+  ide_subprocess_launcher_push_argv (launcher, "-y");
+  ide_subprocess_launcher_push_argv (launcher, "-p");
+
+  for (guint i = 0; self->packages[i]; i++)
+    ide_subprocess_launcher_push_argv (launcher, self->packages[i]);
+
+  subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error);
+
+  if (subprocess == NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  stdout_stream = ide_subprocess_get_stdout_pipe (subprocess);
+  data_stream = g_data_input_stream_new (stdout_stream);
+
+  g_data_input_stream_read_line_async (data_stream,
+                                       G_PRIORITY_DEFAULT,
+                                       cancellable,
+                                       ide_pkcon_transfer_read_line_cb,
+                                       g_object_ref (self));
+
+  ide_subprocess_wait_check_async (subprocess,
+                                   cancellable,
+                                   ide_pkcon_transfer_wait_check_cb,
+                                   g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_pkcon_transfer_execute_finish (IdeTransfer   *transfer,
+                                   GAsyncResult  *result,
+                                   GError       **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TRANSFER (transfer));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_pkcon_transfer_finalize (GObject *object)
+{
+  IdePkconTransfer *self = (IdePkconTransfer *)object;
+
+  g_clear_pointer (&self->packages, g_strfreev);
+  g_clear_pointer (&self->status, g_free);
+
+  G_OBJECT_CLASS (ide_pkcon_transfer_parent_class)->finalize (object);
+}
+
+static void
+ide_pkcon_transfer_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  IdePkconTransfer *self = IDE_PKCON_TRANSFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_PACKAGES:
+      g_value_set_boxed (value, self->packages);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_pkcon_transfer_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  IdePkconTransfer *self = IDE_PKCON_TRANSFER (object);
+
+  switch (prop_id)
+    {
+    case PROP_PACKAGES:
+      self->packages = g_value_dup_boxed (value);
+      ide_pkcon_transfer_update_title (self);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_pkcon_transfer_class_init (IdePkconTransferClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeTransferClass *transfer_class = IDE_TRANSFER_CLASS (klass);
+
+  object_class->finalize = ide_pkcon_transfer_finalize;
+  object_class->get_property = ide_pkcon_transfer_get_property;
+  object_class->set_property = ide_pkcon_transfer_set_property;
+
+  transfer_class->execute_async = ide_pkcon_transfer_execute_async;
+  transfer_class->execute_finish = ide_pkcon_transfer_execute_finish;
+
+  properties [PROP_PACKAGES] =
+    g_param_spec_boxed ("packages",
+                        "Packages",
+                        "The package names to be installed",
+                        G_TYPE_STRV,
+                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_pkcon_transfer_init (IdePkconTransfer *self)
+{
+  ide_transfer_set_icon_name (IDE_TRANSFER (self), "system-software-install-symbolic");
+}
+
+IdePkconTransfer *
+ide_pkcon_transfer_new (const gchar * const *packages)
+{
+  return g_object_new (IDE_TYPE_PKCON_TRANSFER,
+                       "packages", packages,
+                       NULL);
+}
diff --git a/src/libide/io/ide-pkcon-transfer.h b/src/libide/io/ide-pkcon-transfer.h
new file mode 100644
index 000000000..fe798e5ed
--- /dev/null
+++ b/src/libide/io/ide-pkcon-transfer.h
@@ -0,0 +1,39 @@
+/* ide-pkcon-transfer.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_IO_INSIDE) && !defined (IDE_IO_COMPILATION)
+# error "Only <libide-io.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PKCON_TRANSFER (ide_pkcon_transfer_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdePkconTransfer, ide_pkcon_transfer, IDE, PKCON_TRANSFER, IdeTransfer)
+
+IDE_AVAILABLE_IN_3_32
+IdePkconTransfer *ide_pkcon_transfer_new (const gchar * const *packages);
+
+G_END_DECLS
diff --git a/src/libide/io/ide-pty-intercept.c b/src/libide/io/ide-pty-intercept.c
new file mode 100644
index 000000000..c498ce64f
--- /dev/null
+++ b/src/libide/io/ide-pty-intercept.c
@@ -0,0 +1,639 @@
+/* ide-pty-intercept.c
+ *
+ * Copyright 2018-2019 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
+ */
+
+#ifndef _GNU_SOURCE
+# define _GNU_SOURCE
+#endif
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <glib-unix.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "ide-pty-intercept.h"
+
+/*
+ * We really don't need all that much. A PTY on Linux has a some amount of
+ * kernel memory that is non-pageable and therefore small in size. 4k is what
+ * it appears to be. Anything more than that is really just an opportunity for
+ * us to break some deadlock scenarios.
+ */
+#define CHANNEL_BUFFER_SIZE (4096 * 4)
+#define SLAVE_READ_PRIORITY   G_PRIORITY_HIGH
+#define SLAVE_WRITE_PRIORITY  G_PRIORITY_DEFAULT_IDLE
+#define MASTER_READ_PRIORITY  G_PRIORITY_DEFAULT_IDLE
+#define MASTER_WRITE_PRIORITY G_PRIORITY_HIGH
+
+static void     _ide_pty_intercept_side_close (IdePtyInterceptSide *side);
+static gboolean _ide_pty_intercept_in_cb      (GIOChannel          *channel,
+                                               GIOCondition         condition,
+                                               gpointer             user_data);
+static gboolean _ide_pty_intercept_out_cb     (GIOChannel          *channel,
+                                               GIOCondition         condition,
+                                               gpointer             user_data);
+static void     clear_source                  (guint               *source_id);
+
+static gboolean
+_ide_pty_intercept_set_raw (IdePtyFd fd)
+{
+  struct termios t;
+
+  if (tcgetattr (fd, &t) == -1)
+    return FALSE;
+
+  t.c_lflag &= ~(ICANON | ISIG | IEXTEN | ECHO);
+  t.c_iflag &= ~(BRKINT | ICRNL | IGNBRK | IGNCR | INLCR | INPCK | ISTRIP | IXON | PARMRK);
+  t.c_oflag &= ~(OPOST);
+  t.c_cc[VMIN] = 1;
+  t.c_cc[VTIME] = 0;
+
+  if (tcsetattr (fd, TCSAFLUSH, &t) == -1)
+    return FALSE;
+
+  return TRUE;
+}
+
+/**
+ * ide_pty_intercept_create_slave:
+ * @master_fd: a pty master
+ * @blocking: use %FALSE to set O_NONBLOCK
+ *
+ * This creates a new slave to the PTY master @master_fd.
+ *
+ * This uses grantpt(), unlockpt(), and ptsname() to open a new
+ * PTY slave.
+ *
+ * Returns: a FD for the slave PTY that should be closed with close().
+ *   Upon error, %IDE_PTY_FD_INVALID (-1) is returned.
+ *
+ * Since: 3.32
+ */
+IdePtyFd
+ide_pty_intercept_create_slave (IdePtyFd master_fd,
+                                gboolean blocking)
+{
+  g_auto(IdePtyFd) ret = IDE_PTY_FD_INVALID;
+  gint extra = blocking ? 0 : O_NONBLOCK;
+#if defined(HAVE_PTSNAME_R) || defined(__FreeBSD__)
+  char name[256];
+#else
+  const char *name;
+#endif
+
+  g_assert (master_fd != -1);
+
+  if (grantpt (master_fd) != 0)
+    return IDE_PTY_FD_INVALID;
+
+  if (unlockpt (master_fd) != 0)
+    return IDE_PTY_FD_INVALID;
+
+#ifdef HAVE_PTSNAME_R
+  if (ptsname_r (master_fd, name, sizeof name - 1) != 0)
+    return IDE_PTY_FD_INVALID;
+  name[sizeof name - 1] = '\0';
+#elif defined(__FreeBSD__)
+  if (fdevname_r (master_fd, name + 5, sizeof name - 6) == NULL)
+    return IDE_PTY_FD_INVALID;
+  memcpy (name, "/dev/", 5);
+  name[sizeof name - 1] = '\0';
+#else
+  if (NULL == (name = ptsname (master_fd)))
+    return IDE_PTY_FD_INVALID;
+#endif
+
+  ret = open (name, O_RDWR | O_CLOEXEC | extra);
+
+  if (ret == IDE_PTY_FD_INVALID && errno == EINVAL)
+    {
+      gint flags;
+
+      ret = open (name, O_RDWR | O_CLOEXEC);
+      if (ret == IDE_PTY_FD_INVALID && errno == EINVAL)
+        ret = open (name, O_RDWR);
+
+      if (ret == IDE_PTY_FD_INVALID)
+        return IDE_PTY_FD_INVALID;
+
+      /* Add FD_CLOEXEC if O_CLOEXEC failed */
+      flags = fcntl (ret, F_GETFD, 0);
+      if ((flags & FD_CLOEXEC) == 0)
+        {
+          if (fcntl (ret, F_SETFD, flags | FD_CLOEXEC) < 0)
+            return IDE_PTY_FD_INVALID;
+        }
+
+      if (!blocking)
+        {
+          if (!g_unix_set_fd_nonblocking (ret, TRUE, NULL))
+            return IDE_PTY_FD_INVALID;
+        }
+    }
+
+  return pty_fd_steal (&ret);
+}
+
+/**
+ * ide_pty_intercept_create_master:
+ *
+ * Creates a new PTY master using posix_openpt(). Some fallbacks are
+ * provided for non-Linux systems where O_CLOEXEC and O_NONBLOCK may
+ * not be supported.
+ *
+ * Returns: a FD that should be closed with close() if successful.
+ *   Upon error, %IDE_PTY_FD_INVALID (-1) is returned.
+ *
+ * Since: 3.32
+ */
+IdePtyFd
+ide_pty_intercept_create_master (void)
+{
+  g_auto(IdePtyFd) master_fd = IDE_PTY_FD_INVALID;
+
+  master_fd = posix_openpt (O_RDWR | O_NOCTTY | O_NONBLOCK | O_CLOEXEC);
+
+#ifndef __linux__
+  /* Fallback for operating systems that don't support
+   * O_NONBLOCK and O_CLOEXEC when opening.
+   */
+  if (master_fd == IDE_PTY_FD_INVALID && errno == EINVAL)
+    {
+      master_fd = posix_openpt (O_RDWR | O_NOCTTY | O_CLOEXEC);
+
+      if (master_fd == IDE_PTY_FD_INVALID && errno == EINVAL)
+        {
+          gint flags;
+
+          master_fd = posix_openpt (O_RDWR | O_NOCTTY);
+          if (master_fd == -1)
+            return IDE_PTY_FD_INVALID;
+
+          flags = fcntl (master_fd, F_GETFD, 0);
+          if (flags < 0)
+            return IDE_PTY_FD_INVALID;
+
+          if (fcntl (master_fd, F_SETFD, flags | FD_CLOEXEC) < 0)
+            return IDE_PTY_FD_INVALID;
+        }
+
+      if (!g_unix_set_fd_nonblocking (master_fd, TRUE, NULL))
+        return IDE_PTY_FD_INVALID;
+    }
+#endif
+
+  return pty_fd_steal (&master_fd);
+}
+
+static void
+clear_source (guint *source_id)
+{
+  guint id = *source_id;
+  *source_id = 0;
+  if (id != 0)
+    g_source_remove (id);
+}
+
+static void
+_ide_pty_intercept_side_close (IdePtyInterceptSide *side)
+{
+  g_assert (side != NULL);
+
+  clear_source (&side->in_watch);
+  clear_source (&side->out_watch);
+  g_clear_pointer (&side->channel, g_io_channel_unref);
+  g_clear_pointer (&side->out_bytes, g_bytes_unref);
+}
+
+static gboolean
+_ide_pty_intercept_out_cb (GIOChannel   *channel,
+                           GIOCondition  condition,
+                           gpointer      user_data)
+{
+  IdePtyIntercept *self = user_data;
+  IdePtyInterceptSide *us, *them;
+  GIOStatus status;
+  const gchar *wrbuf;
+  gsize n_written = 0;
+  gsize len = 0;
+
+  g_assert (channel != NULL);
+  g_assert (condition & (G_IO_ERR | G_IO_HUP | G_IO_OUT));
+
+  if (channel == self->master.channel)
+    {
+      us = &self->master;
+      them = &self->slave;
+    }
+  else
+    {
+      us = &self->slave;
+      them = &self->master;
+    }
+
+  if ((condition & G_IO_OUT) == 0 ||
+      us->out_bytes == NULL ||
+      us->channel == NULL ||
+      them->channel == NULL)
+    goto close_and_cleanup;
+
+  wrbuf = g_bytes_get_data (us->out_bytes, &len);
+  status = g_io_channel_write_chars (us->channel, wrbuf, len, &n_written, NULL);
+  if (status != G_IO_STATUS_NORMAL)
+    goto close_and_cleanup;
+
+  g_assert (n_written > 0);
+  g_assert (them->in_watch == 0);
+
+  /*
+   * If we didn't write all of our data, wait until another G_IO_OUT
+   * condition to write more data.
+   */
+  if (n_written < len)
+    {
+      g_autoptr(GBytes) bytes = g_steal_pointer (&us->out_bytes);
+      us->out_bytes = g_bytes_new_from_bytes (bytes, n_written, len - n_written);
+      return G_SOURCE_CONTINUE;
+    }
+
+  g_clear_pointer (&us->out_bytes, g_bytes_unref);
+
+  /*
+   * We wrote all the data to this side, so now we can wait for more
+   * data from the input peer.
+   */
+  us->out_watch = 0;
+  them->in_watch =
+    g_io_add_watch_full (them->channel,
+                         them->read_prio,
+                         G_IO_IN | G_IO_ERR | G_IO_HUP,
+                         _ide_pty_intercept_in_cb,
+                         self, NULL);
+
+  return G_SOURCE_REMOVE;
+
+close_and_cleanup:
+
+  _ide_pty_intercept_side_close (us);
+  _ide_pty_intercept_side_close (them);
+
+  return G_SOURCE_REMOVE;
+}
+
+/*
+ * _ide_pty_intercept_in_cb:
+ *
+ * This function is called when we have received a condition that specifies
+ * the channel has data to read. We read that data and then setup a watch
+ * onto the other other side so that we can write that data.
+ *
+ * If the other-side of the of the connection can write, then we write
+ * that data immediately.
+ *
+ * The in watch is disabled until we have completed the write.
+ */
+static gboolean
+_ide_pty_intercept_in_cb (GIOChannel   *channel,
+                          GIOCondition  condition,
+                          gpointer      user_data)
+{
+  IdePtyIntercept *self = user_data;
+  IdePtyInterceptSide *us, *them;
+  GIOStatus status = G_IO_STATUS_AGAIN;
+  gchar buf[4096];
+  gchar *wrbuf = buf;
+  gsize n_read;
+
+  g_assert (channel != NULL);
+  g_assert (condition & (G_IO_ERR | G_IO_HUP | G_IO_IN));
+  g_assert (IDE_IS_PTY_INTERCEPT (self));
+
+  if (channel == self->master.channel)
+    {
+      us = &self->master;
+      them = &self->slave;
+    }
+  else
+    {
+      us = &self->slave;
+      them = &self->master;
+    }
+
+  g_assert (us->in_watch != 0);
+  g_assert (them->out_watch == 0);
+
+  if (condition & (G_IO_ERR | G_IO_HUP) || us->channel == NULL || them->channel == NULL)
+    goto close_and_cleanup;
+
+  g_assert (condition & G_IO_IN);
+
+  while (status == G_IO_STATUS_AGAIN)
+    {
+      n_read = 0;
+      status = g_io_channel_read_chars (us->channel, buf, sizeof buf, &n_read, NULL);
+    }
+
+  if (status == G_IO_STATUS_EOF)
+    goto close_and_cleanup;
+
+  if (n_read > 0 && us->callback != NULL)
+    us->callback (self, us, (const guint8 *)buf, n_read, us->callback_data);
+
+  while (n_read > 0)
+    {
+      gsize n_written = 0;
+
+      status = g_io_channel_write_chars (them->channel, buf, n_read, &n_written, NULL);
+
+      wrbuf += n_written;
+      n_read -= n_written;
+
+      if (n_read > 0 && status == G_IO_STATUS_AGAIN)
+        {
+          /* If we get G_IO_STATUS_AGAIN here, then we are in a situation where
+           * the other side is not in a position to handle the data. We need to
+           * setup a G_IO_OUT watch on the FD to wait until things are writeable.
+           *
+           * We'll cancel our G_IO_IN condition, and wait for the out condition
+           * to make forward progress.
+           */
+          them->out_bytes = g_bytes_new (wrbuf, n_read);
+          them->out_watch = g_io_add_watch_full (them->channel,
+                                                 them->write_prio,
+                                                 G_IO_OUT | G_IO_ERR | G_IO_HUP,
+                                                 _ide_pty_intercept_out_cb,
+                                                 self, NULL);
+          us->in_watch = 0;
+
+          return G_SOURCE_REMOVE;
+        }
+
+      if (status != G_IO_STATUS_NORMAL)
+        goto close_and_cleanup;
+
+      g_io_channel_flush (them->channel, NULL);
+    }
+
+  return G_SOURCE_CONTINUE;
+
+close_and_cleanup:
+
+  _ide_pty_intercept_side_close (us);
+  _ide_pty_intercept_side_close (them);
+
+  return G_SOURCE_REMOVE;
+}
+
+/**
+ * ide_pty_intercept_set_size:
+ *
+ * Proxies a winsize across to the inferior. If the PTY is the
+ * controlling PTY for the process, then SIGWINCH will be signaled
+ * in the inferior process.
+ *
+ * Since we can't track SIGWINCH cleanly in here, we rely on the
+ * external consuming program to notify us of SIGWINCH so that we
+ * can copy the new size across.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_pty_intercept_set_size (IdePtyIntercept *self,
+                            guint            rows,
+                            guint            columns)
+{
+
+  g_return_val_if_fail (IDE_IS_PTY_INTERCEPT (self), FALSE);
+
+  if (self->master.channel != NULL)
+    {
+      IdePtyFd fd = g_io_channel_unix_get_fd (self->master.channel);
+      struct winsize ws = {0};
+
+      ws.ws_col = columns;
+      ws.ws_row = rows;
+
+      return ioctl (fd, TIOCSWINSZ, &ws) == 0;
+    }
+
+  return FALSE;
+}
+
+static guint
+_g_io_add_watch_full_with_context (GMainContext   *main_context,
+                                   GIOChannel     *channel,
+                                   gint            priority,
+                                   GIOCondition    condition,
+                                   GIOFunc         func,
+                                   gpointer        user_data,
+                                   GDestroyNotify  notify)
+{
+  GSource *source;
+  guint id;
+
+  g_return_val_if_fail (channel != NULL, 0);
+
+  source = g_io_create_watch (channel, condition);
+
+  if (priority != G_PRIORITY_DEFAULT)
+    g_source_set_priority (source, priority);
+  g_source_set_callback (source, (GSourceFunc)func, user_data, notify);
+
+  id = g_source_attach (source, main_context);
+  g_source_unref (source);
+
+  return id;
+}
+
+/**
+ * ide_pty_intercept_init:
+ * @self: a location of memory to store a #IdePtyIntercept
+ * @fd: the PTY master fd, possibly from a #VtePty
+ * @main_context: (nullable): a #GMainContext or %NULL for thread-default
+ *
+ * Creates a enw #IdePtyIntercept using the PTY master fd @fd.
+ *
+ * A new PTY slave is created that will communicate with @fd.
+ * Additionally, a new PTY master is created that can communicate
+ * with another side, and will pass that information to @fd after
+ * extracting any necessary information.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_pty_intercept_init (IdePtyIntercept *self,
+                        int              fd,
+                        GMainContext    *main_context)
+{
+  g_auto(IdePtyFd) slave_fd = IDE_PTY_FD_INVALID;
+  g_auto(IdePtyFd) master_fd = IDE_PTY_FD_INVALID;
+  struct winsize ws;
+
+  g_return_val_if_fail (self != NULL, FALSE);
+  g_return_val_if_fail (fd != -1, FALSE);
+
+  memset (self, 0, sizeof *self);
+  self->magic = IDE_PTY_INTERCEPT_MAGIC;
+
+  slave_fd = ide_pty_intercept_create_slave (fd, FALSE);
+  if (slave_fd == IDE_PTY_FD_INVALID)
+    return FALSE;
+
+  /* Do not perform additional processing on the slave_fd created
+   * from the master we were provided. Otherwise, it will be happening
+   * twice instead of just once.
+   */
+  if (!_ide_pty_intercept_set_raw (slave_fd))
+    return FALSE;
+
+  master_fd = ide_pty_intercept_create_master ();
+  if (master_fd == IDE_PTY_FD_INVALID)
+    return FALSE;
+
+  /* Copy the win size across */
+  if (ioctl (slave_fd, TIOCGWINSZ, &ws) >= 0)
+    ioctl (master_fd, TIOCSWINSZ, &ws);
+
+  if (main_context == NULL)
+    main_context = g_main_context_get_thread_default ();
+
+  self->master.read_prio = MASTER_READ_PRIORITY;
+  self->master.write_prio = MASTER_WRITE_PRIORITY;
+  self->slave.read_prio = SLAVE_READ_PRIORITY;
+  self->slave.write_prio = SLAVE_WRITE_PRIORITY;
+
+  self->master.channel = g_io_channel_unix_new (pty_fd_steal (&master_fd));
+  self->slave.channel = g_io_channel_unix_new (pty_fd_steal (&slave_fd));
+
+  g_io_channel_set_close_on_unref (self->master.channel, TRUE);
+  g_io_channel_set_close_on_unref (self->slave.channel, TRUE);
+
+  g_io_channel_set_encoding (self->master.channel, NULL, NULL);
+  g_io_channel_set_encoding (self->slave.channel, NULL, NULL);
+
+  g_io_channel_set_buffer_size (self->master.channel, CHANNEL_BUFFER_SIZE);
+  g_io_channel_set_buffer_size (self->slave.channel, CHANNEL_BUFFER_SIZE);
+
+  self->master.in_watch =
+    _g_io_add_watch_full_with_context (main_context,
+                                       self->master.channel,
+                                       self->master.read_prio,
+                                       G_IO_IN | G_IO_ERR | G_IO_HUP,
+                                       _ide_pty_intercept_in_cb,
+                                       self, NULL);
+
+  self->slave.in_watch =
+    _g_io_add_watch_full_with_context (main_context,
+                                       self->slave.channel,
+                                       self->slave.read_prio,
+                                       G_IO_IN | G_IO_ERR | G_IO_HUP,
+                                       _ide_pty_intercept_in_cb,
+                                       self, NULL);
+
+  return TRUE;
+}
+
+/**
+ * ide_pty_intercept_clear:
+ * @self: a #IdePtyIntercept
+ *
+ * Cleans up a #IdePtyIntercept previously initialized with
+ * ide_pty_intercept_init().
+ *
+ * This diconnects any #GIOChannel that have been attached and
+ * releases any allocated memory.
+ *
+ * It is invalid to use @self after calling this function.
+ *
+ * Since: 3.32
+ */
+void
+ide_pty_intercept_clear (IdePtyIntercept *self)
+{
+  g_return_if_fail (IDE_IS_PTY_INTERCEPT (self));
+
+  clear_source (&self->slave.in_watch);
+  clear_source (&self->slave.out_watch);
+  g_clear_pointer (&self->slave.channel, g_io_channel_unref);
+  g_clear_pointer (&self->slave.out_bytes, g_bytes_unref);
+
+  clear_source (&self->master.in_watch);
+  clear_source (&self->master.out_watch);
+  g_clear_pointer (&self->master.channel, g_io_channel_unref);
+  g_clear_pointer (&self->master.out_bytes, g_bytes_unref);
+
+  memset (self, 0, sizeof *self);
+}
+
+/**
+ * ide_pty_intercept_get_fd:
+ * @self: a #IdePtyIntercept
+ *
+ * Gets a master PTY fd created by the #IdePtyIntercept. This is suitable
+ * to use to create a slave fd which can be passed to a child process.
+ *
+ * Returns: A FD of a PTY master if successful, otherwise -1.
+ *
+ * Since: 3.32
+ */
+IdePtyFd
+ide_pty_intercept_get_fd (IdePtyIntercept *self)
+{
+  g_return_val_if_fail (IDE_IS_PTY_INTERCEPT (self), IDE_PTY_FD_INVALID);
+  g_return_val_if_fail (self->master.channel != NULL, IDE_PTY_FD_INVALID);
+
+  return g_io_channel_unix_get_fd (self->master.channel);
+}
+
+/**
+ * ide_pty_intercept_set_callback:
+ * @self: a IdePtyIntercept
+ * @side: the side containing the data to watch
+ * @callback: (scope notified): the callback to execute when data is received
+ * @user_data: closure data for @callback
+ *
+ * This sets the callback to execute every time data is received
+ * from a particular side of the intercept.
+ *
+ * You may only set one per side.
+ *
+ * Since: 3.32
+ */
+void
+ide_pty_intercept_set_callback (IdePtyIntercept         *self,
+                                IdePtyInterceptSide     *side,
+                                IdePtyInterceptCallback  callback,
+                                gpointer                 callback_data)
+{
+  g_return_if_fail (IDE_IS_PTY_INTERCEPT (self));
+  g_return_if_fail (side == &self->master || side == &self->slave);
+
+  side->callback = callback;
+  side->callback_data = callback_data;
+}
diff --git a/src/libide/io/ide-pty-intercept.h b/src/libide/io/ide-pty-intercept.h
new file mode 100644
index 000000000..9d0c68bdd
--- /dev/null
+++ b/src/libide/io/ide-pty-intercept.h
@@ -0,0 +1,108 @@
+/* ide-pty-intercept.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_IO_INSIDE) && !defined (IDE_IO_COMPILATION)
+# error "Only <libide-io.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <unistd.h>
+
+G_BEGIN_DECLS
+
+#define IDE_PTY_FD_INVALID (-1)
+#define IDE_PTY_INTERCEPT_MAGIC (0x81723647)
+#define IDE_IS_PTY_INTERCEPT(s) ((s) != NULL && (s)->magic == IDE_PTY_INTERCEPT_MAGIC)
+
+typedef int                              IdePtyFd;
+typedef struct _IdePtyIntercept          IdePtyIntercept;
+typedef struct _IdePtyInterceptSide      IdePtyInterceptSide;
+typedef void (*IdePtyInterceptCallback) (const IdePtyIntercept     *intercept,
+                                         const IdePtyInterceptSide *side,
+                                         const guint8              *data,
+                                         gsize                      len,
+                                         gpointer                   user_data);
+
+struct _IdePtyInterceptSide
+{
+  GIOChannel              *channel;
+  guint                    in_watch;
+  guint                    out_watch;
+  gint                     read_prio;
+  gint                     write_prio;
+  GBytes                  *out_bytes;
+  IdePtyInterceptCallback  callback;
+  gpointer                 callback_data;
+};
+
+struct _IdePtyIntercept
+{
+  gsize               magic;
+  IdePtyInterceptSide master;
+  IdePtyInterceptSide slave;
+};
+
+static inline IdePtyFd
+pty_fd_steal (IdePtyFd *fd)
+{
+  IdePtyFd ret = *fd;
+  *fd = -1;
+  return ret;
+}
+
+static void
+pty_fd_clear (IdePtyFd *fd)
+{
+  if (fd != NULL && *fd != -1)
+    {
+      int rfd = *fd;
+      *fd = -1;
+      close (rfd);
+    }
+}
+
+G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC (IdePtyFd, pty_fd_clear)
+
+IDE_AVAILABLE_IN_3_32
+IdePtyFd ide_pty_intercept_create_master (void);
+IDE_AVAILABLE_IN_3_32
+IdePtyFd ide_pty_intercept_create_slave  (IdePtyFd                 master_fd,
+                                          gboolean                 blocking);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_pty_intercept_init          (IdePtyIntercept         *self,
+                                          IdePtyFd                 fd,
+                                          GMainContext            *main_context);
+IDE_AVAILABLE_IN_3_32
+IdePtyFd ide_pty_intercept_get_fd        (IdePtyIntercept         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_pty_intercept_set_size      (IdePtyIntercept         *self,
+                                          guint                    rows,
+                                          guint                    columns);
+IDE_AVAILABLE_IN_3_32
+void     ide_pty_intercept_clear         (IdePtyIntercept         *self);
+IDE_AVAILABLE_IN_3_32
+void     ide_pty_intercept_set_callback  (IdePtyIntercept         *self,
+                                          IdePtyInterceptSide     *side,
+                                          IdePtyInterceptCallback  callback,
+                                          gpointer                 user_data);
+
+G_END_DECLS
diff --git a/src/libide/io/libide-io.h b/src/libide/io/libide-io.h
new file mode 100644
index 000000000..dce1b643e
--- /dev/null
+++ b/src/libide/io/libide-io.h
@@ -0,0 +1,42 @@
+/* ide-io.h
+ *
+ * Copyright 2018-2019 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-core.h>
+#include <libide-threading.h>
+
+G_BEGIN_DECLS
+
+#define IDE_IO_INSIDE
+
+#include "ide-content-type.h"
+#include "ide-gfile.h"
+#include "ide-line-reader.h"
+#include "ide-marked-content.h"
+#include "ide-path.h"
+#include "ide-persistent-map-builder.h"
+#include "ide-persistent-map.h"
+#include "ide-pkcon-transfer.h"
+#include "ide-pty-intercept.h"
+
+#undef IDE_IO_INSIDE
+
+G_END_DECLS
diff --git a/src/libide/io/meson.build b/src/libide/io/meson.build
new file mode 100644
index 000000000..42ffb4b1d
--- /dev/null
+++ b/src/libide/io/meson.build
@@ -0,0 +1,69 @@
+libide_io_header_subdir = join_paths(libide_header_subdir, 'io')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_io_public_headers = [
+  'ide-content-type.h',
+  'ide-gfile.h',
+  'ide-line-reader.h',
+  'ide-marked-content.h',
+  'ide-path.h',
+  'ide-persistent-map.h',
+  'ide-persistent-map-builder.h',
+  'ide-pkcon-transfer.h',
+  'ide-pty-intercept.h',
+  'libide-io.h',
+]
+
+install_headers(libide_io_public_headers, subdir: libide_io_header_subdir)
+
+#
+# Sources
+#
+
+libide_io_public_sources = [
+  'ide-content-type.c',
+  'ide-gfile.c',
+  'ide-line-reader.c',
+  'ide-marked-content.c',
+  'ide-path.c',
+  'ide-persistent-map.c',
+  'ide-persistent-map-builder.c',
+  'ide-pkcon-transfer.c',
+  'ide-pty-intercept.c',
+]
+
+libide_io_sources = libide_io_public_sources
+
+#
+# Dependencies
+#
+
+libide_io_deps = [
+  libgio_dep,
+  libide_core_dep,
+  libide_threading_dep
+]
+
+#
+# Library Definitions
+#
+
+libide_io = static_library('ide-io-' + libide_api_version, libide_io_sources,
+   dependencies: libide_io_deps,
+         c_args: libide_args + release_args + ['-DIDE_IO_COMPILATION'],
+)
+
+libide_io_dep = declare_dependency(
+         dependencies: [ libgio_dep, libide_core_dep, libide_threading_dep ],
+           link_whole: libide_io,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_io_public_sources)
+gnome_builder_public_headers += files(libide_io_public_headers)
+gnome_builder_include_subdirs += libide_io_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-io.h', '-DIDE_IO_COMPILATION']
diff --git a/src/libide/lsp/ide-lsp-client.c b/src/libide/lsp/ide-lsp-client.c
new file mode 100644
index 000000000..8d9e2485c
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-client.c
@@ -0,0 +1,1332 @@
+/* ide-lsp-client.c
+ *
+ * Copyright 2016-2019 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 "ide-lsp-client"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <jsonrpc-glib.h>
+#include <libide-code.h>
+#include <libide-projects.h>
+#include <libide-sourceview.h>
+#include <libide-threading.h>
+#include <unistd.h>
+
+#include "ide-lsp-client.h"
+
+typedef struct
+{
+  DzlSignalGroup *buffer_manager_signals;
+  DzlSignalGroup *project_signals;
+  JsonrpcClient  *rpc_client;
+  GIOStream      *io_stream;
+  GHashTable     *diagnostics_by_file;
+  GPtrArray      *languages;
+} IdeLspClientPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeLspClient, ide_lsp_client, IDE_TYPE_OBJECT)
+
+enum {
+  FILE_CHANGE_TYPE_CREATED = 1,
+  FILE_CHANGE_TYPE_CHANGED = 2,
+  FILE_CHANGE_TYPE_DELETED = 3,
+};
+
+enum {
+  SEVERITY_ERROR       = 1,
+  SEVERITY_WARNING     = 2,
+  SEVERITY_INFORMATION = 3,
+  SEVERITY_HINT        = 4,
+};
+
+enum {
+  PROP_0,
+  PROP_IO_STREAM,
+  N_PROPS
+};
+
+enum {
+  PUBLISHED_DIAGNOSTICS,
+  NOTIFICATION,
+  SUPPORTS_LANGUAGE,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static gboolean
+ide_lsp_client_supports_buffer (IdeLspClient *self,
+                                     IdeBuffer         *buffer)
+{
+  GtkSourceLanguage *language;
+  const gchar *language_id = "text/plain";
+  gboolean ret = FALSE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer));
+  if (language != NULL)
+    language_id = gtk_source_language_get_id (language);
+
+  g_signal_emit (self, signals [SUPPORTS_LANGUAGE], 0, language_id, &ret);
+
+  return ret;
+}
+
+static void
+ide_lsp_client_clear_diagnostics (IdeLspClient *self,
+                                  const gchar  *uri)
+{
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+  g_autoptr(GFile) file = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (uri != NULL);
+
+  IDE_TRACE_MSG ("Clearing diagnostics for %s", uri);
+
+  file = g_file_new_for_uri (uri);
+  g_hash_table_remove (priv->diagnostics_by_file, file);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_buffer_saved (IdeLspClient     *self,
+                             IdeBuffer        *buffer,
+                             IdeBufferManager *buffer_manager)
+{
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+
+  if (!ide_lsp_client_supports_buffer (self, buffer))
+    IDE_EXIT;
+
+  uri = ide_buffer_dup_uri (buffer);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+    "}"
+  );
+
+  ide_lsp_client_send_notification_async (self, "textDocument/didSave", params, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+/*
+ * TODO: This should all be delayed and buffered so we coalesce multiple
+ *       events into a single dispatch.
+ */
+
+static void
+ide_lsp_client_buffer_insert_text (IdeLspClient *self,
+                                   GtkTextIter  *location,
+                                   const gchar  *new_text,
+                                   gint          len,
+                                   IdeBuffer    *buffer)
+{
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+  g_autofree gchar *copy = NULL;
+  gint64 version;
+  gint line;
+  gint column;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (location != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  copy = g_strndup (new_text, len);
+
+  uri = ide_buffer_dup_uri (buffer);
+  version = (gint64)ide_buffer_get_change_count (buffer);
+
+  line = gtk_text_iter_get_line (location);
+  column = gtk_text_iter_get_line_offset (location);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+      "version", JSONRPC_MESSAGE_PUT_INT64 (version),
+    "}",
+    "contentChanges", "[",
+      "{",
+        "range", "{",
+          "start", "{",
+            "line", JSONRPC_MESSAGE_PUT_INT64 (line),
+            "character", JSONRPC_MESSAGE_PUT_INT64 (column),
+          "}",
+          "end", "{",
+            "line", JSONRPC_MESSAGE_PUT_INT64 (line),
+            "character", JSONRPC_MESSAGE_PUT_INT64 (column),
+          "}",
+        "}",
+        "rangeLength", JSONRPC_MESSAGE_PUT_INT64 (0),
+        "text", JSONRPC_MESSAGE_PUT_STRING (copy),
+      "}",
+    "]");
+
+  ide_lsp_client_send_notification_async (self, "textDocument/didChange",
+                                               params, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_buffer_delete_range (IdeLspClient *self,
+                                         GtkTextIter       *begin_iter,
+                                         GtkTextIter       *end_iter,
+                                         IdeBuffer         *buffer)
+{
+
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+  GtkTextIter copy_begin;
+  GtkTextIter copy_end;
+  struct {
+    gint line;
+    gint column;
+  } begin, end;
+  gint version;
+  gint length;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (begin_iter != NULL);
+  g_assert (end_iter != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  uri = ide_buffer_dup_uri (buffer);
+  version = (gint)ide_buffer_get_change_count (buffer);
+
+  copy_begin = *begin_iter;
+  copy_end = *end_iter;
+  gtk_text_iter_order (&copy_begin, &copy_end);
+
+  begin.line = gtk_text_iter_get_line (&copy_begin);
+  begin.column = gtk_text_iter_get_line_offset (&copy_begin);
+
+  end.line = gtk_text_iter_get_line (&copy_end);
+  end.column = gtk_text_iter_get_line_offset (&copy_end);
+
+  length = gtk_text_iter_get_offset (&copy_end) - gtk_text_iter_get_offset (&copy_begin);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+      "version", JSONRPC_MESSAGE_PUT_INT64 (version),
+    "}",
+    "contentChanges", "[",
+      "{",
+        "range", "{",
+          "start", "{",
+            "line", JSONRPC_MESSAGE_PUT_INT64 (begin.line),
+            "character", JSONRPC_MESSAGE_PUT_INT64 (begin.column),
+          "}",
+          "end", "{",
+            "line", JSONRPC_MESSAGE_PUT_INT64 (end.line),
+            "character", JSONRPC_MESSAGE_PUT_INT64 (end.column),
+          "}",
+        "}",
+        "rangeLength", JSONRPC_MESSAGE_PUT_INT64 (length),
+        "text", "",
+      "}",
+    "]");
+
+  ide_lsp_client_send_notification_async (self, "textDocument/didChange",
+                                               params, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_buffer_loaded (IdeLspClient *self,
+                                   IdeBuffer         *buffer,
+                                   IdeBufferManager  *buffer_manager)
+{
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+  g_autofree gchar *text = NULL;
+  GtkSourceLanguage *language;
+  const gchar *language_id;
+  GtkTextIter begin;
+  GtkTextIter end;
+  gint64 version;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+
+  if (!ide_lsp_client_supports_buffer (self, buffer))
+    IDE_EXIT;
+
+  g_signal_connect_object (buffer,
+                           "insert-text",
+                           G_CALLBACK (ide_lsp_client_buffer_insert_text),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (buffer,
+                           "delete-range",
+                           G_CALLBACK (ide_lsp_client_buffer_delete_range),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  uri = ide_buffer_dup_uri (buffer);
+  version = (gint64)ide_buffer_get_change_count (buffer);
+
+  gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end);
+  text = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (buffer), &begin, &end, TRUE);
+
+  language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer));
+  if (language != NULL)
+    language_id = gtk_source_language_get_id (language);
+  else
+    language_id = "text/plain";
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+      "languageId", JSONRPC_MESSAGE_PUT_STRING (language_id),
+      "text", JSONRPC_MESSAGE_PUT_STRING (text),
+      "version", JSONRPC_MESSAGE_PUT_INT64 (version),
+    "}"
+  );
+
+  ide_lsp_client_send_notification_async (self, "textDocument/didOpen",
+                                               params, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_buffer_unloaded (IdeLspClient *self,
+                                     IdeBuffer         *buffer,
+                                     IdeBufferManager  *buffer_manager)
+{
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+
+  if (!ide_lsp_client_supports_buffer (self, buffer))
+    IDE_EXIT;
+
+  uri = ide_buffer_dup_uri (buffer);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+    "}"
+  );
+
+  ide_lsp_client_send_notification_async (self, "textDocument/didClose",
+                                               params, NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_buffer_manager_bind (IdeLspClient *self,
+                                         IdeBufferManager  *buffer_manager,
+                                         DzlSignalGroup    *signal_group)
+{
+  guint n_items;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+  g_assert (DZL_IS_SIGNAL_GROUP (signal_group));
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (buffer_manager));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeBuffer) buffer = NULL;
+
+      buffer = g_list_model_get_item (G_LIST_MODEL (buffer_manager), i);
+      ide_lsp_client_buffer_loaded (self, buffer, buffer_manager);
+    }
+}
+
+static void
+ide_lsp_client_buffer_manager_unbind (IdeLspClient *self,
+                                           DzlSignalGroup    *signal_group)
+{
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (DZL_IS_SIGNAL_GROUP (signal_group));
+
+  /* TODO: We need to track everything we've notified so that we
+   *       can notify the peer to release its resources.
+   */
+}
+
+static void
+ide_lsp_client_project_file_trashed (IdeLspClient *self,
+                                          GFile             *file,
+                                          IdeProject        *project)
+{
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (IDE_IS_PROJECT (project));
+
+  uri = g_file_get_uri (file);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "changes", "[",
+      "{",
+        "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+        "type", JSONRPC_MESSAGE_PUT_INT64 (FILE_CHANGE_TYPE_DELETED),
+      "}",
+    "]"
+  );
+
+  ide_lsp_client_send_notification_async (self, "workspace/didChangeWatchedFiles",
+                                               params, NULL, NULL, NULL);
+
+  ide_lsp_client_clear_diagnostics (self, uri);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_project_file_renamed (IdeLspClient *self,
+                                     GFile        *src,
+                                     GFile        *dst,
+                                     IdeProject   *project)
+{
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *src_uri = NULL;
+  g_autofree gchar *dst_uri = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (G_IS_FILE (src));
+  g_assert (G_IS_FILE (dst));
+  g_assert (IDE_IS_PROJECT (project));
+
+  src_uri = g_file_get_uri (src);
+  dst_uri = g_file_get_uri (dst);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "changes", "["
+      "{",
+        "uri", JSONRPC_MESSAGE_PUT_STRING (src_uri),
+        "type", JSONRPC_MESSAGE_PUT_INT64 (FILE_CHANGE_TYPE_DELETED),
+      "}",
+      "{",
+        "uri", JSONRPC_MESSAGE_PUT_STRING (dst_uri),
+        "type", JSONRPC_MESSAGE_PUT_INT64 (FILE_CHANGE_TYPE_CREATED),
+      "}",
+    "]"
+  );
+
+  ide_lsp_client_send_notification_async (self, "workspace/didChangeWatchedFiles",
+                                               params, NULL, NULL, NULL);
+
+  ide_lsp_client_clear_diagnostics (self, src_uri);
+
+  IDE_EXIT;
+}
+
+static IdeDiagnostics *
+ide_lsp_client_translate_diagnostics (IdeLspClient *self,
+                                      GFile        *file,
+                                      GVariantIter *diagnostics)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+  g_autoptr(IdeDiagnostics) ret = NULL;
+  GVariant *value;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (diagnostics != NULL);
+
+  ar = g_ptr_array_new_with_free_func (g_object_unref);
+
+  while (g_variant_iter_loop (diagnostics, "v", &value))
+    {
+      g_autoptr(IdeLocation) begin_loc = NULL;
+      g_autoptr(IdeLocation) end_loc = NULL;
+      g_autoptr(IdeDiagnostic) diag = NULL;
+      g_autoptr(GVariant) range = NULL;
+      const gchar *message = NULL;
+      const gchar *source = NULL;
+      gint64 severity = 0;
+      gboolean success;
+      struct {
+        gint64 line;
+        gint64 column;
+      } begin, end;
+
+      /* Mandatory fields */
+      if (!JSONRPC_MESSAGE_PARSE (value,
+                                  "range", JSONRPC_MESSAGE_GET_VARIANT (&range),
+                                  "message", JSONRPC_MESSAGE_GET_STRING (&message)))
+        continue;
+
+      /* Optional Fields */
+      JSONRPC_MESSAGE_PARSE (value, "severity", JSONRPC_MESSAGE_GET_INT64 (&severity));
+      JSONRPC_MESSAGE_PARSE (value, "source", JSONRPC_MESSAGE_GET_STRING (&source));
+
+      /* Extract location information */
+      success = JSONRPC_MESSAGE_PARSE (range,
+        "start", "{",
+          "line", JSONRPC_MESSAGE_GET_INT64 (&begin.line),
+          "character", JSONRPC_MESSAGE_GET_INT64 (&begin.column),
+        "}",
+        "end", "{",
+          "line", JSONRPC_MESSAGE_GET_INT64 (&end.line),
+          "character", JSONRPC_MESSAGE_GET_INT64 (&end.column),
+        "}"
+      );
+
+      if (!success)
+        continue;
+
+      begin_loc = ide_location_new (file, begin.line, begin.column);
+      end_loc = ide_location_new (file, end.line, end.column);
+
+      switch (severity)
+        {
+        case SEVERITY_ERROR:
+          severity = IDE_DIAGNOSTIC_ERROR;
+          break;
+
+        case SEVERITY_WARNING:
+          severity = IDE_DIAGNOSTIC_WARNING;
+          break;
+
+        case SEVERITY_INFORMATION:
+        case SEVERITY_HINT:
+        default:
+          severity = IDE_DIAGNOSTIC_NOTE;
+          break;
+        }
+
+      diag = ide_diagnostic_new (severity, message, begin_loc);
+      ide_diagnostic_take_range (diag, ide_range_new (begin_loc, end_loc));
+
+      g_ptr_array_add (ar, g_steal_pointer (&diag));
+    }
+
+  ret = ide_diagnostics_new ();
+
+  if (ar != NULL)
+    {
+      for (guint i = 0; i < ar->len; i++)
+        ide_diagnostics_add (ret, g_ptr_array_index (ar, i));
+    }
+
+  return g_steal_pointer (&ret);
+}
+
+static void
+ide_lsp_client_text_document_publish_diagnostics (IdeLspClient *self,
+                                                  GVariant     *params)
+{
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+  g_autoptr(GVariantIter) json_diagnostics = NULL;
+  const gchar *uri = NULL;
+  gboolean success;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (params != NULL);
+
+  success = JSONRPC_MESSAGE_PARSE (params,
+    "uri", JSONRPC_MESSAGE_GET_STRING (&uri),
+    "diagnostics", JSONRPC_MESSAGE_GET_ITER (&json_diagnostics)
+  );
+
+  if (success)
+    {
+      g_autoptr(GFile) file = NULL;
+      g_autoptr(IdeDiagnostics) diagnostics = NULL;
+
+      file = g_file_new_for_uri (uri);
+
+      diagnostics = ide_lsp_client_translate_diagnostics (self, file, json_diagnostics);
+
+      IDE_TRACE_MSG ("%"G_GSIZE_FORMAT" diagnostics received for %s",
+                     diagnostics ? ide_diagnostics_get_size (diagnostics) : 0,
+                     uri);
+
+      /*
+       * Insert the diagnostics into our cache before emit any signals
+       * so that we have up to date information incase the signal causes
+       * a callback to query back.
+       */
+      g_hash_table_insert (priv->diagnostics_by_file,
+                           g_object_ref (file),
+                           g_object_ref (diagnostics));
+
+      g_signal_emit (self, signals [PUBLISHED_DIAGNOSTICS], 0, file, diagnostics);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_real_notification (IdeLspClient *self,
+                                  const gchar  *method,
+                                  GVariant     *params)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (method != NULL);
+
+  if (params != NULL)
+    {
+      if (g_str_equal (method, "textDocument/publishDiagnostics"))
+        ide_lsp_client_text_document_publish_diagnostics (self, params);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_send_notification (IdeLspClient  *self,
+                                  const gchar   *method,
+                                  GVariant      *params,
+                                  JsonrpcClient *rpc_client)
+{
+  GQuark detail;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (method != NULL);
+  g_assert (JSONRPC_IS_CLIENT (rpc_client));
+
+  IDE_TRACE_MSG ("Notification: %s", method);
+
+  /*
+   * To avoid leaking quarks we do not create a quark for the string unless
+   * it already exists. This should be fine in practice because we only need
+   * the quark if there is a caller that has registered for it. And the callers
+   * registering for it will necessarily create the quark.
+   */
+  detail = g_quark_try_string (method);
+
+  g_signal_emit (self, signals [NOTIFICATION], detail, method, params);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_finalize (GObject *object)
+{
+  IdeLspClient *self = (IdeLspClient *)object;
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_pointer (&priv->diagnostics_by_file, g_hash_table_unref);
+  g_clear_pointer (&priv->languages, g_ptr_array_unref);
+  g_clear_object (&priv->rpc_client);
+  g_clear_object (&priv->buffer_manager_signals);
+  g_clear_object (&priv->project_signals);
+
+  G_OBJECT_CLASS (ide_lsp_client_parent_class)->finalize (object);
+}
+
+static gboolean
+ide_lsp_client_real_supports_language (IdeLspClient *self,
+                                       const gchar  *language_id)
+{
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_CLIENT (self));
+  g_assert (language_id != NULL);
+
+  for (guint i = 0; i < priv->languages->len; i++)
+    {
+      const gchar *id = g_ptr_array_index (priv->languages, i);
+
+      if (g_strcmp0 (language_id, id) == 0)
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_lsp_client_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeLspClient *self = IDE_LSP_CLIENT (object);
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_IO_STREAM:
+      g_value_set_object (value, priv->io_stream);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_client_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeLspClient *self = IDE_LSP_CLIENT (object);
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_IO_STREAM:
+      priv->io_stream = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_client_class_init (IdeLspClientClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_lsp_client_finalize;
+  object_class->get_property = ide_lsp_client_get_property;
+  object_class->set_property = ide_lsp_client_set_property;
+
+  klass->notification = ide_lsp_client_real_notification;
+  klass->supports_language = ide_lsp_client_real_supports_language;
+
+  properties [PROP_IO_STREAM] =
+    g_param_spec_object ("io-stream",
+                         "IO Stream",
+                         "The GIOStream to communicate over",
+                         G_TYPE_IO_STREAM,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [NOTIFICATION] =
+    g_signal_new ("notification",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
+                  G_STRUCT_OFFSET (IdeLspClientClass, notification),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
+                  G_TYPE_VARIANT);
+
+  signals [SUPPORTS_LANGUAGE] =
+    g_signal_new ("supports-language",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeLspClientClass, supports_language),
+                  g_signal_accumulator_true_handled, NULL,
+                  NULL,
+                  G_TYPE_BOOLEAN,
+                  1,
+                  G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+  signals [PUBLISHED_DIAGNOSTICS] =
+    g_signal_new ("published-diagnostics",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeLspClientClass, published_diagnostics),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_FILE,
+                  IDE_TYPE_DIAGNOSTICS);
+
+}
+
+static void
+ide_lsp_client_init (IdeLspClient *self)
+{
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  priv->languages = g_ptr_array_new_with_free_func (g_free);
+
+  priv->diagnostics_by_file = g_hash_table_new_full ((GHashFunc)g_file_hash,
+                                                     (GEqualFunc)g_file_equal,
+                                                     g_object_unref,
+                                                     (GDestroyNotify)g_object_unref);
+
+  priv->buffer_manager_signals = dzl_signal_group_new (IDE_TYPE_BUFFER_MANAGER);
+
+  dzl_signal_group_connect_object (priv->buffer_manager_signals,
+                                   "buffer-loaded",
+                                   G_CALLBACK (ide_lsp_client_buffer_loaded),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (priv->buffer_manager_signals,
+                                   "buffer-saved",
+                                   G_CALLBACK (ide_lsp_client_buffer_saved),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (priv->buffer_manager_signals,
+                                   "buffer-unloaded",
+                                   G_CALLBACK (ide_lsp_client_buffer_unloaded),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (priv->buffer_manager_signals,
+                           "bind",
+                           G_CALLBACK (ide_lsp_client_buffer_manager_bind),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (priv->buffer_manager_signals,
+                           "unbind",
+                           G_CALLBACK (ide_lsp_client_buffer_manager_unbind),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  priv->project_signals = dzl_signal_group_new (IDE_TYPE_PROJECT);
+
+  dzl_signal_group_connect_object (priv->project_signals,
+                                   "file-trashed",
+                                   G_CALLBACK (ide_lsp_client_project_file_trashed),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (priv->project_signals,
+                                   "file-renamed",
+                                   G_CALLBACK (ide_lsp_client_project_file_renamed),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+}
+
+static void
+ide_lsp_client_initialize_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  JsonrpcClient *rpc_client = (JsonrpcClient *)object;
+  g_autoptr(IdeLspClient) self = user_data;
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+  g_autoptr(GVariant) reply = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeBufferManager *buffer_manager;
+  IdeProject *project;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (JSONRPC_IS_CLIENT (rpc_client));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_LSP_CLIENT (self));
+
+  if (!jsonrpc_client_call_finish (rpc_client, result, &reply, &error))
+    {
+      /* translators: %s is replaced with the error message */
+      g_debug (_("Failed to initialize language server: %s"), error->message);
+      ide_lsp_client_stop (self);
+      IDE_EXIT;
+    }
+
+  /* TODO: Check for server capabilities */
+
+  /*
+   * Now that we are connected and have initialized the peer, setup our
+   * buffer_manager and project signals so that we can notify the peer
+   * of open documents and such.
+   */
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  buffer_manager = ide_buffer_manager_from_context (context);
+  dzl_signal_group_set_target (priv->buffer_manager_signals, buffer_manager);
+
+  project = ide_project_from_context (context);
+  dzl_signal_group_set_target (priv->project_signals, project);
+
+  IDE_EXIT;
+}
+
+IdeLspClient *
+ide_lsp_client_new (GIOStream *io_stream)
+{
+  g_return_val_if_fail (G_IS_IO_STREAM (io_stream), NULL);
+
+  return g_object_new (IDE_TYPE_LSP_CLIENT,
+                       "io-stream", io_stream,
+                       NULL);
+}
+
+void
+ide_lsp_client_start (IdeLspClient *self)
+{
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *root_path = NULL;
+  g_autofree gchar *root_uri = NULL;
+  IdeContext *context;
+  GFile *workdir;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_LSP_CLIENT (self));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  if (!G_IS_IO_STREAM (priv->io_stream) || !IDE_IS_CONTEXT (context))
+    {
+      ide_object_message (self,
+                          "Cannot start %s due to misconfiguration.",
+                          G_OBJECT_TYPE_NAME (self));
+      return;
+    }
+
+  priv->rpc_client = jsonrpc_client_new (priv->io_stream);
+
+  g_signal_connect_object (priv->rpc_client,
+                           "notification",
+                           G_CALLBACK (ide_lsp_client_send_notification),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  workdir = ide_context_ref_workdir (context);
+  root_path = g_file_get_path (workdir);
+  root_uri = g_file_get_uri (workdir);
+
+  /*
+   * The first thing we need to do is initialize the client with information
+   * about our project. So that we will perform asynchronously here. It will
+   * also start our read loop.
+   */
+
+  params = JSONRPC_MESSAGE_NEW (
+    "processId", JSONRPC_MESSAGE_PUT_INT64 (getpid ()),
+    "rootUri", JSONRPC_MESSAGE_PUT_STRING (root_uri),
+    "rootPath", JSONRPC_MESSAGE_PUT_STRING (root_path),
+    "capabilities", "{", "}"
+  );
+
+  jsonrpc_client_call_async (priv->rpc_client,
+                             "initialize",
+                             params,
+                             NULL,
+                             ide_lsp_client_initialize_cb,
+                             g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_close_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  g_autoptr(IdeLspClient) self = user_data;
+  JsonrpcClient *client = (JsonrpcClient *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_LSP_CLIENT (self));
+
+  jsonrpc_client_close_finish (client, result, NULL);
+}
+
+static void
+ide_lsp_client_shutdown_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  g_autoptr(IdeLspClient) self = user_data;
+  JsonrpcClient *client = (JsonrpcClient *)object;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (JSONRPC_IS_CLIENT (client));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_LSP_CLIENT (self));
+
+  if (!jsonrpc_client_call_finish (client, result, NULL, &error))
+    g_debug ("%s", error->message);
+  else
+    jsonrpc_client_close_async (client,
+                                NULL,
+                                ide_lsp_client_close_cb,
+                                g_steal_pointer (&self));
+
+  IDE_EXIT;
+}
+
+void
+ide_lsp_client_stop (IdeLspClient *self)
+{
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_LSP_CLIENT (self));
+
+  if (priv->rpc_client != NULL)
+    {
+      jsonrpc_client_call_async (priv->rpc_client,
+                                 "shutdown",
+                                 NULL,
+                                 NULL,
+                                 ide_lsp_client_shutdown_cb,
+                                 g_object_ref (self));
+      g_clear_object (&priv->rpc_client);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_client_call_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  JsonrpcClient *client = (JsonrpcClient *)object;
+  g_autoptr(GVariant) reply = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (JSONRPC_IS_CLIENT (client));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!jsonrpc_client_call_finish (client, result, &reply, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task,
+                             g_steal_pointer (&reply),
+                             (GDestroyNotify)g_variant_unref);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_lsp_client_call_async:
+ * @self: An #IdeLspClient
+ * @method: the method to call
+ * @params: (nullable) (transfer none): An #GVariant or %NULL
+ * @cancellable: (nullable): A cancellable or %NULL
+ * @callback: the callback to receive the result, or %NULL
+ * @user_data: user data for @callback
+ *
+ * Asynchronously queries the Language Server using the JSON-RPC protocol.
+ *
+ * If @params is floating, it's floating reference is consumed.
+ *
+ * Since: 3.26
+ */
+void
+ide_lsp_client_call_async (IdeLspClient        *self,
+                           const gchar         *method,
+                           GVariant            *params,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_LSP_CLIENT (self));
+  g_return_if_fail (method != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (!priv->rpc_client || JSONRPC_IS_CLIENT (priv->rpc_client));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_client_call_async);
+
+  if (priv->rpc_client == NULL)
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_CONNECTED,
+                               "No connection to language server");
+  else
+    jsonrpc_client_call_async (priv->rpc_client,
+                               method,
+                               params,
+                               cancellable,
+                               ide_lsp_client_call_cb,
+                               g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_lsp_client_call_finish (IdeLspClient  *self,
+                            GAsyncResult  *result,
+                            GVariant     **return_value,
+                            GError       **error)
+{
+  g_autoptr(GVariant) local_return_value = NULL;
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_LSP_CLIENT (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  local_return_value = ide_task_propagate_pointer (IDE_TASK (result), error);
+  ret = local_return_value != NULL;
+
+  if (return_value != NULL)
+    *return_value = g_steal_pointer (&local_return_value);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_lsp_client_send_notification_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  JsonrpcClient *client = (JsonrpcClient *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (JSONRPC_IS_CLIENT (client));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!jsonrpc_client_send_notification_finish (client, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_lsp_client_send_notification_async:
+ * @self: An #IdeLspClient
+ * @method: the method to notification
+ * @params: (nullable) (transfer none): An #GVariant or %NULL
+ * @cancellable: (nullable): A cancellable or %NULL
+ * @notificationback: the notificationback to receive the result, or %NULL
+ * @user_data: user data for @notificationback
+ *
+ * Asynchronously sends a notification to the Language Server.
+ *
+ * If @params is floating, it's reference is consumed.
+ *
+ * Since: 3.26
+ */
+void
+ide_lsp_client_send_notification_async (IdeLspClient        *self,
+                                        const gchar         *method,
+                                        GVariant            *params,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  notificationback,
+                                        gpointer             user_data)
+{
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_LSP_CLIENT (self));
+  g_return_if_fail (method != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, notificationback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_client_send_notification_async);
+
+  if (priv->rpc_client == NULL)
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_CONNECTED,
+                               "No connection to language server");
+  else
+    jsonrpc_client_send_notification_async (priv->rpc_client,
+                                            method,
+                                            params,
+                                            cancellable,
+                                            ide_lsp_client_send_notification_cb,
+                                            g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_lsp_client_send_notification_finish (IdeLspClient  *self,
+                                         GAsyncResult  *result,
+                                         GError       **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_LSP_CLIENT (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+void
+ide_lsp_client_get_diagnostics_async (IdeLspClient        *self,
+                                      GFile               *file,
+                                      GBytes              *content,
+                                      const gchar         *lang_id,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  IdeDiagnostics *diagnostics;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_LSP_CLIENT (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_client_get_diagnostics_async);
+
+  diagnostics = g_hash_table_lookup (priv->diagnostics_by_file, file);
+
+  if (diagnostics != NULL)
+    ide_task_return_pointer (task,
+                             g_object_ref (diagnostics),
+                             (GDestroyNotify)g_object_unref);
+  else
+    ide_task_return_pointer (task,
+                             ide_diagnostics_new (),
+                             (GDestroyNotify)g_object_unref);
+}
+
+/**
+ * ide_lsp_client_get_diagnostics_finish:
+ * @self: an #IdeLspClient
+ * @result: a #GAsyncResult
+ * @diagnostics: (nullable) (out): A location for a #IdeDiagnostics or %NULL
+ * @error: A location for a #GError or %NULL
+ *
+ * Completes a request to ide_lsp_client_get_diagnostics_async().
+ *
+ * Returns: %TRUE if successful and @diagnostics is set, otherwise %FALSE
+ *   and @error is set.
+ */
+gboolean
+ide_lsp_client_get_diagnostics_finish (IdeLspClient    *self,
+                                       GAsyncResult    *result,
+                                       IdeDiagnostics **diagnostics,
+                                       GError         **error)
+{
+  g_autoptr(IdeDiagnostics) local_diagnostics = NULL;
+  g_autoptr(GError) local_error = NULL;
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_LSP_CLIENT (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  local_diagnostics = ide_task_propagate_pointer (IDE_TASK (result), &local_error);
+  ret = local_diagnostics != NULL;
+
+  if (local_diagnostics != NULL && diagnostics != NULL)
+    *diagnostics = g_steal_pointer (&local_diagnostics);
+
+  if (local_error)
+    g_propagate_error (error, g_steal_pointer (&local_error));
+
+  return ret;
+}
+
+void
+ide_lsp_client_add_language (IdeLspClient *self,
+                             const gchar  *language_id)
+{
+  IdeLspClientPrivate *priv = ide_lsp_client_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_LSP_CLIENT (self));
+  g_return_if_fail (language_id != NULL);
+
+  g_ptr_array_add (priv->languages, g_strdup (language_id));
+}
diff --git a/src/libide/lsp/ide-lsp-client.h b/src/libide/lsp/ide-lsp-client.h
new file mode 100644
index 000000000..ffcb79fa4
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-client.h
@@ -0,0 +1,99 @@
+/* ide-lsp-client.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_CLIENT (ide_lsp_client_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeLspClient, ide_lsp_client, IDE, LSP_CLIENT, IdeObject)
+
+struct _IdeLspClientClass
+{
+  IdeObjectClass parent_class;
+
+  void     (*notification)          (IdeLspClient *self,
+                                     const gchar       *method,
+                                     GVariant          *params);
+  gboolean (*supports_language)     (IdeLspClient *self,
+                                     const gchar       *language_id);
+  void     (*published_diagnostics) (IdeLspClient *self,
+                                     GFile             *file,
+                                     IdeDiagnostics    *diagnostics);
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeLspClient *ide_lsp_client_new                      (GIOStream            *io_stream);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_client_add_language             (IdeLspClient    *self,
+                                                                 const gchar          *language_id);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_client_start                    (IdeLspClient    *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_client_stop                     (IdeLspClient    *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_client_call_async               (IdeLspClient    *self,
+                                                                 const gchar          *method,
+                                                                 GVariant             *params,
+                                                                 GCancellable         *cancellable,
+                                                                 GAsyncReadyCallback   callback,
+                                                                 gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean           ide_lsp_client_call_finish              (IdeLspClient    *self,
+                                                                 GAsyncResult         *result,
+                                                                 GVariant            **return_value,
+                                                                 GError              **error);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_client_send_notification_async  (IdeLspClient    *self,
+                                                                 const gchar          *method,
+                                                                 GVariant             *params,
+                                                                 GCancellable         *cancellable,
+                                                                 GAsyncReadyCallback   notificationback,
+                                                                 gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean           ide_lsp_client_send_notification_finish (IdeLspClient    *self,
+                                                                 GAsyncResult         *result,
+                                                                 GError              **error);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_client_get_diagnostics_async    (IdeLspClient    *self,
+                                                                 GFile                *file,
+                                                                 GBytes *content,
+                                                                 const gchar *lang_id,
+                                                                 GCancellable         *cancellable,
+                                                                 GAsyncReadyCallback   callback,
+                                                                 gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean           ide_lsp_client_get_diagnostics_finish   (IdeLspClient    *self,
+                                                                 GAsyncResult         *result,
+                                                                 IdeDiagnostics      **diagnostics,
+                                                                 GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-completion-item.c b/src/libide/lsp/ide-lsp-completion-item.c
new file mode 100644
index 000000000..7763b0139
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-completion-item.c
@@ -0,0 +1,150 @@
+/* ide-lsp-completion-item.c
+ *
+ * Copyright 2018-2019 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 "ide-lsp-completion-item"
+
+#include "config.h"
+
+#include <libide-sourceview.h>
+#include <jsonrpc-glib.h>
+
+#include "ide-lsp-completion-item.h"
+#include "ide-lsp-util.h"
+
+struct _IdeLspCompletionItem
+{
+  GObject parent_instance;
+  GVariant *variant;
+  const gchar *label;
+  const gchar *detail;
+  guint kind;
+};
+
+G_DEFINE_TYPE_WITH_CODE (IdeLspCompletionItem, ide_lsp_completion_item, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_COMPLETION_PROPOSAL, NULL))
+
+static void
+ide_lsp_completion_item_finalize (GObject *object)
+{
+  IdeLspCompletionItem *self = (IdeLspCompletionItem *)object;
+
+  g_clear_pointer (&self->variant, g_variant_unref);
+
+  G_OBJECT_CLASS (ide_lsp_completion_item_parent_class)->finalize (object);
+}
+
+static void
+ide_lsp_completion_item_class_init (IdeLspCompletionItemClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_lsp_completion_item_finalize;
+}
+
+static void
+ide_lsp_completion_item_init (IdeLspCompletionItem *self)
+{
+}
+
+IdeLspCompletionItem *
+ide_lsp_completion_item_new (GVariant *variant)
+{
+  g_autoptr(GVariant) unboxed = NULL;
+  IdeLspCompletionItem *self;
+  gint64 kind = 0;
+
+  g_return_val_if_fail (variant != NULL, NULL);
+
+  if (g_variant_is_of_type (variant, G_VARIANT_TYPE_VARIANT))
+    variant = unboxed = g_variant_get_variant (variant);
+
+  self = g_object_new (IDE_TYPE_LSP_COMPLETION_ITEM, NULL);
+  self->variant = g_variant_ref_sink (variant);
+
+  g_variant_lookup (variant, "label", "&s", &self->label);
+  g_variant_lookup (variant, "detail", "&s", &self->detail);
+
+  if (JSONRPC_MESSAGE_PARSE (variant, "kind", JSONRPC_MESSAGE_GET_INT64 (&kind)))
+    self->kind = ide_lsp_decode_completion_kind (kind);
+
+  return self;
+}
+
+gchar *
+ide_lsp_completion_item_get_markup (IdeLspCompletionItem *self,
+                                         const gchar               *typed_text)
+{
+  g_return_val_if_fail (IDE_IS_LSP_COMPLETION_ITEM (self), NULL);
+
+  return ide_completion_fuzzy_highlight (self->label, typed_text);
+}
+
+const gchar *
+ide_lsp_completion_item_get_return_type (IdeLspCompletionItem *self)
+{
+  g_return_val_if_fail (IDE_IS_LSP_COMPLETION_ITEM (self), NULL);
+
+  /* TODO: How do we get this from lsp? */
+
+  return NULL;
+}
+
+const gchar *
+ide_lsp_completion_item_get_icon_name (IdeLspCompletionItem *self)
+{
+  g_return_val_if_fail (IDE_IS_LSP_COMPLETION_ITEM (self), NULL);
+
+  return ide_symbol_kind_get_icon_name (self->kind);
+}
+
+const gchar *
+ide_lsp_completion_item_get_detail (IdeLspCompletionItem *self)
+{
+  g_return_val_if_fail (IDE_IS_LSP_COMPLETION_ITEM (self), NULL);
+
+  return self->detail;
+}
+
+/**
+ * ide_lsp_completion_item_get_snippet:
+ * @self: a #IdeLspCompletionItem
+ *
+ * Creates a new snippet for the completion item to be inserted into
+ * the document.
+ *
+ * Returns: (transfer full): an #IdeSnippet
+ *
+ * Since: 3.30
+ */
+IdeSnippet *
+ide_lsp_completion_item_get_snippet (IdeLspCompletionItem *self)
+{
+  g_autoptr(IdeSnippet) snippet = NULL;
+  g_autoptr(IdeSnippetChunk) chunk = NULL;
+
+  g_return_val_if_fail (IDE_IS_LSP_COMPLETION_ITEM (self), NULL);
+
+  snippet = ide_snippet_new (NULL, NULL);
+  chunk = ide_snippet_chunk_new ();
+  ide_snippet_chunk_set_spec (chunk, self->label);
+  ide_snippet_add_chunk (snippet, chunk);
+
+  return g_steal_pointer (&snippet);
+}
diff --git a/src/libide/lsp/ide-lsp-completion-item.h b/src/libide/lsp/ide-lsp-completion-item.h
new file mode 100644
index 000000000..b61b85277
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-completion-item.h
@@ -0,0 +1,50 @@
+/* ide-lsp-completion-item.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-sourceview.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_COMPLETION_ITEM (ide_lsp_completion_item_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeLspCompletionItem, ide_lsp_completion_item, IDE, LSP_COMPLETION_ITEM, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeLspCompletionItem *ide_lsp_completion_item_new             (GVariant *variant);
+IDE_AVAILABLE_IN_3_32
+const gchar               *ide_lsp_completion_item_get_icon_name   (IdeLspCompletionItem *self);
+IDE_AVAILABLE_IN_3_32
+const gchar               *ide_lsp_completion_item_get_return_type (IdeLspCompletionItem *self);
+IDE_AVAILABLE_IN_3_32
+const gchar               *ide_lsp_completion_item_get_detail      (IdeLspCompletionItem *self);
+IDE_AVAILABLE_IN_3_32
+gchar                     *ide_lsp_completion_item_get_markup      (IdeLspCompletionItem *self,
+                                                                         const gchar               
*typed_text);
+IDE_AVAILABLE_IN_3_32
+IdeSnippet                *ide_lsp_completion_item_get_snippet     (IdeLspCompletionItem *self);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-completion-provider.c b/src/libide/lsp/ide-lsp-completion-provider.c
new file mode 100644
index 000000000..857a088db
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-completion-provider.c
@@ -0,0 +1,373 @@
+/* ide-lsp-completion-provider.c
+ *
+ * Copyright 2016-2019 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 "ide-lsp-completion-provider"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-sourceview.h>
+#include <libide-threading.h>
+#include <jsonrpc-glib.h>
+
+#include "ide-lsp-completion-provider.h"
+#include "ide-lsp-completion-item.h"
+#include "ide-lsp-completion-results.h"
+#include "ide-lsp-util.h"
+
+typedef struct
+{
+  IdeLspClient *client;
+  gchar *word;
+} IdeLspCompletionProviderPrivate;
+
+static void provider_iface_init (IdeCompletionProviderInterface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdeLspCompletionProvider, ide_lsp_completion_provider, IDE_TYPE_OBJECT,
+                                  G_ADD_PRIVATE (IdeLspCompletionProvider)
+                                  G_IMPLEMENT_INTERFACE (IDE_TYPE_COMPLETION_PROVIDER, provider_iface_init))
+
+enum {
+  PROP_0,
+  PROP_CLIENT,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_lsp_completion_provider_finalize (GObject *object)
+{
+  IdeLspCompletionProvider *self = (IdeLspCompletionProvider *)object;
+  IdeLspCompletionProviderPrivate *priv = ide_lsp_completion_provider_get_instance_private (self);
+
+  g_clear_object (&priv->client);
+  g_clear_pointer (&priv->word, g_free);
+
+  G_OBJECT_CLASS (ide_lsp_completion_provider_parent_class)->finalize (object);
+}
+
+static void
+ide_lsp_completion_provider_get_property (GObject    *object,
+                                               guint       prop_id,
+                                               GValue     *value,
+                                               GParamSpec *pspec)
+{
+  IdeLspCompletionProvider *self = IDE_LSP_COMPLETION_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      g_value_set_object (value, ide_lsp_completion_provider_get_client (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_completion_provider_set_property (GObject      *object,
+                                               guint         prop_id,
+                                               const GValue *value,
+                                               GParamSpec   *pspec)
+{
+  IdeLspCompletionProvider *self = IDE_LSP_COMPLETION_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      ide_lsp_completion_provider_set_client (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_completion_provider_class_init (IdeLspCompletionProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_lsp_completion_provider_finalize;
+  object_class->get_property = ide_lsp_completion_provider_get_property;
+  object_class->set_property = ide_lsp_completion_provider_set_property;
+
+  properties [PROP_CLIENT] =
+    g_param_spec_object ("client",
+                         "Client",
+                         "The Language Server client",
+                         IDE_TYPE_LSP_CLIENT,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_lsp_completion_provider_init (IdeLspCompletionProvider *self)
+{
+}
+
+/**
+ * ide_lsp_completion_provider_get_client:
+ * @self: An #IdeLspCompletionProvider
+ *
+ * Gets the client for the completion provider.
+ *
+ * Returns: (transfer none) (nullable): An #IdeLspClient or %NULL
+ */
+IdeLspClient *
+ide_lsp_completion_provider_get_client (IdeLspCompletionProvider *self)
+{
+  IdeLspCompletionProviderPrivate *priv = ide_lsp_completion_provider_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LSP_COMPLETION_PROVIDER (self), NULL);
+
+  return priv->client;
+}
+
+void
+ide_lsp_completion_provider_set_client (IdeLspCompletionProvider *self,
+                                             IdeLspClient             *client)
+{
+  IdeLspCompletionProviderPrivate *priv = ide_lsp_completion_provider_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LSP_COMPLETION_PROVIDER (self));
+  g_return_if_fail (!client || IDE_IS_LSP_CLIENT (client));
+
+  if (g_set_object (&priv->client, client))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLIENT]);
+}
+
+static gint
+ide_lsp_completion_provider_get_priority (IdeCompletionProvider *provider,
+                                               IdeCompletionContext  *context)
+{
+  return IDE_LSP_COMPLETION_PROVIDER_PRIORITY;
+}
+
+static void
+ide_lsp_completion_provider_complete_cb (GObject      *object,
+                                              GAsyncResult *result,
+                                              gpointer      user_data)
+{
+  IdeLspCompletionProviderPrivate *priv;
+  IdeLspCompletionProvider *self;
+  IdeLspClient *client = (IdeLspClient *)object;
+  g_autoptr(GVariant) return_value = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeLspCompletionResults *ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_CLIENT (client));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  if (!ide_lsp_client_call_finish (client, result, &return_value, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self = ide_task_get_source_object (task);
+  priv = ide_lsp_completion_provider_get_instance_private (self);
+
+  ret = ide_lsp_completion_results_new (return_value);
+  if (priv->word != NULL && *priv->word != 0)
+    ide_lsp_completion_results_refilter (ret, priv->word);
+
+  ide_task_return_object (task, g_steal_pointer (&ret));
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_completion_provider_populate_async (IdeCompletionProvider  *provider,
+                                                 IdeCompletionContext   *context,
+                                                 GCancellable           *cancellable,
+                                                 GAsyncReadyCallback     callback,
+                                                 gpointer                user_data)
+{
+  IdeLspCompletionProvider *self = (IdeLspCompletionProvider *)provider;
+  IdeLspCompletionProviderPrivate *priv = ide_lsp_completion_provider_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+  GtkTextIter iter, end;
+  GtkTextBuffer *buffer;
+  gint line;
+  gint column;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_COMPLETION_PROVIDER (self));
+  g_assert (IDE_IS_COMPLETION_CONTEXT (context));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_completion_provider_populate_async);
+
+  if (priv->client == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "No client for completion");
+      IDE_EXIT;
+    }
+
+  ide_completion_context_get_bounds (context, &iter, &end);
+
+  g_clear_pointer (&priv->word, g_free);
+  priv->word = ide_completion_context_get_word (context);
+
+  buffer = ide_completion_context_get_buffer (context);
+  uri = ide_buffer_dup_uri (IDE_BUFFER (buffer));
+
+  line = gtk_text_iter_get_line (&iter);
+  column = gtk_text_iter_get_line_offset (&iter);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+    "}",
+    "position", "{",
+      "line", JSONRPC_MESSAGE_PUT_INT32 (line),
+      "character", JSONRPC_MESSAGE_PUT_INT32 (column),
+    "}"
+  );
+
+  ide_lsp_client_call_async (priv->client,
+                                  "textDocument/completion",
+                                  params,
+                                  cancellable,
+                                  ide_lsp_completion_provider_complete_cb,
+                                  g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static GListModel *
+ide_lsp_completion_provider_populate_finish (IdeCompletionProvider  *provider,
+                                                  GAsyncResult           *result,
+                                                  GError                **error)
+{
+  GListModel *ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_object (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+ide_lsp_completion_provider_refilter (IdeCompletionProvider *provider,
+                                           IdeCompletionContext  *context,
+                                           GListModel            *model)
+{
+  IdeLspCompletionResults *results = (IdeLspCompletionResults *)model;
+  g_autofree gchar *word = NULL;
+
+  g_assert (IDE_IS_LSP_COMPLETION_PROVIDER (provider));
+  g_assert (IDE_IS_COMPLETION_CONTEXT (context));
+  g_assert (IDE_IS_LSP_COMPLETION_RESULTS (results));
+
+  word = ide_completion_context_get_word (context);
+  ide_lsp_completion_results_refilter (results, word);
+
+  return TRUE;
+}
+
+static void
+ide_lsp_completion_provider_display_proposal (IdeCompletionProvider   *provider,
+                                                   IdeCompletionListBoxRow *row,
+                                                   IdeCompletionContext    *context,
+                                                   const gchar             *typed_text,
+                                                   IdeCompletionProposal   *proposal)
+{
+  IdeLspCompletionItem *item = (IdeLspCompletionItem *)proposal;
+  g_autofree gchar *markup = NULL;
+
+  g_assert (IDE_IS_LSP_COMPLETION_PROVIDER (provider));
+  g_assert (IDE_IS_COMPLETION_LIST_BOX_ROW (row));
+  g_assert (IDE_IS_COMPLETION_CONTEXT (context));
+  g_assert (IDE_IS_LSP_COMPLETION_ITEM (proposal));
+
+  markup = ide_lsp_completion_item_get_markup (item, typed_text);
+
+  ide_completion_list_box_row_set_icon_name (row, ide_lsp_completion_item_get_icon_name (item));
+  ide_completion_list_box_row_set_left (row, NULL);
+  ide_completion_list_box_row_set_center_markup (row, markup);
+  ide_completion_list_box_row_set_right (row, NULL);
+}
+
+static void
+ide_lsp_completion_provider_activate_proposal (IdeCompletionProvider *provider,
+                                                    IdeCompletionContext  *context,
+                                                    IdeCompletionProposal *proposal,
+                                                    const GdkEventKey     *key)
+{
+  g_autoptr(IdeSnippet) snippet = NULL;
+  GtkTextBuffer *buffer;
+  GtkTextView *view;
+  GtkTextIter begin, end;
+
+  g_assert (IDE_IS_COMPLETION_PROVIDER (provider));
+  g_assert (IDE_IS_COMPLETION_CONTEXT (context));
+  g_assert (IDE_IS_LSP_COMPLETION_ITEM (proposal));
+
+  buffer = ide_completion_context_get_buffer (context);
+  view = ide_completion_context_get_view (context);
+
+  snippet = ide_lsp_completion_item_get_snippet (IDE_LSP_COMPLETION_ITEM (proposal));
+
+  gtk_text_buffer_begin_user_action (buffer);
+  if (ide_completion_context_get_bounds (context, &begin, &end))
+    gtk_text_buffer_delete (buffer, &begin, &end);
+  ide_source_view_push_snippet (IDE_SOURCE_VIEW (view), snippet, &begin);
+  gtk_text_buffer_end_user_action (buffer);
+}
+
+static gchar *
+ide_lsp_completion_provider_get_comment (IdeCompletionProvider *provider,
+                                              IdeCompletionProposal *proposal)
+{
+  g_assert (IDE_IS_COMPLETION_PROVIDER (provider));
+  g_assert (IDE_IS_LSP_COMPLETION_ITEM (proposal));
+
+  return g_strdup (ide_lsp_completion_item_get_detail (IDE_LSP_COMPLETION_ITEM (proposal)));
+}
+
+static void
+provider_iface_init (IdeCompletionProviderInterface *iface)
+{
+  iface->get_priority = ide_lsp_completion_provider_get_priority;
+  iface->populate_async = ide_lsp_completion_provider_populate_async;
+  iface->populate_finish = ide_lsp_completion_provider_populate_finish;
+  iface->refilter = ide_lsp_completion_provider_refilter;
+  iface->display_proposal = ide_lsp_completion_provider_display_proposal;
+  iface->activate_proposal = ide_lsp_completion_provider_activate_proposal;
+  iface->get_comment = ide_lsp_completion_provider_get_comment;
+}
diff --git a/src/libide/lsp/ide-lsp-completion-provider.h b/src/libide/lsp/ide-lsp-completion-provider.h
new file mode 100644
index 000000000..1c7a5b8c8
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-completion-provider.h
@@ -0,0 +1,54 @@
+/* ide-lsp-completion-provider.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-sourceview.h>
+
+#include "ide-lsp-client.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_COMPLETION_PROVIDER (ide_lsp_completion_provider_get_type())
+
+#define IDE_LSP_COMPLETION_PROVIDER_PRIORITY 200
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeLspCompletionProvider, ide_lsp_completion_provider, IDE, 
LSP_COMPLETION_PROVIDER, IdeObject)
+
+struct _IdeLspCompletionProviderClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeLspClient *ide_lsp_completion_provider_get_client (IdeLspCompletionProvider *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_completion_provider_set_client (IdeLspCompletionProvider *self,
+                                                                IdeLspClient             *client);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-completion-results.c b/src/libide/lsp/ide-lsp-completion-results.c
new file mode 100644
index 000000000..fc7253d19
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-completion-results.c
@@ -0,0 +1,206 @@
+/* ide-lsp-completion-results.c
+ *
+ * Copyright 2018-2019 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 "ide-lsp-completion-results.h"
+
+#include "config.h"
+
+#include <libide-sourceview.h>
+
+#include "ide-lsp-completion-item.h"
+#include "ide-lsp-completion-results.h"
+
+struct _IdeLspCompletionResults
+{
+  GObject   parent_instance;
+  GVariant *results;
+  GArray   *items;
+};
+
+typedef struct
+{
+  guint index;
+  guint priority;
+} Item;
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeLspCompletionResults, ide_lsp_completion_results, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static void
+ide_lsp_completion_results_finalize (GObject *object)
+{
+  IdeLspCompletionResults *self = (IdeLspCompletionResults *)object;
+
+  g_clear_pointer (&self->results, g_variant_unref);
+  g_clear_pointer (&self->items, g_array_unref);
+
+  G_OBJECT_CLASS (ide_lsp_completion_results_parent_class)->finalize (object);
+}
+
+static void
+ide_lsp_completion_results_class_init (IdeLspCompletionResultsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_lsp_completion_results_finalize;
+}
+
+static void
+ide_lsp_completion_results_init (IdeLspCompletionResults *self)
+{
+  self->items = g_array_new (FALSE, FALSE, sizeof (Item));
+}
+
+IdeLspCompletionResults *
+ide_lsp_completion_results_new (GVariant *results)
+{
+  IdeLspCompletionResults *self;
+  g_autoptr(GVariant) items = NULL;
+
+  g_return_val_if_fail (results != NULL, NULL);
+
+  self = g_object_new (IDE_TYPE_LSP_COMPLETION_RESULTS, NULL);
+  self->results = g_variant_ref_sink (results);
+
+  /* Possibly unwrap the {items: []} style result. */
+  if (g_variant_is_of_type (results, G_VARIANT_TYPE_VARDICT) &&
+      (items = g_variant_lookup_value (results, "items", NULL)))
+    {
+      g_clear_pointer (&self->results, g_variant_unref);
+
+      if (g_variant_is_of_type (items, G_VARIANT_TYPE_VARIANT))
+        self->results = g_variant_get_variant (items);
+      else
+        self->results = g_steal_pointer (&items);
+    }
+
+  ide_lsp_completion_results_refilter (self, NULL);
+
+  return self;
+}
+
+static GType
+ide_lsp_completion_results_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_LSP_COMPLETION_ITEM;
+}
+
+static guint
+ide_lsp_completion_results_get_n_items (GListModel *model)
+{
+  IdeLspCompletionResults *self = (IdeLspCompletionResults *)model;
+
+  g_assert (IDE_IS_LSP_COMPLETION_RESULTS (self));
+
+  return self->items->len;
+}
+
+static gpointer
+ide_lsp_completion_results_get_item (GListModel *model,
+                                          guint       position)
+{
+  IdeLspCompletionResults *self = (IdeLspCompletionResults *)model;
+  g_autoptr(GVariant) child = NULL;
+  const Item *item;
+
+  g_assert (IDE_IS_LSP_COMPLETION_RESULTS (self));
+  g_assert (self->results != NULL);
+
+  item = &g_array_index (self->items, Item, position);
+  child = g_variant_get_child_value (self->results, item->index);
+
+  return ide_lsp_completion_item_new (child);
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item = ide_lsp_completion_results_get_item;
+  iface->get_n_items = ide_lsp_completion_results_get_n_items;
+  iface->get_item_type = ide_lsp_completion_results_get_item_type;
+}
+
+static gint
+compare_items (const Item *a,
+               const Item *b)
+{
+  return (gint)a->priority - (gint)b->priority;
+}
+
+void
+ide_lsp_completion_results_refilter (IdeLspCompletionResults *self,
+                                          const gchar                  *typed_text)
+{
+  g_autofree gchar *query = NULL;
+  GVariantIter iter;
+  GVariant *node;
+  guint index = 0;
+  guint old_len;
+
+  g_return_if_fail (IDE_IS_LSP_COMPLETION_RESULTS (self));
+
+  if ((old_len = self->items->len))
+    g_array_remove_range (self->items, 0, old_len);
+
+  if (self->results == NULL)
+    return;
+
+  if (typed_text == NULL || *typed_text == 0)
+    {
+      guint n_items = g_variant_n_children (self->results);
+
+      for (guint i = 0; i < n_items; i++)
+        {
+          Item item = { .index = i };
+          g_array_append_val (self->items, item);
+        }
+
+      g_list_model_items_changed (G_LIST_MODEL (self), 0, old_len, n_items);
+
+      return;
+    }
+
+  query = g_utf8_casefold (typed_text, -1);
+
+  g_variant_iter_init (&iter, self->results);
+
+  while (g_variant_iter_loop (&iter, "v", &node))
+    {
+      const gchar *label;
+      guint priority;
+
+      if (!g_variant_lookup (node, "label", "&s", &label))
+        continue;
+
+      if (ide_completion_fuzzy_match (label, query, &priority))
+        {
+          Item item = { .index = index, .priority = priority };
+          g_array_append_val (self->items, item);
+        }
+
+      index++;
+    }
+
+  g_array_sort (self->items, (GCompareFunc)compare_items);
+
+  g_list_model_items_changed (G_LIST_MODEL (self), 0, old_len, index);
+}
diff --git a/src/libide/lsp/ide-lsp-completion-results.h b/src/libide/lsp/ide-lsp-completion-results.h
new file mode 100644
index 000000000..24e3d85e2
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-completion-results.h
@@ -0,0 +1,42 @@
+/* ide-lsp-completion-results.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_COMPLETION_RESULTS (ide_lsp_completion_results_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeLspCompletionResults, ide_lsp_completion_results, IDE, LSP_COMPLETION_RESULTS, 
GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeLspCompletionResults *ide_lsp_completion_results_new      (GVariant                     *results);
+IDE_AVAILABLE_IN_3_32
+void                          ide_lsp_completion_results_refilter (IdeLspCompletionResults *self,
+                                                                        const gchar                  
*typed_text);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-diagnostic-provider.c b/src/libide/lsp/ide-lsp-diagnostic-provider.c
new file mode 100644
index 000000000..3dfeb5ce2
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-diagnostic-provider.c
@@ -0,0 +1,253 @@
+/* ide-lsp-diagnostic-provider.c
+ *
+ * Copyright 2016-2019 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 "ide-lsp-diagnostic-provider"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <json-glib/json-glib.h>
+#include <libide-code.h>
+#include <libide-threading.h>
+
+#include "ide-lsp-diagnostic-provider.h"
+
+typedef struct
+{
+  IdeLspClient *client;
+  DzlSignalGroup    *client_signals;
+} IdeLspDiagnosticProviderPrivate;
+
+static void diagnostic_provider_iface_init (IdeDiagnosticProviderInterface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdeLspDiagnosticProvider, ide_lsp_diagnostic_provider, IDE_TYPE_OBJECT,
+                                  G_ADD_PRIVATE (IdeLspDiagnosticProvider)
+                                  G_IMPLEMENT_INTERFACE (IDE_TYPE_DIAGNOSTIC_PROVIDER, 
diagnostic_provider_iface_init))
+
+enum {
+  PROP_0,
+  PROP_CLIENT,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_lsp_diagnostic_provider_get_diagnostics_cb (GObject      *object,
+                                                     GAsyncResult *result,
+                                                     gpointer      user_data)
+{
+  IdeLspClient *client = (IdeLspClient *)object;
+  g_autoptr(IdeDiagnostics) diagnostics = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_LSP_CLIENT (client));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_lsp_client_get_diagnostics_finish (client, result, &diagnostics, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task, g_steal_pointer (&diagnostics), g_object_unref);
+}
+
+static void
+ide_lsp_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
+                                            GFile                 *file,
+                                            GBytes                *content,
+                                            const gchar           *lang_id,
+                                            GCancellable          *cancellable,
+                                            GAsyncReadyCallback    callback,
+                                            gpointer               user_data)
+{
+  IdeLspDiagnosticProvider *self = (IdeLspDiagnosticProvider *)provider;
+  IdeLspDiagnosticProviderPrivate *priv = ide_lsp_diagnostic_provider_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_DIAGNOSTIC_PROVIDER (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (content != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_diagnostic_provider_diagnose_async);
+
+  if (priv->client == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Improperly configured %s is missing IdeLspClient",
+                                 G_OBJECT_TYPE_NAME (self));
+      IDE_EXIT;
+    }
+
+  ide_lsp_client_get_diagnostics_async (priv->client,
+                                        file,
+                                        content,
+                                        lang_id,
+                                        cancellable,
+                                        ide_lsp_diagnostic_provider_get_diagnostics_cb,
+                                        g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static IdeDiagnostics *
+ide_lsp_diagnostic_provider_diagnose_finish (IdeDiagnosticProvider  *provider,
+                                             GAsyncResult           *result,
+                                             GError                **error)
+{
+  IdeDiagnostics *ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_DIAGNOSTIC_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+diagnostic_provider_iface_init (IdeDiagnosticProviderInterface *iface)
+{
+  iface->diagnose_async = ide_lsp_diagnostic_provider_diagnose_async;
+  iface->diagnose_finish = ide_lsp_diagnostic_provider_diagnose_finish;
+}
+
+static void
+ide_lsp_diagnostic_provider_finalize (GObject *object)
+{
+  IdeLspDiagnosticProvider *self = (IdeLspDiagnosticProvider *)object;
+  IdeLspDiagnosticProviderPrivate *priv = ide_lsp_diagnostic_provider_get_instance_private (self);
+
+  g_clear_object (&priv->client_signals);
+  g_clear_object (&priv->client);
+
+  G_OBJECT_CLASS (ide_lsp_diagnostic_provider_parent_class)->finalize (object);
+}
+
+static void
+ide_lsp_diagnostic_provider_get_property (GObject    *object,
+                                               guint       prop_id,
+                                               GValue     *value,
+                                               GParamSpec *pspec)
+{
+  IdeLspDiagnosticProvider *self = IDE_LSP_DIAGNOSTIC_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      g_value_set_object (value, ide_lsp_diagnostic_provider_get_client (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_diagnostic_provider_set_property (GObject      *object,
+                                               guint         prop_id,
+                                               const GValue *value,
+                                               GParamSpec   *pspec)
+{
+  IdeLspDiagnosticProvider *self = IDE_LSP_DIAGNOSTIC_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      ide_lsp_diagnostic_provider_set_client (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_diagnostic_provider_class_init (IdeLspDiagnosticProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_lsp_diagnostic_provider_finalize;
+  object_class->get_property = ide_lsp_diagnostic_provider_get_property;
+  object_class->set_property = ide_lsp_diagnostic_provider_set_property;
+
+  properties [PROP_CLIENT] =
+    g_param_spec_object ("client",
+                         "Client",
+                         "The Language Server client",
+                         IDE_TYPE_LSP_CLIENT,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_lsp_diagnostic_provider_init (IdeLspDiagnosticProvider *self)
+{
+  IdeLspDiagnosticProviderPrivate *priv = ide_lsp_diagnostic_provider_get_instance_private (self);
+
+  priv->client_signals = dzl_signal_group_new (IDE_TYPE_LSP_CLIENT);
+
+  dzl_signal_group_connect_object (priv->client_signals,
+                                   "published-diagnostics",
+                                   G_CALLBACK (ide_diagnostic_provider_emit_invalidated),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+}
+
+/**
+ * ide_lsp_diagnostic_provider_get_client:
+ *
+ * Gets the client used by diagnostic provider.
+ *
+ * Returns: (nullable) (transfer none): An #IdeLspClient or %NULL.
+ */
+IdeLspClient *
+ide_lsp_diagnostic_provider_get_client (IdeLspDiagnosticProvider *self)
+{
+  IdeLspDiagnosticProviderPrivate *priv = ide_lsp_diagnostic_provider_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LSP_DIAGNOSTIC_PROVIDER (self), NULL);
+
+  return priv->client;
+}
+
+void
+ide_lsp_diagnostic_provider_set_client (IdeLspDiagnosticProvider *self,
+                                             IdeLspClient             *client)
+{
+  IdeLspDiagnosticProviderPrivate *priv = ide_lsp_diagnostic_provider_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LSP_DIAGNOSTIC_PROVIDER (self));
+  g_return_if_fail (!client || IDE_IS_LSP_CLIENT (client));
+
+  if (g_set_object (&priv->client, client))
+    {
+      dzl_signal_group_set_target (priv->client_signals, client);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLIENT]);
+    }
+}
diff --git a/src/libide/lsp/ide-lsp-diagnostic-provider.h b/src/libide/lsp/ide-lsp-diagnostic-provider.h
new file mode 100644
index 000000000..cc63d9cd7
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-diagnostic-provider.h
@@ -0,0 +1,52 @@
+/* ide-lsp-diagnostic-provider.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+#include "ide-lsp-client.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_DIAGNOSTIC_PROVIDER (ide_lsp_diagnostic_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeLspDiagnosticProvider, ide_lsp_diagnostic_provider, IDE, 
LSP_DIAGNOSTIC_PROVIDER, IdeObject)
+
+struct _IdeLspDiagnosticProviderClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeLspClient *ide_lsp_diagnostic_provider_get_client (IdeLspDiagnosticProvider *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_diagnostic_provider_set_client (IdeLspDiagnosticProvider *self,
+                                                                IdeLspClient             *client);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-formatter.c b/src/libide/lsp/ide-lsp-formatter.c
new file mode 100644
index 000000000..0a5c4c244
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-formatter.c
@@ -0,0 +1,430 @@
+/* ide-lsp-formatter.c
+ *
+ * Copyright 2017-2019 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 "ide-lsp-formatter"
+
+#include "config.h"
+
+#include <jsonrpc-glib.h>
+
+#include <libide-code.h>
+#include <libide-threading.h>
+
+#include "ide-lsp-formatter.h"
+
+typedef struct
+{
+  IdeLspClient *client;
+} IdeLspFormatterPrivate;
+
+enum {
+  PROP_0,
+  PROP_CLIENT,
+  N_PROPS
+};
+
+static void formatter_iface_init (IdeFormatterInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeLspFormatter, ide_lsp_formatter, IDE_TYPE_OBJECT,
+                         G_ADD_PRIVATE (IdeLspFormatter)
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_FORMATTER, formatter_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * ide_lsp_formatter_get_client:
+ * @self: a #IdeLspFormatter
+ *
+ * Gets the client to use for the formatter.
+ *
+ * Returns: (transfer none): An #IdeLspClient or %NULL.
+ */
+IdeLspClient *
+ide_lsp_formatter_get_client (IdeLspFormatter *self)
+{
+  IdeLspFormatterPrivate *priv = ide_lsp_formatter_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LSP_FORMATTER (self), NULL);
+
+  return priv->client;
+}
+
+void
+ide_lsp_formatter_set_client (IdeLspFormatter *self,
+                              IdeLspClient    *client)
+{
+  IdeLspFormatterPrivate *priv = ide_lsp_formatter_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LSP_FORMATTER (self));
+
+  if (g_set_object (&priv->client, client))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLIENT]);
+}
+
+static void
+ide_lsp_formatter_finalize (GObject *object)
+{
+  IdeLspFormatter *self = (IdeLspFormatter *)object;
+  IdeLspFormatterPrivate *priv = ide_lsp_formatter_get_instance_private (self);
+
+  g_clear_object (&priv->client);
+
+  G_OBJECT_CLASS (ide_lsp_formatter_parent_class)->finalize (object);
+}
+
+static void
+ide_lsp_formatter_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeLspFormatter *self = IDE_LSP_FORMATTER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      g_value_set_object (value, ide_lsp_formatter_get_client (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_formatter_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeLspFormatter *self = IDE_LSP_FORMATTER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      ide_lsp_formatter_set_client (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_formatter_class_init (IdeLspFormatterClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_lsp_formatter_finalize;
+  object_class->get_property = ide_lsp_formatter_get_property;
+  object_class->set_property = ide_lsp_formatter_set_property;
+
+  properties [PROP_CLIENT] =
+    g_param_spec_object ("client",
+                         "Client",
+                         "The client to communicate over",
+                         IDE_TYPE_LSP_CLIENT,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_lsp_formatter_init (IdeLspFormatter *self)
+{
+}
+
+static void
+ide_lsp_formatter_apply_changes (IdeLspFormatter *self,
+                                 IdeBuffer       *buffer,
+                                 GVariant        *text_edits)
+{
+  g_autoptr(GPtrArray) edits = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  IdeBufferManager *buffer_manager;
+  GVariant *text_edit;
+  GFile *file;
+  GVariantIter iter;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_FORMATTER (self));
+  g_assert (text_edits != NULL);
+
+  if (!g_variant_is_container (text_edits))
+    {
+      g_warning ("variant is not a container, ignoring");
+      IDE_EXIT;
+    }
+
+  file = ide_buffer_get_file (buffer);
+  edits = g_ptr_array_new_with_free_func (g_object_unref);
+
+  g_variant_iter_init (&iter, text_edits);
+
+  while (g_variant_iter_loop (&iter, "v", &text_edit))
+    {
+      g_autoptr(IdeLocation) begin_location = NULL;
+      g_autoptr(IdeLocation) end_location = NULL;
+      g_autoptr(IdeRange) range = NULL;
+      const gchar *new_text = NULL;
+      gboolean success;
+      struct {
+        gint64 line;
+        gint64 column;
+      } begin, end;
+
+      success = JSONRPC_MESSAGE_PARSE (text_edit,
+        "range", "{",
+          "start", "{",
+            "line", JSONRPC_MESSAGE_GET_INT64 (&begin.line),
+            "character", JSONRPC_MESSAGE_GET_INT64 (&begin.column),
+          "}",
+          "end", "{",
+            "line", JSONRPC_MESSAGE_GET_INT64 (&end.line),
+            "character", JSONRPC_MESSAGE_GET_INT64 (&end.column),
+          "}",
+        "}",
+        "newText", JSONRPC_MESSAGE_GET_STRING (&new_text)
+      );
+
+      if (!success)
+        {
+          IDE_TRACE_MSG ("Failed to extract change from variant");
+          continue;
+        }
+
+      begin_location = ide_location_new (file, begin.line, begin.column);
+      end_location = ide_location_new (file, end.line, end.column);
+      range = ide_range_new (begin_location, end_location);
+
+      g_ptr_array_add (edits, ide_text_edit_new (range, new_text));
+    }
+
+  context = ide_buffer_ref_context (buffer);
+  buffer_manager = ide_buffer_manager_from_context (context);
+
+  ide_buffer_manager_apply_edits_async (buffer_manager,
+                                        IDE_PTR_ARRAY_STEAL_FULL (&edits),
+                                        NULL, NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_formatter_format_call_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  IdeLspClient *client = (IdeLspClient *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) reply = NULL;
+  IdeLspFormatter *self;
+  IdeBuffer *buffer;
+
+  g_return_if_fail (IDE_IS_LSP_CLIENT (client));
+  g_return_if_fail (G_IS_ASYNC_RESULT (result));
+
+  if (!ide_lsp_client_call_finish (client, result, &reply, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self = ide_task_get_source_object (task);
+  buffer = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_LSP_FORMATTER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  ide_lsp_formatter_apply_changes (self, buffer, reply);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_lsp_formatter_format_async (IdeFormatter        *formatter,
+                                IdeBuffer           *buffer,
+                                IdeFormatterOptions *options,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  IdeLspFormatter *self = (IdeLspFormatter *)formatter;
+  IdeLspFormatterPrivate *priv = ide_lsp_formatter_get_instance_private (self);
+  g_autoptr(GVariant) params = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autofree gchar *uri = NULL;
+  g_autofree gchar *text = NULL;
+  GtkTextIter begin;
+  GtkTextIter end;
+  gint64 version;
+  gint tab_size;
+  gboolean insert_spaces;
+
+  g_assert (IDE_IS_LSP_FORMATTER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_formatter_format_async);
+  ide_task_set_task_data (task, g_object_ref (buffer), g_object_unref);
+
+  gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end);
+  gtk_text_iter_order (&begin, &end);
+
+  version = ide_buffer_get_change_count (buffer);
+  uri = ide_buffer_dup_uri (buffer);
+  text = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (buffer), &begin, &end, TRUE);
+
+  tab_size = ide_formatter_options_get_tab_width (options);
+  insert_spaces = ide_formatter_options_get_insert_spaces (options);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+      "text", JSONRPC_MESSAGE_PUT_STRING (text),
+      "version", JSONRPC_MESSAGE_PUT_INT64 (version),
+    "}",
+    "options", "{",
+      "tabSize", JSONRPC_MESSAGE_PUT_INT32 (tab_size),
+      "insertSpaces", JSONRPC_MESSAGE_PUT_BOOLEAN (insert_spaces),
+    "}"
+  );
+
+  ide_lsp_client_call_async (priv->client,
+                                  "textDocument/formatting",
+                                  params,
+                                  cancellable,
+                                  ide_lsp_formatter_format_call_cb,
+                                  g_steal_pointer (&task));
+}
+
+static gboolean
+ide_lsp_formatter_format_finish (IdeFormatter  *self,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  g_assert (IDE_IS_FORMATTER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_lsp_formatter_format_range_async (IdeFormatter        *formatter,
+                                      IdeBuffer           *buffer,
+                                      IdeFormatterOptions *options,
+                                      const GtkTextIter   *begin,
+                                      const GtkTextIter   *end,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  IdeLspFormatter *self = (IdeLspFormatter *)formatter;
+  IdeLspFormatterPrivate *priv = ide_lsp_formatter_get_instance_private (self);
+  g_autoptr(GVariant) params = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autofree gchar *uri = NULL;
+  g_autofree gchar *text = NULL;
+  gint64 version;
+  gint tab_size;
+  gboolean insert_spaces;
+  struct {
+    gint line;
+    gint character;
+  } b, e;
+
+  g_assert (IDE_IS_LSP_FORMATTER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_formatter_format_async);
+  ide_task_set_task_data (task, g_object_ref (buffer), g_object_unref);
+
+  if (gtk_text_iter_compare (begin, end) > 0)
+    {
+      const GtkTextIter *tmp = end;
+      end = begin;
+      begin = tmp;
+    }
+
+  version = ide_buffer_get_change_count (buffer);
+  uri = ide_buffer_dup_uri (buffer);
+  text = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (buffer), begin, end, TRUE);
+
+  tab_size = ide_formatter_options_get_tab_width (options);
+  insert_spaces = ide_formatter_options_get_insert_spaces (options);
+
+  b.line = gtk_text_iter_get_line (begin);
+  b.character = gtk_text_iter_get_line_offset (begin);
+
+  e.line = gtk_text_iter_get_line (end);
+  e.character = gtk_text_iter_get_line_offset (begin);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+      "text", JSONRPC_MESSAGE_PUT_STRING (text),
+      "version", JSONRPC_MESSAGE_PUT_INT64 (version),
+    "}",
+    "options", "{",
+      "tabSize", JSONRPC_MESSAGE_PUT_INT32 (tab_size),
+      "insertSpaces", JSONRPC_MESSAGE_PUT_BOOLEAN (insert_spaces),
+    "}",
+    "range", "{",
+      "start", "{",
+        "line", JSONRPC_MESSAGE_PUT_INT32 (b.line),
+        "character", JSONRPC_MESSAGE_PUT_INT32 (b.character),
+      "}",
+      "end", "{",
+        "line", JSONRPC_MESSAGE_PUT_INT32 (e.line),
+        "character", JSONRPC_MESSAGE_PUT_INT32 (e.character),
+      "}",
+    "}"
+  );
+
+  ide_lsp_client_call_async (priv->client,
+                             "textDocument/rangeFormatting",
+                             params,
+                             cancellable,
+                             ide_lsp_formatter_format_call_cb,
+                             g_steal_pointer (&task));
+}
+
+static gboolean
+ide_lsp_formatter_format_range_finish (IdeFormatter  *self,
+                                       GAsyncResult  *result,
+                                       GError       **error)
+{
+  g_assert (IDE_IS_FORMATTER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+formatter_iface_init (IdeFormatterInterface *iface)
+{
+  iface->format_async = ide_lsp_formatter_format_async;
+  iface->format_finish = ide_lsp_formatter_format_finish;
+  iface->format_range_async = ide_lsp_formatter_format_range_async;
+  iface->format_range_finish = ide_lsp_formatter_format_range_finish;
+}
diff --git a/src/libide/lsp/ide-lsp-formatter.h b/src/libide/lsp/ide-lsp-formatter.h
new file mode 100644
index 000000000..d842ed4bb
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-formatter.h
@@ -0,0 +1,52 @@
+/* ide-lsp-formatter.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+#include "ide-lsp-client.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_FORMATTER (ide_lsp_formatter_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeLspFormatter, ide_lsp_formatter, IDE, LSP_FORMATTER, IdeObject)
+
+struct _IdeLspFormatter
+{
+  IdeObject parent_class;
+
+  /*< private >*/
+  gpointer _reserved[4];
+};
+
+IDE_AVAILABLE_IN_3_32
+void                  ide_lsp_formatter_set_client (IdeLspFormatter *self,
+                                                         IdeLspClient    *client);
+IDE_AVAILABLE_IN_3_32
+IdeLspClient    *ide_lsp_formatter_get_client (IdeLspFormatter *self);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-highlighter.c b/src/libide/lsp/ide-lsp-highlighter.c
new file mode 100644
index 000000000..20b570b67
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-highlighter.c
@@ -0,0 +1,518 @@
+/* ide-lsp-highlighter.c
+ *
+ * Copyright 2016-2019 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 "ide-lsp-highlighter"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-code.h>
+#include <jsonrpc-glib.h>
+
+#include "ide-lsp-highlighter.h"
+
+#define DELAY_TIMEOUT_MSEC 333
+
+/*
+ * NOTE: This is not an ideal way to do an indexer because we don't get all the
+ * symbols that might be available. It also doesn't allow us to restrict the
+ * highlights to the proper scope. However, until Language Server Protocol
+ * provides a way to do this, it's about the best we can do.
+ */
+
+typedef struct
+{
+  IdeHighlightEngine *engine;
+
+  IdeLspClient  *client;
+  IdeHighlightIndex  *index;
+  DzlSignalGroup     *buffer_signals;
+
+  guint               queued_update;
+
+  guint               active : 1;
+  guint               dirty : 1;
+} IdeLspHighlighterPrivate;
+
+static void highlighter_iface_init                (IdeHighlighterInterface *iface);
+static void ide_lsp_highlighter_queue_update (IdeLspHighlighter  *self);
+
+G_DEFINE_TYPE_WITH_CODE (IdeLspHighlighter, ide_lsp_highlighter, IDE_TYPE_OBJECT,
+                         G_ADD_PRIVATE (IdeLspHighlighter)
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_HIGHLIGHTER, highlighter_iface_init))
+
+enum {
+  PROP_0,
+  PROP_CLIENT,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_lsp_highlighter_set_index (IdeLspHighlighter *self,
+                                    IdeHighlightIndex      *index)
+{
+  IdeLspHighlighterPrivate *priv = ide_lsp_highlighter_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_HIGHLIGHTER (self));
+
+  g_clear_pointer (&priv->index, ide_highlight_index_unref);
+  if (index != NULL)
+    priv->index = ide_highlight_index_ref (index);
+
+  if (priv->engine != NULL)
+    {
+      if (priv->index != NULL)
+        ide_highlight_engine_rebuild (priv->engine);
+      else
+        ide_highlight_engine_clear (priv->engine);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_highlighter_document_symbol_cb (GObject      *object,
+                                             GAsyncResult *result,
+                                             gpointer      user_data)
+{
+  IdeLspClient *client = (IdeLspClient *)object;
+  g_autoptr(IdeLspHighlighter) self = user_data;
+  IdeLspHighlighterPrivate *priv = ide_lsp_highlighter_get_instance_private (self);
+  g_autoptr(GVariant) return_value = NULL;
+  g_autoptr(GError) error = NULL;
+  GVariantIter iter;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_CLIENT (client));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_LSP_HIGHLIGHTER (self));
+
+  priv->active = FALSE;
+
+  if (!ide_lsp_client_call_finish (client, result, &return_value, &error))
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        g_debug ("%s", error->message);
+      IDE_EXIT;
+    }
+
+  /* TODO: We should get the tag to have the proper name based on the type. */
+
+  if (g_variant_iter_init (&iter, return_value))
+    {
+      g_autoptr(IdeHighlightIndex) index = ide_highlight_index_new ();
+      GVariant *member = NULL;
+
+      while (g_variant_iter_loop (&iter, "v", &member))
+        {
+          const gchar *name = NULL;
+          const gchar *tag;
+          gboolean success;
+          gint64 kind = 0;
+
+          success = JSONRPC_MESSAGE_PARSE (member,
+            "name", JSONRPC_MESSAGE_GET_STRING (&name),
+            "kind", JSONRPC_MESSAGE_GET_INT64 (&kind)
+          );
+
+          if (!success)
+            {
+              IDE_TRACE_MSG ("Failed to unwrap name and kind from symbol");
+              continue;
+            }
+
+          switch (kind)
+            {
+            case 6:   /* METHOD */
+            case 12:  /* FUNCTION */
+            case 9:   /* CONSTRUCTOR */
+              tag = "def:function";
+              break;
+
+            case 2:   /* MODULE */
+            case 3:   /* NAMESPACE */
+            case 4:   /* PACKAGE */
+            case 5:   /* CLASS */
+            case 10:  /* ENUM */
+            case 11:  /* INTERFACE */
+              tag = "def:type";
+              break;
+
+            case 14:  /* CONSTANT */
+              tag = "def:constant";
+              break;
+
+            case 7:   /* PROPERTY */
+            case 8:   /* FIELD */
+            case 13:  /* VARIABLE */
+              tag = "def:identifier";
+              break;
+
+            default:
+              tag = NULL;
+            }
+
+          if (tag != NULL)
+            ide_highlight_index_insert (index, name, (gpointer)tag);
+        }
+
+      ide_lsp_highlighter_set_index (self, index);
+    }
+
+  if (priv->dirty)
+    ide_lsp_highlighter_queue_update (self);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_lsp_highlighter_update_symbols (gpointer data)
+{
+  IdeLspHighlighter *self = data;
+  IdeLspHighlighterPrivate *priv = ide_lsp_highlighter_get_instance_private (self);
+
+  g_assert (IDE_IS_LSP_HIGHLIGHTER (self));
+
+  priv->queued_update = 0;
+
+  if (priv->client != NULL && priv->engine != NULL)
+    {
+      g_autoptr(GVariant) params = NULL;
+      g_autofree gchar *uri = NULL;
+      IdeBuffer *buffer;
+
+      buffer = ide_highlight_engine_get_buffer (priv->engine);
+      uri = ide_buffer_dup_uri (buffer);
+
+      params = JSONRPC_MESSAGE_NEW (
+        "textDocument", "{",
+          "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+        "}"
+      );
+
+      priv->active = TRUE;
+      priv->dirty = FALSE;
+
+      ide_lsp_client_call_async (priv->client,
+                                      "textDocument/documentSymbol",
+                                      params,
+                                      NULL,
+                                      ide_lsp_highlighter_document_symbol_cb,
+                                      g_object_ref (self));
+    }
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_lsp_highlighter_queue_update (IdeLspHighlighter *self)
+{
+  IdeLspHighlighterPrivate *priv = ide_lsp_highlighter_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_HIGHLIGHTER (self));
+
+  priv->dirty = TRUE;
+
+  /*
+   * Queue an update to get the newest symbol list (which we'll use to build
+   * the highlight index).
+   */
+
+  if (priv->queued_update == 0 && priv->active == FALSE)
+    {
+      priv->queued_update = g_timeout_add (DELAY_TIMEOUT_MSEC,
+                                           ide_lsp_highlighter_update_symbols,
+                                           self);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_highlighter_buffer_line_flags_changed (IdeLspHighlighter *self,
+                                                    IdeBuffer              *buffer)
+{
+  g_assert (IDE_IS_LSP_HIGHLIGHTER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  ide_lsp_highlighter_queue_update (self);
+}
+
+static void
+ide_lsp_highlighter_dispose (GObject *object)
+{
+  IdeLspHighlighter *self = (IdeLspHighlighter *)object;
+  IdeLspHighlighterPrivate *priv = ide_lsp_highlighter_get_instance_private (self);
+
+  priv->engine = NULL;
+
+  dzl_clear_source (&priv->queued_update);
+
+  g_clear_pointer (&priv->index, ide_highlight_index_unref);
+  g_clear_object (&priv->buffer_signals);
+  g_clear_object (&priv->client);
+
+  G_OBJECT_CLASS (ide_lsp_highlighter_parent_class)->dispose (object);
+}
+
+static void
+ide_lsp_highlighter_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  IdeLspHighlighter *self = IDE_LSP_HIGHLIGHTER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      g_value_set_object (value, ide_lsp_highlighter_get_client (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_highlighter_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  IdeLspHighlighter *self = IDE_LSP_HIGHLIGHTER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      ide_lsp_highlighter_set_client (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_highlighter_class_init (IdeLspHighlighterClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_lsp_highlighter_dispose;
+  object_class->get_property = ide_lsp_highlighter_get_property;
+  object_class->set_property = ide_lsp_highlighter_set_property;
+
+  properties [PROP_CLIENT] =
+    g_param_spec_object ("client",
+                         "Client",
+                         "Client",
+                         IDE_TYPE_LSP_CLIENT,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_lsp_highlighter_init (IdeLspHighlighter *self)
+{
+  IdeLspHighlighterPrivate *priv = ide_lsp_highlighter_get_instance_private (self);
+
+  priv->buffer_signals = dzl_signal_group_new (IDE_TYPE_BUFFER);
+
+  /*
+   * We sort of cheat here by using ::line-flags-changed instead of :;changed
+   * because it signifies to us that a diagnostics query has come back from the
+   * language server and therefore we are more likely to get a valid response
+   * for the documentation lookup. Otherwise, we can often get in a situation
+   * where the language server gives us an empty set back (or at least with
+   * the rust language server).
+   */
+  dzl_signal_group_connect_object (priv->buffer_signals,
+                                   "line-flags-changed",
+                                   G_CALLBACK (ide_lsp_highlighter_buffer_line_flags_changed),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+}
+
+/**
+ * ide_lsp_highlighter_get_client:
+ *
+ * Returns: (transfer none) (nullable): An #IdeLspHighlighter or %NULL.
+ */
+IdeLspClient *
+ide_lsp_highlighter_get_client (IdeLspHighlighter *self)
+{
+  IdeLspHighlighterPrivate *priv = ide_lsp_highlighter_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LSP_HIGHLIGHTER (self), NULL);
+
+  return priv->client;
+}
+
+void
+ide_lsp_highlighter_set_client (IdeLspHighlighter *self,
+                                     IdeLspClient      *client)
+{
+  IdeLspHighlighterPrivate *priv = ide_lsp_highlighter_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LSP_HIGHLIGHTER (self));
+  g_return_if_fail (!client || IDE_IS_LSP_CLIENT (client));
+
+  if (g_set_object (&priv->client, client))
+    {
+      ide_lsp_highlighter_queue_update (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLIENT]);
+    }
+}
+
+static inline gboolean
+accepts_char (gunichar ch)
+{
+  return (ch == '_' || g_unichar_isalnum (ch));
+}
+
+static inline gboolean
+select_next_word (GtkTextIter *begin,
+                  GtkTextIter *end)
+{
+
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+  *end = *begin;
+
+  while (!accepts_char (gtk_text_iter_get_char (begin)))
+    if (!gtk_text_iter_forward_char (begin))
+      return FALSE;
+
+  *end = *begin;
+
+  while (accepts_char (gtk_text_iter_get_char (end)))
+    if (!gtk_text_iter_forward_char (end))
+      return !gtk_text_iter_equal (begin, end);
+
+  return TRUE;
+}
+
+static void
+ide_lsp_highlighter_update (IdeHighlighter       *highlighter,
+                                 IdeHighlightCallback  callback,
+                                 const GtkTextIter    *range_begin,
+                                 const GtkTextIter    *range_end,
+                                 GtkTextIter          *location)
+{
+  IdeLspHighlighter *self = (IdeLspHighlighter *)highlighter;
+  IdeLspHighlighterPrivate *priv = ide_lsp_highlighter_get_instance_private (self);
+  GtkSourceBuffer *source_buffer;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_LSP_HIGHLIGHTER (self));
+  g_assert (callback != NULL);
+
+  if (priv->index == NULL)
+    {
+      *location = *range_end;
+      return;
+    }
+
+  source_buffer = GTK_SOURCE_BUFFER (gtk_text_iter_get_buffer (range_begin));
+
+  begin = end = *location = *range_begin;
+
+  while (gtk_text_iter_compare (&begin, range_end) < 0)
+    {
+      if (!select_next_word (&begin, &end))
+        goto completed;
+
+      if (gtk_text_iter_compare (&begin, range_end) >= 0)
+        goto completed;
+
+      g_assert (!gtk_text_iter_equal (&begin, &end));
+
+      if (!gtk_source_buffer_iter_has_context_class (source_buffer, &begin, "string") &&
+          !gtk_source_buffer_iter_has_context_class (source_buffer, &begin, "path") &&
+          !gtk_source_buffer_iter_has_context_class (source_buffer, &begin, "comment"))
+        {
+          const gchar *tag;
+          gchar *word;
+
+          word = gtk_text_iter_get_slice (&begin, &end);
+          tag = ide_highlight_index_lookup (priv->index, word);
+          g_free (word);
+
+          if (tag != NULL)
+            {
+              if (callback (&begin, &end, tag) == IDE_HIGHLIGHT_STOP)
+                {
+                  *location = end;
+                  return;
+                }
+            }
+        }
+
+      begin = end;
+    }
+
+completed:
+  *location = *range_end;
+}
+
+static void
+ide_lsp_highlighter_set_engine (IdeHighlighter     *highlighter,
+                                     IdeHighlightEngine *engine)
+{
+  IdeLspHighlighter *self = (IdeLspHighlighter *)highlighter;
+  IdeLspHighlighterPrivate *priv = ide_lsp_highlighter_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_HIGHLIGHTER (self));
+  g_assert (!engine || IDE_IS_HIGHLIGHT_ENGINE (engine));
+
+  priv->engine = engine;
+
+  dzl_signal_group_set_target (priv->buffer_signals, NULL);
+
+  if (engine != NULL)
+    {
+      IdeBuffer *buffer;
+
+      buffer = ide_highlight_engine_get_buffer (engine);
+      dzl_signal_group_set_target (priv->buffer_signals, buffer);
+      ide_lsp_highlighter_queue_update (self);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+highlighter_iface_init (IdeHighlighterInterface *iface)
+{
+  iface->update = ide_lsp_highlighter_update;
+  iface->set_engine = ide_lsp_highlighter_set_engine;
+}
diff --git a/src/libide/lsp/ide-lsp-highlighter.h b/src/libide/lsp/ide-lsp-highlighter.h
new file mode 100644
index 000000000..603c63be0
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-highlighter.h
@@ -0,0 +1,52 @@
+/* ide-lsp-highlighter.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+#include "ide-lsp-client.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_HIGHLIGHTER (ide_lsp_highlighter_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeLspHighlighter, ide_lsp_highlighter, IDE, LSP_HIGHLIGHTER, IdeObject)
+
+struct _IdeLspHighlighterClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeLspClient *ide_lsp_highlighter_get_client (IdeLspHighlighter *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_highlighter_set_client (IdeLspHighlighter *self,
+                                                        IdeLspClient      *client);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-hover-provider.c b/src/libide/lsp/ide-lsp-hover-provider.c
new file mode 100644
index 000000000..1e38087f7
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-hover-provider.c
@@ -0,0 +1,479 @@
+/* ide-lsp-hover-provider.c
+ *
+ * Copyright 2018-2019 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 "ide-lsp-hover-provider"
+
+#include "config.h"
+
+#include <jsonrpc-glib.h>
+#include <libide-code.h>
+#include <libide-sourceview.h>
+#include <libide-threading.h>
+
+#include "ide-lsp-hover-provider.h"
+
+/**
+ * SECTION:ide-lsp-hover-provider
+ * @title: IdeLspHoverProvider
+ * @short_description: Interactive hover integration for language servers
+ *
+ * The #IdeLspHoverProvider provides integration with language servers
+ * that support hover requests. This can display markup in the interactive
+ * tooltip that is displayed in the editor.
+ *
+ * Since: 3.30
+ */
+
+typedef struct
+{
+  IdeLspClient *client;
+  gchar *category;
+  gint priority;
+} IdeLspHoverProviderPrivate;
+
+static void hover_provider_iface_init (IdeHoverProviderInterface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdeLspHoverProvider,
+                                  ide_lsp_hover_provider,
+                                  IDE_TYPE_OBJECT,
+                                  G_ADD_PRIVATE (IdeLspHoverProvider)
+                                  G_IMPLEMENT_INTERFACE (IDE_TYPE_HOVER_PROVIDER, hover_provider_iface_init))
+
+enum {
+  PROP_0,
+  PROP_CATEGORY,
+  PROP_CLIENT,
+  PROP_PRIORITY,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static IdeMarkedContent *
+parse_marked_string (GVariant *v)
+{
+  g_autoptr(GString) gstr = g_string_new (NULL);
+  g_autoptr(GVariant) child = NULL;
+  GVariant *item;
+  GVariantIter iter;
+
+  g_assert (v != NULL);
+
+  /*
+   * @v can be (MarkedString | MarkedString[] | MarkupContent)
+   *
+   * MarkedString is (string | { language: string, value: string })
+   */
+
+  if (g_variant_is_of_type (v, G_VARIANT_TYPE_STRING))
+    {
+      gsize len = 0;
+      const gchar *str = g_variant_get_string (v, &len);
+
+      if (str && *str == '\0')
+        return NULL;
+
+      return ide_marked_content_new_from_data (str, len, IDE_MARKED_KIND_PLAINTEXT);
+    }
+
+  if (g_variant_is_of_type (v, G_VARIANT_TYPE_VARIANT))
+    v = child = g_variant_get_variant (v);
+
+  g_variant_iter_init (&iter, v);
+
+  if ((item = g_variant_iter_next_value (&iter)))
+    {
+      GVariant *asv = item;
+      g_autoptr(GVariant) child2 = NULL;
+
+      if (g_variant_is_of_type (item, G_VARIANT_TYPE_VARIANT))
+        asv = child2 = g_variant_get_variant (item);
+
+      if (g_variant_is_of_type (asv, G_VARIANT_TYPE_STRING))
+        g_string_append (gstr, g_variant_get_string (asv, NULL));
+      else if (g_variant_is_of_type (asv, G_VARIANT_TYPE_VARDICT))
+        {
+          const gchar *lang = "";
+          const gchar *value = "";
+
+          g_variant_lookup (asv, "language", "&s", &lang);
+          g_variant_lookup (asv, "value", "&s", &value);
+
+#if 0
+          if (!ide_str_empty0 (lang) && !ide_str_empty0 (value))
+            g_string_append_printf (str, "```%s\n%s\n```", lang, value);
+          else if (!ide_str_empty0 (value))
+            g_string_append (str, value);
+#else
+          if (!ide_str_empty0 (value))
+            g_string_append_printf (gstr, "```\n%s\n```", value);
+#endif
+        }
+
+      g_variant_unref (item);
+    }
+
+  if (gstr->len)
+    return ide_marked_content_new_from_data (gstr->str, gstr->len, IDE_MARKED_KIND_MARKDOWN);
+
+  return NULL;
+}
+
+static void
+ide_lsp_hover_provider_dispose (GObject *object)
+{
+  IdeLspHoverProvider *self = (IdeLspHoverProvider *)object;
+  IdeLspHoverProviderPrivate *priv = ide_lsp_hover_provider_get_instance_private (self);
+
+  IDE_ENTRY;
+
+  g_clear_object (&priv->client);
+  g_clear_pointer (&priv->category, g_free);
+
+  G_OBJECT_CLASS (ide_lsp_hover_provider_parent_class)->dispose (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_hover_provider_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  IdeLspHoverProvider *self = IDE_LSP_HOVER_PROVIDER (object);
+  IdeLspHoverProviderPrivate *priv = ide_lsp_hover_provider_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_CATEGORY:
+      g_value_set_string (value, priv->category);
+      break;
+
+    case PROP_CLIENT:
+      g_value_set_object (value, ide_lsp_hover_provider_get_client (self));
+      break;
+
+    case PROP_PRIORITY:
+      g_value_set_int (value, priv->priority);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_hover_provider_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  IdeLspHoverProvider *self = IDE_LSP_HOVER_PROVIDER (object);
+  IdeLspHoverProviderPrivate *priv = ide_lsp_hover_provider_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_CATEGORY:
+      g_free (priv->category);
+      priv->category = g_value_dup_string (value);
+      break;
+
+    case PROP_CLIENT:
+      ide_lsp_hover_provider_set_client (self, g_value_get_object (value));
+      break;
+
+    case PROP_PRIORITY:
+      priv->priority = g_value_get_int (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_hover_provider_class_init (IdeLspHoverProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_lsp_hover_provider_dispose;
+  object_class->get_property = ide_lsp_hover_provider_get_property;
+  object_class->set_property = ide_lsp_hover_provider_set_property;
+
+  /**
+   * IdeLspHoverProvider:client:
+   *
+   * The "client" property is the #IdeLspClient that should be used to
+   * communicate with the Language Server peer process.
+   *
+   * Since: 3.30
+   */
+  properties [PROP_CLIENT] =
+    g_param_spec_object ("client",
+                         "Client",
+                         "The client to communicate with",
+                         IDE_TYPE_LSP_CLIENT,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeLspHoverProvider:category:
+   *
+   * The "category" property is the category name to use when displaying
+   * the hover contents.
+   *
+   * Since: 3.30
+   */
+  properties [PROP_CATEGORY] =
+    g_param_spec_string ("category",
+                         "Category",
+                         "The category to display in the hover popover",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PRIORITY] =
+    g_param_spec_int ("priority",
+                      "Priority",
+                      "Priority for hover content",
+                      G_MININT,
+                      G_MAXINT,
+                      0,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_lsp_hover_provider_init (IdeLspHoverProvider *self)
+{
+}
+
+static void
+ide_lsp_hover_provider_hover_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  IdeLspClient *client = (IdeLspClient *)object;
+  IdeLspHoverProvider *self;
+  IdeLspHoverProviderPrivate *priv;
+  g_autoptr(GVariant) reply = NULL;
+  g_autoptr(GVariant) contents = NULL;
+  g_autoptr(IdeMarkedContent) marked = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeHoverContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_CLIENT (client));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  priv = ide_lsp_hover_provider_get_instance_private (self);
+
+  g_assert (IDE_IS_LSP_HOVER_PROVIDER (self));
+
+  if (!ide_lsp_client_call_finish (client, result, &reply, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (!g_variant_is_of_type (reply, G_VARIANT_TYPE_VARDICT) ||
+      !(contents = g_variant_lookup_value (reply, "contents", NULL)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_DATA,
+                                 "Expected 'contents' in reply");
+      IDE_EXIT;
+    }
+
+  if (!(marked = parse_marked_string (contents)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_DATA,
+                                 "Unusable contents from language server");
+      IDE_EXIT;
+    }
+
+  context = ide_task_get_task_data (task);
+
+  g_assert (context != NULL);
+  g_assert (IDE_IS_HOVER_CONTEXT (context));
+
+  ide_hover_context_add_content (context,
+                                 priv->priority,
+                                 priv->category,
+                                 marked);
+
+  ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_hover_provider_hover_async (IdeHoverProvider    *provider,
+                                         IdeHoverContext     *context,
+                                         const GtkTextIter   *iter,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  IdeLspHoverProvider *self = (IdeLspHoverProvider *)provider;
+  IdeLspHoverProviderPrivate *priv = ide_lsp_hover_provider_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+  IdeBuffer *buffer;
+  gint line;
+  gint column;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_HOVER_PROVIDER (self));
+  g_assert (IDE_IS_HOVER_CONTEXT (context));
+  g_assert (iter != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_task_data (task, g_object_ref (context), g_object_unref);
+  ide_task_set_source_tag (task, ide_lsp_hover_provider_hover_async);
+
+  if (priv->client == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_CONNECTED,
+                                 "No client to deliver request");
+      return;
+    }
+
+  buffer = IDE_BUFFER (gtk_text_iter_get_buffer (iter));
+  uri = ide_buffer_dup_uri (buffer);
+  line = gtk_text_iter_get_line (iter);
+  column = gtk_text_iter_get_line_offset (iter);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+    "}",
+    "position", "{",
+      "line", JSONRPC_MESSAGE_PUT_INT32 (line),
+      "character", JSONRPC_MESSAGE_PUT_INT32 (column),
+    "}"
+  );
+
+  g_assert (IDE_IS_LSP_CLIENT (priv->client));
+
+  ide_lsp_client_call_async (priv->client,
+                                  "textDocument/hover",
+                                  params,
+                                  cancellable,
+                                  ide_lsp_hover_provider_hover_cb,
+                                  g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_lsp_hover_provider_hover_finish (IdeHoverProvider  *provider,
+                                          GAsyncResult      *result,
+                                          GError           **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_HOVER_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_lsp_hover_provider_real_load (IdeHoverProvider *provider,
+                                       IdeSourceView    *view)
+{
+  IdeLspHoverProvider *self = (IdeLspHoverProvider *)provider;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_LSP_HOVER_PROVIDER (self));
+
+  if (IDE_LSP_HOVER_PROVIDER_GET_CLASS (self)->prepare)
+    IDE_LSP_HOVER_PROVIDER_GET_CLASS (self)->prepare (self);
+}
+
+static void
+hover_provider_iface_init (IdeHoverProviderInterface *iface)
+{
+  iface->load = ide_lsp_hover_provider_real_load;
+  iface->hover_async = ide_lsp_hover_provider_hover_async;
+  iface->hover_finish = ide_lsp_hover_provider_hover_finish;
+}
+
+/**
+ * ide_lsp_hover_provider_get_client:
+ * @self: an #IdeLspHoverProvider
+ *
+ * Gets the client that is used for communication.
+ *
+ * Returns: (transfer none) (nullable): an #IdeLspClient or %NULL
+ *
+ * Since: 3.30
+ */
+IdeLspClient *
+ide_lsp_hover_provider_get_client (IdeLspHoverProvider *self)
+{
+  IdeLspHoverProviderPrivate *priv = ide_lsp_hover_provider_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LSP_HOVER_PROVIDER (self), NULL);
+
+  return priv->client;
+}
+
+/**
+ * ide_lsp_hover_provider_set_client:
+ * @self: an #IdeLspHoverProvider
+ * @client: an #IdeLspClient
+ *
+ * Sets the client to be used to query for hover information.
+ *
+ * Since: 3.30
+ */
+void
+ide_lsp_hover_provider_set_client (IdeLspHoverProvider *self,
+                                        IdeLspClient        *client)
+{
+  IdeLspHoverProviderPrivate *priv = ide_lsp_hover_provider_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LSP_HOVER_PROVIDER (self));
+  g_return_if_fail (!client || IDE_IS_LSP_CLIENT (client));
+
+  if (g_set_object (&priv->client, client))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLIENT]);
+}
diff --git a/src/libide/lsp/ide-lsp-hover-provider.h b/src/libide/lsp/ide-lsp-hover-provider.h
new file mode 100644
index 000000000..73b254135
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-hover-provider.h
@@ -0,0 +1,52 @@
+/* ide-lsp-hover-provider.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include "ide-lsp-client.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_HOVER_PROVIDER (ide_lsp_hover_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeLspHoverProvider, ide_lsp_hover_provider, IDE, LSP_HOVER_PROVIDER, IdeObject)
+
+struct _IdeLspHoverProviderClass
+{
+  IdeObjectClass parent_class;
+
+  void (*prepare) (IdeLspHoverProvider *self);
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeLspClient *ide_lsp_hover_provider_get_client (IdeLspHoverProvider *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_hover_provider_set_client (IdeLspHoverProvider *self,
+                                                           IdeLspClient        *client);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-rename-provider.c b/src/libide/lsp/ide-lsp-rename-provider.c
new file mode 100644
index 000000000..e0ae48aff
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-rename-provider.c
@@ -0,0 +1,367 @@
+/* ide-lsp-rename-provider.c
+ *
+ * Copyright 2016-2019 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 "ide-lsp-rename-provider"
+
+#include "config.h"
+
+#include <jsonrpc-glib.h>
+#include <libide-code.h>
+#include <libide-threading.h>
+
+#include "ide-lsp-client.h"
+#include "ide-lsp-rename-provider.h"
+
+typedef struct
+{
+  IdeLspClient *client;
+  IdeBuffer         *buffer;
+} IdeLspRenameProviderPrivate;
+
+static void rename_provider_iface_init (IdeRenameProviderInterface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdeLspRenameProvider, ide_lsp_rename_provider, IDE_TYPE_OBJECT,
+                                  G_ADD_PRIVATE (IdeLspRenameProvider)
+                                  G_IMPLEMENT_INTERFACE (IDE_TYPE_RENAME_PROVIDER, 
rename_provider_iface_init))
+
+enum {
+  PROP_0,
+  PROP_BUFFER,
+  PROP_CLIENT,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_lsp_rename_provider_set_buffer (IdeLspRenameProvider *self,
+                                         IdeBuffer                 *buffer)
+{
+  IdeLspRenameProviderPrivate *priv = ide_lsp_rename_provider_get_instance_private (self);
+
+  g_set_weak_pointer (&priv->buffer, buffer);
+}
+
+static void
+ide_lsp_rename_provider_finalize (GObject *object)
+{
+  IdeLspRenameProvider *self = (IdeLspRenameProvider *)object;
+  IdeLspRenameProviderPrivate *priv = ide_lsp_rename_provider_get_instance_private (self);
+
+  g_clear_object (&priv->client);
+  g_clear_weak_pointer (&priv->buffer);
+
+  G_OBJECT_CLASS (ide_lsp_rename_provider_parent_class)->finalize (object);
+}
+
+static void
+ide_lsp_rename_provider_get_property (GObject    *object,
+                                           guint       prop_id,
+                                           GValue     *value,
+                                           GParamSpec *pspec)
+{
+  IdeLspRenameProvider *self = IDE_LSP_RENAME_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      g_value_set_object (value, ide_lsp_rename_provider_get_client (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_rename_provider_set_property (GObject      *object,
+                                           guint         prop_id,
+                                           const GValue *value,
+                                           GParamSpec   *pspec)
+{
+  IdeLspRenameProvider *self = IDE_LSP_RENAME_PROVIDER (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      ide_lsp_rename_provider_set_buffer (self, g_value_get_object (value));
+      break;
+
+    case PROP_CLIENT:
+      ide_lsp_rename_provider_set_client (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_rename_provider_class_init (IdeLspRenameProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_lsp_rename_provider_finalize;
+  object_class->get_property = ide_lsp_rename_provider_get_property;
+  object_class->set_property = ide_lsp_rename_provider_set_property;
+
+  properties [PROP_CLIENT] =
+    g_param_spec_object ("client",
+                         "Client",
+                         "The Language Server client",
+                         IDE_TYPE_LSP_CLIENT,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_BUFFER] =
+    g_param_spec_object ("buffer",
+                         "Buffer",
+                         "The buffer for renames",
+                         IDE_TYPE_BUFFER,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_lsp_rename_provider_init (IdeLspRenameProvider *self)
+{
+}
+
+static void
+ide_lsp_rename_provider_rename_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  IdeLspClient *client = (IdeLspClient *)object;
+  IdeLspRenameProvider *self;
+  g_autoptr(GVariant) return_value = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GPtrArray) ret = NULL;
+  g_autoptr(GVariantIter) changes_by_uri = NULL;
+  const gchar *uri;
+  GVariant *changes;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_CLIENT (client));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_LSP_RENAME_PROVIDER (self));
+
+  if (!ide_lsp_client_call_finish (client, result, &return_value, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (!JSONRPC_MESSAGE_PARSE (return_value, "changes", JSONRPC_MESSAGE_GET_ITER (&changes_by_uri)))
+    IDE_EXIT;
+
+  ret = g_ptr_array_new_with_free_func (g_object_unref);
+
+  while (g_variant_iter_loop (changes_by_uri, "{sv}", &uri, &changes))
+    {
+      g_autoptr(GFile) gfile = g_file_new_for_uri (uri);
+      GVariantIter changes_iter;
+      GVariant *change;
+
+      if (!g_variant_is_container (changes))
+        continue;
+
+      g_variant_iter_init (&changes_iter, changes);
+
+      while (g_variant_iter_loop (&changes_iter, "v", &change))
+        {
+          g_autoptr(IdeLocation) begin_location = NULL;
+          g_autoptr(IdeLocation) end_location = NULL;
+          g_autoptr(IdeRange) range = NULL;
+          const gchar *new_text = NULL;
+          gboolean success;
+          struct {
+            gint64 line;
+            gint64 column;
+          } begin, end;
+
+          success = JSONRPC_MESSAGE_PARSE (change,
+            "range", "{",
+              "start", "{",
+                "line", JSONRPC_MESSAGE_GET_INT64 (&begin.line),
+                "character", JSONRPC_MESSAGE_GET_INT64 (&begin.column),
+              "}",
+              "end", "{",
+                "line", JSONRPC_MESSAGE_GET_INT64 (&end.line),
+                "character", JSONRPC_MESSAGE_GET_INT64 (&end.column),
+              "}",
+            "}",
+            "newText", JSONRPC_MESSAGE_GET_STRING (&new_text)
+          );
+
+          if (!success)
+            {
+              IDE_TRACE_MSG ("Failed to extract change from variant");
+              continue;
+            }
+
+          begin_location = ide_location_new (gfile, begin.line, begin.column);
+          end_location = ide_location_new (gfile, end.line, end.column);
+          range = ide_range_new (begin_location, end_location);
+
+          g_ptr_array_add (ret, ide_text_edit_new (range, new_text));
+        }
+    }
+
+  ide_task_return_pointer (task, g_steal_pointer (&ret), (GDestroyNotify)g_ptr_array_unref);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_rename_provider_rename_async (IdeRenameProvider   *provider,
+                                           IdeLocation   *location,
+                                           const gchar         *new_name,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  IdeLspRenameProvider *self = (IdeLspRenameProvider *)provider;
+  IdeLspRenameProviderPrivate *priv = ide_lsp_rename_provider_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *text = NULL;
+  g_autofree gchar *uri = NULL;
+  GtkTextIter begin;
+  GtkTextIter end;
+  GFile *gfile;
+  gint64 version;
+  gint line;
+  gint column;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_RENAME_PROVIDER (self));
+  g_assert (location != NULL);
+  g_assert (new_name != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_rename_provider_rename_async);
+
+  if (priv->client == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_CONNECTED,
+                                 "No client set, cannot rename symbol");
+      IDE_EXIT;
+    }
+
+  gfile = ide_location_get_file (location);
+  uri = g_file_get_uri (gfile);
+
+  line = ide_location_get_line (location);
+  column = ide_location_get_line_offset (location);
+
+  version = ide_buffer_get_change_count (priv->buffer);
+
+  gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (priv->buffer), &begin, &end);
+  text = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (priv->buffer), &begin, &end, TRUE);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+      "version", JSONRPC_MESSAGE_PUT_INT64 (version),
+      "text", JSONRPC_MESSAGE_PUT_STRING (text),
+    "}",
+    "position", "{",
+      "line", JSONRPC_MESSAGE_PUT_INT32 (line),
+      "character", JSONRPC_MESSAGE_PUT_INT32 (column),
+    "}",
+    "newName", JSONRPC_MESSAGE_PUT_STRING (new_name)
+  );
+
+  ide_lsp_client_call_async (priv->client,
+                                  "textDocument/rename",
+                                  params,
+                                  cancellable,
+                                  ide_lsp_rename_provider_rename_cb,
+                                  g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_lsp_rename_provider_rename_finish (IdeRenameProvider  *provider,
+                                            GAsyncResult       *result,
+                                            GPtrArray         **edits,
+                                            GError            **error)
+{
+  g_autoptr(GPtrArray) ar = NULL;
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_RENAME_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  ar = ide_task_propagate_pointer (IDE_TASK (result), error);
+  ret = (ar != NULL);
+
+  if (edits != NULL)
+    *edits = IDE_PTR_ARRAY_STEAL_FULL (&ar);
+
+  IDE_RETURN (ret);
+}
+
+static void
+rename_provider_iface_init (IdeRenameProviderInterface *iface)
+{
+  iface->rename_async = ide_lsp_rename_provider_rename_async;
+  iface->rename_finish = ide_lsp_rename_provider_rename_finish;
+}
+
+/**
+ * ide_lsp_rename_provider_get_client:
+ *
+ * Returns: (transfer none) (nullable): an #IdeLspClient or %NULL.
+ */
+IdeLspClient *
+ide_lsp_rename_provider_get_client (IdeLspRenameProvider *self)
+{
+  IdeLspRenameProviderPrivate *priv = ide_lsp_rename_provider_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LSP_RENAME_PROVIDER (self), NULL);
+
+  return priv->client;
+}
+
+void
+ide_lsp_rename_provider_set_client (IdeLspRenameProvider *self,
+                                         IdeLspClient         *client)
+{
+  IdeLspRenameProviderPrivate *priv = ide_lsp_rename_provider_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LSP_RENAME_PROVIDER (self));
+  g_return_if_fail (!client || IDE_IS_LSP_CLIENT (client));
+
+  if (g_set_object (&priv->client, client))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLIENT]);
+}
diff --git a/src/libide/lsp/ide-lsp-rename-provider.h b/src/libide/lsp/ide-lsp-rename-provider.h
new file mode 100644
index 000000000..164b6d366
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-rename-provider.h
@@ -0,0 +1,52 @@
+/* ide-lsp-rename-provider.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+#include "ide-lsp-client.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_RENAME_PROVIDER (ide_lsp_rename_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeLspRenameProvider, ide_lsp_rename_provider, IDE, LSP_RENAME_PROVIDER, IdeObject)
+
+struct _IdeLspRenameProviderClass
+{
+  IdeObjectClass parent_instance;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeLspClient *ide_lsp_rename_provider_get_client (IdeLspRenameProvider *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_rename_provider_set_client (IdeLspRenameProvider *self,
+                                                            IdeLspClient         *client);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-symbol-node-private.h b/src/libide/lsp/ide-lsp-symbol-node-private.h
new file mode 100644
index 000000000..b20782828
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-symbol-node-private.h
@@ -0,0 +1,42 @@
+/* ide-lsp-symbol-node-private.h
+ *
+ * Copyright 2016-2019 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 "ide-lsp-symbol-node.h"
+
+G_BEGIN_DECLS
+
+struct _IdeLspSymbolNode
+{
+  IdeSymbolNode parent_instance;
+  GNode         gnode;
+};
+
+IdeLspSymbolNode *ide_lsp_symbol_node_new (GFile       *file,
+                                                     const gchar *name,
+                                                     const gchar *parent_name,
+                                                     gint         kind,
+                                                     guint        begin_line,
+                                                     guint        begin_column,
+                                                     guint        end_line,
+                                                     guint        end_column);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-symbol-node.c b/src/libide/lsp/ide-lsp-symbol-node.c
new file mode 100644
index 000000000..4fa7d9870
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-symbol-node.c
@@ -0,0 +1,188 @@
+/* ide-lsp-symbol-node.c
+ *
+ * Copyright 2016-2019 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 "ide-lsp-symbol-node"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-threading.h>
+
+#include "ide-lsp-symbol-node.h"
+#include "ide-lsp-symbol-node-private.h"
+#include "ide-lsp-util.h"
+
+typedef struct
+{
+  guint line;
+  guint column;
+} Location;
+
+typedef struct
+{
+  GFile *file;
+  gchar *parent_name;
+  IdeSymbolKind kind;
+  Location begin;
+  Location end;
+} IdeLspSymbolNodePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeLspSymbolNode, ide_lsp_symbol_node, IDE_TYPE_SYMBOL_NODE)
+
+static inline gint
+location_compare (const Location *a,
+                  const Location *b)
+{
+  gint ret;
+
+  ret = (gint)a->line - (gint)b->line;
+  if (ret == 0)
+    ret = (gint)a->column - (gint)b->column;
+
+  return ret;
+}
+
+static void
+ide_lsp_symbol_node_get_location_async (IdeSymbolNode       *node,
+                                             GCancellable        *cancellable,
+                                             GAsyncReadyCallback  callback,
+                                             gpointer             user_data)
+{
+  IdeLspSymbolNode *self = (IdeLspSymbolNode *)node;
+  IdeLspSymbolNodePrivate *priv = ide_lsp_symbol_node_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_SYMBOL_NODE (node));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_symbol_node_get_location_async);
+  ide_task_return_pointer (task,
+                           ide_location_new (priv->file, priv->begin.line, priv->begin.column),
+                           g_object_unref);
+
+  IDE_EXIT;
+}
+
+static IdeLocation *
+ide_lsp_symbol_node_get_location_finish (IdeSymbolNode  *node,
+                                              GAsyncResult   *result,
+                                              GError        **error)
+{
+  IdeLocation *ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_SYMBOL_NODE (node));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_lsp_symbol_node_finalize (GObject *object)
+{
+  IdeLspSymbolNode *self = (IdeLspSymbolNode *)object;
+  IdeLspSymbolNodePrivate *priv = ide_lsp_symbol_node_get_instance_private (self);
+
+  g_clear_pointer (&priv->parent_name, g_free);
+  g_clear_object (&priv->file);
+
+  G_OBJECT_CLASS (ide_lsp_symbol_node_parent_class)->finalize (object);
+}
+
+static void
+ide_lsp_symbol_node_class_init (IdeLspSymbolNodeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeSymbolNodeClass *symbol_node_class = IDE_SYMBOL_NODE_CLASS (klass);
+
+  object_class->finalize = ide_lsp_symbol_node_finalize;
+
+  symbol_node_class->get_location_async = ide_lsp_symbol_node_get_location_async;
+  symbol_node_class->get_location_finish = ide_lsp_symbol_node_get_location_finish;
+}
+
+static void
+ide_lsp_symbol_node_init (IdeLspSymbolNode *self)
+{
+  self->gnode.data = self;
+}
+
+IdeLspSymbolNode *
+ide_lsp_symbol_node_new (GFile       *file,
+                              const gchar *name,
+                              const gchar *parent_name,
+                              gint         kind,
+                              guint        begin_line,
+                              guint        begin_column,
+                              guint        end_line,
+                              guint        end_column)
+{
+  IdeLspSymbolNode *self;
+  IdeLspSymbolNodePrivate *priv;
+
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+
+  kind = ide_lsp_decode_symbol_kind (kind);
+
+  self = g_object_new (IDE_TYPE_LSP_SYMBOL_NODE,
+                       "flags", 0,
+                       "kind", kind,
+                       "name", name,
+                       NULL);
+  priv = ide_lsp_symbol_node_get_instance_private (self);
+
+  priv->file = g_object_ref (file);
+  priv->parent_name = g_strdup (parent_name);
+  priv->begin.line = begin_line;
+  priv->begin.column = begin_column;
+  priv->end.line = end_line;
+  priv->end.column = end_column;
+
+  return self;
+}
+
+const gchar *
+ide_lsp_symbol_node_get_parent_name (IdeLspSymbolNode *self)
+{
+  IdeLspSymbolNodePrivate *priv = ide_lsp_symbol_node_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LSP_SYMBOL_NODE (self), NULL);
+
+  return priv->parent_name;
+}
+
+gboolean
+ide_lsp_symbol_node_is_parent_of (IdeLspSymbolNode *self,
+                                       IdeLspSymbolNode *other)
+{
+  IdeLspSymbolNodePrivate *priv = ide_lsp_symbol_node_get_instance_private (self);
+  IdeLspSymbolNodePrivate *opriv = ide_lsp_symbol_node_get_instance_private (other);
+
+  g_return_val_if_fail (IDE_IS_LSP_SYMBOL_NODE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_LSP_SYMBOL_NODE (other), FALSE);
+
+  return (location_compare (&priv->begin, &opriv->begin) <= 0) &&
+         (location_compare (&priv->end, &opriv->end) >= 0);
+}
diff --git a/src/libide/lsp/ide-lsp-symbol-node.h b/src/libide/lsp/ide-lsp-symbol-node.h
new file mode 100644
index 000000000..0e6573dd9
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-symbol-node.h
@@ -0,0 +1,42 @@
+/* ide-lsp-symbol-node.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_SYMBOL_NODE (ide_lsp_symbol_node_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeLspSymbolNode, ide_lsp_symbol_node, IDE, LSP_SYMBOL_NODE, IdeSymbolNode)
+
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_lsp_symbol_node_get_parent_name (IdeLspSymbolNode *self);
+IDE_AVAILABLE_IN_3_32
+gboolean     ide_lsp_symbol_node_is_parent_of    (IdeLspSymbolNode *self,
+                                                       IdeLspSymbolNode *other);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-symbol-resolver.c b/src/libide/lsp/ide-lsp-symbol-resolver.c
new file mode 100644
index 000000000..64d5f5e7d
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-symbol-resolver.c
@@ -0,0 +1,665 @@
+/* ide-lsp-symbol-resolver.c
+ *
+ * Copyright 2016-2019 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 "ide-lsp-symbol-resolver"
+
+#include "config.h"
+
+#include <jsonrpc-glib.h>
+
+#include <libide-code.h>
+#include <libide-threading.h>
+
+#include "ide-lsp-symbol-node.h"
+#include "ide-lsp-symbol-node-private.h"
+#include "ide-lsp-symbol-resolver.h"
+#include "ide-lsp-symbol-tree.h"
+#include "ide-lsp-symbol-tree-private.h"
+
+typedef struct
+{
+  IdeLspClient *client;
+} IdeLspSymbolResolverPrivate;
+
+static void symbol_resolver_iface_init (IdeSymbolResolverInterface *iface);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (IdeLspSymbolResolver, ide_lsp_symbol_resolver, IDE_TYPE_OBJECT,
+                                  G_ADD_PRIVATE (IdeLspSymbolResolver)
+                                  G_IMPLEMENT_INTERFACE (IDE_TYPE_SYMBOL_RESOLVER, 
symbol_resolver_iface_init))
+
+enum {
+  PROP_0,
+  PROP_CLIENT,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_lsp_symbol_resolver_finalize (GObject *object)
+{
+  IdeLspSymbolResolver *self = (IdeLspSymbolResolver *)object;
+  IdeLspSymbolResolverPrivate *priv = ide_lsp_symbol_resolver_get_instance_private (self);
+
+  g_clear_object (&priv->client);
+
+  G_OBJECT_CLASS (ide_lsp_symbol_resolver_parent_class)->finalize (object);
+}
+
+static void
+ide_lsp_symbol_resolver_get_property (GObject    *object,
+                                           guint       prop_id,
+                                           GValue     *value,
+                                           GParamSpec *pspec)
+{
+  IdeLspSymbolResolver *self = IDE_LSP_SYMBOL_RESOLVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      g_value_set_object (value, ide_lsp_symbol_resolver_get_client (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_symbol_resolver_set_property (GObject      *object,
+                                           guint         prop_id,
+                                           const GValue *value,
+                                           GParamSpec   *pspec)
+{
+  IdeLspSymbolResolver *self = IDE_LSP_SYMBOL_RESOLVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLIENT:
+      ide_lsp_symbol_resolver_set_client (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_lsp_symbol_resolver_class_init (IdeLspSymbolResolverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_lsp_symbol_resolver_finalize;
+  object_class->get_property = ide_lsp_symbol_resolver_get_property;
+  object_class->set_property = ide_lsp_symbol_resolver_set_property;
+
+  properties [PROP_CLIENT] =
+    g_param_spec_object ("client",
+                         "Client",
+                         "The Language Server client",
+                         IDE_TYPE_LSP_CLIENT,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_lsp_symbol_resolver_init (IdeLspSymbolResolver *self)
+{
+}
+
+/**
+ * ide_lsp_symbol_resolver_get_client:
+ *
+ * Gets the client used by the symbol resolver.
+ *
+ * Returns: (transfer none) (nullable): An #IdeLspClient or %NULL.
+ */
+IdeLspClient *
+ide_lsp_symbol_resolver_get_client (IdeLspSymbolResolver *self)
+{
+  IdeLspSymbolResolverPrivate *priv = ide_lsp_symbol_resolver_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LSP_SYMBOL_RESOLVER (self), NULL);
+
+  return priv->client;
+}
+
+void
+ide_lsp_symbol_resolver_set_client (IdeLspSymbolResolver *self,
+                                         IdeLspClient         *client)
+{
+  IdeLspSymbolResolverPrivate *priv = ide_lsp_symbol_resolver_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_LSP_SYMBOL_RESOLVER (self));
+  g_return_if_fail (!client || IDE_IS_LSP_CLIENT (client));
+
+  if (g_set_object (&priv->client, client))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLIENT]);
+}
+
+static void
+ide_lsp_symbol_resolver_definition_cb (GObject      *object,
+                                            GAsyncResult *result,
+                                            gpointer      user_data)
+{
+  IdeLspClient *client = (IdeLspClient *)object;
+  IdeLspSymbolResolver *self;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) return_value = NULL;
+  g_autoptr(IdeSymbol) symbol = NULL;
+  g_autoptr(GFile) gfile = NULL;
+  g_autoptr(IdeLocation) location = NULL;
+  g_autoptr(GVariant) variant = NULL;
+  GVariantIter iter;
+  const gchar *uri;
+  struct {
+    gint64 line;
+    gint64 column;
+  } begin, end;
+  gboolean success = FALSE;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_CLIENT (client));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_LSP_SYMBOL_RESOLVER (self));
+
+  if (!ide_lsp_client_call_finish (client, result, &return_value, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+#if 0
+  {
+    g_autofree gchar *str = g_variant_print (return_value, TRUE);
+    IDE_TRACE_MSG ("Got reply: %s", str);
+  }
+#endif
+
+  g_variant_iter_init (&iter, return_value);
+
+  if (g_variant_iter_next (&iter, "v", &variant))
+    {
+      success = JSONRPC_MESSAGE_PARSE (variant,
+        "uri", JSONRPC_MESSAGE_GET_STRING (&uri),
+        "range", "{",
+          "start", "{",
+            "line", JSONRPC_MESSAGE_GET_INT64 (&begin.line),
+            "character", JSONRPC_MESSAGE_GET_INT64 (&begin.column),
+          "}",
+          "end", "{",
+            "line", JSONRPC_MESSAGE_GET_INT64 (&end.line),
+            "character", JSONRPC_MESSAGE_GET_INT64 (&end.column),
+          "}",
+        "}"
+      );
+    }
+
+  if (!success)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_DATA,
+                                 "Got invalid reply for textDocument/definition");
+      IDE_EXIT;
+    }
+
+  IDE_TRACE_MSG ("Definition location is %s %d:%d",
+                 uri, (gint)begin.line + 1, (gint)begin.column + 1);
+
+  gfile = g_file_new_for_uri (uri);
+  location = ide_location_new (gfile, begin.line, begin.column);
+  symbol = ide_symbol_new ("", IDE_SYMBOL_KIND_NONE, IDE_SYMBOL_FLAGS_NONE, location, location);
+
+  ide_task_return_pointer (task, g_steal_pointer (&symbol), g_object_unref);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolver,
+                                                  IdeLocation   *location,
+                                                  GCancellable        *cancellable,
+                                                  GAsyncReadyCallback  callback,
+                                                  gpointer             user_data)
+{
+  IdeLspSymbolResolver *self = (IdeLspSymbolResolver *)resolver;
+  IdeLspSymbolResolverPrivate *priv = ide_lsp_symbol_resolver_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+  GFile *gfile;
+  gint line;
+  gint column;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_SYMBOL_RESOLVER (self));
+  g_assert (location != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_symbol_resolver_lookup_symbol_async);
+
+  if (priv->client == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_CONNECTED,
+                                 "%s requires a client to resolve symbols",
+                                 G_OBJECT_TYPE_NAME (self));
+      IDE_EXIT;
+    }
+
+  if (!(gfile = ide_location_get_file (location)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Cannot resolve symbol, invalid source location");
+      IDE_EXIT;
+    }
+
+  uri = g_file_get_uri (gfile);
+  line = ide_location_get_line (location);
+  column = ide_location_get_line_offset (location);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+    "}",
+    "position", "{",
+      "line", JSONRPC_MESSAGE_PUT_INT32 (line),
+      "character", JSONRPC_MESSAGE_PUT_INT32 (column),
+    "}"
+  );
+
+  ide_lsp_client_call_async (priv->client,
+                                  "textDocument/definition",
+                                  params,
+                                  cancellable,
+                                  ide_lsp_symbol_resolver_definition_cb,
+                                  g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static IdeSymbol *
+ide_lsp_symbol_resolver_lookup_symbol_finish (IdeSymbolResolver  *resolver,
+                                                   GAsyncResult       *result,
+                                                   GError            **error)
+{
+  IdeSymbol *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_LSP_SYMBOL_RESOLVER (resolver), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_lsp_symbol_resolver_document_symbol_cb (GObject      *object,
+                                                 GAsyncResult *result,
+                                                 gpointer      user_data)
+{
+  IdeLspClient *client = (IdeLspClient *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeLspSymbolTree) tree = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GVariant) return_value = NULL;
+  g_autoptr(GPtrArray) symbols = NULL;
+  GVariantIter iter;
+  GVariant *node;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_CLIENT (client));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_lsp_client_call_finish (client, result, &return_value, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (!g_variant_is_of_type (return_value, G_VARIANT_TYPE ("av")))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_DATA,
+                                 "Invalid result for textDocument/documentSymbol");
+      IDE_EXIT;
+    }
+
+  symbols = g_ptr_array_new_with_free_func (g_object_unref);
+
+  g_variant_iter_init (&iter, return_value);
+
+  while (g_variant_iter_loop (&iter, "v", &node))
+    {
+      g_autoptr(IdeLspSymbolNode) symbol = NULL;
+      g_autoptr(GFile) file = NULL;
+      const gchar *name = NULL;
+      const gchar *container_name = NULL;
+      const gchar *uri = NULL;
+      gboolean success;
+      gint64 kind = -1;
+      struct {
+        gint64 line;
+        gint64 column;
+      } begin, end;
+
+      /* Mandatory fields */
+      success = JSONRPC_MESSAGE_PARSE (node,
+        "name", JSONRPC_MESSAGE_GET_STRING (&name),
+        "kind", JSONRPC_MESSAGE_GET_INT64 (&kind),
+        "location", "{",
+          "uri", JSONRPC_MESSAGE_GET_STRING (&uri),
+          "range", "{",
+            "start", "{",
+              "line", JSONRPC_MESSAGE_GET_INT64 (&begin.line),
+              "character", JSONRPC_MESSAGE_GET_INT64 (&begin.column),
+            "}",
+            "end", "{",
+              "line", JSONRPC_MESSAGE_GET_INT64 (&end.line),
+              "character", JSONRPC_MESSAGE_GET_INT64 (&end.column),
+            "}",
+          "}",
+        "}"
+      );
+
+      if (!success)
+        {
+          IDE_TRACE_MSG ("Failed to parse reply from language server");
+          continue;
+        }
+
+      /* Optional fields */
+      JSONRPC_MESSAGE_PARSE (node, "containerName", JSONRPC_MESSAGE_GET_STRING (&container_name));
+
+      file = g_file_new_for_uri (uri);
+
+      symbol = ide_lsp_symbol_node_new (file, name, container_name, kind,
+                                             begin.line, begin.column,
+                                             end.line, end.column);
+
+      g_ptr_array_add (symbols, g_steal_pointer (&symbol));
+    }
+
+  tree = ide_lsp_symbol_tree_new (IDE_PTR_ARRAY_STEAL_FULL (&symbols));
+
+  ide_task_return_pointer (task, g_steal_pointer (&tree), g_object_unref);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *resolver,
+                                                    GFile               *file,
+                                                    GBytes              *bytes,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data)
+{
+  IdeLspSymbolResolver *self = (IdeLspSymbolResolver *)resolver;
+  IdeLspSymbolResolverPrivate *priv = ide_lsp_symbol_resolver_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_SYMBOL_RESOLVER (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_symbol_resolver_get_symbol_tree_async);
+
+  if (priv->client == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_CONNECTED,
+                                 "Cannot query language server, not connected");
+      IDE_EXIT;
+    }
+
+  uri = g_file_get_uri (file);
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+    "}"
+  );
+
+  ide_lsp_client_call_async (priv->client,
+                                  "textDocument/documentSymbol",
+                                  params,
+                                  cancellable,
+                                  ide_lsp_symbol_resolver_document_symbol_cb,
+                                  g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static IdeSymbolTree *
+ide_lsp_symbol_resolver_get_symbol_tree_finish (IdeSymbolResolver  *resolver,
+                                                     GAsyncResult       *result,
+                                                     GError            **error)
+{
+  IdeSymbolTree *ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_LSP_SYMBOL_RESOLVER (resolver), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+ide_lsp_symbol_resolver_find_references_cb (GObject      *object,
+                                                 GAsyncResult *result,
+                                                 gpointer      user_data)
+{
+  IdeLspClient *client = (IdeLspClient *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GVariant) reply = NULL;
+  g_autoptr(GPtrArray) references = NULL;
+  g_autoptr(GError) error = NULL;
+  GVariant *locationv;
+  GVariantIter iter;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_CLIENT (client));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_lsp_client_call_finish (client, result, &reply, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (!g_variant_is_of_type (reply, G_VARIANT_TYPE ("av")))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_DATA,
+                                 "Invalid reply type from peer: %s",
+                                 g_variant_get_type_string (reply));
+      IDE_EXIT;
+    }
+
+  references = g_ptr_array_new_with_free_func (g_object_unref);
+
+  g_variant_iter_init (&iter, reply);
+
+  while (g_variant_iter_loop (&iter, "v", &locationv))
+    {
+      g_autoptr(IdeLocation) begin_loc = NULL;
+      g_autoptr(IdeLocation) end_loc = NULL;
+      g_autoptr(IdeRange) range = NULL;
+      const gchar *uri = NULL;
+      GFile *gfile;
+      gboolean success;
+      struct {
+        gint64 line;
+        gint64 line_offset;
+      } begin, end;
+
+      success = JSONRPC_MESSAGE_PARSE (locationv,
+        "uri", JSONRPC_MESSAGE_GET_STRING (&uri),
+        "range", "{",
+          "start", "{",
+            "line", JSONRPC_MESSAGE_GET_INT64 (&begin.line),
+            "character", JSONRPC_MESSAGE_GET_INT64 (&begin.line_offset),
+          "}",
+          "end", "{",
+            "line", JSONRPC_MESSAGE_GET_INT64 (&end.line),
+            "character", JSONRPC_MESSAGE_GET_INT64 (&end.line_offset),
+          "}",
+        "}"
+      );
+
+      if (!success)
+        {
+          ide_task_return_new_error (task,
+                                     G_IO_ERROR,
+                                     G_IO_ERROR_INVALID_DATA,
+                                     "Failed to parse location object");
+          IDE_EXIT;
+        }
+
+      gfile = g_file_new_for_uri (uri);
+
+      begin_loc = ide_location_new (gfile, begin.line, begin.line_offset);
+      end_loc = ide_location_new (gfile, end.line, end.line_offset);
+      range = ide_range_new (begin_loc, end_loc);
+
+      g_ptr_array_add (references, g_steal_pointer (&range));
+    }
+
+  ide_task_return_pointer (task, g_steal_pointer (&references), (GDestroyNotify)g_ptr_array_unref);
+
+  IDE_EXIT;
+}
+
+static void
+ide_lsp_symbol_resolver_find_references_async (IdeSymbolResolver   *resolver,
+                                                    IdeLocation         *location,
+                                                    const gchar         *language_id,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data)
+{
+  IdeLspSymbolResolver *self = (IdeLspSymbolResolver *)resolver;
+  IdeLspSymbolResolverPrivate *priv = ide_lsp_symbol_resolver_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GVariant) params = NULL;
+  g_autofree gchar *uri = NULL;
+  GFile *gfile;
+  guint line;
+  guint line_offset;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_SYMBOL_RESOLVER (self));
+  g_assert (location != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_lsp_symbol_resolver_find_references_async);
+
+  gfile = ide_location_get_file (location);
+  uri = g_file_get_uri (gfile);
+
+  line = ide_location_get_line (location);
+  line_offset = ide_location_get_line_offset (location);
+
+  if (language_id == NULL)
+    language_id = "plain";
+
+  params = JSONRPC_MESSAGE_NEW (
+    "textDocument", "{",
+      "uri", JSONRPC_MESSAGE_PUT_STRING (uri),
+      "languageId", JSONRPC_MESSAGE_PUT_STRING (language_id),
+    "}",
+    "position", "{",
+      "line", JSONRPC_MESSAGE_PUT_INT32 (line),
+      "character", JSONRPC_MESSAGE_PUT_INT32 (line_offset),
+    "}",
+    "context", "{",
+      "includeDeclaration", JSONRPC_MESSAGE_PUT_BOOLEAN (TRUE),
+    "}"
+  );
+
+  ide_lsp_client_call_async (priv->client,
+                                  "textDocument/references",
+                                  params,
+                                  cancellable,
+                                  ide_lsp_symbol_resolver_find_references_cb,
+                                  g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static GPtrArray *
+ide_lsp_symbol_resolver_find_references_finish (IdeSymbolResolver  *self,
+                                                     GAsyncResult       *result,
+                                                     GError            **error)
+{
+  GPtrArray *ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_LSP_SYMBOL_RESOLVER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  IDE_PTR_ARRAY_CLEAR_FREE_FUNC (ret);
+
+  IDE_RETURN (ret);
+}
+
+static void
+symbol_resolver_iface_init (IdeSymbolResolverInterface *iface)
+{
+  iface->lookup_symbol_async = ide_lsp_symbol_resolver_lookup_symbol_async;
+  iface->lookup_symbol_finish = ide_lsp_symbol_resolver_lookup_symbol_finish;
+  iface->get_symbol_tree_async = ide_lsp_symbol_resolver_get_symbol_tree_async;
+  iface->get_symbol_tree_finish = ide_lsp_symbol_resolver_get_symbol_tree_finish;
+  iface->find_references_async = ide_lsp_symbol_resolver_find_references_async;
+  iface->find_references_finish = ide_lsp_symbol_resolver_find_references_finish;
+}
diff --git a/src/libide/lsp/ide-lsp-symbol-resolver.h b/src/libide/lsp/ide-lsp-symbol-resolver.h
new file mode 100644
index 000000000..4fffeec5a
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-symbol-resolver.h
@@ -0,0 +1,52 @@
+/* ide-lsp-symbol-resolver.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+#include "ide-lsp-client.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_SYMBOL_RESOLVER (ide_lsp_symbol_resolver_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeLspSymbolResolver, ide_lsp_symbol_resolver, IDE, LSP_SYMBOL_RESOLVER, IdeObject)
+
+struct _IdeLspSymbolResolverClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeLspClient *ide_lsp_symbol_resolver_get_client (IdeLspSymbolResolver *self);
+IDE_AVAILABLE_IN_3_32
+void               ide_lsp_symbol_resolver_set_client (IdeLspSymbolResolver *self,
+                                                            IdeLspClient         *client);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-symbol-tree-private.h b/src/libide/lsp/ide-lsp-symbol-tree-private.h
new file mode 100644
index 000000000..b674ffc2e
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-symbol-tree-private.h
@@ -0,0 +1,29 @@
+/* ide-lsp-symbol-tree-private.h
+ *
+ * Copyright 2016-2019 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 "ide-lsp-symbol-tree.h"
+
+G_BEGIN_DECLS
+
+IdeLspSymbolTree *ide_lsp_symbol_tree_new (GPtrArray *symbols);
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-symbol-tree.c b/src/libide/lsp/ide-lsp-symbol-tree.c
new file mode 100644
index 000000000..53ecb51ab
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-symbol-tree.c
@@ -0,0 +1,190 @@
+/* ide-lsp-symbol-tree.c
+ *
+ * Copyright 2016-2019 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 "ide-lsp-symbol-tree"
+
+#include "config.h"
+
+#include "ide-lsp-symbol-node.h"
+#include "ide-lsp-symbol-node-private.h"
+#include "ide-lsp-symbol-tree.h"
+#include "ide-lsp-symbol-tree-private.h"
+
+typedef struct
+{
+  GPtrArray *symbols;
+  GNode      root;
+} IdeLspSymbolTreePrivate;
+
+static void symbol_tree_iface_init (IdeSymbolTreeInterface *iface);
+
+struct _IdeLspSymbolTree { GObject object; };
+G_DEFINE_TYPE_WITH_CODE (IdeLspSymbolTree, ide_lsp_symbol_tree, G_TYPE_OBJECT,
+                         G_ADD_PRIVATE (IdeLspSymbolTree)
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_SYMBOL_TREE, symbol_tree_iface_init))
+
+static guint
+ide_lsp_symbol_tree_get_n_children (IdeSymbolTree *tree,
+                                         IdeSymbolNode *parent)
+{
+  IdeLspSymbolTree *self = (IdeLspSymbolTree *)tree;
+  IdeLspSymbolTreePrivate *priv = ide_lsp_symbol_tree_get_instance_private (self);
+
+  g_assert (IDE_IS_LSP_SYMBOL_TREE (self));
+  g_assert (!parent || IDE_IS_LSP_SYMBOL_NODE (parent));
+
+  if (parent == NULL)
+    return g_node_n_children (&priv->root);
+
+  return g_node_n_children (&IDE_LSP_SYMBOL_NODE (parent)->gnode);
+}
+
+static IdeSymbolNode *
+ide_lsp_symbol_tree_get_nth_child (IdeSymbolTree *tree,
+                                        IdeSymbolNode *parent,
+                                        guint          nth)
+{
+  IdeLspSymbolTree *self = (IdeLspSymbolTree *)tree;
+  IdeLspSymbolTreePrivate *priv = ide_lsp_symbol_tree_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_LSP_SYMBOL_TREE (self), NULL);
+  g_return_val_if_fail (!parent || IDE_IS_LSP_SYMBOL_NODE (parent), NULL);
+
+  if (parent == NULL)
+    {
+      g_return_val_if_fail (nth < g_node_n_children (&priv->root), NULL);
+      return g_object_ref (g_node_nth_child (&priv->root, nth)->data);
+    }
+
+  g_return_val_if_fail (nth < g_node_n_children (&IDE_LSP_SYMBOL_NODE (parent)->gnode), NULL);
+  return g_object_ref (g_node_nth_child (&IDE_LSP_SYMBOL_NODE (parent)->gnode, nth)->data);
+}
+
+static void
+symbol_tree_iface_init (IdeSymbolTreeInterface *iface)
+{
+  iface->get_n_children = ide_lsp_symbol_tree_get_n_children;
+  iface->get_nth_child = ide_lsp_symbol_tree_get_nth_child;
+}
+
+static void
+ide_lsp_symbol_tree_finalize (GObject *object)
+{
+  IdeLspSymbolTree *self = (IdeLspSymbolTree *)object;
+  IdeLspSymbolTreePrivate *priv = ide_lsp_symbol_tree_get_instance_private (self);
+
+  g_clear_pointer (&priv->symbols, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (ide_lsp_symbol_tree_parent_class)->finalize (object);
+}
+
+static void
+ide_lsp_symbol_tree_class_init (IdeLspSymbolTreeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_lsp_symbol_tree_finalize;
+}
+
+static void
+ide_lsp_symbol_tree_init (IdeLspSymbolTree *self)
+{
+}
+
+static void
+add_to_node (GNode                 *node,
+             IdeLspSymbolNode *symbol)
+{
+  /* First, check to see if any of the children are parents of the range of
+   * this symbol. If so, we will defer to adding it to that node.
+   */
+
+  for (GNode *iter = node->children; iter != NULL; iter = iter->next)
+    {
+      IdeLspSymbolNode *child = iter->data;
+
+      /*
+       * If this node is an ancestor of ours, then we can defer to
+       * adding to that node.
+       */
+      if (ide_lsp_symbol_node_is_parent_of (child, symbol))
+        {
+          add_to_node (iter, symbol);
+          return;
+        }
+
+      /*
+       * If we are the parent of the child, then we need to insert
+       * ourselves in its place and add it to our node.
+       */
+      if (ide_lsp_symbol_node_is_parent_of (symbol, child))
+        {
+          /* Add this node to our children */
+          g_node_unlink (&child->gnode);
+          g_node_append (&symbol->gnode, &child->gnode);
+
+          /* add ourselves to the tree at this level */
+          g_node_append (node, &symbol->gnode);
+
+          return;
+        }
+    }
+
+  g_node_append (node, &symbol->gnode);
+}
+
+static void
+ide_lsp_symbol_tree_build (IdeLspSymbolTree *self)
+{
+  IdeLspSymbolTreePrivate *priv = ide_lsp_symbol_tree_get_instance_private (self);
+
+  g_assert (IDE_IS_LSP_SYMBOL_TREE (self));
+  g_assert (priv->symbols != NULL);
+
+  for (guint i = 0; i < priv->symbols->len; i++)
+    add_to_node (&priv->root, g_ptr_array_index (priv->symbols, i));
+}
+
+/**
+ * ide_lsp_symbol_tree_new:
+ * @symbols: (transfer full) (element-type Ide.LspSymbolNode): The symbols
+ *
+ * Creates a new #IdeLspSymbolTree but takes ownership of @ar.
+ *
+ * Returns: (transfer full): A newly allocated #IdeLspSymbolTree.
+ */
+IdeLspSymbolTree *
+ide_lsp_symbol_tree_new (GPtrArray *symbols)
+{
+  IdeLspSymbolTreePrivate *priv;
+  IdeLspSymbolTree *self;
+
+  g_return_val_if_fail (symbols != NULL, NULL);
+
+  IDE_PTR_ARRAY_SET_FREE_FUNC (symbols, g_object_unref);
+
+  self = g_object_new (IDE_TYPE_LSP_SYMBOL_TREE, NULL);
+  priv = ide_lsp_symbol_tree_get_instance_private (self);
+  priv->symbols = symbols;
+
+  ide_lsp_symbol_tree_build (self);
+
+  return self;
+}
diff --git a/src/libide/lsp/ide-lsp-symbol-tree.h b/src/libide/lsp/ide-lsp-symbol-tree.h
new file mode 100644
index 000000000..2b12c3d02
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-symbol-tree.h
@@ -0,0 +1,36 @@
+/* ide-lsp-symbol-tree.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_LSP_SYMBOL_TREE (ide_lsp_symbol_tree_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeLspSymbolTree, ide_lsp_symbol_tree, IDE, LSP_SYMBOL_TREE, GObject)
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-types.h b/src/libide/lsp/ide-lsp-types.h
new file mode 100644
index 000000000..57913ce08
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-types.h
@@ -0,0 +1,58 @@
+/* ide-langserv-types.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+       IDE_LSP_COMPLETION_TEXT           = 1,
+       IDE_LSP_COMPLETION_METHOD         = 2,
+       IDE_LSP_COMPLETION_FUNCTION       = 3,
+       IDE_LSP_COMPLETION_CONSTRUCTOR    = 4,
+       IDE_LSP_COMPLETION_FIELD          = 5,
+       IDE_LSP_COMPLETION_VARIABLE       = 6,
+       IDE_LSP_COMPLETION_CLASS          = 7,
+       IDE_LSP_COMPLETION_INTERFACE      = 8,
+       IDE_LSP_COMPLETION_MODULE         = 9,
+       IDE_LSP_COMPLETION_PROPERTY       = 10,
+       IDE_LSP_COMPLETION_UNIT           = 11,
+       IDE_LSP_COMPLETION_VALUE          = 12,
+       IDE_LSP_COMPLETION_ENUM           = 13,
+       IDE_LSP_COMPLETION_KEYWORD        = 14,
+       IDE_LSP_COMPLETION_SNIPPET        = 15,
+       IDE_LSP_COMPLETION_COLOR          = 16,
+       IDE_LSP_COMPLETION_FILE           = 17,
+       IDE_LSP_COMPLETION_REFERENCE      = 18,
+       IDE_LSP_COMPLETION_FOLDER         = 19,
+       IDE_LSP_COMPLETION_ENUM_MEMBER    = 20,
+       IDE_LSP_COMPLETION_CONSTANT       = 21,
+       IDE_LSP_COMPLETION_STRUCT         = 22,
+       IDE_LSP_COMPLETION_EVENT          = 23,
+       IDE_LSP_COMPLETION_OPERATOR       = 24,
+       IDE_LSP_COMPLETION_TYPE_PARAMETER = 25,
+} IdeLspCompletionKind;
+
+G_END_DECLS
diff --git a/src/libide/lsp/ide-lsp-util.c b/src/libide/lsp/ide-lsp-util.c
new file mode 100644
index 000000000..bf3950c1b
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-util.c
@@ -0,0 +1,80 @@
+/* ide-lsp-util.c
+ *
+ * Copyright 2017-2019 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
+ */
+
+#include "ide-lsp-util.h"
+
+IdeSymbolKind
+ide_lsp_decode_symbol_kind (guint kind)
+{
+  switch (kind)
+    {
+    case 1:   kind = IDE_SYMBOL_KIND_FILE;         break;
+    case 2:   kind = IDE_SYMBOL_KIND_MODULE;       break;
+    case 3:   kind = IDE_SYMBOL_KIND_NAMESPACE;    break;
+    case 4:   kind = IDE_SYMBOL_KIND_PACKAGE;      break;
+    case 5:   kind = IDE_SYMBOL_KIND_CLASS;        break;
+    case 6:   kind = IDE_SYMBOL_KIND_METHOD;       break;
+    case 7:   kind = IDE_SYMBOL_KIND_PROPERTY;     break;
+    case 8:   kind = IDE_SYMBOL_KIND_FIELD;        break;
+    case 9:   kind = IDE_SYMBOL_KIND_CONSTRUCTOR;  break;
+    case 10:  kind = IDE_SYMBOL_KIND_ENUM;         break;
+    case 11:  kind = IDE_SYMBOL_KIND_INTERFACE;    break;
+    case 12:  kind = IDE_SYMBOL_KIND_FUNCTION;     break;
+    case 13:  kind = IDE_SYMBOL_KIND_VARIABLE;     break;
+    case 14:  kind = IDE_SYMBOL_KIND_CONSTANT;     break;
+    case 15:  kind = IDE_SYMBOL_KIND_STRING;       break;
+    case 16:  kind = IDE_SYMBOL_KIND_NUMBER;       break;
+    case 17:  kind = IDE_SYMBOL_KIND_BOOLEAN;      break;
+    case 18:  kind = IDE_SYMBOL_KIND_ARRAY;        break;
+    default:  kind = IDE_SYMBOL_KIND_NONE;         break;
+    }
+
+  return kind;
+}
+
+IdeSymbolKind
+ide_lsp_decode_completion_kind (guint kind)
+{
+  switch (kind)
+    {
+    case 1:   kind = IDE_SYMBOL_KIND_STRING;       break;
+    case 2:   kind = IDE_SYMBOL_KIND_METHOD;       break;
+    case 3:   kind = IDE_SYMBOL_KIND_FUNCTION;     break;
+    case 4:   kind = IDE_SYMBOL_KIND_CONSTRUCTOR;  break;
+    case 5:   kind = IDE_SYMBOL_KIND_FIELD;        break;
+    case 6:   kind = IDE_SYMBOL_KIND_VARIABLE;     break;
+    case 7:   kind = IDE_SYMBOL_KIND_CLASS;        break;
+    case 8:   kind = IDE_SYMBOL_KIND_INTERFACE;    break;
+    case 9:   kind = IDE_SYMBOL_KIND_MODULE;       break;
+    case 10:  kind = IDE_SYMBOL_KIND_PROPERTY;     break;
+    case 11:  kind = IDE_SYMBOL_KIND_NUMBER;       break;
+    case 12:  kind = IDE_SYMBOL_KIND_SCALAR;       break;
+    case 13:  kind = IDE_SYMBOL_KIND_ENUM_VALUE;   break;
+    case 14:  kind = IDE_SYMBOL_KIND_KEYWORD;      break;
+    case 17:  kind = IDE_SYMBOL_KIND_FILE;         break;
+
+    case 15: /* Snippet */
+    case 16: /* Color */
+    case 18: /* Reference */
+    default:  kind = IDE_SYMBOL_KIND_NONE;         break;
+    }
+
+  return kind;
+}
diff --git a/src/libide/lsp/ide-lsp-util.h b/src/libide/lsp/ide-lsp-util.h
new file mode 100644
index 000000000..e36f56c33
--- /dev/null
+++ b/src/libide/lsp/ide-lsp-util.h
@@ -0,0 +1,36 @@
+/* ide-lsp-util.h
+ *
+ * Copyright 2017-2019 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
+
+#if !defined (IDE_LSP_INSIDE) && !defined (IDE_LSP_COMPILATION)
+# error "Only <libide-lsp.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+IdeSymbolKind ide_lsp_decode_symbol_kind     (guint kind);
+IDE_AVAILABLE_IN_3_32
+IdeSymbolKind ide_lsp_decode_completion_kind (guint kind);
+
+G_END_DECLS
diff --git a/src/libide/lsp/libide-lsp.h b/src/libide/lsp/libide-lsp.h
new file mode 100644
index 000000000..a91bcedb4
--- /dev/null
+++ b/src/libide/lsp/libide-lsp.h
@@ -0,0 +1,42 @@
+/* libide-lsp.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+#define IDE_LSP_INSIDE
+
+#include "ide-lsp-types.h"
+
+#include "ide-lsp-client.h"
+#include "ide-lsp-completion-item.h"
+#include "ide-lsp-completion-provider.h"
+#include "ide-lsp-completion-results.h"
+#include "ide-lsp-diagnostic-provider.h"
+#include "ide-lsp-formatter.h"
+#include "ide-lsp-highlighter.h"
+#include "ide-lsp-hover-provider.h"
+#include "ide-lsp-rename-provider.h"
+#include "ide-lsp-symbol-node.h"
+#include "ide-lsp-symbol-resolver.h"
+#include "ide-lsp-symbol-tree.h"
+
+#undef IDE_LSP_INSIDE
diff --git a/src/libide/lsp/meson.build b/src/libide/lsp/meson.build
new file mode 100644
index 000000000..23aba74fa
--- /dev/null
+++ b/src/libide/lsp/meson.build
@@ -0,0 +1,94 @@
+libide_lsp_header_subdir = join_paths(libide_header_subdir, 'lsp')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_lsp_public_headers = [
+  'libide-lsp.h',
+  'ide-lsp-client.h',
+  'ide-lsp-completion-item.h',
+  'ide-lsp-completion-provider.h',
+  'ide-lsp-completion-results.h',
+  'ide-lsp-diagnostic-provider.h',
+  'ide-lsp-formatter.h',
+  'ide-lsp-highlighter.h',
+  'ide-lsp-hover-provider.h',
+  'ide-lsp-rename-provider.h',
+  'ide-lsp-symbol-node.h',
+  'ide-lsp-symbol-resolver.h',
+  'ide-lsp-symbol-tree.h',
+  'ide-lsp-types.h',
+]
+
+libide_lsp_private_headers = [
+  'ide-lsp-util.h',
+  'ide-lsp-symbol-node-private.h',
+  'ide-lsp-symbol-tree-private.h',
+]
+
+install_headers(libide_lsp_public_headers, subdir: libide_lsp_header_subdir)
+
+#
+# Sources
+#
+
+libide_lsp_public_sources = [
+  'ide-lsp-client.c',
+  'ide-lsp-completion-item.c',
+  'ide-lsp-completion-provider.c',
+  'ide-lsp-completion-results.c',
+  'ide-lsp-diagnostic-provider.c',
+  'ide-lsp-formatter.c',
+  'ide-lsp-highlighter.c',
+  'ide-lsp-hover-provider.c',
+  'ide-lsp-rename-provider.c',
+  'ide-lsp-symbol-node.c',
+  'ide-lsp-symbol-resolver.c',
+  'ide-lsp-symbol-tree.c',
+]
+
+libide_lsp_private_sources = [
+  'ide-lsp-util.c',
+]
+
+libide_lsp_sources = libide_lsp_public_sources + libide_lsp_private_sources
+
+#
+# Dependencies
+#
+
+libide_lsp_deps = [
+  libgio_dep,
+  libjsonrpc_glib_dep,
+  libdazzle_dep,
+
+  libide_code_dep,
+  libide_core_dep,
+  libide_io_dep,
+  libide_projects_dep,
+  libide_sourceview_dep,
+  libide_threading_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_lsp = static_library('ide-lsp-' + libide_api_version, libide_lsp_sources,
+   dependencies: libide_lsp_deps,
+         c_args: libide_args + release_args + ['-DIDE_LSP_COMPILATION'],
+)
+
+libide_lsp_dep = declare_dependency(
+              sources: libide_lsp_private_headers,
+         dependencies: libide_lsp_deps,
+           link_whole: libide_lsp,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_lsp_public_sources)
+gnome_builder_public_headers += files(libide_lsp_public_headers)
+gnome_builder_include_subdirs += libide_lsp_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-lsp.h', '-DIDE_LSP_COMPILATION']
diff --git a/src/libide/meson.build b/src/libide/meson.build
index 249623891..3412c50c9 100644
--- a/src/libide/meson.build
+++ b/src/libide/meson.build
@@ -1,167 +1,26 @@
-libide_header_dir = join_paths(get_option('includedir'), 'gnome-builder', 'libide')
-libide_header_subdir = join_paths('gnome-builder', 'libide')
+libide_header_subdir = join_paths('gnome-builder-@0@.@1@'.format(MAJOR_VERSION, MINOR_VERSION), 'libide')
+libide_header_dir = join_paths(get_option('includedir'), libide_header_subdir)
+libide_include_directories = []
 
-libide_enum_headers = []
-libide_generated_headers = []
-libide_public_headers = []
-libide_public_sources = []
-libide_private_sources = []
-
-version_data = configuration_data()
-version_data.set('MAJOR_VERSION', MAJOR_VERSION)
-version_data.set('MINOR_VERSION', MINOR_VERSION)
-version_data.set('MICRO_VERSION', MICRO_VERSION)
-version_data.set('VERSION', meson.project_version())
-version_data.set_quoted('BUILD_CHANNEL', get_option('with_channel'))
-version_data.set_quoted('BUILD_TYPE', get_option('buildtype'))
-
-libide_version_h = configure_file(
-          input: 'ide-version.h.in',
-         output: 'ide-version.h',
-    install_dir: libide_header_dir,
-        install: true,
-  configuration: version_data)
-libide_generated_headers += [libide_version_h]
-
-libide_build_ident_h = vcs_tag(
-     fallback: meson.project_version(),
-        input: 'ide-build-ident.h.in',
-       output: 'ide-build-ident.h',
-)
-libide_generated_headers += [libide_build_ident_h]
-
-libide_public_headers += [
-  'ide.h',
-  'ide-context.h',
-  'ide-global.h',
-  'ide-object.h',
-  'ide-pausable.h',
-  'ide-service.h',
-  'ide-types.h',
-  'ide-version-macros.h',
-]
-
-libide_public_sources += [
-  'ide.c',
-  'ide-context.c',
-  'ide-object.c',
-  'ide-pausable.c',
-  'ide-service.c',
-]
-
-subdir('application')
-subdir('buildconfig')
-subdir('buildui')
-subdir('buildsystem')
-subdir('buffers')
-subdir('completion')
-subdir('config')
-subdir('debugger')
-subdir('devices')
-subdir('diagnostics')
-subdir('doap')
-subdir('directory')
-subdir('editor')
-subdir('files')
-subdir('formatting')
-subdir('genesis')
-subdir('greeter')
-subdir('gsettings')
-subdir('highlighting')
-subdir('hover')
-subdir('keybindings')
-subdir('langserv')
-subdir('layout')
-subdir('local')
-subdir('logging')
-subdir('modelines')
+subdir('core')
 subdir('plugins')
-subdir('preferences')
+subdir('threading')
+subdir('io')
+subdir('code')
+subdir('vcs')
 subdir('projects')
-subdir('rename')
-subdir('runner')
-subdir('runtimes')
 subdir('search')
-subdir('session')
-subdir('snippets')
-subdir('sourceview')
-subdir('storage')
-subdir('subprocess')
-subdir('symbols')
-subdir('template')
+subdir('foundry')
+subdir('debugger')
+subdir('themes')
+subdir('gui')
 subdir('terminal')
-subdir('testing')
-subdir('threading')
-subdir('toolchain')
-subdir('transfers')
-subdir('util')
-subdir('vcs')
-subdir('workbench')
-subdir('workers')
-
-libide_enums = gnome.mkenums('ide-enums',
-      h_template: 'ide-enums.h.in',
-      c_template: 'ide-enums.c.in',
-         sources: libide_enum_headers,
-  install_header: true,
-     install_dir: libide_header_dir,
-)
-libide_public_sources += [libide_enums[0]]
-libide_generated_headers += [libide_enums[1]]
-
-libide_conf = configuration_data()
-libide_conf.set10('ENABLE_TRACING', get_option('enable_tracing'))
-libide_conf.set('BUGREPORT_URL', 'https://gitlab.gnome.org/GNOME/gnome-builder/issues')
-libide_debug_h = configure_file(
-         input: 'ide-debug.h.in',
-         output: 'ide-debug.h',
-  configuration: libide_conf,
-        install: true,
-    install_dir: libide_header_dir,
-)
-libide_generated_headers += [libide_debug_h]
-
-install_headers([
-  'ide.h',
-  'ide-context.h',
-  'ide-global.h',
-  'ide-object.h',
-  'ide-pausable.h',
-  'ide-service.h',
-  'ide-types.h',
-  'ide-version-macros.h',
-], subdir: libide_header_subdir)
-
-libide_resources = gnome.compile_resources('ide-resources',
-  'libide.gresource.xml',
-  c_name: 'ide',
-)
-libide_generated_headers += [libide_resources[1]]
-
-libide_icons_resources = gnome.compile_resources('ide-icons-resources',
-  join_paths(meson.source_root(), 'data/icons/hicolor/icons.gresource.xml'),
-  source_dir: join_paths(meson.source_root(), 'data/icons/hicolor'),
-  c_name: 'ide_icons',
-)
-libide_generated_headers += [libide_icons_resources[1]]
-
-libide_sources = ['gconstructor.h']
-libide_sources += libide_private_sources
-libide_sources += libide_generated_headers
-libide_sources += libide_public_sources
-
-contrib_dir = join_paths(meson.source_root(), 'contrib/')
-
-if get_option('with_webkit')
-  libide_sources += ['webkit/ide-webkit.c']
-endif
-
-if get_option('with_editorconfig')
-  libide_sources += [
-    'editorconfig/editorconfig-glib.c',
-    'editorconfig/ide-editorconfig-file-settings.c',
-  ]
-endif
+subdir('sourceview')
+subdir('lsp')
+subdir('editor')
+subdir('greeter')
+subdir('webkit')
+subdir('tree')
 
 # We want to find the subdirectory to install our override into:
 python_libprefix = get_option('python_libprefix')
@@ -187,11 +46,14 @@ except ImportError:
 
 if overridedir.startswith(libdir):
   overridedir = overridedir[len(libdir) + 1:]
+elif overridedir.startswith('@0@'):
+  # Do nothing if its in our same prefix
+  pass
 else:
   overridedir = overridedir[len('/usr/lib') + 1:]
 
 print(overridedir)
-'''
+'''.format(get_option('prefix'))
 
 ret = run_command([python3, '-c', get_overridedir])
 if ret.returncode() != 0
@@ -202,121 +64,3 @@ endif
 endif
 
 install_data('Ide.py', install_dir: pygobject_override_dir)
-
-libide_deps = [
-  libdazzle_dep,
-  libgio_dep,
-  libgiounix_dep,
-  libgtk_dep,
-  libgtksource_dep,
-  libjson_glib_dep,
-  libjsonrpc_glib_dep,
-  libm_dep,
-  libpangoft2_dep,
-  libpeas_dep,
-  libtemplate_glib_dep,
-  libvte_dep,
-  libxml2_dep,
-]
-
-if get_option('with_webkit')
-  libide_deps += [dependency('webkit2gtk-4.0', version: '>=2.12.0')]
-endif
-
-if get_option('with_editorconfig')
-  libide_args += '-DENABLE_EDITORCONFIG'
-  libide_deps += libeditorconfig_dep
-endif
-
-# Limit visibility to public API
-libide_args += hidden_visibility_args
-
-libide = shared_library('ide-' + libide_api_version,
-  libide_resources + libide_icons_resources + libide_sources,
-  dependencies: libide_deps,
-        c_args: libide_args + release_args,
-       install: true,
-   install_dir: pkglibdir_abs,
- install_rpath: pkglibdir_abs,
-)
-
-libide_dep = declare_dependency(
-              sources: libide_generated_headers,
-         dependencies: [ libdazzle_dep,
-                         libgio_dep,
-                         libgtk_dep,
-                         libgtksource_dep,
-                         libpeas_dep,
-                         libjson_glib_dep,
-                         libtemplate_glib_dep,
-                         libjsonrpc_glib_dep,
-                         libvte_dep ],
-            link_with: libide,
-  include_directories: include_directories('.'),
-)
-
-# Doesn't link to libide
-# TODO: I think we can remove most of the links here and just setup includes
-libide_plugin_dep = declare_dependency(
-              sources: libide_generated_headers,
-  include_directories: include_directories('.'),
-         dependencies: [ libdazzle_dep,
-                         libgio_dep,
-                         libgtk_dep,
-                         libgtksource_dep,
-                         libtemplate_glib_dep,
-                         libjson_glib_dep,
-                         libjsonrpc_glib_dep,
-                         libvte_dep ],
-)
-
-pkgg = import('pkgconfig')
-
-pkgg.generate(
-    libraries: [libide],
-      subdirs: [ 'gnome-builder/libide' ],
-      version: meson.project_version(),
-         name: 'Libide',
-     filebase: 'libide-1.0',
-  description: 'Libide contains the components used to build the GNOME Builder IDE.',
-     requires: [ 'gtk+-3.0', 'gtksourceview-4', 'libdazzle-1.0', 'template-glib-1.0', 'jsonrpc-glib-1.0', 
'libpeas-1.0', 'vte-2.91' ],
-  install_dir: join_paths(pkglibdir, 'pkgconfig'),
-)
-
-libide_gir = gnome.generate_gir(libide,
-             sources: libide_generated_headers + libide_public_headers + libide_public_sources,
-            nsversion: libide_api_version,
-            namespace: 'Ide',
-        symbol_prefix: 'ide',
-    identifier_prefix: 'Ide',
-             includes: [ 'Gio-2.0', 'GtkSource-4', 'Peas-1.0', 'Dazzle-1.0', 'Json-1.0', 'Template-1.0', 
'Vte-2.91' ],
-             install: true,
-      install_dir_gir: pkggirdir,
-  install_dir_typelib: pkgtypelibdir,
-           extra_args: [ '--c-include=ide.h', '--pkg-export=libide-1.0' ]
-)
-
-if get_option('with_vapi')
-
-configure_file(
-          input: 'libide-' + libide_api_version + '.deps',
-         output: 'libide-' + libide_api_version + '.deps',
-           copy: true,
-        install: true,
-    install_dir: pkgvapidir,
-)
-
-libide_vapi = gnome.generate_vapi('libide-' + libide_api_version,
-      sources: libide_gir[0],
-      install: true,
-  install_dir: pkgvapidir,
-     packages: [ 'gio-2.0',
-                 'gtk+-3.0',
-                 'gtksourceview-4',
-                 'json-glib-1.0',
-                 'libdazzle-1.0',
-                 'libpeas-1.0',
-                 'template-glib-1.0' ],
-)
-
-endif
diff --git a/src/libide/plugins/ide-extension-adapter.c b/src/libide/plugins/ide-extension-adapter.c
index e25e24827..d6043385f 100644
--- a/src/libide/plugins/ide-extension-adapter.c
+++ b/src/libide/plugins/ide-extension-adapter.c
@@ -1,6 +1,6 @@
 /* ide-extension-adapter.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-extension-adapter"
@@ -23,11 +25,8 @@
 #include <dazzle.h>
 #include <glib/gi18n.h>
 
-#include "ide-debug.h"
-
-#include "application/ide-application.h"
-#include "plugins/ide-extension-adapter.h"
-#include "plugins/ide-extension-util.h"
+#include "ide-extension-adapter.h"
+#include "ide-extension-util-private.h"
 
 struct _IdeExtensionAdapter
 {
@@ -60,6 +59,20 @@ enum {
 
 static GParamSpec *properties [LAST_PROP];
 
+static gchar *
+ide_extension_adapter_repr (IdeObject *object)
+{
+  IdeExtensionAdapter *self = (IdeExtensionAdapter *)object;
+
+  g_assert (IDE_IS_EXTENSION_ADAPTER (self));
+
+  return g_strdup_printf ("%s interface=“%s” key=“%s” value=“%s”",
+                          G_OBJECT_TYPE_NAME (self),
+                          g_type_name (self->interface_type),
+                          self->key ?: "",
+                          self->value ?: "");
+}
+
 static GSettings *
 ide_extension_adapter_get_settings (IdeExtensionAdapter *self,
                                     PeasPluginInfo      *plugin_info)
@@ -105,9 +118,18 @@ ide_extension_adapter_set_extension (IdeExtensionAdapter *self,
 
   self->plugin_info = plugin_info;
 
-  if (g_set_object (&self->extension, extension))
+  if (extension != self->extension)
     {
+      if (IDE_IS_OBJECT (self->extension))
+        ide_object_destroy (IDE_OBJECT (self->extension));
+
+      g_set_object (&self->extension, extension);
+
+      if (IDE_IS_OBJECT (extension))
+        ide_object_append (IDE_OBJECT (self), IDE_OBJECT (extension));
+
       ide_extension_adapter_monitor (self, plugin_info);
+
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EXTENSION]);
     }
 }
@@ -166,30 +188,10 @@ ide_extension_adapter_reload (IdeExtensionAdapter *self)
     return;
 
   if (best_match != NULL)
-    {
-      IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
-
-      if (g_type_is_a (self->interface_type, IDE_TYPE_OBJECT))
-        extension = ide_extension_new (self->engine,
-                                       best_match,
-                                       self->interface_type,
-                                       "context", context,
-                                       NULL);
-      else
-        {
-          extension = ide_extension_new (self->engine,
-                                         best_match,
-                                         self->interface_type,
-                                         NULL);
-          /*
-           * If the plugin object turned out to have IdeObject
-           * as a base, try to set it now (even though we couldn't
-           * do it at construction time).
-           */
-          if (IDE_IS_OBJECT (extension))
-            ide_object_set_context (IDE_OBJECT (extension), context);
-        }
-    }
+    extension = ide_extension_new (self->engine,
+                                   best_match,
+                                   self->interface_type,
+                                   NULL);
 
   ide_extension_adapter_set_extension (self, best_match, extension);
 
@@ -218,7 +220,7 @@ ide_extension_adapter_queue_reload (IdeExtensionAdapter *self)
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_EXTENSION_ADAPTER (self));
 
-  dzl_clear_source (&self->queue_handler);
+  g_clear_handle_id (&self->queue_handler, g_source_remove);
   self->queue_handler = g_timeout_add (0, ide_extension_adapter_do_reload, self);
 }
 
@@ -309,7 +311,7 @@ ide_extension_adapter__changed_disabled (IdeExtensionAdapter *self,
   g_assert (IDE_IS_EXTENSION_ADAPTER (self));
   g_assert (G_IS_SETTINGS (settings));
 
-  if (dzl_str_equal0 (changed_key, "disabled"))
+  if (ide_str_equal0 (changed_key, "disabled"))
     ide_extension_adapter_queue_reload (self);
 }
 
@@ -320,7 +322,7 @@ ide_extension_adapter_dispose (GObject *object)
 
   self->interface_type = G_TYPE_INVALID;
 
-  dzl_clear_source (&self->queue_handler);
+  g_clear_handle_id (&self->queue_handler, g_source_remove);
 
   ide_extension_adapter_monitor (self, NULL);
 
@@ -416,12 +418,15 @@ static void
 ide_extension_adapter_class_init (IdeExtensionAdapterClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
 
   object_class->dispose = ide_extension_adapter_dispose;
   object_class->finalize = ide_extension_adapter_finalize;
   object_class->get_property = ide_extension_adapter_get_property;
   object_class->set_property = ide_extension_adapter_set_property;
 
+  i_object_class->repr = ide_extension_adapter_repr;
+
   properties [PROP_ENGINE] =
     g_param_spec_object ("engine",
                          "Engine",
@@ -489,7 +494,7 @@ ide_extension_adapter_set_key (IdeExtensionAdapter *self,
   g_return_if_fail (IDE_IS_MAIN_THREAD ());
   g_return_if_fail (IDE_IS_EXTENSION_ADAPTER (self));
 
-  if (!dzl_str_equal0 (self->key, key))
+  if (!ide_str_equal0 (self->key, key))
     {
       g_free (self->key);
       self->key = g_strdup (key);
@@ -514,7 +519,7 @@ ide_extension_adapter_set_value (IdeExtensionAdapter *self,
   g_return_if_fail (IDE_IS_MAIN_THREAD ());
   g_return_if_fail (IDE_IS_EXTENSION_ADAPTER (self));
 
-  if (!dzl_str_equal0 (self->value, value))
+  if (!ide_str_equal0 (self->value, value))
     {
       g_free (self->value);
       self->value = g_strdup (value);
@@ -539,6 +544,8 @@ ide_extension_adapter_get_interface_type (IdeExtensionAdapter *self)
  * Gets the #IdeExtensionAdapter:engine property.
  *
  * Returns: (transfer none): a #PeasEngine.
+ *
+ * Since: 3.32
  */
 PeasEngine *
 ide_extension_adapter_get_engine (IdeExtensionAdapter *self)
@@ -555,6 +562,8 @@ ide_extension_adapter_get_engine (IdeExtensionAdapter *self)
  * Gets the extension object managed by the adapter.
  *
  * Returns: (transfer none) (type GObject.Object): a #GObject or %NULL.
+ *
+ * Since: 3.32
  */
 gpointer
 ide_extension_adapter_get_extension (IdeExtensionAdapter *self)
@@ -567,44 +576,54 @@ ide_extension_adapter_get_extension (IdeExtensionAdapter *self)
 
 /**
  * ide_extension_adapter_new:
- * @context: An #IdeContext.
- * @engine: (allow-none): a #PeasEngine or %NULL.
+ * @parent: (nullable): An #IdeObject or %NULL
+ * @engine: (allow-none): a #PeasEngine or %NULL
  * @interface_type: The #GType of the interface to be implemented.
  * @key: The key for matching extensions from plugin info external data.
  * @value: (allow-none): The value to use when matching keys.
  *
  * Creates a new #IdeExtensionAdapter.
  *
- * The #IdeExtensionAdapter object can be used to wrap an extension that might need to change
- * at runtime based on various changing parameters. For example, it can watch the loading and
- * unloading of plugins and reload the #IdeExtensionAdapter:extension property.
+ * The #IdeExtensionAdapter object can be used to wrap an extension that might
+ * need to change at runtime based on various changing parameters. For example,
+ * it can watch the loading and unloading of plugins and reload the
+ * #IdeExtensionAdapter:extension property.
  *
  * Additionally, it can match a specific plugin based on the @value provided.
  *
- * This uses #IdeExtensionPoint to create the extension implementation, which means that
- * extension points that are disabled (such as from the plugins GSettings) will be ignored.
- * As such, if one plugin that is higher priority than another, but is disabled, will be
- * ignored and the secondary plugin will be used.
+ * This uses #IdeExtensionPoint to create the extension implementation, which
+ * means that extension points that are disabled (such as from the plugins
+ * GSettings) will be ignored.  As such, if one plugin that is higher priority
+ * than another, but is disabled, will be ignored and the secondary plugin will
+ * be used.
  *
  * Returns: (transfer full): A newly created #IdeExtensionAdapter.
+ *
+ * Since: 3.32
  */
 IdeExtensionAdapter *
-ide_extension_adapter_new (IdeContext  *context,
+ide_extension_adapter_new (IdeObject   *parent,
                            PeasEngine  *engine,
                            GType        interface_type,
                            const gchar *key,
                            const gchar *value)
 {
+  IdeExtensionAdapter *self;
+
   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
   g_return_val_if_fail (!engine || PEAS_IS_ENGINE (engine), NULL);
   g_return_val_if_fail (G_TYPE_IS_INTERFACE (interface_type), NULL);
   g_return_val_if_fail (key != NULL, NULL);
 
-  return g_object_new (IDE_TYPE_EXTENSION_ADAPTER,
-                       "context", context,
+  self = g_object_new (IDE_TYPE_EXTENSION_ADAPTER,
                        "engine", engine,
                        "interface-type", interface_type,
                        "key", key,
                        "value", value,
                        NULL);
+
+  if (parent != NULL)
+    ide_object_append (parent, IDE_OBJECT (self));
+
+  return g_steal_pointer (&self);
 }
diff --git a/src/libide/plugins/ide-extension-adapter.h b/src/libide/plugins/ide-extension-adapter.h
index 5e5d8aea7..e8499ec69 100644
--- a/src/libide/plugins/ide-extension-adapter.h
+++ b/src/libide/plugins/ide-extension-adapter.h
@@ -1,6 +1,6 @@
 /* ide-extension-adapter.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,42 +14,46 @@
  *
  * 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 <libpeas/peas.h>
+#if !defined (IDE_PLUGINS_INSIDE) && !defined (IDE_PLUGINS_COMPILATION)
+# error "Only <libide-plugins.h> can be included directly."
+#endif
 
-#include "ide-object.h"
-#include "ide-version-macros.h"
+#include <libpeas/peas.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_EXTENSION_ADAPTER (ide_extension_adapter_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeExtensionAdapter, ide_extension_adapter, IDE, EXTENSION_ADAPTER, IdeObject)
 
-IDE_AVAILABLE_IN_ALL
-IdeExtensionAdapter *ide_extension_adapter_new                (IdeContext          *context,
+IDE_AVAILABLE_IN_3_32
+IdeExtensionAdapter *ide_extension_adapter_new                (IdeObject           *parent,
                                                                PeasEngine          *engine,
                                                                GType                interface_type,
                                                                const gchar         *key,
                                                                const gchar         *value);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 PeasEngine          *ide_extension_adapter_get_engine         (IdeExtensionAdapter *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gpointer             ide_extension_adapter_get_extension      (IdeExtensionAdapter *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GType                ide_extension_adapter_get_interface_type (IdeExtensionAdapter *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_extension_adapter_get_key            (IdeExtensionAdapter *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_extension_adapter_set_key            (IdeExtensionAdapter *self,
                                                                const gchar         *key);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar         *ide_extension_adapter_get_value          (IdeExtensionAdapter *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                 ide_extension_adapter_set_value          (IdeExtensionAdapter *self,
                                                                const gchar         *value);
 
diff --git a/src/libide/plugins/ide-extension-set-adapter.c b/src/libide/plugins/ide-extension-set-adapter.c
index 105832948..d4586b1f6 100644
--- a/src/libide/plugins/ide-extension-set-adapter.c
+++ b/src/libide/plugins/ide-extension-set-adapter.c
@@ -1,6 +1,6 @@
 /* ide-extension-set-adapter.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-extension-set-adapter"
@@ -24,12 +26,8 @@
 #include <glib/gi18n.h>
 #include <stdlib.h>
 
-#include "ide-context.h"
-#include "ide-debug.h"
-
-#include "application/ide-application.h"
-#include "plugins/ide-extension-set-adapter.h"
-#include "plugins/ide-extension-util.h"
+#include "ide-extension-set-adapter.h"
+#include "ide-extension-util-private.h"
 
 struct _IdeExtensionSetAdapter
 {
@@ -69,6 +67,20 @@ static guint signals [LAST_SIGNAL];
 
 static void ide_extension_set_adapter_queue_reload (IdeExtensionSetAdapter *);
 
+static gchar *
+ide_extension_set_adapter_repr (IdeObject *object)
+{
+  IdeExtensionSetAdapter *self = (IdeExtensionSetAdapter *)object;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (self));
+
+  return g_strdup_printf ("%s interface=\"%s\" key=\"%s\" value=\"%s\"",
+                          G_OBJECT_TYPE_NAME (self),
+                          g_type_name (self->interface_type),
+                          self->key ?: "",
+                          self->value ?: "");
+}
+
 static void
 add_extension (IdeExtensionSetAdapter *self,
                PeasPluginInfo         *plugin_info,
@@ -81,6 +93,19 @@ add_extension (IdeExtensionSetAdapter *self,
   g_assert (g_type_is_a (G_OBJECT_TYPE (exten), self->interface_type));
 
   g_hash_table_insert (self->extensions, plugin_info, exten);
+
+  /* Ensure that we take the reference in case it's a floating ref */
+  if (G_IS_INITIALLY_UNOWNED (exten) && g_object_is_floating (exten))
+    g_object_ref_sink (exten);
+
+  /*
+   * If the plugin object turned out to have IdeObject as a
+   * base, make it a child of ourselves, because we're an
+   * IdeObject too and that gives it access to the context.
+   */
+  if (IDE_IS_OBJECT (exten))
+    ide_object_append (IDE_OBJECT (self), IDE_OBJECT (exten));
+
   g_signal_emit (self, signals [EXTENSION_ADDED], 0, plugin_info, exten);
 }
 
@@ -102,6 +127,9 @@ remove_extension (IdeExtensionSetAdapter *self,
 
   g_hash_table_remove (self->extensions, plugin_info);
   g_signal_emit (self, signals [EXTENSION_REMOVED], 0, plugin_info, hold);
+
+  if (IDE_IS_OBJECT (hold))
+    ide_object_destroy (IDE_OBJECT (hold));
 }
 
 static void
@@ -128,7 +156,7 @@ watch_extension (IdeExtensionSetAdapter *self,
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (self));
   g_assert (plugin_info != NULL);
-  g_assert (G_TYPE_IS_INTERFACE (interface_type));
+  g_assert (G_TYPE_IS_INTERFACE (interface_type) || G_TYPE_IS_OBJECT (interface_type));
 
   path = g_strdup_printf ("/org/gnome/builder/extension-types/%s/%s/",
                           peas_plugin_info_get_module_name (plugin_info),
@@ -150,7 +178,6 @@ watch_extension (IdeExtensionSetAdapter *self,
 static void
 ide_extension_set_adapter_reload (IdeExtensionSetAdapter *self)
 {
-  IdeContext *context;
   const GList *plugins;
 
   g_assert (IDE_IS_MAIN_THREAD ());
@@ -168,11 +195,8 @@ ide_extension_set_adapter_reload (IdeExtensionSetAdapter *self)
       g_ptr_array_remove_index (self->settings, self->settings->len - 1);
     }
 
-  context = ide_object_get_context (IDE_OBJECT (self));
   plugins = peas_engine_get_plugin_list (self->engine);
 
-  g_assert (IDE_IS_CONTEXT (context));
-
   for (; plugins; plugins = plugins->next)
     {
       PeasPluginInfo *plugin_info = plugins->data;
@@ -181,8 +205,10 @@ ide_extension_set_adapter_reload (IdeExtensionSetAdapter *self)
       if (!peas_plugin_info_is_loaded (plugin_info))
         continue;
 
-      if (peas_engine_provides_extension (self->engine, plugin_info, self->interface_type))
-        watch_extension (self, plugin_info, self->interface_type);
+      if (!peas_engine_provides_extension (self->engine, plugin_info, self->interface_type))
+        continue;
+
+      watch_extension (self, plugin_info, self->interface_type);
 
       if (ide_extension_util_can_use_plugin (self->engine,
                                              plugin_info,
@@ -195,26 +221,10 @@ ide_extension_set_adapter_reload (IdeExtensionSetAdapter *self)
             {
               PeasExtension *exten;
 
-              if (g_type_is_a (self->interface_type, IDE_TYPE_OBJECT))
-                exten = ide_extension_new (self->engine,
-                                           plugin_info,
-                                           self->interface_type,
-                                           "context", context,
-                                           NULL);
-              else
-                {
-                  exten = ide_extension_new (self->engine,
-                                             plugin_info,
-                                             self->interface_type,
-                                             NULL);
-                  /*
-                   * If the plugin object turned out to have IdeObject
-                   * as a base, try to set it now (even though we couldn't
-                   * do it at construction time).
-                   */
-                  if (IDE_IS_OBJECT (exten))
-                    ide_object_set_context (IDE_OBJECT (exten), context);
-                }
+              exten = ide_extension_new (self->engine,
+                                         plugin_info,
+                                         self->interface_type,
+                                         NULL);
 
               add_extension (self, plugin_info, exten);
             }
@@ -253,7 +263,7 @@ ide_extension_set_adapter_queue_reload (IdeExtensionSetAdapter *self)
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (self));
 
-  dzl_clear_source (&self->reload_handler);
+  g_clear_handle_id (&self->reload_handler, g_source_remove);
 
   self->reload_handler = g_idle_add_full (G_PRIORITY_HIGH,
                                           ide_extension_set_adapter_do_reload,
@@ -261,16 +271,57 @@ ide_extension_set_adapter_queue_reload (IdeExtensionSetAdapter *self)
                                           NULL);
 }
 
+static void
+ide_extension_set_adapter_load_plugin (IdeExtensionSetAdapter *self,
+                                       PeasPluginInfo         *plugin_info,
+                                       PeasEngine             *engine)
+{
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (self));
+  g_assert (plugin_info != NULL);
+  g_assert (PEAS_IS_ENGINE (engine));
+
+  ide_extension_set_adapter_queue_reload (self);
+}
+
+static void
+ide_extension_set_adapter_unload_plugin (IdeExtensionSetAdapter *self,
+                                         PeasPluginInfo         *plugin_info,
+                                         PeasEngine             *engine)
+{
+  PeasExtension *exten;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (self));
+  g_assert (plugin_info != NULL);
+  g_assert (PEAS_IS_ENGINE (engine));
+
+  if ((exten = g_hash_table_lookup (self->extensions, plugin_info)))
+    {
+      remove_extension (self, plugin_info, exten);
+      g_hash_table_remove (self->extensions, plugin_info);
+    }
+}
+
 static void
 ide_extension_set_adapter_set_engine (IdeExtensionSetAdapter *self,
                                       PeasEngine             *engine)
 {
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (self));
-  g_assert (PEAS_IS_ENGINE (engine));
+  g_assert (!engine || PEAS_IS_ENGINE (engine));
+
+  if (engine == NULL)
+    engine = peas_engine_get_default ();
 
   if (g_set_object (&self->engine, engine))
     {
+      g_signal_connect_object (self->engine, "load-plugin",
+                               G_CALLBACK (ide_extension_set_adapter_load_plugin),
+                               self,
+                               G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+      g_signal_connect_object (self->engine, "unload-plugin",
+                               G_CALLBACK (ide_extension_set_adapter_unload_plugin),
+                               self,
+                               G_CONNECT_SWAPPED);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ENGINE]);
       ide_extension_set_adapter_queue_reload (self);
     }
@@ -282,7 +333,7 @@ ide_extension_set_adapter_set_interface_type (IdeExtensionSetAdapter *self,
 {
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (self));
-  g_assert (G_TYPE_IS_INTERFACE (interface_type));
+  g_assert (G_TYPE_IS_INTERFACE (interface_type) || G_TYPE_IS_OBJECT (interface_type));
 
   if (interface_type != self->interface_type)
     {
@@ -293,7 +344,7 @@ ide_extension_set_adapter_set_interface_type (IdeExtensionSetAdapter *self,
 }
 
 static void
-ide_extension_set_adapter_dispose (GObject *object)
+ide_extension_set_adapter_destroy (IdeObject *object)
 {
   IdeExtensionSetAdapter *self = (IdeExtensionSetAdapter *)object;
   g_autoptr(GHashTable) extensions = NULL;
@@ -305,7 +356,7 @@ ide_extension_set_adapter_dispose (GObject *object)
   g_assert (IDE_IS_EXTENSION_SET_ADAPTER (self));
 
   self->interface_type = G_TYPE_INVALID;
-  dzl_clear_source (&self->reload_handler);
+  g_clear_handle_id (&self->reload_handler, g_source_remove);
 
   /*
    * Steal the extensions so we can be re-entrant safe and not break
@@ -325,7 +376,7 @@ ide_extension_set_adapter_dispose (GObject *object)
       g_hash_table_iter_remove (&iter);
     }
 
-  G_OBJECT_CLASS (ide_extension_set_adapter_parent_class)->dispose (object);
+  IDE_OBJECT_CLASS (ide_extension_set_adapter_parent_class)->destroy (object);
 }
 
 static void
@@ -419,12 +470,15 @@ static void
 ide_extension_set_adapter_class_init (IdeExtensionSetAdapterClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
 
-  object_class->dispose = ide_extension_set_adapter_dispose;
   object_class->finalize = ide_extension_set_adapter_finalize;
   object_class->get_property = ide_extension_set_adapter_get_property;
   object_class->set_property = ide_extension_set_adapter_set_property;
 
+  i_object_class->destroy = ide_extension_set_adapter_destroy;
+  i_object_class->repr = ide_extension_set_adapter_repr;
+
   properties [PROP_ENGINE] =
     g_param_spec_object ("engine",
                          "Engine",
@@ -436,7 +490,7 @@ ide_extension_set_adapter_class_init (IdeExtensionSetAdapterClass *klass)
     g_param_spec_gtype ("interface-type",
                         "Interface Type",
                         "Interface Type",
-                        G_TYPE_INTERFACE,
+                        G_TYPE_OBJECT,
                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_KEY] =
@@ -499,6 +553,8 @@ ide_extension_set_adapter_init (IdeExtensionSetAdapter *self)
  * Gets the #IdeExtensionSetAdapter:engine property.
  *
  * Returns: (transfer none): a #PeasEngine.
+ *
+ * Since: 3.32
  */
 PeasEngine *
 ide_extension_set_adapter_get_engine (IdeExtensionSetAdapter *self)
@@ -531,7 +587,7 @@ ide_extension_set_adapter_set_key (IdeExtensionSetAdapter *self,
   g_return_if_fail (IDE_IS_MAIN_THREAD ());
   g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (self));
 
-  if (!dzl_str_equal0 (self->key, key))
+  if (!ide_str_equal0 (self->key, key))
     {
       g_free (self->key);
       self->key = g_strdup (key);
@@ -560,7 +616,7 @@ ide_extension_set_adapter_set_value (IdeExtensionSetAdapter *self,
                  g_type_name (self->interface_type),
                  value ?: "");
 
-  if (!dzl_str_equal0 (self->value, value))
+  if (!ide_str_equal0 (self->value, value))
     {
       g_free (self->value);
       self->value = g_strdup (value);
@@ -576,28 +632,33 @@ ide_extension_set_adapter_set_value (IdeExtensionSetAdapter *self,
  * @user_data: user data for @foreach_func
  *
  * Calls @foreach_func for every extension loaded by the extension set.
+ *
+ * Since: 3.32
  */
 void
 ide_extension_set_adapter_foreach (IdeExtensionSetAdapter            *self,
                                    IdeExtensionSetAdapterForeachFunc  foreach_func,
                                    gpointer                           user_data)
 {
-  GHashTableIter iter;
-  gpointer key;
-  gpointer value;
+  const GList *list;
 
-  g_return_if_fail (IDE_IS_MAIN_THREAD ());
   g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (self));
   g_return_if_fail (foreach_func != NULL);
 
-  g_hash_table_iter_init (&iter, self->extensions);
+  /*
+   * Use the ordered list of plugins as it is sorted including any
+   * dependencies of plugins.
+   */
 
-  while (g_hash_table_iter_next (&iter, &key, &value))
+  list = peas_engine_get_plugin_list (self->engine);
+
+  for (const GList *iter = list; iter; iter = iter->next)
     {
-      PeasPluginInfo *plugin_info = key;
-      PeasExtension *exten = value;
+      PeasPluginInfo *plugin_info = iter->data;
+      PeasExtension *exten = g_hash_table_lookup (self->extensions, plugin_info);
 
-      foreach_func (self, plugin_info, exten, user_data);
+      if (exten != NULL)
+        foreach_func (self, plugin_info, exten, user_data);
     }
 }
 
@@ -627,6 +688,8 @@ sort_by_priority (gconstpointer a,
  * @user_data: user data for @foreach_func
  *
  * Calls @foreach_func for every extension loaded by the extension set.
+ *
+ * Since: 3.32
  */
 void
 ide_extension_set_adapter_foreach_by_priority (IdeExtensionSetAdapter            *self,
@@ -643,6 +706,12 @@ ide_extension_set_adapter_foreach_by_priority (IdeExtensionSetAdapter
   g_return_if_fail (IDE_IS_EXTENSION_SET_ADAPTER (self));
   g_return_if_fail (foreach_func != NULL);
 
+  if (self->key == NULL)
+    {
+      ide_extension_set_adapter_foreach (self, foreach_func, user_data);
+      return;
+    }
+
   prio_key = g_strdup_printf ("%s-Priority", self->key);
   sorted = g_array_new (FALSE, FALSE, sizeof (SortedInfo));
 
@@ -681,25 +750,40 @@ ide_extension_set_adapter_get_n_extensions (IdeExtensionSetAdapter *self)
 }
 
 IdeExtensionSetAdapter *
-ide_extension_set_adapter_new (IdeContext  *context,
+ide_extension_set_adapter_new (IdeObject   *parent,
                                PeasEngine  *engine,
                                GType        interface_type,
                                const gchar *key,
                                const gchar *value)
 {
+  IdeExtensionSetAdapter *ret;
+
   g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
-  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+  g_return_val_if_fail (!parent || IDE_IS_OBJECT (parent), NULL);
   g_return_val_if_fail (!engine || PEAS_IS_ENGINE (engine), NULL);
-  g_return_val_if_fail (G_TYPE_IS_INTERFACE (interface_type), NULL);
-  g_return_val_if_fail (key != NULL, NULL);
-
-  return g_object_new (IDE_TYPE_EXTENSION_SET_ADAPTER,
-                       "context", context,
-                       "engine", engine,
-                       "interface-type", interface_type,
-                       "key", key,
-                       "value", value,
-                       NULL);
+  g_return_val_if_fail (G_TYPE_IS_INTERFACE (interface_type) ||
+                        G_TYPE_IS_OBJECT (interface_type), NULL);
+
+  ret = g_object_new (IDE_TYPE_EXTENSION_SET_ADAPTER,
+                      "engine", engine,
+                      "interface-type", interface_type,
+                      "key", key,
+                      "value", value,
+                      NULL);
+
+  if (parent != NULL)
+    ide_object_append (parent, IDE_OBJECT (ret));
+
+  /* If we have a reload queued, just process it immediately so that
+   * there is some determinism in plugin loading.
+   */
+  if (ret->reload_handler != 0)
+    {
+      g_clear_handle_id (&ret->reload_handler, g_source_remove);
+      ide_extension_set_adapter_do_reload (ret);
+    }
+
+  return ret;
 }
 
 /**
@@ -710,6 +794,8 @@ ide_extension_set_adapter_new (IdeContext  *context,
  * Locates the extension owned by @plugin_info if such extension exists.
  *
  * Returns: (transfer none) (nullable): a #PeasExtension or %NULL
+ *
+ * Since: 3.32
  */
 PeasExtension *
 ide_extension_set_adapter_get_extension (IdeExtensionSetAdapter *self,
diff --git a/src/libide/plugins/ide-extension-set-adapter.h b/src/libide/plugins/ide-extension-set-adapter.h
index af9da2ed1..24708e56f 100644
--- a/src/libide/plugins/ide-extension-set-adapter.h
+++ b/src/libide/plugins/ide-extension-set-adapter.h
@@ -1,6 +1,6 @@
 /* ide-extension-set-adapter.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,20 +14,24 @@
  *
  * 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 <libpeas/peas.h>
+#if !defined (IDE_PLUGINS_INSIDE) && !defined (IDE_PLUGINS_COMPILATION)
+# error "Only <libide-plugins.h> can be included directly."
+#endif
 
-#include "ide-object.h"
-#include "ide-version-macros.h"
+#include <libpeas/peas.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_EXTENSION_SET_ADAPTER (ide_extension_set_adapter_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeExtensionSetAdapter, ide_extension_set_adapter, IDE, EXTENSION_SET_ADAPTER, 
IdeObject)
 
 typedef void (*IdeExtensionSetAdapterForeachFunc) (IdeExtensionSetAdapter *set,
@@ -35,37 +39,37 @@ typedef void (*IdeExtensionSetAdapterForeachFunc) (IdeExtensionSetAdapter *set,
                                                    PeasExtension          *extension,
                                                    gpointer                user_data);
 
-IDE_AVAILABLE_IN_ALL
-IdeExtensionSetAdapter *ide_extension_set_adapter_new                (IdeContext                        
*context,
+IDE_AVAILABLE_IN_3_32
+IdeExtensionSetAdapter *ide_extension_set_adapter_new                (IdeObject                         
*parent,
                                                                       PeasEngine                        
*engine,
                                                                       GType                              
interface_type,
                                                                       const gchar                       *key,
                                                                       const gchar                       
*value);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 PeasEngine             *ide_extension_set_adapter_get_engine         (IdeExtensionSetAdapter            
*self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GType                   ide_extension_set_adapter_get_interface_type (IdeExtensionSetAdapter            
*self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_extension_set_adapter_get_key            (IdeExtensionSetAdapter            
*self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_extension_set_adapter_set_key            (IdeExtensionSetAdapter            
*self,
                                                                       const gchar                       
*key);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar            *ide_extension_set_adapter_get_value          (IdeExtensionSetAdapter            
*self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_extension_set_adapter_set_value          (IdeExtensionSetAdapter            
*self,
                                                                       const gchar                       
*value);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 guint                   ide_extension_set_adapter_get_n_extensions   (IdeExtensionSetAdapter            
*self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_extension_set_adapter_foreach            (IdeExtensionSetAdapter            
*self,
                                                                       IdeExtensionSetAdapterForeachFunc  
foreach_func,
                                                                       gpointer                           
user_data);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void                    ide_extension_set_adapter_foreach_by_priority(IdeExtensionSetAdapter            
*self,
                                                                       IdeExtensionSetAdapterForeachFunc  
foreach_func,
                                                                       gpointer                           
user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 PeasExtension          *ide_extension_set_adapter_get_extension      (IdeExtensionSetAdapter            
*self,
                                                                       PeasPluginInfo                    
*plugin_info);
 
diff --git a/src/libide/plugins/ide-extension-util-private.h b/src/libide/plugins/ide-extension-util-private.h
new file mode 100644
index 000000000..248c14ada
--- /dev/null
+++ b/src/libide/plugins/ide-extension-util-private.h
@@ -0,0 +1,43 @@
+/* ide-extension-util.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <libpeas/peas.h>
+
+G_BEGIN_DECLS
+
+gboolean         ide_extension_util_can_use_plugin (PeasEngine     *engine,
+                                                    PeasPluginInfo *plugin_info,
+                                                    GType           interface_type,
+                                                    const gchar    *key,
+                                                    const gchar    *value,
+                                                    gint           *priority);
+PeasExtensionSet *ide_extension_set_new            (PeasEngine     *engine,
+                                                    GType           type,
+                                                    const gchar    *first_property,
+                                                    ...);
+PeasExtension    *ide_extension_new                (PeasEngine     *engine,
+                                                    PeasPluginInfo *plugin_info,
+                                                    GType           interface_type,
+                                                    const gchar    *first_property,
+                                                    ...);
+
+G_END_DECLS
diff --git a/src/libide/plugins/ide-extension-util.c b/src/libide/plugins/ide-extension-util.c
index 6d87d4f5a..bcd32b1cc 100644
--- a/src/libide/plugins/ide-extension-util.c
+++ b/src/libide/plugins/ide-extension-util.c
@@ -1,6 +1,6 @@
 /* ide-extension-util.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,16 +14,19 @@
  *
  * 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 "ide-extension-util"
 
 #include "config.h"
 
+#include <libide-core.h>
 #include <gobject/gvaluecollector.h>
 #include <stdlib.h>
 
-#include "plugins/ide-extension-util.h"
+#include "ide-extension-util-private.h"
 
 gboolean
 ide_extension_util_can_use_plugin (PeasEngine     *engine,
@@ -37,7 +40,8 @@ ide_extension_util_can_use_plugin (PeasEngine     *engine,
   g_autoptr(GSettings) settings = NULL;
 
   g_return_val_if_fail (plugin_info != NULL, FALSE);
-  g_return_val_if_fail (g_type_is_a (interface_type, G_TYPE_INTERFACE), FALSE);
+  g_return_val_if_fail (g_type_is_a (interface_type, G_TYPE_INTERFACE) ||
+                        g_type_is_a (interface_type, G_TYPE_OBJECT), FALSE);
   g_return_val_if_fail (priority != NULL, FALSE);
 
   *priority = 0;
@@ -47,7 +51,18 @@ ide_extension_util_can_use_plugin (PeasEngine     *engine,
    * information to do so.
    */
   if ((key != NULL) && (value == NULL))
-    return FALSE;
+    {
+      const gchar *found;
+
+      /* If the plugin has the key and its empty, or doesn't have the key,
+       * then we can assume it wants the equivalent of "*".
+       */
+      found = peas_plugin_info_get_external_data (plugin_info, key);
+      if (ide_str_empty0 (found))
+        return TRUE;
+
+      return FALSE;
+    }
 
   /*
    * If the plugin isn't loaded, then we shouldn't use it.
@@ -68,12 +83,15 @@ ide_extension_util_can_use_plugin (PeasEngine     *engine,
   if (key != NULL)
     {
       g_autofree gchar *priority_name = NULL;
+      g_autofree gchar *delimit = NULL;
       g_auto(GStrv) values_array = NULL;
       const gchar *values;
       const gchar *priority_value;
 
       values = peas_plugin_info_get_external_data (plugin_info, key);
-      values_array = g_strsplit (values ? values : "", ",", 0);
+      /* Canonicalize input (for both , and ;) */
+      delimit = g_strdelimit (g_strdup (values ? values : ""), ";,", ';');
+      values_array = g_strsplit (delimit, ";", 0);
 
       /* An empty value implies "*" to match anything */
       if (!values || g_strv_contains ((const gchar * const *)values_array, "*"))
@@ -224,6 +242,8 @@ collect_parameters (GType        type,
  * but looking at base-classes in addition to interface properties.
  *
  * Returns: (transfer full): a #PeasExtensionSet.
+ *
+ * Since: 3.32
  */
 PeasExtensionSet *
 ide_extension_set_new (PeasEngine     *engine,
@@ -252,7 +272,7 @@ ide_extension_new (PeasEngine     *engine,
   va_list args;
 
   g_return_val_if_fail (!engine || PEAS_IS_ENGINE (engine), NULL);
-  g_return_val_if_fail (G_TYPE_IS_INTERFACE (type), NULL);
+  g_return_val_if_fail (G_TYPE_IS_INTERFACE (type) || G_TYPE_IS_OBJECT (type), NULL);
 
   if (engine == NULL)
     engine = peas_engine_get_default ();
diff --git a/src/libide/plugins/libide-plugins.h b/src/libide/plugins/libide-plugins.h
new file mode 100644
index 000000000..1260890cd
--- /dev/null
+++ b/src/libide/plugins/libide-plugins.h
@@ -0,0 +1,34 @@
+/* libide-plugins.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_PLUGINS_INSIDE
+
+#include "ide-extension-adapter.h"
+#include "ide-extension-set-adapter.h"
+
+#undef IDE_PLUGINS_INSIDE
+
+G_END_DECLS
diff --git a/src/libide/plugins/meson.build b/src/libide/plugins/meson.build
index 07466addd..a33c528c9 100644
--- a/src/libide/plugins/meson.build
+++ b/src/libide/plugins/meson.build
@@ -1,20 +1,61 @@
-plugins_headers = [
+libide_plugins_header_subdir = join_paths(libide_header_subdir, 'plugins')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_plugins_public_headers = [
   'ide-extension-adapter.h',
   'ide-extension-set-adapter.h',
+  'libide-plugins.h',
+]
+
+libide_plugins_private_headers = [
+  'ide-extension-util-private.h',
 ]
 
-plugins_sources = [
+install_headers(libide_plugins_public_headers, subdir: libide_plugins_header_subdir)
+
+#
+# Sources
+#
+
+libide_plugins_public_sources = [
   'ide-extension-adapter.c',
   'ide-extension-set-adapter.c',
 ]
 
-plugins_private_sources = [
+libide_plugins_private_sources = [
   'ide-extension-util.c',
-  'ide-extension-util.h',
 ]
 
-libide_public_headers += files(plugins_headers)
-libide_public_sources += files(plugins_sources)
-libide_private_sources += files(plugins_private_sources)
+#
+# Library Definitions
+#
+
+libide_plugins_deps = [
+  libgio_dep,
+  libpeas_dep,
+  libdazzle_dep,
+
+  libide_core_dep,
+]
+
+libide_plugins = static_library('ide-plugins-' + libide_api_version,
+  libide_plugins_public_sources, libide_plugins_private_sources,
+   dependencies: libide_plugins_deps,
+         c_args: libide_args + release_args + ['-DIDE_PLUGINS_COMPILATION'],
+)
+
+libide_plugins_dep = declare_dependency(
+              sources: libide_plugins_private_headers,
+         dependencies: libide_plugins_deps,
+           link_whole: libide_plugins,
+  include_directories: include_directories('.'),
+)
 
-install_headers(plugins_headers, subdir: join_paths(libide_header_subdir, 'plugins'))
+gnome_builder_public_sources += files(libide_plugins_public_sources)
+gnome_builder_public_headers += files(libide_plugins_public_headers)
+gnome_builder_include_subdirs += libide_plugins_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-plugins.h', '-DIDE_PLUGINS_COMPILATION']
diff --git a/src/libide/projects/ide-doap-person.c b/src/libide/projects/ide-doap-person.c
new file mode 100644
index 000000000..7f386458d
--- /dev/null
+++ b/src/libide/projects/ide-doap-person.c
@@ -0,0 +1,184 @@
+/* ide-doap-person.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-doap-person"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-doap-person.h"
+
+struct _IdeDoapPerson
+{
+  GObject parent_instance;
+
+  gchar *email;
+  gchar *name;
+};
+
+G_DEFINE_TYPE (IdeDoapPerson, ide_doap_person, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_EMAIL,
+  PROP_NAME,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+IdeDoapPerson *
+ide_doap_person_new (void)
+{
+  return g_object_new (IDE_TYPE_DOAP_PERSON, NULL);
+}
+
+const gchar *
+ide_doap_person_get_name (IdeDoapPerson *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP_PERSON (self), NULL);
+
+  return self->name;
+}
+
+void
+ide_doap_person_set_name (IdeDoapPerson *self,
+                          const gchar   *name)
+{
+  g_return_if_fail (IDE_IS_DOAP_PERSON (self));
+
+  if (g_strcmp0 (self->name, name) != 0)
+    {
+      g_free (self->name);
+      self->name = g_strdup (name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]);
+    }
+}
+
+const gchar *
+ide_doap_person_get_email (IdeDoapPerson *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP_PERSON (self), NULL);
+
+  return self->email;
+}
+
+void
+ide_doap_person_set_email (IdeDoapPerson *self,
+                           const gchar   *email)
+{
+  g_return_if_fail (IDE_IS_DOAP_PERSON (self));
+
+  if (g_strcmp0 (self->email, email) != 0)
+    {
+      g_free (self->email);
+      self->email = g_strdup (email);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EMAIL]);
+    }
+}
+
+static void
+ide_doap_person_finalize (GObject *object)
+{
+  IdeDoapPerson *self = (IdeDoapPerson *)object;
+
+  g_clear_pointer (&self->email, g_free);
+  g_clear_pointer (&self->name, g_free);
+
+  G_OBJECT_CLASS (ide_doap_person_parent_class)->finalize (object);
+}
+
+static void
+ide_doap_person_get_property (GObject    *object,
+                              guint       prop_id,
+                              GValue     *value,
+                              GParamSpec *pspec)
+{
+  IdeDoapPerson *self = IDE_DOAP_PERSON (object);
+
+  switch (prop_id)
+    {
+    case PROP_EMAIL:
+      g_value_set_string (value, ide_doap_person_get_email (self));
+      break;
+
+    case PROP_NAME:
+      g_value_set_string (value, ide_doap_person_get_name (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_doap_person_set_property (GObject      *object,
+                              guint         prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  IdeDoapPerson *self = IDE_DOAP_PERSON (object);
+
+  switch (prop_id)
+    {
+    case PROP_EMAIL:
+      ide_doap_person_set_email (self, g_value_get_string (value));
+      break;
+
+    case PROP_NAME:
+      ide_doap_person_set_name (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_doap_person_class_init (IdeDoapPersonClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_doap_person_finalize;
+  object_class->get_property = ide_doap_person_get_property;
+  object_class->set_property = ide_doap_person_set_property;
+
+  properties [PROP_EMAIL] =
+    g_param_spec_string ("email",
+                         "Email",
+                         "The email of the person.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "The name of the person.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_doap_person_init (IdeDoapPerson *self)
+{
+}
diff --git a/src/libide/projects/ide-doap-person.h b/src/libide/projects/ide-doap-person.h
new file mode 100644
index 000000000..2d77305ad
--- /dev/null
+++ b/src/libide/projects/ide-doap-person.h
@@ -0,0 +1,49 @@
+/* ide-doap-person.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DOAP_PERSON (ide_doap_person_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeDoapPerson, ide_doap_person, IDE, DOAP_PERSON, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeDoapPerson *ide_doap_person_new       (void);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_doap_person_get_name  (IdeDoapPerson *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_doap_person_set_name  (IdeDoapPerson *self,
+                                          const gchar   *name);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_doap_person_get_email (IdeDoapPerson *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_doap_person_set_email (IdeDoapPerson *self,
+                                          const gchar   *email);
+
+G_END_DECLS
diff --git a/src/libide/projects/ide-doap.c b/src/libide/projects/ide-doap.c
new file mode 100644
index 000000000..542c80bc2
--- /dev/null
+++ b/src/libide/projects/ide-doap.c
@@ -0,0 +1,639 @@
+/* ide-doap.c
+ *
+ * Copyright 2015-2019 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 "ide-doap"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-doap.h"
+#include "xml-reader-private.h"
+
+/* TODO: We don't do any XMLNS checking or anything here. */
+
+struct _IdeDoap
+{
+  GObject    parent_instance;
+
+  gchar     *bug_database;
+  gchar     *category;
+  gchar     *description;
+  gchar     *download_page;
+  gchar     *homepage;;
+  gchar     *name;
+  gchar     *shortdesc;
+
+  GPtrArray *languages;
+  GList     *maintainers;
+};
+
+G_DEFINE_QUARK (ide_doap_error, ide_doap_error)
+G_DEFINE_TYPE (IdeDoap, ide_doap, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_BUG_DATABASE,
+  PROP_CATEGORY,
+  PROP_DESCRIPTION,
+  PROP_DOWNLOAD_PAGE,
+  PROP_HOMEPAGE,
+  PROP_LANGUAGES,
+  PROP_NAME,
+  PROP_SHORTDESC,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+IdeDoap *
+ide_doap_new (void)
+{
+  return g_object_new (IDE_TYPE_DOAP, NULL);
+}
+
+const gchar *
+ide_doap_get_name (IdeDoap *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP (self), NULL);
+
+  return self->name;
+}
+
+const gchar *
+ide_doap_get_shortdesc (IdeDoap *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP (self), NULL);
+
+  return self->shortdesc;
+}
+
+const gchar *
+ide_doap_get_description (IdeDoap *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP (self), NULL);
+
+  return self->description;
+}
+
+const gchar *
+ide_doap_get_bug_database (IdeDoap *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP (self), NULL);
+
+  return self->bug_database;
+}
+
+const gchar *
+ide_doap_get_download_page (IdeDoap *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP (self), NULL);
+
+  return self->download_page;
+}
+
+const gchar *
+ide_doap_get_homepage (IdeDoap *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP (self), NULL);
+
+  return self->homepage;
+}
+
+const gchar *
+ide_doap_get_category (IdeDoap *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP (self), NULL);
+
+  return self->category;
+}
+
+/**
+ * ide_doap_get_languages:
+ *
+ * Returns: (transfer none): a #GStrv.
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_doap_get_languages (IdeDoap *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP (self), NULL);
+
+  if (self->languages != NULL)
+    return (gchar **)self->languages->pdata;
+
+  return NULL;
+}
+
+static void
+ide_doap_set_bug_database (IdeDoap     *self,
+                           const gchar *bug_database)
+{
+  g_return_if_fail (IDE_IS_DOAP (self));
+
+  if (g_strcmp0 (self->bug_database, bug_database) != 0)
+    {
+      g_free (self->bug_database);
+      self->bug_database = g_strdup (bug_database);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUG_DATABASE]);
+    }
+}
+
+static void
+ide_doap_set_category (IdeDoap     *self,
+                       const gchar *category)
+{
+  g_return_if_fail (IDE_IS_DOAP (self));
+
+  if (g_strcmp0 (self->category, category) != 0)
+    {
+      g_free (self->category);
+      self->category = g_strdup (category);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CATEGORY]);
+    }
+}
+
+static void
+ide_doap_set_description (IdeDoap     *self,
+                          const gchar *description)
+{
+  g_return_if_fail (IDE_IS_DOAP (self));
+
+  if (g_strcmp0 (self->description, description) != 0)
+    {
+      g_free (self->description);
+      self->description = g_strdup (description);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DESCRIPTION]);
+    }
+}
+
+static void
+ide_doap_set_download_page (IdeDoap     *self,
+                            const gchar *download_page)
+{
+  g_return_if_fail (IDE_IS_DOAP (self));
+
+  if (g_strcmp0 (self->download_page, download_page) != 0)
+    {
+      g_free (self->download_page);
+      self->download_page = g_strdup (download_page);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DOWNLOAD_PAGE]);
+    }
+}
+
+static void
+ide_doap_set_homepage (IdeDoap     *self,
+                       const gchar *homepage)
+{
+  g_return_if_fail (IDE_IS_DOAP (self));
+
+  if (g_strcmp0 (self->homepage, homepage) != 0)
+    {
+      g_free (self->homepage);
+      self->homepage = g_strdup (homepage);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HOMEPAGE]);
+    }
+}
+
+static void
+ide_doap_set_name (IdeDoap     *self,
+                   const gchar *name)
+{
+  g_return_if_fail (IDE_IS_DOAP (self));
+
+  if (g_strcmp0 (self->name, name) != 0)
+    {
+      g_free (self->name);
+      self->name = g_strdup (name);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]);
+    }
+}
+
+static void
+ide_doap_set_shortdesc (IdeDoap     *self,
+                        const gchar *shortdesc)
+{
+  g_return_if_fail (IDE_IS_DOAP (self));
+
+  if (g_strcmp0 (self->shortdesc, shortdesc) != 0)
+    {
+      g_free (self->shortdesc);
+      self->shortdesc = g_strdelimit (g_strdup (shortdesc), "\n", ' ');
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHORTDESC]);
+    }
+}
+
+/**
+ * ide_doap_get_maintainers:
+ *
+ *
+ *
+ * Returns: (transfer none) (element-type IdeDoapPerson): a #GList of #IdeDoapPerson.
+ *
+ * Since: 3.32
+ */
+GList *
+ide_doap_get_maintainers (IdeDoap *self)
+{
+  g_return_val_if_fail (IDE_IS_DOAP (self), NULL);
+
+  return self->maintainers;
+}
+
+static void
+ide_doap_add_language (IdeDoap     *self,
+                       const gchar *language)
+{
+  g_return_if_fail (IDE_IS_DOAP (self));
+  g_return_if_fail (language != NULL);
+
+  if (self->languages == NULL)
+    {
+      self->languages = g_ptr_array_new_with_free_func (g_free);
+      g_ptr_array_add (self->languages, NULL);
+    }
+
+  g_assert (self->languages->len > 0);
+
+  g_ptr_array_index (self->languages, self->languages->len - 1) = g_strdup (language);
+  g_ptr_array_add (self->languages, NULL);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LANGUAGES]);
+}
+
+static void
+ide_doap_set_languages (IdeDoap  *self,
+                        gchar   **languages)
+{
+  gsize i;
+
+  g_return_if_fail (IDE_IS_DOAP (self));
+
+  if ((self->languages != NULL) && (self->languages->len > 0))
+    g_ptr_array_remove_range (self->languages, 0, self->languages->len);
+
+  g_object_freeze_notify (G_OBJECT (self));
+  for (i = 0; languages [i]; i++)
+    ide_doap_add_language (self, languages [i]);
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+static void
+ide_doap_finalize (GObject *object)
+{
+  IdeDoap *self = (IdeDoap *)object;
+
+  g_clear_pointer (&self->bug_database, g_free);
+  g_clear_pointer (&self->category, g_free);
+  g_clear_pointer (&self->description, g_free);
+  g_clear_pointer (&self->download_page, g_free);
+  g_clear_pointer (&self->homepage, g_free);
+  g_clear_pointer (&self->languages, g_ptr_array_unref);
+  g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->shortdesc, g_free);
+
+  g_list_free_full (self->maintainers, g_object_unref);
+  self->maintainers = NULL;
+
+  G_OBJECT_CLASS (ide_doap_parent_class)->finalize (object);
+}
+
+static void
+ide_doap_get_property (GObject    *object,
+                       guint       prop_id,
+                       GValue     *value,
+                       GParamSpec *pspec)
+{
+  IdeDoap *self = IDE_DOAP (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUG_DATABASE:
+      g_value_set_string (value, ide_doap_get_bug_database (self));
+      break;
+
+    case PROP_CATEGORY:
+      g_value_set_string (value, ide_doap_get_category (self));
+      break;
+
+    case PROP_DESCRIPTION:
+      g_value_set_string (value, ide_doap_get_description (self));
+      break;
+
+    case PROP_DOWNLOAD_PAGE:
+      g_value_set_string (value, ide_doap_get_download_page (self));
+      break;
+
+    case PROP_HOMEPAGE:
+      g_value_set_string (value, ide_doap_get_homepage (self));
+      break;
+
+    case PROP_LANGUAGES:
+      g_value_set_boxed (value, ide_doap_get_languages (self));
+      break;
+
+    case PROP_NAME:
+      g_value_set_string (value, ide_doap_get_name (self));
+      break;
+
+    case PROP_SHORTDESC:
+      g_value_set_string (value, ide_doap_get_shortdesc (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_doap_set_property (GObject      *object,
+                       guint         prop_id,
+                       const GValue *value,
+                       GParamSpec   *pspec)
+{
+  IdeDoap *self = IDE_DOAP (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUG_DATABASE:
+      ide_doap_set_bug_database (self, g_value_get_string (value));
+      break;
+
+    case PROP_CATEGORY:
+      ide_doap_set_category (self, g_value_get_string (value));
+      break;
+
+    case PROP_DESCRIPTION:
+      ide_doap_set_description (self, g_value_get_string (value));
+      break;
+
+    case PROP_DOWNLOAD_PAGE:
+      ide_doap_set_download_page (self, g_value_get_string (value));
+      break;
+
+    case PROP_HOMEPAGE:
+      ide_doap_set_homepage (self, g_value_get_string (value));
+      break;
+
+    case PROP_LANGUAGES:
+      ide_doap_set_languages (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_NAME:
+      ide_doap_set_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_SHORTDESC:
+      ide_doap_set_shortdesc (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_doap_class_init (IdeDoapClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_doap_finalize;
+  object_class->get_property = ide_doap_get_property;
+  object_class->set_property = ide_doap_set_property;
+
+  properties [PROP_BUG_DATABASE] =
+    g_param_spec_string ("bug-database",
+                         "Bug Database",
+                         "Bug Database",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CATEGORY] =
+    g_param_spec_string ("category",
+                         "Category",
+                         "Category",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DESCRIPTION] =
+    g_param_spec_string ("description",
+                         "Description",
+                         "Description",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_DOWNLOAD_PAGE] =
+    g_param_spec_string ("download-page",
+                         "Download Page",
+                         "Download Page",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HOMEPAGE] =
+    g_param_spec_string ("homepage",
+                         "Homepage",
+                         "Homepage",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LANGUAGES] =
+    g_param_spec_string ("languages",
+                         "Languages",
+                         "Languages",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_NAME] =
+    g_param_spec_string ("name",
+                         "Name",
+                         "Name",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_SHORTDESC] =
+    g_param_spec_string ("shortdesc",
+                         "Shortdesc",
+                         "Shortdesc",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_doap_init (IdeDoap *self)
+{
+}
+
+static gboolean
+ide_doap_parse_maintainer (IdeDoap   *self,
+                           XmlReader *reader)
+{
+  g_assert (IDE_IS_DOAP (self));
+  g_assert (XML_IS_READER (reader));
+
+  if (!xml_reader_read (reader))
+    return FALSE;
+
+  do
+    {
+      if (xml_reader_is_a_local (reader, "Person") && xml_reader_read (reader))
+        {
+          g_autoptr(IdeDoapPerson) person = ide_doap_person_new ();
+
+          do
+            {
+              if (xml_reader_is_a_local (reader, "name"))
+                {
+                  gchar *str;
+
+                  str = xml_reader_read_string (reader);
+                  ide_doap_person_set_name (person, str);
+                  g_free (str);
+                }
+              else if (xml_reader_is_a_local (reader, "mbox"))
+                {
+                  gchar *str;
+
+                  str = xml_reader_get_attribute (reader, "rdf:resource");
+                  if (str != NULL && str[0] != '\0' && g_str_has_prefix (str, "mailto:";))
+                    ide_doap_person_set_email (person, str + strlen ("mailto:";));
+                  g_free (str);
+                }
+            }
+          while (xml_reader_read_to_next (reader));
+
+          if (ide_doap_person_get_name (person) || ide_doap_person_get_email (person))
+            self->maintainers = g_list_append (self->maintainers, g_object_ref (person));
+        }
+    }
+  while (xml_reader_read_to_next (reader));
+
+  return TRUE;
+}
+
+static gboolean
+load_doap (IdeDoap       *self,
+           XmlReader     *reader,
+           GError       **error)
+{
+  if (!xml_reader_read_start_element (reader, "Project"))
+    {
+      g_set_error (error,
+                   IDE_DOAP_ERROR,
+                   IDE_DOAP_ERROR_INVALID_FORMAT,
+                   "Project element is missing from doap.");
+      return FALSE;
+    }
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  xml_reader_read (reader);
+
+  do
+    {
+      const gchar *element_name;
+
+      element_name = xml_reader_get_local_name (reader);
+
+      if (g_strcmp0 (element_name, "name") == 0 ||
+          g_strcmp0 (element_name, "shortdesc") == 0 ||
+          g_strcmp0 (element_name, "description") == 0)
+        {
+          gchar *str;
+
+          str = xml_reader_read_string (reader);
+          if (str != NULL)
+            g_object_set (self, element_name, g_strstrip (str), NULL);
+          g_free (str);
+        }
+      else if (g_strcmp0 (element_name, "category") == 0 ||
+               g_strcmp0 (element_name, "homepage") == 0 ||
+               g_strcmp0 (element_name, "download-page") == 0 ||
+               g_strcmp0 (element_name, "bug-database") == 0)
+        {
+          gchar *str;
+
+          str = xml_reader_get_attribute (reader, "rdf:resource");
+          if (str != NULL)
+            g_object_set (self, element_name, g_strstrip (str), NULL);
+          g_free (str);
+        }
+      else if (g_strcmp0 (element_name, "programming-language") == 0)
+        {
+          gchar *str;
+
+          str = xml_reader_read_string (reader);
+          if (str != NULL && str[0] != '\0')
+            ide_doap_add_language (self, g_strstrip (str));
+          g_free (str);
+        }
+      else if (g_strcmp0 (element_name, "maintainer") == 0)
+        {
+          if (!ide_doap_parse_maintainer (self, reader))
+            break;
+        }
+    }
+  while (xml_reader_read_to_next (reader));
+
+  g_object_thaw_notify (G_OBJECT (self));
+
+  return TRUE;
+}
+
+gboolean
+ide_doap_load_from_file (IdeDoap       *self,
+                         GFile         *file,
+                         GCancellable  *cancellable,
+                         GError       **error)
+{
+  g_autoptr(XmlReader) reader = NULL;
+
+  g_return_val_if_fail (IDE_IS_DOAP (self), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  reader = xml_reader_new ();
+
+  if (!xml_reader_load_from_file (reader, file, cancellable, error))
+    return FALSE;
+
+  return load_doap (self, reader, error);
+}
+
+gboolean
+ide_doap_load_from_data (IdeDoap       *self,
+                         const gchar   *data,
+                         gsize          length,
+                         GError       **error)
+{
+  g_autoptr(XmlReader) reader = NULL;
+
+  g_return_val_if_fail (IDE_IS_DOAP (self), FALSE);
+  g_return_val_if_fail (data != NULL, FALSE);
+
+  reader = xml_reader_new ();
+
+  if (!xml_reader_load_from_data (reader, (const gchar *)data, length, NULL, NULL))
+    return FALSE;
+
+  return load_doap (self, reader, error);
+}
diff --git a/src/libide/projects/ide-doap.h b/src/libide/projects/ide-doap.h
new file mode 100644
index 000000000..2e65d96e0
--- /dev/null
+++ b/src/libide/projects/ide-doap.h
@@ -0,0 +1,77 @@
+/* ide-doap.h
+ *
+ * Copyright 2015-2019 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
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-doap-person.h"
+
+G_BEGIN_DECLS
+
+#define IDE_DOAP_ERROR (ide_doap_error_quark())
+#define IDE_TYPE_DOAP  (ide_doap_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeDoap, ide_doap, IDE, DOAP, GObject)
+
+typedef enum
+{
+  IDE_DOAP_ERROR_INVALID_FORMAT = 1,
+} IdeDoapError;
+
+IDE_AVAILABLE_IN_3_32
+IdeDoap       *ide_doap_new               (void);
+IDE_AVAILABLE_IN_3_32
+GQuark         ide_doap_error_quark       (void);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_doap_load_from_file    (IdeDoap        *self,
+                                           GFile          *file,
+                                           GCancellable   *cancellable,
+                                           GError        **error);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_doap_load_from_data    (IdeDoap        *self,
+                                           const gchar    *data,
+                                           gsize           length,
+                                           GError        **error);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_doap_get_name          (IdeDoap        *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_doap_get_shortdesc     (IdeDoap        *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_doap_get_description   (IdeDoap        *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_doap_get_bug_database  (IdeDoap        *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_doap_get_download_page (IdeDoap        *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_doap_get_homepage      (IdeDoap        *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_doap_get_category      (IdeDoap        *self);
+IDE_AVAILABLE_IN_3_32
+gchar        **ide_doap_get_languages     (IdeDoap        *self);
+IDE_AVAILABLE_IN_3_32
+GList         *ide_doap_get_maintainers   (IdeDoap        *self);
+
+G_END_DECLS
diff --git a/src/libide/projects/ide-project-file.c b/src/libide/projects/ide-project-file.c
new file mode 100644
index 000000000..ebe18fa10
--- /dev/null
+++ b/src/libide/projects/ide-project-file.c
@@ -0,0 +1,617 @@
+/* ide-project-file.c
+ *
+ * Copyright 2018-2019 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 "ide-project-file"
+
+#include "config.h"
+
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-project.h"
+#include "ide-project-file.h"
+
+typedef struct
+{
+  GFile     *directory;
+  GFileInfo *info;
+  guint      checked_for_icon_override : 1;
+} IdeProjectFilePrivate;
+
+enum {
+  PROP_0,
+  PROP_DIRECTORY,
+  PROP_FILE,
+  PROP_INFO,
+  N_PROPS
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeProjectFile, ide_project_file, IDE_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static gchar *
+ide_project_file_repr (IdeObject *object)
+{
+  IdeProjectFile *self = (IdeProjectFile *)object;
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_assert (IDE_IS_PROJECT_FILE (self));
+
+  if (priv->info && priv->directory)
+    return g_strdup_printf ("%s name=\"%s\" directory=\"%s\"",
+                            G_OBJECT_TYPE_NAME (self),
+                            g_file_info_get_display_name (priv->info),
+                            g_file_peek_path (priv->directory));
+  else
+    return IDE_OBJECT_CLASS (ide_project_file_parent_class)->repr (object);
+}
+
+static void
+ide_project_file_dispose (GObject *object)
+{
+  IdeProjectFile *self = (IdeProjectFile *)object;
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_clear_object (&priv->directory);
+  g_clear_object (&priv->info);
+
+  G_OBJECT_CLASS (ide_project_file_parent_class)->dispose (object);
+}
+
+static void
+ide_project_file_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  IdeProjectFile *self = IDE_PROJECT_FILE (object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      g_value_set_object (value, ide_project_file_get_directory (self));
+      break;
+
+    case PROP_FILE:
+      g_value_take_object (value, ide_project_file_ref_file (self));
+      break;
+
+    case PROP_INFO:
+      g_value_set_object (value, ide_project_file_get_info (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_project_file_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  IdeProjectFile *self = IDE_PROJECT_FILE (object);
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      priv->directory = g_value_dup_object (value);
+      break;
+
+    case PROP_INFO:
+      priv->info = g_value_dup_object (value);
+      if (priv->info &&
+          g_file_info_has_attribute (priv->info, G_FILE_ATTRIBUTE_STANDARD_NAME))
+        break;
+      /* Fall-through */
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_project_file_class_init (IdeProjectFileClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_project_file_dispose;
+  object_class->get_property = ide_project_file_get_property;
+  object_class->set_property = ide_project_file_set_property;
+
+  i_object_class->repr = ide_project_file_repr;
+
+  properties [PROP_DIRECTORY] =
+    g_param_spec_object ("directory",
+                         "Directory",
+                         "The directory containing the file",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_INFO] =
+    g_param_spec_object ("info",
+                         "Info",
+                         "The file info the file",
+                         G_TYPE_FILE_INFO,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FILE] =
+    g_param_spec_object ("file",
+                         "File",
+                         "The file",
+                         G_TYPE_FILE,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_project_file_init (IdeProjectFile *self)
+{
+}
+
+/**
+ * ide_project_file_get_directory:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the project file.
+ *
+ * Returns: (transfer none): an #IdeProjectFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_project_file_get_directory (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  return priv->directory;
+}
+
+/**
+ * ide_project_file_ref_file:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the file for the #IdeProjectFile.
+ *
+ * Returns: (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_project_file_ref_file (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  return g_file_get_child (priv->directory, g_file_info_get_name (priv->info));
+}
+
+/**
+ * ide_project_file_get_info:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the #GFileInfo for the file. This combined with
+ * #IdeProjectFile:directory can be used to determine the underlying
+ * file, such as via #IdeProjectFile:file.
+ *
+ * Returns: (transfer none): a #GFileInfo
+ *
+ * Since: 3.32
+ */
+GFileInfo *
+ide_project_file_get_info (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  return priv->info;
+}
+
+/**
+ * ide_project_file_get_name:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the name for the file, which matches the encoding on disk.
+ *
+ * Returns: a string containing the name
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_project_file_get_name (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  return g_file_info_get_name (priv->info);
+}
+
+/**
+ * ide_project_file_get_display_name:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the display-name for the file, which should be shown to users.
+ *
+ * Returns: a string containing the display name
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_project_file_get_display_name (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  return g_file_info_get_display_name (priv->info);
+}
+
+/**
+ * ide_project_file_is_directory:
+ * @self: a #IdeProjectFile
+ *
+ * Checks if @self represents a directory. If ide_project_file_is_symlink() is
+ * %TRUE, this may still return %TRUE.
+ *
+ * Returns: %TRUE if @self is a directory, or symlink to a directory
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_project_file_is_directory (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), FALSE);
+
+  return g_file_info_get_file_type (priv->info) == G_FILE_TYPE_DIRECTORY;
+}
+
+
+/**
+ * ide_project_file_is_symlink:
+ * @self: a #IdeProjectFile
+ *
+ * Checks if @self represents a symlink.
+ *
+ * Returns: %TRUE if @self is a symlink
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_project_file_is_symlink (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), FALSE);
+
+  return g_file_info_get_is_symlink (priv->info);
+}
+
+gint
+ide_project_file_compare (IdeProjectFile *a,
+                          IdeProjectFile *b)
+{
+  GFileInfo *info_a = ide_project_file_get_info (a);
+  GFileInfo *info_b = ide_project_file_get_info (b);
+  const gchar *display_name_a = g_file_info_get_display_name (info_a);
+  const gchar *display_name_b = g_file_info_get_display_name (info_b);
+  gchar *casefold_a = NULL;
+  gchar *casefold_b = NULL;
+  gboolean ret;
+
+  casefold_a = g_utf8_collate_key_for_filename (display_name_a, -1);
+  casefold_b = g_utf8_collate_key_for_filename (display_name_b, -1);
+
+  ret = strcmp (casefold_a, casefold_b);
+
+  g_free (casefold_a);
+  g_free (casefold_b);
+
+  return ret;
+}
+
+gint
+ide_project_file_compare_directories_first (IdeProjectFile *a,
+                                            IdeProjectFile *b)
+{
+  GFileInfo *info_a = ide_project_file_get_info (a);
+  GFileInfo *info_b = ide_project_file_get_info (b);
+  GFileType file_type_a = g_file_info_get_file_type (info_a);
+  GFileType file_type_b = g_file_info_get_file_type (info_b);
+  gint dir_a = (file_type_a == G_FILE_TYPE_DIRECTORY);
+  gint dir_b = (file_type_b == G_FILE_TYPE_DIRECTORY);
+  gint ret;
+
+  ret = dir_b - dir_a;
+  if (ret == 0)
+    ret = ide_project_file_compare (a, b);
+
+  return ret;
+}
+
+/**
+ * ide_project_file_get_symbolic_icon:
+ * @self: a #IdeProjectFile
+ *
+ * Gets the symbolic icon to represent the file.
+ *
+ * Returns: (transfer none) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_project_file_get_symbolic_icon (IdeProjectFile *self)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+
+  /*
+   * We might want to override the symbolic icon based on an override
+   * icon we ship with Builder.
+   */
+  if (!priv->checked_for_icon_override)
+    {
+      const gchar *content_type;
+
+      priv->checked_for_icon_override = TRUE;
+
+      if ((content_type = g_file_info_get_content_type (priv->info)))
+        {
+          g_autoptr(GIcon) override = NULL;
+
+          if ((override = ide_g_content_type_get_symbolic_icon (content_type)))
+            g_file_info_set_symbolic_icon (priv->info, override);
+        }
+    }
+
+  return g_file_info_get_symbolic_icon (priv->info);
+}
+
+static void
+ide_project_file_list_children_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  GFile *parent = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GPtrArray) files = NULL;
+  g_autoptr(GPtrArray) ret = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_FILE (parent));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!(files = ide_g_file_get_children_finish (parent, result, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  IDE_PTR_ARRAY_SET_FREE_FUNC (files, g_object_unref);
+
+  ret = g_ptr_array_new_full (files->len, g_object_unref);
+
+  for (guint i = 0; i < files->len; i++)
+    {
+      GFileInfo *info = g_ptr_array_index (files, i);
+      IdeProjectFile *project_file;
+
+      project_file = g_object_new (IDE_TYPE_PROJECT_FILE,
+                                   "info", info,
+                                   "directory", parent,
+                                   NULL);
+      g_ptr_array_add (ret, g_steal_pointer (&project_file));
+    }
+
+  ide_task_return_pointer (task,
+                           g_steal_pointer (&ret),
+                           (GDestroyNotify)g_ptr_array_unref);
+}
+
+/**
+ * ide_project_file_list_children_async:
+ * @self: a #IdeProjectFile
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: callback to execute upon completion
+ * @user_data: user data for @callback
+ *
+ * List the children of @self.
+ *
+ * Call ide_project_file_list_children_finish() to get the result
+ * of this operation.
+ *
+ * Since: 3.32
+ */
+void
+ide_project_file_list_children_async (IdeProjectFile      *self,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) file = NULL;
+
+  g_assert (IDE_IS_PROJECT_FILE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_project_file_list_children_async);
+
+  file = ide_project_file_ref_file (self);
+
+  ide_g_file_get_children_async (file,
+                                 IDE_PROJECT_FILE_ATTRIBUTES,
+                                 G_FILE_QUERY_INFO_NONE,
+                                 G_PRIORITY_DEFAULT,
+                                 cancellable,
+                                 ide_project_file_list_children_cb,
+                                 g_steal_pointer (&task));
+}
+
+/**
+ * ide_project_file_list_children_finish:
+ * @self: a #IdeProjectFile
+ * @result: a #GAsyncResult
+ * @error: a location for a #GError or %NULL
+ *
+ * Completes an asynchronous request to
+ * ide_project_file_list_children_async().
+ *
+ * Returns: (transfer full) (element-type IdeProjectFile): a #GPtrArray
+ *   of #IdeProjectFile
+ *
+ * Since: 3.32
+ */
+GPtrArray *
+ide_project_file_list_children_finish (IdeProjectFile  *self,
+                                       GAsyncResult    *result,
+                                       GError         **error)
+{
+  GPtrArray *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ret);
+}
+
+static void
+ide_project_file_trash_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  IdeProject *project = (IdeProject *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_PROJECT (project));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_project_trash_file_finish (project, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+void
+ide_project_file_trash_async (IdeProjectFile      *self,
+                              GCancellable        *cancellable,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(IdeProject) project = NULL;
+  g_autoptr(GFile) file = NULL;
+
+  g_return_if_fail (IDE_IS_PROJECT_FILE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_project_file_trash_async);
+
+  context = ide_object_ref_context (IDE_OBJECT (self));
+  project = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_PROJECT);
+  file = ide_project_file_ref_file (self);
+
+  ide_project_trash_file_async (project,
+                                file,
+                                cancellable,
+                                ide_project_file_trash_cb,
+                                g_steal_pointer (&task));
+}
+
+gboolean
+ide_project_file_trash_finish (IdeProjectFile  *self,
+                               GAsyncResult    *result,
+                               GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_project_file_create_child:
+ * @self: a #IdeProjectFile
+ * @info: a #GFileInfo
+ *
+ * Creates a new child project file of @self.
+ *
+ * Returns: (transfer full): an #IdeProjectFile
+ *
+ * Since: 3.32
+ */
+IdeProjectFile *
+ide_project_file_create_child (IdeProjectFile *self,
+                               GFileInfo      *info)
+{
+  IdeProjectFilePrivate *priv = ide_project_file_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_PROJECT_FILE (self), NULL);
+  g_return_val_if_fail (G_IS_FILE_INFO (info), NULL);
+
+  return g_object_new (IDE_TYPE_PROJECT_FILE,
+                       "directory", priv->directory,
+                       "info", info,
+                       NULL);
+}
+
+/**
+ * ide_project_file_new:
+ * @directory: a #GFile
+ * @info: a #GFileInfo
+ *
+ * Creates a new project file for a child of @directory.
+ *
+ * Returns: (transfer full): an #IdeProjectFile
+ *
+ * Since: 3.32
+ */
+IdeProjectFile *
+ide_project_file_new (GFile     *directory,
+                      GFileInfo *info)
+{
+  g_return_val_if_fail (G_IS_FILE (directory), NULL);
+  g_return_val_if_fail (G_IS_FILE_INFO (info), NULL);
+
+  return g_object_new (IDE_TYPE_PROJECT_FILE,
+                       "directory", directory,
+                       "info", info,
+                       NULL);
+}
diff --git a/src/libide/projects/ide-project-file.h b/src/libide/projects/ide-project-file.h
new file mode 100644
index 000000000..36023e831
--- /dev/null
+++ b/src/libide/projects/ide-project-file.h
@@ -0,0 +1,103 @@
+/* ide-project-file.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-code.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PROJECT_FILE (ide_project_file_get_type())
+
+
+#define IDE_PROJECT_FILE_ATTRIBUTES \
+  G_FILE_ATTRIBUTE_STANDARD_NAME"," \
+  G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME"," \
+  G_FILE_ATTRIBUTE_STANDARD_FAST_CONTENT_TYPE"," \
+  G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON"," \
+  G_FILE_ATTRIBUTE_STANDARD_TYPE"," \
+  G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK"," \
+  G_FILE_ATTRIBUTE_ACCESS_CAN_READ"," \
+  G_FILE_ATTRIBUTE_ACCESS_CAN_RENAME"," \
+  G_FILE_ATTRIBUTE_ACCESS_CAN_TRASH
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeProjectFile, ide_project_file, IDE, PROJECT_FILE, IdeObject)
+
+struct _IdeProjectFileClass
+{
+  IdeObjectClass parent_class;
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeProjectFile *ide_project_file_new                       (GFile                *directory,
+                                                            GFileInfo            *info);
+IDE_AVAILABLE_IN_3_32
+GFile          *ide_project_file_get_directory             (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+GFileInfo      *ide_project_file_get_info                  (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+GFile          *ide_project_file_ref_file                  (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_project_file_get_display_name          (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_project_file_get_name                  (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_project_file_is_directory              (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_project_file_is_symlink                (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+gint            ide_project_file_compare_directories_first (IdeProjectFile       *a,
+                                                            IdeProjectFile       *b);
+IDE_AVAILABLE_IN_3_32
+gint            ide_project_file_compare                   (IdeProjectFile       *a,
+                                                            IdeProjectFile       *b);
+IDE_AVAILABLE_IN_3_32
+GIcon          *ide_project_file_get_symbolic_icon         (IdeProjectFile       *self);
+IDE_AVAILABLE_IN_3_32
+IdeProjectFile *ide_project_file_create_child              (IdeProjectFile       *self,
+                                                            GFileInfo            *info);
+IDE_AVAILABLE_IN_3_32
+void            ide_project_file_list_children_async       (IdeProjectFile       *self,
+                                                            GCancellable         *cancellable,
+                                                            GAsyncReadyCallback   callback,
+                                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+GPtrArray      *ide_project_file_list_children_finish      (IdeProjectFile       *self,
+                                                            GAsyncResult         *result,
+                                                            GError              **error);
+IDE_AVAILABLE_IN_3_32
+void            ide_project_file_trash_async               (IdeProjectFile       *self,
+                                                            GCancellable         *cancellable,
+                                                            GAsyncReadyCallback   callback,
+                                                            gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_project_file_trash_finish              (IdeProjectFile       *self,
+                                                            GAsyncResult         *result,
+                                                            GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/projects/ide-project-info.c b/src/libide/projects/ide-project-info.c
index 083f05dad..290e2b64b 100644
--- a/src/libide/projects/ide-project-info.c
+++ b/src/libide/projects/ide-project-info.c
@@ -1,6 +1,6 @@
 /* ide-project-info.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #ifndef _GNU_SOURCE
@@ -24,11 +26,10 @@
 
 #include "config.h"
 
-#include <dazzle.h>
 #include <glib/gi18n.h>
 #include <string.h>
 
-#include "projects/ide-project-info.h"
+#include "ide-project-info.h"
 
 /**
  * SECTION:ideprojectinfo:
@@ -37,12 +38,15 @@
  *
  * This class contains information about a project that can be loaded.
  * This information should be used to display a list of available projects.
+ *
+ * Since: 3.32
  */
 
 struct _IdeProjectInfo
 {
   GObject     parent_instance;
 
+  gchar      *id;
   IdeDoap    *doap;
   GDateTime  *last_modified_at;
   GFile      *directory;
@@ -51,7 +55,7 @@ struct _IdeProjectInfo
   gchar      *name;
   gchar      *description;
   gchar     **languages;
-  IdeVcsUri  *vcs_uri;
+  gchar      *vcs_uri;
 
   gint        priority;
 
@@ -67,6 +71,7 @@ enum {
   PROP_DIRECTORY,
   PROP_DOAP,
   PROP_FILE,
+  PROP_ID,
   PROP_IS_RECENT,
   PROP_LANGUAGES,
   PROP_LAST_MODIFIED_AT,
@@ -83,6 +88,8 @@ static GParamSpec *properties [LAST_PROP];
  *
  *
  * Returns: (nullable) (transfer none): An #IdeDoap or %NULL.
+ *
+ * Since: 3.32
  */
 IdeDoap *
 ide_project_info_get_doap (IdeProjectInfo *self)
@@ -107,6 +114,8 @@ ide_project_info_set_doap (IdeProjectInfo *self,
  * ide_project_info_get_languages:
  *
  * Returns: (transfer none): An array of language names.
+ *
+ * Since: 3.32
  */
 const gchar * const *
 ide_project_info_get_languages (IdeProjectInfo *self)
@@ -156,6 +165,8 @@ ide_project_info_set_priority (IdeProjectInfo *self,
  * This is the directory containing the project (if known).
  *
  * Returns: (nullable) (transfer none): a #GFile.
+ *
+ * Since: 3.32
  */
 GFile *
 ide_project_info_get_directory (IdeProjectInfo *self)
@@ -173,6 +184,8 @@ ide_project_info_get_directory (IdeProjectInfo *self)
  * This is the project file (such as configure.ac) of the project.
  *
  * Returns: (nullable) (transfer none): a #GFile.
+ *
+ * Since: 3.32
  */
 GFile *
 ide_project_info_get_file (IdeProjectInfo *self)
@@ -187,6 +200,8 @@ ide_project_info_get_file (IdeProjectInfo *self)
  *
  *
  * Returns: (transfer none) (nullable): a #GDateTime or %NULL.
+ *
+ * Since: 3.32
  */
 GDateTime *
 ide_project_info_get_last_modified_at (IdeProjectInfo *self)
@@ -210,7 +225,7 @@ ide_project_info_set_build_system_name (IdeProjectInfo *self,
 {
   g_return_if_fail (IDE_IS_PROJECT_INFO (self));
 
-  if (!dzl_str_equal0 (self->build_system_name, build_system_name))
+  if (!ide_str_equal0 (self->build_system_name, build_system_name))
     {
       g_free (self->build_system_name);
       self->build_system_name = g_strdup (build_system_name);
@@ -232,7 +247,7 @@ ide_project_info_set_description (IdeProjectInfo *self,
 {
   g_return_if_fail (IDE_IS_PROJECT_INFO (self));
 
-  if (!dzl_str_equal0 (self->description, description))
+  if (!ide_str_equal0 (self->description, description))
     {
       g_free (self->description);
       self->description = g_strdup (description);
@@ -254,7 +269,7 @@ ide_project_info_set_name (IdeProjectInfo *self,
 {
   g_return_if_fail (IDE_IS_PROJECT_INFO (self));
 
-  if (!dzl_str_equal0 (self->name, name))
+  if (!ide_str_equal0 (self->name, name))
     {
       g_free (self->name);
       self->name = g_strdup (name);
@@ -326,6 +341,7 @@ ide_project_info_finalize (GObject *object)
 {
   IdeProjectInfo *self = (IdeProjectInfo *)object;
 
+  g_clear_pointer (&self->id, g_free);
   g_clear_pointer (&self->last_modified_at, g_date_time_unref);
   g_clear_pointer (&self->build_system_name, g_free);
   g_clear_pointer (&self->description, g_free);
@@ -367,6 +383,10 @@ ide_project_info_get_property (GObject    *object,
       g_value_set_object (value, ide_project_info_get_file (self));
       break;
 
+    case PROP_ID:
+      g_value_set_string (value, ide_project_info_get_id (self));
+      break;
+
     case PROP_IS_RECENT:
       g_value_set_boolean (value, ide_project_info_get_is_recent (self));
       break;
@@ -388,7 +408,7 @@ ide_project_info_get_property (GObject    *object,
       break;
 
     case PROP_VCS_URI:
-      g_value_set_boxed (value, ide_project_info_get_vcs_uri (self));
+      g_value_set_string (value, ide_project_info_get_vcs_uri (self));
       break;
 
     default:
@@ -426,6 +446,10 @@ ide_project_info_set_property (GObject      *object,
       ide_project_info_set_file (self, g_value_get_object (value));
       break;
 
+    case PROP_ID:
+      ide_project_info_set_id (self, g_value_get_string (value));
+      break;
+
     case PROP_IS_RECENT:
       ide_project_info_set_is_recent (self, g_value_get_boolean (value));
       break;
@@ -447,7 +471,7 @@ ide_project_info_set_property (GObject      *object,
       break;
 
     case PROP_VCS_URI:
-      ide_project_info_set_vcs_uri (self, g_value_get_boxed (value));
+      ide_project_info_set_vcs_uri (self, g_value_get_string (value));
       break;
 
     default:
@@ -469,63 +493,70 @@ ide_project_info_class_init (IdeProjectInfoClass *klass)
                          "Build System name",
                          "Build System name",
                          NULL,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_DESCRIPTION] =
     g_param_spec_string ("description",
                          "Description",
                          "The project description.",
                          NULL,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ID] =
+    g_param_spec_string ("id",
+                         "Id",
+                         "The identifier for the project",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_NAME] =
     g_param_spec_string ("name",
                          "Name",
                          "The project name.",
                          NULL,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_DIRECTORY] =
     g_param_spec_object ("directory",
                          "Directory",
                          "The project directory.",
                          G_TYPE_FILE,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_DOAP] =
     g_param_spec_object ("doap",
                          "DOAP",
                          "A DOAP describing the project.",
                          IDE_TYPE_DOAP,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_FILE] =
     g_param_spec_object ("file",
                          "File",
                          "The toplevel project file.",
                          G_TYPE_FILE,
-                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_IS_RECENT] =
     g_param_spec_boolean ("is-recent",
                           "Is Recent",
                           "Is Recent",
                           FALSE,
-                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_LANGUAGES] =
     g_param_spec_boxed ("languages",
                         "Languages",
                         "Languages",
                         G_TYPE_STRV,
-                        (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_LAST_MODIFIED_AT] =
     g_param_spec_boxed ("last-modified-at",
                         "Last Modified At",
                         "Last Modified At",
                         G_TYPE_DATE_TIME,
-                        (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_PRIORITY] =
     g_param_spec_int ("priority",
@@ -534,14 +565,14 @@ ide_project_info_class_init (IdeProjectInfoClass *klass)
                       G_MININT,
                       G_MAXINT,
                       0,
-                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+                      (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   properties [PROP_VCS_URI] =
-    g_param_spec_boxed ("vcs-uri",
-                        "Vcs Uri",
-                        "The vcs uri of the project, in case it is not local",
-                        IDE_TYPE_VCS_URI,
-                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+    g_param_spec_string ("vcs-uri",
+                         "Vcs Uri",
+                         "The VCS URI of the project, in case it is not local",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, LAST_PROP, properties);
 }
@@ -566,6 +597,9 @@ ide_project_info_compare (IdeProjectInfo *info1,
   g_assert (IDE_IS_PROJECT_INFO (info1));
   g_assert (IDE_IS_PROJECT_INFO (info2));
 
+  if (info1 == info2)
+    return 0;
+
   prio1 = ide_project_info_get_priority (info1);
   prio2 = ide_project_info_get_priority (info2);
 
@@ -595,15 +629,15 @@ ide_project_info_compare (IdeProjectInfo *info1,
  * ide_project_info_get_vcs_uri:
  * @self: an #IdeProjectInfo
  *
- * Gets the #IdeVcsUri for the project info. This should be set with the
+ * Gets the VCS URI for the project info. This should be set with the
  * remote URI for the version control system. It can be used to clone the
  * project when activated from the greeter.
  *
  * Returns: (transfer none) (nullable): a #IdeVcsUri or %NULL
  *
- * Since: 3.28
+ * Since: 3.32
  */
-IdeVcsUri *
+const gchar *
 ide_project_info_get_vcs_uri (IdeProjectInfo *self)
 {
   g_return_val_if_fail (IDE_IS_PROJECT_INFO (self), NULL);
@@ -613,14 +647,114 @@ ide_project_info_get_vcs_uri (IdeProjectInfo *self)
 
 void
 ide_project_info_set_vcs_uri (IdeProjectInfo *self,
-                              IdeVcsUri      *vcs_uri)
+                              const gchar    *vcs_uri)
 {
   g_return_if_fail (IDE_IS_PROJECT_INFO (self));
 
-  if (self->vcs_uri != vcs_uri)
+  if (!ide_str_equal0 (self->vcs_uri, vcs_uri))
     {
-      g_clear_pointer (&self->vcs_uri, ide_vcs_uri_unref);
-      self->vcs_uri = ide_vcs_uri_ref (vcs_uri);
+      g_free (self->vcs_uri);
+      self->vcs_uri = g_strdup (vcs_uri);
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VCS_URI]);
     }
 }
+
+IdeProjectInfo *
+ide_project_info_new (void)
+{
+  return g_object_new (IDE_TYPE_PROJECT_INFO, NULL);
+}
+
+/**
+ * ide_project_info_equal:
+ * @self: a #IdeProjectInfo
+ * @other: a #IdeProjectInfo
+ *
+ * This function will check to see if information about @self and @other are
+ * similar enough that a request to open @other would instead activate
+ * @self. This is useful when a user tries to open the same project twice.
+ *
+ * However, some case is taken to ensure that things like the build system
+ * are the same so that a project may be opened twice with two build systems
+ * as is sometimes necessary when projects are porting to a new build
+ * system.
+ *
+ * Returns: %TRUE if @self and @other are the same project and similar
+ *   enough to be considered equal.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_project_info_equal (IdeProjectInfo *self,
+                        IdeProjectInfo *other)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_INFO (self), FALSE);
+  g_return_val_if_fail (IDE_IS_PROJECT_INFO (other), FALSE);
+
+  if (!self->file || !other->file ||
+      !g_file_equal (self->file, other->file))
+    {
+      if (!self->directory || !other->directory ||
+          !g_file_equal (self->directory, other->directory))
+        return FALSE;
+    }
+
+  /* build-system only set in one of the project-info?
+   * That's fine, we'll consider them the same to avoid over
+   * activating a second workbench
+   */
+  if ((!self->build_system_name && other->build_system_name) ||
+      (self->build_system_name && !other->build_system_name))
+    return TRUE;
+
+  return ide_str_equal0 (self->build_system_name, other->build_system_name);
+}
+
+const gchar *
+ide_project_info_get_id (IdeProjectInfo *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_INFO (self), NULL);
+
+  if (!self->id && self->directory)
+    self->id = g_file_get_basename (self->directory);
+
+  if (!self->id && self->file)
+    {
+      g_autoptr(GFile) parent = g_file_get_parent (self->file);
+      self->id = g_file_get_basename (parent);
+    }
+
+  if (!self->id && self->doap)
+    self->id = g_strdup (ide_doap_get_name (self->doap));
+
+  if (!self->id && self->vcs_uri)
+    {
+      const gchar *path = self->vcs_uri;
+
+      if (strstr (path, "//"))
+        path = strstr (path, "//") + 1;
+
+      if (strchr (path, '/'))
+        path = strchr (path, '/');
+      else if (strrchr (path, ':'))
+        path = strrchr (path, ':');
+
+      self->id = g_path_get_basename (path);
+    }
+
+  return self->id;
+}
+
+void
+ide_project_info_set_id (IdeProjectInfo *self,
+                         const gchar    *id)
+{
+  g_return_if_fail (IDE_IS_PROJECT_INFO (self));
+
+  if (!ide_str_equal0 (id, self->id))
+    {
+      g_free (self->id);
+      self->id = g_strdup (id);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ID]);
+    }
+}
diff --git a/src/libide/projects/ide-project-info.h b/src/libide/projects/ide-project-info.h
index e4b18661e..b47307b19 100644
--- a/src/libide/projects/ide-project-info.h
+++ b/src/libide/projects/ide-project-info.h
@@ -1,6 +1,6 @@
 /* ide-project-info.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 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
@@ -14,82 +14,96 @@
  *
  * 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>
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <gio/gio.h>
+#include <libide-core.h>
 
-#include "doap/ide-doap.h"
-#include "vcs/ide-vcs-uri.h"
+#include "ide-doap.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_PROJECT_INFO (ide_project_info_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeProjectInfo, ide_project_info, IDE, PROJECT_INFO, GObject)
 
-IDE_AVAILABLE_IN_ALL
-gint         ide_project_info_compare                (IdeProjectInfo  *info1,
-                                                      IdeProjectInfo  *info2);
-IDE_AVAILABLE_IN_ALL
-GFile        *ide_project_info_get_file              (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_ALL
-IdeDoap      *ide_project_info_get_doap              (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_ALL
-void          ide_project_info_set_doap              (IdeProjectInfo  *self,
-                                                      IdeDoap         *doap);
-IDE_AVAILABLE_IN_ALL
-const gchar  *ide_project_info_get_build_system_name (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_ALL
-const gchar  *ide_project_info_get_description       (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_ALL
-GFile        *ide_project_info_get_directory         (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_ALL
-gboolean      ide_project_info_get_is_recent         (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_ALL
-gint          ide_project_info_get_priority          (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_ALL
-GDateTime    *ide_project_info_get_last_modified_at  (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_ALL
-void          ide_project_info_set_last_modified_at  (IdeProjectInfo  *self,
-                                                      GDateTime       *modified_at);
-IDE_AVAILABLE_IN_ALL
-const gchar * const *
-              ide_project_info_get_languages         (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_ALL
-const gchar  *ide_project_info_get_name              (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_3_28
-IdeVcsUri    *ide_project_info_get_vcs_uri           (IdeProjectInfo  *self);
-IDE_AVAILABLE_IN_ALL
-void          ide_project_info_set_file              (IdeProjectInfo  *self,
-                                                      GFile           *file);
-IDE_AVAILABLE_IN_ALL
-void          ide_project_info_set_build_system_name (IdeProjectInfo  *self,
-                                                      const gchar     *build_system_name);
-IDE_AVAILABLE_IN_ALL
-void          ide_project_info_set_description       (IdeProjectInfo  *self,
-                                                      const gchar     *description);
-IDE_AVAILABLE_IN_ALL
-void          ide_project_info_set_directory         (IdeProjectInfo  *self,
-                                                      GFile           *directory);
-IDE_AVAILABLE_IN_ALL
-void          ide_project_info_set_is_recent         (IdeProjectInfo  *self,
-                                                      gboolean         is_recent);
-IDE_AVAILABLE_IN_ALL
-void          ide_project_info_set_languages         (IdeProjectInfo  *self,
-                                                      gchar          **languages);
-IDE_AVAILABLE_IN_ALL
-void          ide_project_info_set_name              (IdeProjectInfo  *self,
-                                                      const gchar     *name);
-IDE_AVAILABLE_IN_ALL
-void          ide_project_info_set_priority          (IdeProjectInfo  *self,
-                                                      gint             priority);
-IDE_AVAILABLE_IN_3_28
-void          ide_project_info_set_vcs_uri           (IdeProjectInfo  *self,
-                                                      IdeVcsUri       *uri);
+IDE_AVAILABLE_IN_3_32
+IdeProjectInfo      *ide_project_info_new                   (void);
+IDE_AVAILABLE_IN_3_32
+const gchar         *ide_project_info_get_id                (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_id                (IdeProjectInfo  *self,
+                                                             const gchar     *id);
+IDE_AVAILABLE_IN_3_32
+gint                 ide_project_info_compare               (IdeProjectInfo  *info1,
+                                                             IdeProjectInfo  *info2);
+IDE_AVAILABLE_IN_3_32
+gboolean             ide_project_info_equal                 (IdeProjectInfo  *self,
+                                                             IdeProjectInfo  *other);
+IDE_AVAILABLE_IN_3_32
+GFile               *ide_project_info_get_file              (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+IdeDoap             *ide_project_info_get_doap              (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_doap              (IdeProjectInfo  *self,
+                                                             IdeDoap         *doap);
+IDE_AVAILABLE_IN_3_32
+const gchar         *ide_project_info_get_build_system_name (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+const gchar         *ide_project_info_get_description       (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+GFile               *ide_project_info_get_directory         (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+gboolean             ide_project_info_get_is_recent         (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+gint                 ide_project_info_get_priority          (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+GDateTime           *ide_project_info_get_last_modified_at  (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_last_modified_at  (IdeProjectInfo  *self,
+                                                             GDateTime       *modified_at);
+IDE_AVAILABLE_IN_3_32
+const gchar * const *ide_project_info_get_languages         (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+const gchar         *ide_project_info_get_name              (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+const gchar         *ide_project_info_get_vcs_uri           (IdeProjectInfo  *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_file              (IdeProjectInfo  *self,
+                                                             GFile           *file);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_build_system_name (IdeProjectInfo  *self,
+                                                             const gchar     *build_system_name);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_description       (IdeProjectInfo  *self,
+                                                             const gchar     *description);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_directory         (IdeProjectInfo  *self,
+                                                             GFile           *directory);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_is_recent         (IdeProjectInfo  *self,
+                                                             gboolean         is_recent);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_languages         (IdeProjectInfo  *self,
+                                                             gchar          **languages);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_name              (IdeProjectInfo  *self,
+                                                             const gchar     *name);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_priority          (IdeProjectInfo  *self,
+                                                             gint             priority);
+IDE_AVAILABLE_IN_3_32
+void                 ide_project_info_set_vcs_uri           (IdeProjectInfo  *self,
+                                                             const gchar     *vcs_uri);
+
 
 G_END_DECLS
diff --git a/src/libide/projects/ide-project-template.c b/src/libide/projects/ide-project-template.c
new file mode 100644
index 000000000..ac95b5e8c
--- /dev/null
+++ b/src/libide/projects/ide-project-template.c
@@ -0,0 +1,188 @@
+/* ide-project-template.c
+ *
+ * Copyright 2015-2019 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 "ide-project-template"
+
+#include "config.h"
+
+#include "ide-project-template.h"
+
+G_DEFINE_INTERFACE (IdeProjectTemplate, ide_project_template, G_TYPE_OBJECT)
+
+static void
+ide_project_template_default_init (IdeProjectTemplateInterface *iface)
+{
+}
+
+gchar *
+ide_project_template_get_id (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_id (self);
+}
+
+gchar *
+ide_project_template_get_name (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_name (self);
+}
+
+gchar *
+ide_project_template_get_description (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_description (self);
+}
+
+/**
+ * ide_project_template_get_widget:
+ * @self: An #IdeProjectTemplate
+ *
+ * Get's the configuration widget for the template if there is one.
+ *
+ * Returns: (transfer none): a #GtkWidget.
+ *
+ * Since: 3.32
+ */
+GtkWidget *
+ide_project_template_get_widget (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_widget (self);
+}
+
+/**
+ * ide_project_template_get_languages:
+ * @self: an #IdeProjectTemplate
+ *
+ * Gets the list of languages that this template can support when generating
+ * the project.
+ *
+ * Returns: (transfer full): A newly allocated, NULL terminated list of
+ *   supported languages.
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_project_template_get_languages (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_languages (self);
+}
+
+gchar *
+ide_project_template_get_icon_name (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), NULL);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_icon_name (self);
+}
+
+/**
+ * ide_project_template_expand_async:
+ * @self: an #IdeProjectTemplate
+ * @params: (element-type utf8 GLib.Variant): A hashtable of template parameters.
+ * @cancellable: (nullable): a #GCancellable or %NULL.
+ * @callback: the callback for the asynchronous operation.
+ * @user_data: user data for @callback.
+ *
+ * Asynchronously requests expansion of the template.
+ *
+ * This may involve creating files and directories on disk as well as
+ * expanding files based on the contents of @params.
+ *
+ * It is expected that this method is only called once on an #IdeProjectTemplate.
+ *
+ * Since: 3.32
+ */
+void
+ide_project_template_expand_async (IdeProjectTemplate  *self,
+                                   GHashTable          *params,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_PROJECT_TEMPLATE (self));
+  g_return_if_fail (params != NULL);
+  g_return_if_fail (g_hash_table_contains (params, "name"));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_PROJECT_TEMPLATE_GET_IFACE (self)->expand_async (self, params, cancellable, callback, user_data);
+}
+
+gboolean
+ide_project_template_expand_finish (IdeProjectTemplate  *self,
+                                    GAsyncResult        *result,
+                                    GError             **error)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->expand_finish (self, result, error);
+}
+
+/**
+ * ide_project_template_get_priority:
+ * @self: a #IdeProjectTemplate
+ *
+ * Gets the priority of the template. This can be used to sort the templates
+ * in the "new project" view.
+ *
+ * Returns: the priority of the template
+ *
+ * Since: 3.32
+ */
+gint
+ide_project_template_get_priority (IdeProjectTemplate *self)
+{
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (self), 0);
+
+  if (IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_priority)
+    return IDE_PROJECT_TEMPLATE_GET_IFACE (self)->get_priority (self);
+
+  return 0;
+}
+
+gint
+ide_project_template_compare (IdeProjectTemplate *a,
+                              IdeProjectTemplate *b)
+{
+  gint ret;
+
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (a), 0);
+  g_return_val_if_fail (IDE_IS_PROJECT_TEMPLATE (b), 0);
+
+  ret = ide_project_template_get_priority (a) - ide_project_template_get_priority (b);
+
+  if (ret == 0)
+    {
+      g_autofree gchar *a_name = ide_project_template_get_name (a);
+      g_autofree gchar *b_name = ide_project_template_get_name (b);
+      ret = g_utf8_collate (a_name, b_name);
+    }
+
+  return ret;
+}
diff --git a/src/libide/projects/ide-project-template.h b/src/libide/projects/ide-project-template.h
new file mode 100644
index 000000000..91a342dcc
--- /dev/null
+++ b/src/libide/projects/ide-project-template.h
@@ -0,0 +1,86 @@
+/* ide-project-template.h
+ *
+ * Copyright 2015-2019 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
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_PROJECT_TEMPLATE (ide_project_template_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeProjectTemplate, ide_project_template, IDE, PROJECT_TEMPLATE, GObject)
+
+struct _IdeProjectTemplateInterface
+{
+  GTypeInterface parent;
+
+  gchar      *(*get_id)          (IdeProjectTemplate   *self);
+  gchar      *(*get_name)        (IdeProjectTemplate   *self);
+  gchar      *(*get_description) (IdeProjectTemplate   *self);
+  GtkWidget  *(*get_widget)      (IdeProjectTemplate   *self);
+  gchar     **(*get_languages)   (IdeProjectTemplate   *self);
+  gchar      *(*get_icon_name)   (IdeProjectTemplate   *self);
+  void        (*expand_async)    (IdeProjectTemplate   *self,
+                                  GHashTable           *params,
+                                  GCancellable         *cancellable,
+                                  GAsyncReadyCallback   callback,
+                                  gpointer              user_data);
+  gboolean    (*expand_finish)   (IdeProjectTemplate   *self,
+                                  GAsyncResult         *result,
+                                  GError              **error);
+  gint        (*get_priority)    (IdeProjectTemplate   *self);
+};
+
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_project_template_get_id          (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+gint        ide_project_template_get_priority    (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_project_template_get_name        (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_project_template_get_description (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+GtkWidget  *ide_project_template_get_widget      (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+gchar     **ide_project_template_get_languages   (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_project_template_get_icon_name   (IdeProjectTemplate  *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_project_template_expand_async    (IdeProjectTemplate   *self,
+                                                  GHashTable           *params,
+                                                  GCancellable         *cancellable,
+                                                  GAsyncReadyCallback   callback,
+                                                  gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_project_template_expand_finish   (IdeProjectTemplate   *self,
+                                                  GAsyncResult         *result,
+                                                  GError              **error);
+IDE_AVAILABLE_IN_3_32
+gint        ide_project_template_compare         (IdeProjectTemplate   *a,
+                                                  IdeProjectTemplate   *b);
+
+G_END_DECLS
diff --git a/src/libide/projects/ide-project-tree-addin.c b/src/libide/projects/ide-project-tree-addin.c
index 4883ef5de..7289dd94c 100644
--- a/src/libide/projects/ide-project-tree-addin.c
+++ b/src/libide/projects/ide-project-tree-addin.c
@@ -1,6 +1,6 @@
 /* ide-project-tree-addin.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -18,11 +18,11 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-project-tree-addin"
 
-#include "projects/ide-project-tree-addin.h"
+#include "config.h"
+
+#include "ide-project-tree-addin.h"
 
 /**
  * SECTION:ide-project-tree-addin
diff --git a/src/libide/projects/ide-project-tree-addin.h b/src/libide/projects/ide-project-tree-addin.h
index cb38ce583..33412fb99 100644
--- a/src/libide/projects/ide-project-tree-addin.h
+++ b/src/libide/projects/ide-project-tree-addin.h
@@ -1,6 +1,6 @@
 /* ide-project-tree-addin.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -21,8 +21,7 @@
 #pragma once
 
 #include <dazzle.h>
-
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/libide/projects/ide-project.c b/src/libide/projects/ide-project.c
index 8565a069f..7b7b2ebfb 100644
--- a/src/libide/projects/ide-project.c
+++ b/src/libide/projects/ide-project.c
@@ -1,6 +1,6 @@
 /* ide-project.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-project"
@@ -21,30 +23,15 @@
 #include "config.h"
 
 #include <glib/gi18n.h>
+#include <libide-code.h>
+
+#include "ide-buffer-private.h"
 
-#include "ide-context.h"
-#include "ide-debug.h"
-
-#include "application/ide-application.h"
-#include "buffers/ide-buffer.h"
-#include "buffers/ide-buffer-manager.h"
-#include "files/ide-file.h"
-#include "projects/ide-project-item.h"
-#include "projects/ide-project.h"
-#include "subprocess/ide-subprocess.h"
-#include "subprocess/ide-subprocess-launcher.h"
-#include "util/ide-flatpak.h"
-#include "vcs/ide-vcs.h"
-#include "threading/ide-task.h"
+#include "ide-project.h"
 
 struct _IdeProject
 {
-  IdeObject       parent_instance;
-
-  GRWLock         rw_lock;
-  IdeProjectItem *root;
-  gchar          *name;
-  gchar          *id;
+  IdeObject parent_instance;
 };
 
 typedef struct
@@ -54,248 +41,19 @@ typedef struct
   IdeBuffer *buffer;
 } RenameFile;
 
-G_DEFINE_TYPE (IdeProject, ide_project, IDE_TYPE_OBJECT)
-
-enum {
-  PROP_0,
-  PROP_ID,
-  PROP_NAME,
-  PROP_ROOT,
-  LAST_PROP
-};
-
 enum {
   FILE_RENAMED,
   FILE_TRASHED,
-  LAST_SIGNAL
+  N_SIGNALS
 };
 
-static GParamSpec *properties [LAST_PROP];
-static guint signals [LAST_SIGNAL];
-
-void
-ide_project_reader_lock (IdeProject *self)
-{
-  g_return_if_fail (IDE_IS_PROJECT (self));
-
-  g_rw_lock_reader_lock (&self->rw_lock);
-}
-
-void
-ide_project_reader_unlock (IdeProject *self)
-{
-  g_return_if_fail (IDE_IS_PROJECT (self));
-
-  g_rw_lock_reader_unlock (&self->rw_lock);
-}
-
-void
-ide_project_writer_lock (IdeProject *self)
-{
-  g_return_if_fail (IDE_IS_PROJECT (self));
-
-  g_rw_lock_writer_lock (&self->rw_lock);
-}
-
-void
-ide_project_writer_unlock (IdeProject *self)
-{
-  g_return_if_fail (IDE_IS_PROJECT (self));
-
-  g_rw_lock_writer_unlock (&self->rw_lock);
-}
-
-/**
- * ide_project_create_id:
- * @name: the name of the project
- *
- * Escapes the project name into something suitable using as an id.
- * This can be uesd to determine the directory name when the project
- * name should be used.
- *
- * Returns: (transfer full): a new string
- *
- * Since: 3.28
- */
-gchar *
-ide_project_create_id (const gchar *name)
-{
-  g_return_val_if_fail (name != NULL, NULL);
-
-  return g_strdelimit (g_strdup (name), " /|<>\n\t", '-');
-}
-
-const gchar *
-ide_project_get_id (IdeProject *self)
-{
-  g_return_val_if_fail (IDE_IS_PROJECT (self), NULL);
-
-  return self->id;
-}
-
-const gchar *
-ide_project_get_name (IdeProject *self)
-{
-  g_return_val_if_fail (IDE_IS_PROJECT (self), NULL);
-
-  return self->name;
-}
-
-void
-_ide_project_set_name (IdeProject  *self,
-                       const gchar *name)
-{
-  g_return_if_fail (IDE_IS_PROJECT (self));
-
-  if (self->name != name)
-    {
-      g_free (self->name);
-      self->name = g_strdup (name);
-      self->id = ide_project_create_id (name);
-      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_NAME]);
-    }
-}
-
-/**
- * ide_project_get_root:
- *
- * Retrieves the root item of the project tree.
- *
- * You must be holding the reader lock while calling and using the result of
- * this function. Other thread may be accessing or modifying the tree without
- * your knowledge. See ide_project_reader_lock() and ide_project_reader_unlock()
- * for more information.
- *
- * If you need to modify the tree, you must hold a writer lock that has been
- * acquired with ide_project_writer_lock() and released with
- * ide_project_writer_unlock() when you are no longer modifiying the tree.
- *
- * Returns: (transfer none): An #IdeProjectItem.
- */
-IdeProjectItem *
-ide_project_get_root (IdeProject *self)
-{
-  g_return_val_if_fail (IDE_IS_PROJECT (self),  NULL);
-
-  return self->root;
-}
-
-static void
-ide_project_set_root (IdeProject     *self,
-                      IdeProjectItem *root)
-{
-  g_autoptr(IdeProjectItem) allocated = NULL;
-  IdeContext *context;
-
-  g_return_if_fail (IDE_IS_PROJECT (self));
-  g_return_if_fail (!root || IDE_IS_PROJECT_ITEM (root));
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-
-  if (!root)
-    {
-      allocated = g_object_new (IDE_TYPE_PROJECT_ITEM,
-                                "context", context,
-                                NULL);
-      root = allocated;
-    }
-
-  if (g_set_object (&self->root, root))
-    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ROOT]);
-}
-
-static void
-ide_project_finalize (GObject *object)
-{
-  IdeProject *self = (IdeProject *)object;
-
-  g_clear_object (&self->root);
-  g_clear_pointer (&self->name, g_free);
-  g_rw_lock_clear (&self->rw_lock);
-
-  G_OBJECT_CLASS (ide_project_parent_class)->finalize (object);
-}
-
-static void
-ide_project_get_property (GObject    *object,
-                          guint       prop_id,
-                          GValue     *value,
-                          GParamSpec *pspec)
-{
-  IdeProject *self = IDE_PROJECT (object);
-
-  switch (prop_id)
-    {
-    case PROP_ID:
-      g_value_set_string (value, ide_project_get_id (self));
-      break;
-
-    case PROP_NAME:
-      g_value_set_string (value, ide_project_get_name (self));
-      break;
-
-    case PROP_ROOT:
-      g_value_set_object (value, ide_project_get_root (self));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
-
-static void
-ide_project_set_property (GObject      *object,
-                          guint         prop_id,
-                          const GValue *value,
-                          GParamSpec   *pspec)
-{
-  IdeProject *self = IDE_PROJECT (object);
+G_DEFINE_TYPE (IdeProject, ide_project, IDE_TYPE_OBJECT)
 
-  switch (prop_id)
-    {
-    case PROP_ROOT:
-      ide_project_set_root (self, g_value_get_object (value));
-      break;
-
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-    }
-}
+static guint signals [N_SIGNALS];
 
 static void
 ide_project_class_init (IdeProjectClass *klass)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-
-  object_class->finalize = ide_project_finalize;
-  object_class->get_property = ide_project_get_property;
-  object_class->set_property = ide_project_set_property;
-
-  properties [PROP_ID] =
-    g_param_spec_string ("id",
-                         "ID",
-                         "The unique project identifier.",
-                         NULL,
-                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_NAME] =
-    g_param_spec_string ("name",
-                         "Name",
-                         "The name of the project.",
-                         NULL,
-                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  properties [PROP_ROOT] =
-    g_param_spec_object ("root",
-                         "Root",
-                         "The root object for the project.",
-                         IDE_TYPE_PROJECT_ITEM,
-                         (G_PARAM_READWRITE |
-                          G_PARAM_CONSTRUCT_ONLY |
-                          G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_properties (object_class, LAST_PROP, properties);
-
   signals [FILE_RENAMED] =
     g_signal_new ("file-renamed",
                   G_TYPE_FROM_CLASS (klass),
@@ -314,7 +72,31 @@ ide_project_class_init (IdeProjectClass *klass)
 static void
 ide_project_init (IdeProject *self)
 {
-  g_rw_lock_init (&self->rw_lock);
+}
+
+/**
+ * ide_project_from_context:
+ * @context: #IdeContext
+ *
+ * Gets the project for an #IdeContext.
+ *
+ * Returns: (transfer none): an #IdeProject
+ *
+ * Since: 3.32
+ */
+IdeProject *
+ide_project_from_context (IdeContext *context)
+{
+  IdeProject *self;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  /* Return borrowed reference */
+  self = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_PROJECT);
+  g_object_unref (self);
+
+  return self;
 }
 
 static void
@@ -368,11 +150,10 @@ ide_project_rename_file_worker (IdeTask      *task,
   IdeProject *self = source_object;
   g_autofree gchar *path = NULL;
   g_autoptr(GFile) parent = NULL;
+  g_autoptr(GFile) workdir = NULL;
   g_autoptr(GError) error = NULL;
   RenameFile *op = task_data;
   IdeContext *context;
-  IdeVcs *vcs;
-  GFile *workdir;
 
   g_assert (IDE_IS_PROJECT (self));
   g_assert (op != NULL);
@@ -381,8 +162,7 @@ ide_project_rename_file_worker (IdeTask      *task,
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  workdir = ide_context_ref_workdir (context);
   path = g_file_get_relative_path (workdir, op->new_file);
 
 #ifdef IDE_ENABLE_TRACE
@@ -436,19 +216,17 @@ ide_project_rename_buffer_save_cb (GObject      *object,
                                    GAsyncResult *result,
                                    gpointer      user_data)
 {
-  IdeBufferManager *bufmgr = (IdeBufferManager *)object;
+  IdeBuffer *buffer = (IdeBuffer *)object;
   g_autoptr(IdeTask) task = user_data;
-  g_autoptr(IdeFile) file = NULL;
   g_autoptr(GError) error = NULL;
-  IdeContext *context;
   RenameFile *rf;
 
   g_assert (IDE_IS_MAIN_THREAD ());
-  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+  g_assert (IDE_IS_BUFFER (buffer));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
 
-  if (!ide_buffer_manager_save_file_finish (bufmgr, result, &error))
+  if (!ide_buffer_save_file_finish (buffer, result, &error))
     {
       ide_task_return_error (task, g_steal_pointer (&error));
       return;
@@ -464,9 +242,7 @@ ide_project_rename_buffer_save_cb (GObject      *object,
    * Change the filename in the buffer so that the user doesn't continue
    * to edit the file under the old name.
    */
-  context = ide_object_get_context (IDE_OBJECT (bufmgr));
-  file = ide_file_new (context, rf->new_file);
-  ide_buffer_set_file (rf->buffer, file);
+  _ide_buffer_set_file (rf->buffer, rf->new_file);
 
   ide_task_run_in_thread (task, ide_project_rename_file_worker);
 }
@@ -496,7 +272,7 @@ ide_project_rename_file_async (IdeProject          *self,
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  bufmgr = ide_context_get_buffer_manager (context);
+  bufmgr = ide_buffer_manager_from_context (context);
   buffer = ide_buffer_manager_find_buffer (bufmgr, orig_file);
 
   op = g_slice_new0 (RenameFile);
@@ -511,22 +287,18 @@ ide_project_rename_file_async (IdeProject          *self,
    */
   if (buffer != NULL)
     {
-      g_autoptr(IdeFile) from = ide_file_new (context, orig_file);
-      g_autoptr(IdeFile) to = ide_file_new (context, new_file);
-
       if (gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (buffer)))
         {
-          ide_buffer_manager_save_file_async (bufmgr,
-                                              buffer,
-                                              from,
-                                              NULL,
-                                              NULL,
-                                              ide_project_rename_buffer_save_cb,
-                                              g_steal_pointer (&task));
+          ide_buffer_save_file_async (buffer,
+                                      orig_file,
+                                      NULL,
+                                      NULL,
+                                      ide_project_rename_buffer_save_cb,
+                                      g_steal_pointer (&task));
           return;
         }
 
-      ide_buffer_set_file (buffer, to);
+      _ide_buffer_set_file (buffer, new_file);
     }
 
   ide_task_run_in_thread (task, ide_project_rename_file_worker);
@@ -622,9 +394,8 @@ ide_project_trash_file_async (IdeProject          *self,
                               gpointer             user_data)
 {
   g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) workdir = NULL;
   IdeContext *context;
-  IdeVcs *vcs;
-  GFile *workdir;
 
   IDE_ENTRY;
 
@@ -632,8 +403,7 @@ ide_project_trash_file_async (IdeProject          *self,
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  workdir = ide_context_ref_workdir (context);
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_project_trash_file_async);
diff --git a/src/libide/projects/ide-project.h b/src/libide/projects/ide-project.h
index 90aaf9a7d..ebf2b28b0 100644
--- a/src/libide/projects/ide-project.h
+++ b/src/libide/projects/ide-project.h
@@ -1,6 +1,6 @@
 /* ide-project.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2018-2019 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
@@ -14,59 +14,43 @@
  *
  * 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 "ide-version-macros.h"
-
-#include "ide-object.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_PROJECT (ide_project_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeProject, ide_project, IDE, PROJECT, IdeObject)
 
-IDE_AVAILABLE_IN_3_28
-gchar           *ide_project_create_id          (const gchar          *name);
-IDE_AVAILABLE_IN_ALL
-IdeProjectItem  *ide_project_get_root           (IdeProject           *self);
-IDE_AVAILABLE_IN_ALL
-const gchar     *ide_project_get_name           (IdeProject           *self);
-IDE_AVAILABLE_IN_ALL
-const gchar     *ide_project_get_id             (IdeProject           *self);
-IDE_AVAILABLE_IN_ALL
-void             ide_project_reader_lock        (IdeProject           *self);
-IDE_AVAILABLE_IN_ALL
-void             ide_project_reader_unlock      (IdeProject           *self);
-IDE_AVAILABLE_IN_ALL
-void             ide_project_writer_lock        (IdeProject           *self);
-IDE_AVAILABLE_IN_ALL
-void             ide_project_writer_unlock      (IdeProject           *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
+IdeProject      *ide_project_from_context       (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
 void             ide_project_rename_file_async  (IdeProject           *self,
                                                  GFile                *orig_file,
                                                  GFile                *new_file,
                                                  GCancellable         *cancellable,
                                                  GAsyncReadyCallback   callback,
                                                  gpointer              user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean         ide_project_rename_file_finish (IdeProject           *self,
                                                  GAsyncResult         *result,
                                                  GError              **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void             ide_project_trash_file_async   (IdeProject           *self,
                                                  GFile                *file,
                                                  GCancellable         *cancellable,
                                                  GAsyncReadyCallback   callback,
                                                  gpointer              user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean         ide_project_trash_file_finish  (IdeProject           *self,
                                                  GAsyncResult         *result,
                                                  GError              **error);
-void             _ide_project_set_name          (IdeProject           *project,
-                                                 const gchar          *name) G_GNUC_INTERNAL;
 
 G_END_DECLS
diff --git a/src/libide/projects/ide-projects-global.c b/src/libide/projects/ide-projects-global.c
new file mode 100644
index 000000000..d762d072c
--- /dev/null
+++ b/src/libide/projects/ide-projects-global.c
@@ -0,0 +1,132 @@
+/* ide-projects-global.c
+ *
+ * Copyright 2018-2019 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 "ide-projects-global"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-io.h>
+
+#include "ide-projects-global.h"
+
+static GSettings *g_settings;
+static gchar *projects_directory;
+
+static void
+on_projects_directory_changed_cb (GSettings   *settings,
+                                  const gchar *key,
+                                  gpointer     user_data)
+{
+  g_assert (G_IS_SETTINGS (settings));
+  g_assert (key != NULL);
+
+  g_clear_pointer (&projects_directory, g_free);
+}
+
+/**
+ * ide_get_projects_dir:
+ *
+ * Gets the directory to store projects within.
+ *
+ * First, this checks GSettings for a directory. If that directory exists,
+ * it is returned.
+ *
+ * If not, it then checks for the non-translated name "Projects" in the
+ * users home directory. If it exists, that is returned.
+ *
+ * If that does not exist, and a GSetting path existed, but was non-existant
+ * that is returned.
+ *
+ * If the GSetting was empty, the translated name "Projects" is returned.
+ *
+ * Returns: (not nullable) (transfer full): a #GFile
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_get_projects_dir (void)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+
+  if G_UNLIKELY (g_settings == NULL)
+    {
+      g_settings = g_settings_new ("org.gnome.builder");
+      g_signal_connect (g_settings,
+                        "changed::projects-directory",
+                        G_CALLBACK (on_projects_directory_changed_cb),
+                        NULL);
+    }
+
+  if G_UNLIKELY (projects_directory == NULL)
+    {
+      g_autofree gchar *dir = g_settings_get_string (g_settings, "projects-directory");
+      g_autofree gchar *expanded = ide_path_expand (dir);
+      g_autofree gchar *projects = NULL;
+      g_autofree gchar *translated = NULL;
+
+      if (g_file_test (expanded, G_FILE_TEST_IS_DIR))
+        {
+          projects_directory = g_steal_pointer (&expanded);
+          goto completed;
+        }
+
+      projects = g_build_filename (g_get_home_dir (), "Projects", NULL);
+
+      if (g_file_test (projects, G_FILE_TEST_IS_DIR))
+        {
+          projects_directory = g_steal_pointer (&projects);
+          goto completed;
+        }
+
+      if (!ide_str_empty0 (dir) && !ide_str_empty0 (expanded))
+        {
+          projects_directory = g_steal_pointer (&expanded);
+          goto completed;
+        }
+
+      translated = g_build_filename (g_get_home_dir (), _("Projects"), NULL);
+      projects_directory = g_steal_pointer (&translated);
+    }
+
+completed:
+
+  return projects_directory;
+}
+
+/**
+ * ide_create_project_id:
+ * @name: the name of the project
+ *
+ * Escapes the project name into something suitable using as an id.
+ * This can be uesd to determine the directory name when the project
+ * name should be used.
+ *
+ * Returns: (transfer full): a new string
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_create_project_id (const gchar *name)
+{
+  g_return_val_if_fail (name != NULL, NULL);
+
+  return g_strdelimit (g_strdup (name), " /|<>\n\t", '-');
+}
diff --git a/src/libide/projects/ide-projects-global.h b/src/libide/projects/ide-projects-global.h
new file mode 100644
index 000000000..f32401be4
--- /dev/null
+++ b/src/libide/projects/ide-projects-global.h
@@ -0,0 +1,36 @@
+/* ide-projects-global.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_get_projects_dir  (void);
+IDE_AVAILABLE_IN_3_32
+gchar       *ide_create_project_id (const gchar *name);
+
+G_END_DECLS
diff --git a/src/libide/projects/ide-recent-projects.c b/src/libide/projects/ide-recent-projects.c
index 27cc8fae8..53ccbfca5 100644
--- a/src/libide/projects/ide-recent-projects.c
+++ b/src/libide/projects/ide-recent-projects.c
@@ -1,6 +1,6 @@
 /* ide-recent-projects.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-recent-projects"
@@ -22,10 +24,9 @@
 
 #include <glib/gi18n.h>
 #include <gtk/gtk.h>
+#include <libide-core.h>
 
-#include "ide-global.h"
-
-#include "projects/ide-recent-projects.h"
+#include "ide-recent-projects.h"
 
 struct _IdeRecentProjects
 {
@@ -51,6 +52,29 @@ ide_recent_projects_new (void)
   return g_object_new (IDE_TYPE_RECENT_PROJECTS, NULL);
 }
 
+/**
+ * ide_recent_projects_get_default:
+ *
+ * Gets a shared #IdeRecentProjects instance.
+ *
+ * If this instance is unref'd, a new instance will be created on the next
+ * request to get the default #IdeRecentProjects instance.
+ *
+ * Returns: (transfer none): an #IdeRecentProjects
+ *
+ * Since: 3.32
+ */
+IdeRecentProjects *
+ide_recent_projects_get_default (void)
+{
+  static IdeRecentProjects *instance;
+
+  if (instance == NULL)
+    g_set_weak_pointer (&instance, ide_recent_projects_new ());
+
+  return instance;
+}
+
 static void
 ide_recent_projects_added (IdeRecentProjects *self,
                            IdeProjectInfo    *project_info)
@@ -85,22 +109,23 @@ static GBookmarkFile *
 ide_recent_projects_get_bookmarks (IdeRecentProjects  *self,
                                    GError            **error)
 {
-  GBookmarkFile *bookmarks;
+  g_autoptr(GBookmarkFile) bookmarks = NULL;
+  g_autoptr(GError) local_error = NULL;
 
   g_assert (IDE_IS_RECENT_PROJECTS (self));
 
   bookmarks = g_bookmark_file_new ();
 
-  if (!g_bookmark_file_load_from_file (bookmarks, self->file_uri, error))
+  if (!g_bookmark_file_load_from_file (bookmarks, self->file_uri, &local_error))
     {
-      if (!g_error_matches (*error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+      if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
         {
-          g_object_unref (bookmarks);
+          g_propagate_error (error, g_steal_pointer (&local_error));
           return NULL;
         }
     }
 
-  return bookmarks;
+  return g_steal_pointer (&bookmarks);
 }
 
 static void
@@ -110,13 +135,10 @@ ide_recent_projects_load_recent (IdeRecentProjects *self)
   g_autoptr(GError) error = NULL;
   gboolean needs_sync = FALSE;
   gchar **uris;
-  gssize z;
 
   g_assert (IDE_IS_RECENT_PROJECTS (self));
 
-  projects_file = ide_recent_projects_get_bookmarks (self, &error);
-
-  if (projects_file == NULL)
+  if (!(projects_file = ide_recent_projects_get_bookmarks (self, &error)))
     {
       g_warning ("Unable to open recent projects file: %s", error->message);
       return;
@@ -124,7 +146,7 @@ ide_recent_projects_load_recent (IdeRecentProjects *self)
 
   uris = g_bookmark_file_get_uris (projects_file, NULL);
 
-  for (z = 0; uris[z]; z++)
+  for (gsize z = 0; uris[z]; z++)
     {
       g_autoptr(GDateTime) last_modified_at = NULL;
       g_autoptr(GFile) project_file = NULL;
@@ -135,14 +157,14 @@ ide_recent_projects_load_recent (IdeRecentProjects *self)
       g_autofree gchar *description = NULL;
       const gchar *build_system_name = NULL;
       const gchar *uri = uris[z];
+      const gchar *diruri = NULL;
       time_t modified;
       g_auto(GStrv) groups = NULL;
       gsize len;
-      gsize i;
 
       groups = g_bookmark_file_get_groups (projects_file, uri, &len, NULL);
 
-      for (i = 0; i < len; i++)
+      for (gsize i = 0; i < len; i++)
         {
           if (g_str_equal (groups [i], IDE_RECENT_PROJECTS_GROUP))
             goto is_project;
@@ -164,10 +186,20 @@ ide_recent_projects_load_recent (IdeRecentProjects *self)
       description = g_bookmark_file_get_description (projects_file, uri, NULL);
       modified = g_bookmark_file_get_modified  (projects_file, uri, NULL);
       last_modified_at = g_date_time_new_from_unix_local (modified);
-      directory = g_file_get_parent (project_file);
+
+      for (gsize i = 0; i < len; i++)
+        {
+          if (g_str_has_prefix (groups [i], IDE_RECENT_PROJECTS_DIRECTORY))
+            diruri = groups [i] + strlen (IDE_RECENT_PROJECTS_DIRECTORY);
+        }
+
+      if (diruri == NULL)
+        directory = g_file_get_parent (project_file);
+      else
+        directory = g_file_new_for_uri (diruri);
 
       languages = g_ptr_array_new ();
-      for (i = 0; i < len; i++)
+      for (gsize i = 0; i < len; i++)
         {
           if (g_str_has_prefix (groups [i], IDE_RECENT_PROJECTS_LANGUAGE_GROUP_PREFIX))
             g_ptr_array_add (languages, groups [i] + strlen (IDE_RECENT_PROJECTS_LANGUAGE_GROUP_PREFIX));
@@ -280,9 +312,12 @@ ide_recent_projects_init (IdeRecentProjects *self)
 /**
  * ide_recent_projects_remove:
  * @self: An #IdeRecentProjects
- * @project_infos: (transfer none) (element-type Ide.ProjectInfo): a #GList of #IdeProjectInfo.
+ * @project_infos: (transfer none) (element-type IdeProjectInfo): a #GList
+ *   of #IdeProjectInfo.
  *
  * Removes the provided projects from the recent projects file.
+ *
+ * Since: 3.32
  */
 void
 ide_recent_projects_remove (IdeRecentProjects *self,
@@ -294,9 +329,7 @@ ide_recent_projects_remove (IdeRecentProjects *self,
 
   g_return_if_fail (IDE_IS_RECENT_PROJECTS (self));
 
-  projects_file = ide_recent_projects_get_bookmarks (self, &error);
-
-  if (projects_file == NULL)
+  if (!(projects_file = ide_recent_projects_get_bookmarks (self, &error)))
     {
       g_warning ("Failed to load bookmarks file: %s", error->message);
       return;
@@ -371,7 +404,7 @@ ide_recent_projects_find_by_directory (IdeRecentProjects *self,
   if (!g_file_test (directory, G_FILE_TEST_IS_DIR))
     return NULL;
 
-  if (NULL == (bookmarks = ide_recent_projects_get_bookmarks (self, NULL)))
+  if (!(bookmarks = ide_recent_projects_get_bookmarks (self, NULL)))
     return NULL;
 
   uris = g_bookmark_file_get_uris (bookmarks, &len);
diff --git a/src/libide/projects/ide-recent-projects.h b/src/libide/projects/ide-recent-projects.h
index 7f2ae592a..d5f4c2435 100644
--- a/src/libide/projects/ide-recent-projects.h
+++ b/src/libide/projects/ide-recent-projects.h
@@ -1,6 +1,6 @@
 /* ide-recent-projects.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,13 +14,14 @@
  *
  * 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 "ide-version-macros.h"
-
-#include "projects/ide-project-info.h"
+#include <libide-core.h>
+#include <libide-projects.h>
 
 G_BEGIN_DECLS
 
@@ -29,17 +30,20 @@ G_BEGIN_DECLS
 #define IDE_RECENT_PROJECTS_GROUP                     "X-GNOME-Builder-Project"
 #define IDE_RECENT_PROJECTS_LANGUAGE_GROUP_PREFIX     "X-GNOME-Builder-Language:"
 #define IDE_RECENT_PROJECTS_BUILD_SYSTEM_GROUP_PREFIX "X-GNOME-Builder-Build-System:"
+#define IDE_RECENT_PROJECTS_DIRECTORY                 "X-GNOME-Builder-Directory:"
 #define IDE_RECENT_PROJECTS_BOOKMARK_FILENAME         "recent-projects.xbel"
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeRecentProjects, ide_recent_projects, IDE, RECENT_PROJECTS, GObject)
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
+IdeRecentProjects *ide_recent_projects_get_default       (void);
+IDE_AVAILABLE_IN_3_32
 IdeRecentProjects *ide_recent_projects_new               (void);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_recent_projects_remove            (IdeRecentProjects *self,
                                                           GList             *project_infos);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gchar             *ide_recent_projects_find_by_directory (IdeRecentProjects *self,
                                                           const gchar       *directory);
 
diff --git a/src/libide/projects/ide-template-base.c b/src/libide/projects/ide-template-base.c
new file mode 100644
index 000000000..81879f3da
--- /dev/null
+++ b/src/libide/projects/ide-template-base.c
@@ -0,0 +1,724 @@
+/* ide-template-base.c
+ *
+ * Copyright 2016-2019 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 "ide-template-base"
+
+#include "config.h"
+
+#include <glib/gstdio.h>
+#include <errno.h>
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-template-base.h"
+
+#define TIMEOUT_INTERVAL_MSEC 17
+#define TIMEOUT_DURATION_MSEC  2
+
+typedef struct
+{
+  TmplTemplateLocator *locator;
+  GArray              *files;
+
+  guint                has_expanded : 1;
+} IdeTemplateBasePrivate;
+
+typedef struct
+{
+  GFile        *file;
+  GInputStream *stream;
+  TmplScope    *scope;
+  GFile        *destination;
+  TmplTemplate *template;
+  gchar        *result;
+  gint          mode;
+} FileExpansion;
+
+typedef struct
+{
+  GArray    *files;
+  guint      index;
+  guint      completed;
+} ExpansionTask;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (IdeTemplateBase, ide_template_base, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_LOCATOR,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+ide_template_base_mkdirs_worker (IdeTask      *task,
+                                 gpointer      source_object,
+                                 gpointer      task_data,
+                                 GCancellable *cancellable)
+{
+  IdeTemplateBase *self = source_object;
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+
+  for (guint i = 0; i < priv->files->len; i++)
+    {
+      FileExpansion *fexp = &g_array_index (priv->files, FileExpansion, i);
+      g_autoptr(GFile) directory = NULL;
+      g_autoptr(GError) error = NULL;
+
+      directory = g_file_get_parent (fexp->destination);
+
+      if (!g_file_make_directory_with_parents (directory, cancellable, &error))
+        {
+          if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
+            {
+              ide_task_return_error (task, g_steal_pointer (&error));
+              return;
+            }
+        }
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_template_base_mkdirs_async (IdeTemplateBase     *self,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_run_in_thread (task, ide_template_base_mkdirs_worker);
+}
+
+static gboolean
+ide_template_base_mkdirs_finish (IdeTemplateBase  *self,
+                                 GAsyncResult     *result,
+                                 GError          **error)
+{
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_template_base_get_locator:
+ * @self: An #IdeTemplateBase
+ *
+ * Fetches the #TmplTemplateLocator used for resolving templates.
+ *
+ * Returns: (transfer none) (nullable): a #TmplTemplateLocator or %NULL.
+ *
+ * Since: 3.32
+ */
+TmplTemplateLocator *
+ide_template_base_get_locator (IdeTemplateBase *self)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TEMPLATE_BASE (self), NULL);
+
+  return priv->locator;
+}
+
+void
+ide_template_base_set_locator (IdeTemplateBase     *self,
+                               TmplTemplateLocator *locator)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+  g_return_if_fail (!locator || TMPL_IS_TEMPLATE_LOCATOR (locator));
+
+  if (priv->has_expanded)
+    {
+      g_warning ("Cannot change template locator after "
+                 "ide_template_base_expand_all_async() has been called.");
+      return;
+    }
+
+  if (g_set_object (&priv->locator, locator))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LOCATOR]);
+}
+
+static void
+clear_file_expansion (gpointer data)
+{
+  FileExpansion *expansion = data;
+
+  g_clear_object (&expansion->file);
+  g_clear_object (&expansion->stream);
+  g_clear_pointer (&expansion->scope, tmpl_scope_unref);
+  g_clear_object (&expansion->destination);
+  g_clear_object (&expansion->template);
+  g_clear_pointer (&expansion->result, g_free);
+}
+
+static void
+ide_template_base_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeTemplateBase *self = IDE_TEMPLATE_BASE(object);
+
+  switch (prop_id)
+    {
+    case PROP_LOCATOR:
+      g_value_set_object (value, ide_template_base_get_locator (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_template_base_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeTemplateBase *self = IDE_TEMPLATE_BASE(object);
+
+  switch (prop_id)
+    {
+    case PROP_LOCATOR:
+      ide_template_base_set_locator (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_template_base_finalize (GObject *object)
+{
+  IdeTemplateBase *self = (IdeTemplateBase *)object;
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_clear_pointer (&priv->files, g_array_unref);
+  g_clear_object (&priv->locator);
+
+  G_OBJECT_CLASS (ide_template_base_parent_class)->finalize (object);
+}
+
+static void
+ide_template_base_class_init (IdeTemplateBaseClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_template_base_finalize;
+  object_class->get_property = ide_template_base_get_property;
+  object_class->set_property = ide_template_base_set_property;
+
+  /**
+   * IdeTemplateBase:locator:
+   *
+   * The #IdeTemplateBase:locator property contains the #TmplTemplateLocator
+   * that should be used to resolve template includes. If %NULL, templates
+   * will not be allowed to include other templates.
+   * directive.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_LOCATOR] =
+    g_param_spec_object ("locator",
+                         "Locator",
+                         "Locator",
+                         TMPL_TYPE_TEMPLATE_LOCATOR,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_template_base_init (IdeTemplateBase *self)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  priv->files = g_array_new (FALSE, TRUE, sizeof (FileExpansion));
+  g_array_set_clear_func (priv->files, clear_file_expansion);
+}
+
+static void
+ide_template_base_parse_worker (IdeTask      *task,
+                                gpointer      source_object,
+                                gpointer      task_data,
+                                GCancellable *cancellable)
+{
+  IdeTemplateBase *self = source_object;
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  for (guint i = 0; i < priv->files->len; i++)
+    {
+      FileExpansion *fexp = &g_array_index (priv->files, FileExpansion, i);
+      g_autoptr(TmplTemplate) template = NULL;
+      g_autoptr(GError) error = NULL;
+
+      if (fexp->template != NULL)
+        continue;
+
+      template = tmpl_template_new (priv->locator);
+
+      if (!tmpl_template_parse_file (template, fexp->file, cancellable, &error))
+        {
+          ide_task_return_error (task, g_steal_pointer (&error));
+          return;
+        }
+
+      fexp->template = g_object_ref (template);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_template_base_parse_async (IdeTemplateBase     *self,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_run_in_thread (task, ide_template_base_parse_worker);
+}
+
+static gboolean
+ide_template_base_parse_finish (IdeTemplateBase  *self,
+                                GAsyncResult     *result,
+                                GError          **error)
+{
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_template_base_replace_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  ExpansionTask *expansion;
+  FileExpansion *fexp = NULL;
+  guint i;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  expansion = ide_task_get_task_data (task);
+
+  g_assert (expansion != NULL);
+  g_assert (expansion->files != NULL);
+
+  expansion->completed++;
+
+  /*
+   * Complete the file replacement operation.
+   */
+  if (!g_file_replace_contents_finish (file, result, NULL, &error))
+    {
+      if (!ide_task_get_completed (task))
+        ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  /*
+   * Locate the FileExpansion. We could remove this by tracking some
+   * state in the callback, but that is more complex than it's worth
+   * since we share the task between all the callbacks.
+   */
+  for (i = 0; i < expansion->files->len; i++)
+    {
+      FileExpansion *item = &g_array_index (expansion->files, FileExpansion, i);
+
+      if (g_file_equal (item->destination, file))
+        {
+          fexp = item;
+          break;
+        }
+    }
+
+  /*
+   * Unfortunately, we don't have a nice portable API to define modes.
+   * So we limit our ability to chmod() to the local file-system.
+   * This still works for things like FUSE, so much as they support
+   * the posix chmod() API.
+   */
+  if ((fexp != NULL) && (fexp->mode != 0) && g_file_is_native (file))
+    {
+      g_autofree gchar *path = g_file_get_path (file);
+
+      if (0 != g_chmod (path, fexp->mode))
+        g_warning ("chmod(\"%s\", 0%o) failed with: %s",
+                   path, fexp->mode, strerror (errno));
+    }
+
+  if (expansion->completed == expansion->files->len)
+    {
+      if (!ide_task_get_completed (task))
+        ide_task_return_boolean (task, TRUE);
+    }
+}
+
+static gboolean
+ide_template_base_expand (IdeTask *task)
+{
+  ExpansionTask *expansion;
+  gint64 end;
+  gint64 now;
+
+  g_assert (IDE_IS_TASK (task));
+
+  expansion = ide_task_get_task_data (task);
+
+  g_assert (expansion != NULL);
+  g_assert (expansion->files != NULL);
+
+  /*
+   * We will only run for up to 2 milliseconds before we want to yield
+   * back to the main loop and schedule future expansions as low-priority
+   * so that we do not block the frame-clock;
+   */
+  for (end = (now = g_get_monotonic_time ()) + ((G_USEC_PER_SEC / 1000) * TIMEOUT_DURATION_MSEC);
+       now < end;
+       now = g_get_monotonic_time ())
+    {
+      FileExpansion *fexp;
+      g_autoptr(GError) error = NULL;
+
+      g_assert (expansion->index <= expansion->files->len);
+
+      if (expansion->index == expansion->files->len)
+        break;
+
+      fexp = &g_array_index (expansion->files, FileExpansion, expansion->index);
+
+      g_assert (fexp != NULL);
+      g_assert (fexp->template != NULL);
+      g_assert (fexp->scope != NULL);
+      g_assert (fexp->result == NULL);
+
+      fexp->result = tmpl_template_expand_string (fexp->template, fexp->scope, &error);
+
+      if (fexp->result == NULL)
+        {
+          ide_task_return_error (task, g_steal_pointer (&error));
+          return G_SOURCE_REMOVE;
+        }
+
+      expansion->index++;
+    }
+
+  /*
+   * If we have completed expanding all the templates, we need to start
+   * writing the results to the destination files asynchronously, and in
+   * parallel. When all of the async operations have completed, we will
+   * cleanup and complete the task.
+   */
+  if (expansion->index == expansion->files->len)
+    {
+      guint i;
+
+      expansion->completed = 0;
+
+      //ide_template_base_make_directories (task);
+
+      for (i = 0; i < expansion->files->len; i++)
+        {
+          FileExpansion *fexp = &g_array_index (expansion->files, FileExpansion, i);
+
+          g_assert (fexp != NULL);
+          g_assert (G_IS_FILE (fexp->destination));
+          g_assert (fexp->result != NULL);
+
+          g_file_replace_contents_async (fexp->destination,
+                                         fexp->result,
+                                         strlen (fexp->result),
+                                         NULL,
+                                         FALSE,
+                                         G_FILE_CREATE_REPLACE_DESTINATION,
+                                         ide_task_get_cancellable (task),
+                                         ide_template_base_replace_cb,
+                                         g_object_ref (task));
+        }
+
+      return G_SOURCE_REMOVE;
+    }
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+ide_template_base_expand_parse_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeTemplateBase *self = (IdeTemplateBase *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+
+  if (!ide_template_base_parse_finish (self, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  g_timeout_add_full (G_PRIORITY_LOW,
+                      TIMEOUT_INTERVAL_MSEC,
+                      (GSourceFunc)ide_template_base_expand,
+                      g_object_ref (task),
+                      g_object_unref);
+}
+
+static void
+ide_template_base_expand_mkdirs_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  IdeTemplateBase *self = (IdeTemplateBase *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GCancellable *cancellable;
+
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (IDE_IS_TASK (task));
+
+  cancellable = ide_task_get_cancellable (task);
+
+  if (!ide_template_base_mkdirs_finish (self, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_template_base_parse_async (self,
+                                   cancellable,
+                                   ide_template_base_expand_parse_cb,
+                                   g_steal_pointer (&task));
+}
+
+void
+ide_template_base_expand_all_async (IdeTemplateBase     *self,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  ExpansionTask *task_data;
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task_data = g_new0 (ExpansionTask, 1);
+  task_data->files = priv->files;
+  task_data->index = 0;
+  task_data->completed = 0;
+
+  /*
+   * The expand process will need to call tmpl_template_expand() and we want
+   * that to happen in the main loop so that all scoped objects need not be
+   * thread-safe.
+   *
+   * Therefore, the first step is to asynchronously load all of the templates
+   * from storage. After that, we will expand the templates into memory,
+   * being careful about how long we run per-cycle in the main-loop. If we
+   * run too long, we risk adding jitter to the frame-clock and causing UI
+   * elements to feel sluggish.
+   *
+   * Once we have all of our templates expanded, we progress to asynchronously
+   * write them to the requested underlying storage.
+   */
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_task_data (task, task_data, g_free);
+
+  /*
+   * You can only call ide_template_base_expand_all_async() once, since we maintain
+   * a bunch of state inline.
+   */
+  if (priv->has_expanded)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_PENDING,
+                                 "%s() has already been called.",
+                                 G_STRFUNC);
+      return;
+    }
+
+  priv->has_expanded = TRUE;
+
+  /*
+   * If we have nothing to do, we still need to preserve our "executed" state.
+   * So if there is nothing to do, short circuit now.
+   */
+  if (priv->files->len == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  ide_template_base_mkdirs_async (self,
+                                  cancellable,
+                                  ide_template_base_expand_mkdirs_cb,
+                                  g_object_ref (task));
+}
+
+gboolean
+ide_template_base_expand_all_finish (IdeTemplateBase  *self,
+                                     GAsyncResult     *result,
+                                     GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_TEMPLATE_BASE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static TmplScope *
+create_scope (IdeTemplateBase *self,
+              TmplScope       *parent,
+              GFile           *destination)
+{
+  TmplScope *scope;
+  TmplSymbol *symbol;
+  g_autofree gchar *filename = NULL;
+  g_autofree gchar *year = NULL;
+  g_autoptr(GDateTime) now = NULL;
+
+  g_assert (IDE_IS_TEMPLATE_BASE (self));
+  g_assert (G_IS_FILE (destination));
+
+  scope = tmpl_scope_new_with_parent (parent);
+
+  symbol = tmpl_scope_get (scope, "filename");
+  filename = g_file_get_basename (destination);
+  tmpl_symbol_assign_string (symbol, filename);
+
+  now = g_date_time_new_now_local ();
+  year = g_date_time_format (now, "%Y");
+  symbol = tmpl_scope_get (scope, "year");
+  tmpl_symbol_assign_string (symbol, year);
+
+  return scope;
+}
+
+void
+ide_template_base_add_resource (IdeTemplateBase *self,
+                                const gchar     *resource_path,
+                                GFile           *destination,
+                                TmplScope       *scope,
+                                gint             mode)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+  FileExpansion expansion = { 0 };
+  g_autofree gchar *uri = NULL;
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+  g_return_if_fail (resource_path != NULL);
+  g_return_if_fail (G_IS_FILE (destination));
+
+  if (priv->has_expanded)
+    {
+      g_warning ("%s() called after ide_template_base_expand_all_async(). "
+                 "Ignoring request to add resource.",
+                 G_STRFUNC);
+      return;
+    }
+
+  uri = g_strdup_printf ("resource://%s", resource_path);
+
+  expansion.file = g_file_new_for_uri (uri);
+  expansion.stream = NULL;
+  expansion.scope = create_scope (self, scope, destination);
+  expansion.destination = g_object_ref (destination);
+  expansion.result = NULL;
+  expansion.mode = mode;
+
+  g_array_append_val (priv->files, expansion);
+}
+
+void
+ide_template_base_add_path (IdeTemplateBase *self,
+                            const gchar     *path,
+                            GFile           *destination,
+                            TmplScope       *scope,
+                            gint             mode)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+  FileExpansion expansion = { 0 };
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+  g_return_if_fail (path != NULL);
+  g_return_if_fail (G_IS_FILE (destination));
+
+  if (priv->has_expanded)
+    {
+      g_warning ("%s() called after ide_template_base_expand_all_async(). "
+                 "Ignoring request to add resource.",
+                 G_STRFUNC);
+      return;
+    }
+
+  expansion.file = g_file_new_for_path (path);
+  expansion.stream = NULL;
+  expansion.scope = create_scope (self, scope, destination);
+  expansion.destination = g_object_ref (destination);
+  expansion.result = NULL;
+  expansion.mode = mode;
+
+  g_array_append_val (priv->files, expansion);
+}
+
+void
+ide_template_base_reset (IdeTemplateBase *self)
+{
+  IdeTemplateBasePrivate *priv = ide_template_base_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TEMPLATE_BASE (self));
+
+  g_clear_pointer (&priv->files, g_array_unref);
+  priv->files = g_array_new (FALSE, TRUE, sizeof (FileExpansion));
+
+  priv->has_expanded = FALSE;
+}
diff --git a/src/libide/projects/ide-template-base.h b/src/libide/projects/ide-template-base.h
new file mode 100644
index 000000000..83df2affd
--- /dev/null
+++ b/src/libide/projects/ide-template-base.h
@@ -0,0 +1,71 @@
+/* ide-template-base.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <tmpl-glib.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEMPLATE_BASE (ide_template_base_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTemplateBase, ide_template_base, IDE, TEMPLATE_BASE, GObject)
+
+struct _IdeTemplateBaseClass
+{
+  GObjectClass parent_class;
+};
+
+IDE_AVAILABLE_IN_3_32
+TmplTemplateLocator *ide_template_base_get_locator       (IdeTemplateBase       *self);
+IDE_AVAILABLE_IN_3_32
+void                 ide_template_base_set_locator       (IdeTemplateBase       *self,
+                                                          TmplTemplateLocator   *locator);
+IDE_AVAILABLE_IN_3_32
+void                 ide_template_base_add_resource      (IdeTemplateBase       *self,
+                                                          const gchar           *resource_path,
+                                                          GFile                 *destination,
+                                                          TmplScope             *scope,
+                                                          gint                   mode);
+IDE_AVAILABLE_IN_3_32
+void                 ide_template_base_add_path          (IdeTemplateBase       *self,
+                                                          const gchar           *path,
+                                                          GFile                 *destination,
+                                                          TmplScope             *scope,
+                                                          gint                   mode);
+IDE_AVAILABLE_IN_3_32
+void                 ide_template_base_expand_all_async  (IdeTemplateBase       *self,
+                                                          GCancellable          *cancellable,
+                                                          GAsyncReadyCallback    callback,
+                                                          gpointer               user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean             ide_template_base_expand_all_finish (IdeTemplateBase       *self,
+                                                          GAsyncResult          *result,
+                                                          GError               **error);
+IDE_AVAILABLE_IN_3_32
+void                 ide_template_base_reset             (IdeTemplateBase       *self);
+
+G_END_DECLS
diff --git a/src/libide/projects/ide-template-provider.c b/src/libide/projects/ide-template-provider.c
new file mode 100644
index 000000000..58014d433
--- /dev/null
+++ b/src/libide/projects/ide-template-provider.c
@@ -0,0 +1,61 @@
+/* ide-template-provider.c
+ *
+ * Copyright 2016-2019 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 "ide-template-provider"
+
+#include "config.h"
+
+#include "ide-template-provider.h"
+
+G_DEFINE_INTERFACE (IdeTemplateProvider, ide_template_provider, G_TYPE_OBJECT)
+
+static GList *
+ide_template_provider_real_get_project_templates (IdeTemplateProvider *self)
+{
+  return NULL;
+}
+
+static void
+ide_template_provider_default_init (IdeTemplateProviderInterface *iface)
+{
+  iface->get_project_templates = ide_template_provider_real_get_project_templates;
+}
+
+/**
+ * ide_template_provider_get_project_templates:
+ * @self: An #IdeTemplateProvider
+ *
+ * Gets a list of templates for this provider.
+ *
+ * Plugins should implement this interface to feed #IdeProjectTemplate's into
+ * the project creation workflow.
+ *
+ * Returns: (transfer full) (element-type Ide.ProjectTemplate): a #GList of
+ *   #IdeProjectTemplate instances.
+ *
+ * Since: 3.32
+ */
+GList *
+ide_template_provider_get_project_templates (IdeTemplateProvider *self)
+{
+  g_return_val_if_fail (IDE_IS_TEMPLATE_PROVIDER (self), NULL);
+
+  return IDE_TEMPLATE_PROVIDER_GET_IFACE (self)->get_project_templates (self);
+}
diff --git a/src/libide/projects/ide-template-provider.h b/src/libide/projects/ide-template-provider.h
new file mode 100644
index 000000000..8325794ed
--- /dev/null
+++ b/src/libide/projects/ide-template-provider.h
@@ -0,0 +1,48 @@
+/* ide-template-provider.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_PROJECTS_INSIDE) && !defined (IDE_PROJECTS_COMPILATION)
+# error "Only <libide-projects.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-project-template.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TEMPLATE_PROVIDER (ide_template_provider_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeTemplateProvider, ide_template_provider, IDE, TEMPLATE_PROVIDER, GObject)
+
+struct _IdeTemplateProviderInterface
+{
+  GTypeInterface parent_iface;
+
+  GList *(*get_project_templates) (IdeTemplateProvider *self);
+};
+
+IDE_AVAILABLE_IN_3_32
+GList *ide_template_provider_get_project_templates (IdeTemplateProvider *self);
+
+G_END_DECLS
diff --git a/src/libide/projects/libide-projects.h b/src/libide/projects/libide-projects.h
new file mode 100644
index 000000000..f1f7db00d
--- /dev/null
+++ b/src/libide/projects/libide-projects.h
@@ -0,0 +1,40 @@
+/* ide-projects.h
+ *
+ * Copyright 2018-2019 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-core.h>
+#include <libide-io.h>
+
+#define IDE_PROJECTS_INSIDE
+
+#include "ide-doap.h"
+#include "ide-doap-person.h"
+#include "ide-project.h"
+#include "ide-project-info.h"
+#include "ide-project-file.h"
+#include "ide-project-template.h"
+#include "ide-project-tree-addin.h"
+#include "ide-projects-global.h"
+#include "ide-recent-projects.h"
+#include "ide-template-base.h"
+#include "ide-template-provider.h"
+
+#undef IDE_PROJECTS_INSIDE
diff --git a/src/libide/projects/meson.build b/src/libide/projects/meson.build
index ab7587693..0a68c01cb 100644
--- a/src/libide/projects/meson.build
+++ b/src/libide/projects/meson.build
@@ -1,27 +1,85 @@
-projects_headers = [
-  'ide-project-edit.h',
-  'ide-project-info.h',
-  'ide-project-item.h',
+libide_projects_header_subdir = join_paths(libide_header_subdir, 'projects')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_projects_public_headers = [
+  'ide-doap.h',
+  'ide-doap-person.h',
   'ide-project.h',
+  'ide-project-info.h',
+  'ide-project-file.h',
+  'ide-projects-global.h',
+  'ide-project-template.h',
   'ide-project-tree-addin.h',
   'ide-recent-projects.h',
+  'ide-template-base.h',
+  'ide-template-provider.h',
+  'libide-projects.h',
 ]
 
-projects_sources = [
-  'ide-project-edit.c',
-  'ide-project-info.c',
-  'ide-project-item.c',
+install_headers(libide_projects_public_headers, subdir: libide_projects_header_subdir)
+
+#
+# Sources
+#
+
+libide_projects_private_headers = [ 'xml-reader-private.h', ]
+libide_projects_private_sources = [ 'xml-reader.c', ]
+
+libide_projects_public_sources = [
+  'ide-doap.c',
+  'ide-doap-person.c',
   'ide-project.c',
+  'ide-project-info.c',
+  'ide-project-file.c',
+  'ide-projects-global.c',
+  'ide-project-template.c',
   'ide-project-tree-addin.c',
   'ide-recent-projects.c',
+  'ide-template-base.c',
+  'ide-template-provider.c',
 ]
 
-projects_private_sources = [
-  'ide-project-edit-private.h',
+libide_projects_sources = libide_projects_public_sources + libide_projects_private_sources
+
+#
+# Dependencies
+#
+
+libide_projects_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+  libtemplate_glib_dep,
+  libxml2_dep,
+
+  libide_core_dep,
+  libide_io_dep,
+  libide_threading_dep,
+  libide_code_dep,
+  libide_vcs_dep,
 ]
 
-libide_public_headers += files(projects_headers)
-libide_public_sources += files(projects_sources)
-libide_private_sources += files(projects_private_sources)
+#
+# Library Definitions
+#
+
+libide_projects = static_library('ide-projects-' + libide_api_version, libide_projects_sources,
+   dependencies: libide_projects_deps,
+         c_args: libide_args + release_args + ['-DIDE_PROJECTS_COMPILATION'],
+)
+
+libide_projects_dep = declare_dependency(
+              sources: libide_projects_private_headers,
+         dependencies: libide_projects_deps,
+           link_whole: libide_projects,
+  include_directories: include_directories('.'),
+)
 
-install_headers(projects_headers, subdir: join_paths(libide_header_subdir, 'projects'))
+gnome_builder_public_sources += files(libide_projects_public_sources)
+gnome_builder_public_headers += files(libide_projects_public_headers)
+gnome_builder_include_subdirs += libide_projects_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-projects.h', '-DIDE_PROJECTS_COMPILATION']
diff --git a/src/libide/projects/xml-reader-private.h b/src/libide/projects/xml-reader-private.h
new file mode 100644
index 000000000..0c7e574de
--- /dev/null
+++ b/src/libide/projects/xml-reader-private.h
@@ -0,0 +1,99 @@
+/* xml-reader.h
+ *
+ * Copyright 2009 Christian Hergert  <chris dronelabs com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * Author:
+ *   Christian Hergert <chris dronelabs com>
+ *
+ * Based upon work by:
+ *   Emmanuele Bassi
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <gio/gio.h>
+#include <libxml/xmlreader.h>
+
+G_BEGIN_DECLS
+
+#define XML_TYPE_READER (xml_reader_get_type ())
+
+#define XML_READER_ERROR (xml_reader_error_quark())
+
+G_DECLARE_FINAL_TYPE (XmlReader, xml_reader, XML, READER, GObject)
+
+typedef enum
+{
+   XML_READER_ERROR_INVALID,
+} XmlReaderError;
+
+GQuark                xml_reader_error_quark             (void);
+XmlReader            *xml_reader_new                     (void);
+gboolean              xml_reader_load_from_path          (XmlReader     *reader,
+                                                          const gchar   *path);
+gboolean              xml_reader_load_from_file          (XmlReader     *reader,
+                                                          GFile         *file,
+                                                          GCancellable  *cancellable,
+                                                          GError       **error);
+gboolean              xml_reader_load_from_data          (XmlReader     *reader,
+                                                          const gchar   *data,
+                                                          gssize         length,
+                                                          const gchar   *uri,
+                                                          const gchar   *encoding);
+gboolean              xml_reader_load_from_stream        (XmlReader     *reader,
+                                                          GInputStream  *stream,
+                                                          GError       **error);
+
+gint                  xml_reader_get_depth               (XmlReader   *reader);
+xmlReaderTypes        xml_reader_get_node_type           (XmlReader   *reader);
+const gchar          *xml_reader_get_value               (XmlReader   *reader);
+const gchar          *xml_reader_get_name                (XmlReader   *reader);
+const gchar          *xml_reader_get_local_name          (XmlReader   *reader);
+gchar                *xml_reader_read_string             (XmlReader   *reader);
+gchar                *xml_reader_get_attribute           (XmlReader   *reader,
+                                                          const gchar *name);
+gboolean              xml_reader_is_a                    (XmlReader   *reader,
+                                                          const gchar *name);
+gboolean              xml_reader_is_a_local              (XmlReader   *reader,
+                                                          const gchar *local_name);
+gboolean              xml_reader_is_namespace            (XmlReader   *reader,
+                                                          const gchar *ns);
+gboolean              xml_reader_is_empty_element        (XmlReader   *reader);
+
+gboolean              xml_reader_read_start_element      (XmlReader   *reader,
+                                                          const gchar *name);
+gboolean              xml_reader_read_end_element        (XmlReader   *reader);
+
+gchar                *xml_reader_read_inner_xml          (XmlReader   *reader);
+gchar                *xml_reader_read_outer_xml          (XmlReader   *reader);
+
+gboolean              xml_reader_read                    (XmlReader   *reader);
+gboolean              xml_reader_read_to_next            (XmlReader   *reader);
+gboolean              xml_reader_read_to_next_sibling    (XmlReader   *reader);
+
+gboolean              xml_reader_move_to_element         (XmlReader   *reader);
+gboolean              xml_reader_move_to_attribute       (XmlReader   *reader,
+                                                          const gchar *name);
+void                  xml_reader_move_up_to_depth        (XmlReader   *reader,
+                                                          gint         depth);
+
+gboolean              xml_reader_move_to_first_attribute (XmlReader   *reader);
+gboolean              xml_reader_move_to_next_attribute  (XmlReader   *reader);
+gint                  xml_reader_count_attributes        (XmlReader   *reader);
+gboolean              xml_reader_move_to_nth_attribute   (XmlReader   *reader,
+                                                          gint         nth);
+gint                  xml_reader_get_line_number         (XmlReader   *reader);
+
+G_END_DECLS
diff --git a/src/libide/projects/xml-reader.c b/src/libide/projects/xml-reader.c
new file mode 100644
index 000000000..bcd9ca042
--- /dev/null
+++ b/src/libide/projects/xml-reader.c
@@ -0,0 +1,599 @@
+/* xml-reader.c
+ *
+ * Copyright 2009  Christian Hergert  <chris dronelabs com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * Author:
+ *   Christian Hergert  <chris dronelabs com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include <glib/gi18n.h>
+#include <string.h>
+#include <libxml/xmlreader.h>
+
+#include "xml-reader-private.h"
+
+#define XML_TO_CHAR(s)  ((char *) (s))
+#define CHAR_TO_XML(s)  ((unsigned char *) (s))
+#define RETURN_STRDUP_AND_XMLFREE(stmt) \
+  G_STMT_START {                        \
+    guchar *x;                          \
+    gchar *y;                           \
+    x = stmt;                           \
+    y = g_strdup((char *)x);            \
+    xmlFree(x);                         \
+    return y;                           \
+  } G_STMT_END
+
+struct _XmlReader
+{
+  GObject           parent_instance;
+  xmlTextReaderPtr  xml;
+  GInputStream     *stream;
+  gchar            *cur_name;
+  gchar            *encoding;
+  gchar            *uri;
+};
+
+enum {
+  PROP_0,
+  PROP_ENCODING,
+  PROP_URI,
+  LAST_PROP
+};
+
+enum {
+  ERROR,
+  LAST_SIGNAL
+};
+
+G_DEFINE_QUARK (xml_reader_error, xml_reader_error)
+G_DEFINE_TYPE (XmlReader, xml_reader, G_TYPE_OBJECT)
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [LAST_SIGNAL];
+
+#define XML_NODE_TYPE_ELEMENT      1
+#define XML_NODE_TYPE_END_ELEMENT 15
+#define XML_NODE_TYPE_ATTRIBUTE    2
+
+static void
+xml_reader_set_encoding (XmlReader   *reader,
+                         const gchar *encoding)
+{
+   g_return_if_fail (XML_IS_READER (reader));
+   g_free (reader->encoding);
+   reader->encoding = g_strdup (encoding);
+}
+
+static void
+xml_reader_set_uri (XmlReader   *reader,
+                    const gchar *uri)
+{
+   g_return_if_fail (XML_IS_READER (reader));
+   g_free (reader->uri);
+   reader->uri = g_strdup (uri);
+}
+
+static void
+xml_reader_clear (XmlReader *reader)
+{
+   g_return_if_fail(XML_IS_READER(reader));
+
+   g_free (reader->cur_name);
+   reader->cur_name = NULL;
+
+   if (reader->xml) {
+      xmlTextReaderClose(reader->xml);
+      xmlFreeTextReader(reader->xml);
+      reader->xml = NULL;
+   }
+
+   if (reader->stream) {
+      g_object_unref(reader->stream);
+      reader->stream = NULL;
+   }
+}
+
+static void
+xml_reader_finalize (GObject *object)
+{
+   XmlReader *reader = (XmlReader *)object;
+
+   xml_reader_clear (reader);
+
+   g_free (reader->encoding);
+   reader->encoding = NULL;
+
+   g_free (reader->uri);
+   reader->uri = NULL;
+
+   G_OBJECT_CLASS (xml_reader_parent_class)->finalize (object);
+}
+
+static void
+xml_reader_get_property (GObject    *object,
+                         guint       prop_id,
+                         GValue     *value,
+                         GParamSpec *pspec)
+{
+   XmlReader *reader = (XmlReader *)object;
+
+   switch (prop_id)
+     {
+     case PROP_ENCODING:
+        g_value_set_string (value, reader->encoding);
+        break;
+
+     case PROP_URI:
+        g_value_set_string (value, reader->uri);
+        break;
+
+     default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+     }
+}
+
+static void
+xml_reader_set_property (GObject      *object,
+                         guint         prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+   XmlReader *reader = (XmlReader *)object;
+
+   switch (prop_id)
+     {
+     case PROP_ENCODING:
+        xml_reader_set_encoding (reader, g_value_get_string (value));
+        break;
+
+     case PROP_URI:
+        xml_reader_set_uri (reader, g_value_get_string (value));
+        break;
+
+     default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+     }
+}
+
+static void
+xml_reader_class_init (XmlReaderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = xml_reader_finalize;
+  object_class->get_property = xml_reader_get_property;
+  object_class->set_property = xml_reader_set_property;
+
+  properties [PROP_ENCODING] =
+    g_param_spec_string ("encoding",
+                         "Encoding",
+                         "Encoding",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_URI] =
+    g_param_spec_string ("uri",
+                         "URI",
+                         "URI",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  signals [ERROR] =
+    g_signal_new ("error",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_STRING);
+}
+
+static void
+xml_reader_init (XmlReader *reader)
+{
+}
+
+XmlReader*
+xml_reader_new (void)
+{
+  return g_object_new (XML_TYPE_READER, NULL);
+}
+
+static void
+xml_reader_error_cb (void                    *arg,
+                     const char              *msg,
+                     xmlParserSeverities      severity,
+                     xmlTextReaderLocatorPtr  locator)
+{
+  XmlReader *reader = arg;
+
+  g_assert (XML_IS_READER (reader));
+
+  g_signal_emit (reader, signals [ERROR], 0, msg);
+}
+
+gboolean
+xml_reader_load_from_path (XmlReader   *reader,
+                           const gchar *path)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  xml_reader_clear (reader);
+
+  if ((reader->xml = xmlNewTextReaderFilename (path)))
+    xmlTextReaderSetErrorHandler (reader->xml, xml_reader_error_cb, reader);
+
+  return (reader->xml != NULL);
+}
+
+gboolean
+xml_reader_load_from_file (XmlReader     *reader,
+                           GFile         *file,
+                           GCancellable  *cancellable,
+                           GError       **error)
+{
+  GFileInputStream *stream;
+  gboolean ret;
+
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  if (!(stream = g_file_read (file, cancellable, error)))
+    return FALSE;
+
+  ret = xml_reader_load_from_stream (reader, G_INPUT_STREAM (stream), error);
+
+  g_clear_object (&stream);
+
+  return ret;
+}
+
+gboolean
+xml_reader_load_from_data (XmlReader   *reader,
+                           const gchar *data,
+                           gssize       length,
+                           const gchar *uri,
+                           const gchar *encoding)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  xml_reader_clear (reader);
+
+  if (length == -1)
+    length = strlen (data);
+
+  reader->xml = xmlReaderForMemory (data, length, uri, encoding, 0);
+  xmlTextReaderSetErrorHandler (reader->xml, xml_reader_error_cb, reader);
+
+  return (reader->xml != NULL);
+}
+
+static int
+xml_reader_io_read_cb (void *context,
+                       char *buffer,
+                       int   len)
+{
+  GInputStream *stream = (GInputStream *)context;
+  g_return_val_if_fail (G_IS_INPUT_STREAM(stream), -1);
+  return g_input_stream_read (stream, buffer, len, NULL, NULL);
+}
+
+static int
+xml_reader_io_close_cb (void *context)
+{
+  GInputStream *stream = (GInputStream *)context;
+
+  g_return_val_if_fail (G_IS_INPUT_STREAM(stream), -1);
+
+  return g_input_stream_close (stream, NULL, NULL) ? 0 : -1;
+}
+
+gboolean
+xml_reader_load_from_stream (XmlReader     *reader,
+                             GInputStream  *stream,
+                             GError       **error)
+{
+  g_return_val_if_fail (XML_IS_READER(reader), FALSE);
+
+  xml_reader_clear (reader);
+
+  reader->xml = xmlReaderForIO (xml_reader_io_read_cb,
+                                xml_reader_io_close_cb,
+                                stream,
+                                reader->uri,
+                                reader->encoding,
+                                XML_PARSE_RECOVER | XML_PARSE_NOBLANKS | XML_PARSE_COMPACT);
+
+  if (!reader->xml)
+    {
+      g_set_error (error,
+                   XML_READER_ERROR,
+                   XML_READER_ERROR_INVALID,
+                   _("Could not parse XML from stream"));
+      return FALSE;
+    }
+
+   reader->stream = g_object_ref (stream);
+
+   xmlTextReaderSetErrorHandler (reader->xml, xml_reader_error_cb, reader);
+
+   return TRUE;
+}
+
+G_CONST_RETURN gchar *
+xml_reader_get_value (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), NULL);
+
+  g_return_val_if_fail (reader->xml != NULL, NULL);
+
+  return XML_TO_CHAR (xmlTextReaderConstValue (reader->xml));
+}
+
+G_CONST_RETURN gchar *
+xml_reader_get_name (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), NULL);
+  g_return_val_if_fail (reader->xml != NULL, NULL);
+
+  return XML_TO_CHAR (xmlTextReaderConstName (reader->xml));
+}
+
+gchar *
+xml_reader_read_string (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), NULL);
+  g_return_val_if_fail (reader->xml != NULL, NULL);
+
+  RETURN_STRDUP_AND_XMLFREE (xmlTextReaderReadString (reader->xml));
+}
+
+gchar *
+xml_reader_get_attribute (XmlReader   *reader,
+                          const gchar *name)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), NULL);
+  g_return_val_if_fail (reader->xml != NULL, NULL);
+
+  RETURN_STRDUP_AND_XMLFREE (xmlTextReaderGetAttribute (reader->xml, CHAR_TO_XML (name)));
+}
+
+static gboolean
+read_to_type_and_name (XmlReader   *reader,
+                       gint         type,
+                       const gchar *name)
+{
+  gboolean success = FALSE;
+
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  g_return_val_if_fail (reader->xml != NULL, FALSE);
+
+  while (xmlTextReaderRead (reader->xml) == 1)
+    {
+      if (xmlTextReaderNodeType (reader->xml) == type)
+        {
+          if (g_strcmp0 (XML_TO_CHAR (xmlTextReaderConstName (reader->xml)), name) == 0)
+            {
+              success = TRUE;
+              break;
+            }
+        }
+    }
+
+  return success;
+}
+
+gboolean
+xml_reader_read_start_element (XmlReader   *reader,
+                               const gchar *name)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  if (read_to_type_and_name (reader, XML_NODE_TYPE_ELEMENT, name))
+    {
+      g_free (reader->cur_name);
+      reader->cur_name = g_strdup (name);
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+gboolean
+xml_reader_read_end_element (XmlReader *reader)
+{
+  gboolean success = FALSE;
+
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  if (reader->cur_name)
+    success = read_to_type_and_name (reader, XML_NODE_TYPE_END_ELEMENT, reader->cur_name);
+
+  return success;
+}
+
+gchar *
+xml_reader_read_inner_xml (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  RETURN_STRDUP_AND_XMLFREE (xmlTextReaderReadInnerXml (reader->xml));
+}
+
+gchar*
+xml_reader_read_outer_xml (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  RETURN_STRDUP_AND_XMLFREE (xmlTextReaderReadOuterXml (reader->xml));
+}
+
+gboolean
+xml_reader_read_to_next (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  return (xmlTextReaderNext (reader->xml) == 1);
+}
+
+gboolean
+xml_reader_read (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  return (xmlTextReaderRead (reader->xml) == 1);
+}
+
+gboolean
+xml_reader_read_to_next_sibling (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  xmlTextReaderMoveToElement (reader->xml);
+
+  return (xmlTextReaderNextSibling (reader->xml) == 1);
+}
+
+gint
+xml_reader_get_depth (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER(reader), -1);
+
+  return xmlTextReaderDepth (reader->xml);
+}
+
+void
+xml_reader_move_up_to_depth (XmlReader *reader,
+                             gint       depth)
+{
+  g_return_if_fail(XML_IS_READER(reader));
+
+  while (xml_reader_get_depth(reader) > depth)
+    xml_reader_read_end_element(reader);
+}
+
+xmlReaderTypes
+xml_reader_get_node_type (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), 0);
+
+  return xmlTextReaderNodeType (reader->xml);
+}
+
+gboolean
+xml_reader_is_empty_element (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  return xmlTextReaderIsEmptyElement (reader->xml);
+}
+
+gboolean
+xml_reader_is_a (XmlReader   *reader,
+                 const gchar *name)
+{
+   return (g_strcmp0 (xml_reader_get_name (reader), name) == 0);
+}
+
+gboolean
+xml_reader_is_a_local (XmlReader   *reader,
+                       const gchar *local_name)
+{
+   return (g_strcmp0 (xml_reader_get_local_name (reader), local_name) == 0);
+}
+
+gboolean
+xml_reader_is_namespace (XmlReader   *reader,
+                         const gchar *ns)
+{
+   g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+   return (g_strcmp0 (XML_TO_CHAR(xmlTextReaderConstNamespaceUri (reader->xml)), ns) == 0);
+}
+
+G_CONST_RETURN gchar *
+xml_reader_get_local_name (XmlReader *reader)
+{
+   g_return_val_if_fail(XML_IS_READER (reader), NULL);
+
+   return XML_TO_CHAR (xmlTextReaderConstLocalName (reader->xml));
+}
+
+gboolean
+xml_reader_move_to_element (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  return (xmlTextReaderMoveToElement (reader->xml) == 1);
+}
+
+gboolean
+xml_reader_move_to_attribute (XmlReader   *reader,
+                              const gchar *name)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  return (xmlTextReaderMoveToAttribute (reader->xml, CHAR_TO_XML (name)) == 1);
+}
+
+gboolean
+xml_reader_move_to_first_attribute (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  return (xmlTextReaderMoveToFirstAttribute (reader->xml) == 1);
+}
+
+gboolean
+xml_reader_move_to_next_attribute (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  return (xmlTextReaderMoveToNextAttribute (reader->xml) == 1);
+}
+
+gboolean
+xml_reader_move_to_nth_attribute (XmlReader *reader,
+                                  gint       nth)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  return (xmlTextReaderMoveToAttributeNo (reader->xml, nth) == 1);
+}
+
+gint
+xml_reader_count_attributes (XmlReader *reader)
+{
+  g_return_val_if_fail (XML_IS_READER (reader), FALSE);
+
+  return xmlTextReaderAttributeCount (reader->xml);
+}
+
+gint
+xml_reader_get_line_number (XmlReader *reader)
+{
+  g_return_val_if_fail(XML_IS_READER(reader), -1);
+
+  if (reader->xml)
+    return xmlTextReaderGetParserLineNumber(reader->xml);
+
+  return -1;
+}
diff --git a/src/libide/search/ide-search-engine.c b/src/libide/search/ide-search-engine.c
index 7ee9d21bf..85e82d3f5 100644
--- a/src/libide/search/ide-search-engine.c
+++ b/src/libide/search/ide-search-engine.c
@@ -1,6 +1,6 @@
 /* ide-search-engine.c
  *
- * Copyright 2015-2017 Christian Hergert <chergert redhat com>
+ * Copyright 2015-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-search-engine"
@@ -21,12 +23,12 @@
 #include "config.h"
 
 #include <libpeas/peas.h>
+#include <libide-core.h>
+#include <libide-threading.h>
 
-#include "search/ide-search-engine.h"
-#include "search/ide-search-provider.h"
-#include "search/ide-search-result.h"
-#include "threading/ide-task.h"
-#include "util/ide-glib.h"
+#include "ide-search-engine.h"
+#include "ide-search-provider.h"
+#include "ide-search-result.h"
 
 #define DEFAULT_MAX_RESULTS 50
 
@@ -80,31 +82,65 @@ request_destroy (Request *r)
 }
 
 static void
-ide_search_engine_constructed (GObject *object)
+on_extension_added_cb (PeasExtensionSet *set,
+                       PeasPluginInfo   *plugin_info,
+                       PeasExtension    *exten,
+                       gpointer          user_data)
+{
+  ide_object_append (IDE_OBJECT (user_data), IDE_OBJECT (exten));
+}
+
+static void
+on_extension_removed_cb (PeasExtensionSet *set,
+                         PeasPluginInfo   *plugin_info,
+                         PeasExtension    *exten,
+                         gpointer          user_data)
+{
+  ide_object_remove (IDE_OBJECT (user_data), IDE_OBJECT (exten));
+}
+
+static void
+ide_search_engine_parent_set (IdeObject *object,
+                              IdeObject *parent)
 {
   IdeSearchEngine *self = (IdeSearchEngine *)object;
-  IdeContext *context;
 
   g_assert (IDE_IS_SEARCH_ENGINE (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
 
-  G_OBJECT_CLASS (ide_search_engine_parent_class)->constructed (object);
-
-  context = ide_object_get_context (IDE_OBJECT (self));
+  if (parent == NULL)
+    {
+      g_clear_object (&self->extensions);
+      return;
+    }
 
   self->extensions = peas_extension_set_new (peas_engine_get_default (),
                                              IDE_TYPE_SEARCH_PROVIDER,
-                                             "context", context,
                                              NULL);
+
+  g_signal_connect (self->extensions,
+                    "extension-added",
+                    G_CALLBACK (on_extension_added_cb),
+                    self);
+
+  g_signal_connect (self->extensions,
+                    "extension-removed",
+                    G_CALLBACK (on_extension_removed_cb),
+                    self);
+
+  peas_extension_set_foreach (self->extensions,
+                              on_extension_added_cb,
+                              self);
 }
 
 static void
-ide_search_engine_dispose (GObject *object)
+ide_search_engine_destroy (IdeObject *object)
 {
   IdeSearchEngine *self = (IdeSearchEngine *)object;
 
   g_clear_object (&self->extensions);
 
-  G_OBJECT_CLASS (ide_search_engine_parent_class)->dispose (object);
+  IDE_OBJECT_CLASS (ide_search_engine_parent_class)->destroy (object);
 }
 
 static void
@@ -129,11 +165,13 @@ ide_search_engine_get_property (GObject    *object,
 static void
 ide_search_engine_class_init (IdeSearchEngineClass *klass)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GObjectClass *g_object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *object_class = IDE_OBJECT_CLASS (klass);
+
+  g_object_class->get_property = ide_search_engine_get_property;
 
-  object_class->constructed = ide_search_engine_constructed;
-  object_class->dispose = ide_search_engine_dispose;
-  object_class->get_property = ide_search_engine_get_property;
+  object_class->destroy = ide_search_engine_destroy;
+  object_class->parent_set = ide_search_engine_parent_set;
 
   properties [PROP_BUSY] =
     g_param_spec_boolean ("busy",
@@ -142,7 +180,7 @@ ide_search_engine_class_init (IdeSearchEngineClass *klass)
                           FALSE,
                           (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
-  g_object_class_install_properties (object_class, N_PROPS, properties);
+  g_object_class_install_properties (g_object_class, N_PROPS, properties);
 }
 
 static void
@@ -163,6 +201,8 @@ ide_search_engine_new (void)
  * Checks if the #IdeSearchEngine is currently executing a query.
  *
  * Returns: %TRUE if queries are being processed.
+ *
+ * Since: 3.32
  */
 gboolean
 ide_search_engine_get_busy (IdeSearchEngine *self)
@@ -302,6 +342,8 @@ ide_search_engine_search_async (IdeSearchEngine     *self,
  * The result is a #GListModel of #IdeSearchResult when successful.
  *
  * Returns: (transfer full): a #GListModel of #IdeSearchResult items.
+ *
+ * Since: 3.32
  */
 GListModel *
 ide_search_engine_search_finish (IdeSearchEngine  *self,
diff --git a/src/libide/search/ide-search-engine.h b/src/libide/search/ide-search-engine.h
index c4ad37dcb..bb9ef1a73 100644
--- a/src/libide/search/ide-search-engine.h
+++ b/src/libide/search/ide-search-engine.h
@@ -1,6 +1,6 @@
 /* ide-search-engine.h
  *
- * Copyright 2015-2017 Christian Hergert <chergert redhat com>
+ * Copyright 2015-2019 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
@@ -14,33 +14,37 @@
  *
  * 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 "ide-version-macros.h"
+#if !defined (IDE_SEARCH_INSIDE) && !defined (IDE_SEARCH_COMPILATION)
+# error "Only <libide-search.h> can be included directly."
+#endif
 
-#include "ide-object.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_SEARCH_ENGINE (ide_search_engine_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeSearchEngine, ide_search_engine, IDE, SEARCH_ENGINE, IdeObject)
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeSearchEngine *ide_search_engine_new           (void);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean         ide_search_engine_get_busy      (IdeSearchEngine      *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void             ide_search_engine_search_async  (IdeSearchEngine      *self,
                                                   const gchar          *query,
                                                   guint                 max_results,
                                                   GCancellable         *cancellable,
                                                   GAsyncReadyCallback   callback,
                                                   gpointer              user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GListModel      *ide_search_engine_search_finish (IdeSearchEngine      *self,
                                                   GAsyncResult         *result,
                                                   GError              **error);
diff --git a/src/libide/search/ide-search-provider.c b/src/libide/search/ide-search-provider.c
index db096385e..95dd176a3 100644
--- a/src/libide/search/ide-search-provider.c
+++ b/src/libide/search/ide-search-provider.c
@@ -1,6 +1,6 @@
 /* ide-search-provider.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,14 +14,17 @@
  *
  * 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 "ide-search-provider"
 
 #include "config.h"
 
-#include "search/ide-search-provider.h"
-#include "threading/ide-task.h"
+#include <libide-threading.h>
+
+#include "ide-search-provider.h"
 
 G_DEFINE_INTERFACE (IdeSearchProvider, ide_search_provider, IDE_TYPE_OBJECT)
 
@@ -86,8 +89,10 @@ ide_search_provider_search_async (IdeSearchProvider   *self,
  *
  * Completes a request to a search provider.
  *
- * Returns: (transfer full) (element-type Ide.SearchResult): a #GPtrArray
+ * Returns: (transfer full) (element-type IdeSearchResult): a #GPtrArray
  *    of #IdeSearchResult elements.
+ *
+ * Since: 3.32
  */
 GPtrArray *
 ide_search_provider_search_finish (IdeSearchProvider  *self,
diff --git a/src/libide/search/ide-search-provider.h b/src/libide/search/ide-search-provider.h
index c2cce3176..dee906538 100644
--- a/src/libide/search/ide-search-provider.h
+++ b/src/libide/search/ide-search-provider.h
@@ -1,6 +1,6 @@
 /* ide-search-provider.h
  *
- * Copyright 2015-2017 Christian Hergert <chergert redhat com>
+ * Copyright 2015-2019 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
@@ -14,19 +14,23 @@
  *
  * 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 "ide-version-macros.h"
+#if !defined (IDE_SEARCH_INSIDE) && !defined (IDE_SEARCH_COMPILATION)
+# error "Only <libide-search.h> can be included directly."
+#endif
 
-#include "ide-object.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_SEARCH_PROVIDER (ide_search_provider_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_INTERFACE (IdeSearchProvider, ide_search_provider, IDE, SEARCH_PROVIDER, IdeObject)
 
 struct _IdeSearchProviderInterface
@@ -44,14 +48,14 @@ struct _IdeSearchProviderInterface
                                GError              **error);
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void       ide_search_provider_search_async  (IdeSearchProvider    *self,
                                               const gchar          *query,
                                               guint                 max_results,
                                               GCancellable         *cancellable,
                                               GAsyncReadyCallback   callback,
                                               gpointer              user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GPtrArray *ide_search_provider_search_finish (IdeSearchProvider    *self,
                                               GAsyncResult         *result,
                                               GError              **error);
diff --git a/src/libide/search/ide-search-reducer.c b/src/libide/search/ide-search-reducer.c
index 9f4849142..2ea4642e2 100644
--- a/src/libide/search/ide-search-reducer.c
+++ b/src/libide/search/ide-search-reducer.c
@@ -1,6 +1,6 @@
 /* ide-search-reducer.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 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
@@ -14,14 +14,16 @@
  *
  * 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 "ide-search-reducer"
 
 #include "config.h"
 
-#include "search/ide-search-reducer.h"
-#include "search/ide-search-result.h"
+#include "ide-search-reducer.h"
+#include "ide-search-result.h"
 
 /**
  * SECTION:ide-search-reducer
@@ -30,6 +32,8 @@
  *
  * This is a helper structure for search engines to reduce the number
  * of items they inflate when performing a search.
+ *
+ * Since: 3.32
  */
 
 #define DEFAULT_MAX_ITEMS 1000
@@ -42,6 +46,8 @@
  * Initializes a new #IdeSearchReducer to be used to reduce the number of
  * search results that are created. This is generally just used to help
  * keep search performance good.
+ *
+ * Since: 3.32
  */
 void
 ide_search_reducer_init (IdeSearchReducer  *reducer,
@@ -59,6 +65,8 @@ ide_search_reducer_init (IdeSearchReducer  *reducer,
  * @reducer: a #IdeSearchReducer
  *
  * Frees the results.
+ *
+ * Since: 3.32
  */
 void
 ide_search_reducer_destroy (IdeSearchReducer *reducer)
@@ -77,9 +85,11 @@ ide_search_reducer_destroy (IdeSearchReducer *reducer)
  * Frees all items associated with the result set, unless @free_results is
  * %FALSE and then the results are returned as an array.
  *
- * Returns: (nullable) (transfer container) (element-type Ide.SearchResult):
+ * Returns: (nullable) (transfer container) (element-type IdeSearchResult):
  *   An array of #IdeSearchResult unless @free_results is %TRUE, then
  *   %NULL is returned.
+ *
+ * Since: 3.32
  */
 GPtrArray *
 ide_search_reducer_free (IdeSearchReducer *reducer,
@@ -125,6 +135,8 @@ ide_search_reducer_free (IdeSearchReducer *reducer,
  *
  * Like ide_search_reducer_push() but takes ownership of @result by
  * stealing the reference.
+ *
+ * Since: 3.32
  */
 void
 ide_search_reducer_take (IdeSearchReducer *reducer,
@@ -151,6 +163,8 @@ ide_search_reducer_take (IdeSearchReducer *reducer,
  * @result: an #IdeSearchResult
  *
  * Adds result to the set unless it scores too low.
+ *
+ * Since: 3.32
  */
 void
 ide_search_reducer_push (IdeSearchReducer *reducer,
@@ -172,6 +186,8 @@ ide_search_reducer_push (IdeSearchReducer *reducer,
  * where you want to avoid inflating an #IdeSearchResult unless necessary.
  *
  * Returns: %TRUE if there is space for a result with a score of @score.
+ *
+ * Since: 3.32
  */
 gboolean
 ide_search_reducer_accepts (IdeSearchReducer *reducer,
diff --git a/src/libide/search/ide-search-reducer.h b/src/libide/search/ide-search-reducer.h
index 0ae4ab52f..9d3a2610c 100644
--- a/src/libide/search/ide-search-reducer.h
+++ b/src/libide/search/ide-search-reducer.h
@@ -1,6 +1,6 @@
 /* ide-search-reducer.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,13 +14,19 @@
  *
  * 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 "ide-version-macros.h"
+#if !defined (IDE_SEARCH_INSIDE) && !defined (IDE_SEARCH_COMPILATION)
+# error "Only <libide-search.h> can be included directly."
+#endif
+
+#include <libide-core.h>
 
-#include "ide-types.h"
+#include "ide-search-result.h"
 
 G_BEGIN_DECLS
 
@@ -31,21 +37,21 @@ typedef struct
   gsize      count;
 } IdeSearchReducer;
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void       ide_search_reducer_init    (IdeSearchReducer  *reducer,
                                        gsize              max_results);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean   ide_search_reducer_accepts (IdeSearchReducer  *reducer,
                                        gfloat             score);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void       ide_search_reducer_take    (IdeSearchReducer  *reducer,
                                        IdeSearchResult   *result);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void       ide_search_reducer_push    (IdeSearchReducer  *reducer,
                                        IdeSearchResult   *result);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void       ide_search_reducer_destroy (IdeSearchReducer  *reducer);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GPtrArray *ide_search_reducer_free    (IdeSearchReducer  *reducer,
                                        gboolean           free_results);
 
diff --git a/src/libide/search/ide-search-result.c b/src/libide/search/ide-search-result.c
index 45849ed7c..3d6385b58 100644
--- a/src/libide/search/ide-search-result.c
+++ b/src/libide/search/ide-search-result.c
@@ -1,6 +1,6 @@
 /* ide-search-result.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-search-result"
 
 #include "config.h"
 
-#include "search/ide-search-result.h"
+#include "ide-search-result.h"
 
 typedef struct
 {
@@ -229,25 +231,24 @@ ide_search_result_set_priority (IdeSearchResult *self,
 }
 
 /**
- * ide_search_result_get_source_location:
+ * ide_search_result_activate:
  * @self: a #IdeSearchResult
+ * @last_focus: a #GtkWidget of the last focus
  *
- * Gets the file associated with the search result if any.
- *
- * Many search providers ultimately just open a file, so this may
- * be used in lieu of handling the activate signal.
+ * Requests that @self activate. @last_focus is provided so that the search
+ * result may activate #GAction or other context-specific actions.
  *
- * Returns: (transfer full) (nullable): An #IdeUri
+ * Since: 3.32
  */
-IdeSourceLocation *
-ide_search_result_get_source_location (IdeSearchResult *self)
+void
+ide_search_result_activate (IdeSearchResult *self,
+                            GtkWidget       *last_focus)
 {
-  g_return_val_if_fail (IDE_IS_SEARCH_RESULT (self), NULL);
-
-  if (IDE_SEARCH_RESULT_GET_CLASS (self)->get_source_location != NULL)
-    return IDE_SEARCH_RESULT_GET_CLASS (self)->get_source_location (self);
+  g_return_if_fail (IDE_IS_SEARCH_RESULT (self));
+  g_return_if_fail (GTK_IS_WIDGET (last_focus));
 
-  return NULL;
+  if (IDE_SEARCH_RESULT_GET_CLASS (self)->activate)
+    IDE_SEARCH_RESULT_GET_CLASS (self)->activate (self, last_focus);
 }
 
 void
diff --git a/src/libide/search/ide-search-result.h b/src/libide/search/ide-search-result.h
index 93c6501e0..7f0b25237 100644
--- a/src/libide/search/ide-search-result.h
+++ b/src/libide/search/ide-search-result.h
@@ -1,6 +1,6 @@
 /* ide-search-result.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,59 +14,56 @@
  *
  * 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>
-#include <dazzle.h>
-
-#include "ide-version-macros.h"
+#if !defined (IDE_SEARCH_INSIDE) && !defined (IDE_SEARCH_COMPILATION)
+# error "Only <libide-search.h> can be included directly."
+#endif
 
-#include "diagnostics/ide-source-location.h"
+#include <libide-core.h>
+#include <dazzle.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_SEARCH_RESULT (ide_search_result_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeSearchResult, ide_search_result, IDE, SEARCH_RESULT, DzlSuggestion)
 
 struct _IdeSearchResultClass
 {
   DzlSuggestionClass parent_class;
 
-  IdeSourceLocation *(*get_source_location) (IdeSearchResult *self);
+  void (*activate) (IdeSearchResult *self,
+                    GtkWidget       *last_focus);
 
   /*< private >*/
-  gpointer _reserved1;
-  gpointer _reserved2;
-  gpointer _reserved3;
-  gpointer _reserved4;
-  gpointer _reserved5;
-  gpointer _reserved6;
-  gpointer _reserved7;
-  gpointer _reserved8;
+  gpointer _reserved[8];
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeSearchResult   *ide_search_result_new                 (void);
-IDE_AVAILABLE_IN_ALL
-IdeSourceLocation *ide_search_result_get_source_location (IdeSearchResult       *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
+void               ide_search_result_activate            (IdeSearchResult       *self,
+                                                          GtkWidget             *last_focus);
+IDE_AVAILABLE_IN_3_32
 gint               ide_search_result_compare             (gconstpointer          a,
                                                           gconstpointer          b);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gint               ide_search_result_get_priority        (IdeSearchResult       *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_search_result_set_priority        (IdeSearchResult       *self,
                                                           gint                   priority);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gfloat             ide_search_result_get_score           (IdeSearchResult       *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void               ide_search_result_set_score           (IdeSearchResult       *self,
                                                           gfloat                 score);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void               ide_search_result_set_icon            (IdeSearchResult       *self,
                                                           GIcon                 *icon);
 
diff --git a/src/libide/search/libide-search.h b/src/libide/search/libide-search.h
new file mode 100644
index 000000000..e0c0df866
--- /dev/null
+++ b/src/libide/search/libide-search.h
@@ -0,0 +1,34 @@
+/* libide-search.h
+ *
+ * Copyright 2014-2019 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 <dazzle.h>
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#define IDE_SEARCH_INSIDE
+
+#include "ide-search-engine.h"
+#include "ide-search-provider.h"
+#include "ide-search-reducer.h"
+#include "ide-search-result.h"
+
+#undef IDE_SEARCH_INSIDE
diff --git a/src/libide/search/meson.build b/src/libide/search/meson.build
index 967ceacfd..e5b3b43ab 100644
--- a/src/libide/search/meson.build
+++ b/src/libide/search/meson.build
@@ -1,22 +1,61 @@
-search_headers = [
+libide_search_header_subdir = join_paths(libide_header_subdir, 'search')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_search_public_headers = [
   'ide-search-engine.h',
-  'ide-search-entry.h',
   'ide-search-provider.h',
-  'ide-search-result.h',
   'ide-search-reducer.h',
-  'ide-tagged-entry.h',
+  'ide-search-result.h',
+  'libide-search.h',
 ]
 
-search_sources = [
+install_headers(libide_search_public_headers, subdir: libide_search_header_subdir)
+
+#
+# Sources
+#
+
+libide_search_public_sources = [
   'ide-search-engine.c',
-  'ide-search-entry.c',
   'ide-search-provider.c',
-  'ide-search-result.c',
   'ide-search-reducer.c',
-  'ide-tagged-entry.c',
+  'ide-search-result.c',
 ]
 
-libide_public_headers += files(search_headers)
-libide_public_sources += files(search_sources)
+libide_search_sources = libide_search_public_sources
+
+#
+# Dependencies
+#
+
+libide_search_deps = [
+  libgio_dep,
+  libdazzle_dep,
+  libpeas_dep,
+  libide_core_dep,
+  libide_threading_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_search = static_library('ide-search-' + libide_api_version, libide_search_sources,
+   dependencies: libide_search_deps,
+         c_args: libide_args + release_args + ['-DIDE_SEARCH_COMPILATION'],
+)
+
+libide_search_dep = declare_dependency(
+         dependencies: libide_search_deps,
+           link_whole: libide_search,
+  include_directories: include_directories('.'),
+)
 
-install_headers(search_headers, subdir: join_paths(libide_header_subdir, 'search'))
+gnome_builder_public_sources += files(libide_search_public_sources)
+gnome_builder_public_headers += files(libide_search_public_headers)
+gnome_builder_include_subdirs += libide_search_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-search.h', '-DIDE_SEARCH_COMPILATION']
diff --git a/src/libide/sourceview/gtk/menus.ui b/src/libide/sourceview/gtk/menus.ui
new file mode 100644
index 000000000..2af02abd7
--- /dev/null
+++ b/src/libide/sourceview/gtk/menus.ui
@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-source-view-popup-menu">
+    <section id="ide-source-view-popup-menu-jump-section">
+      <item>
+        <attribute name="label" translatable="yes">_Go to Definition</attribute>
+        <attribute name="action">sourceview.goto-definition</attribute>
+      </item>
+    </section>
+    <section id="ide-source-view-popup-menu-undo-section">
+      <item>
+        <attribute name="label" translatable="yes">_Undo</attribute>
+        <attribute name="action">sourceview.undo</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Redo</attribute>
+        <attribute name="action">sourceview.redo</attribute>
+      </item>
+    </section>
+    <section id="ide-source-view-popup-menu-clipboard-section">
+      <item>
+        <attribute name="label" translatable="yes">C_ut</attribute>
+        <attribute name="action">sourceview.cut-clipboard</attribute>
+      </item>
+      <item>
+        <attribute name="id">copy</attribute>
+        <attribute name="label" translatable="yes">_Copy</attribute>
+        <attribute name="action">sourceview.copy-clipboard</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Paste</attribute>
+        <attribute name="action">sourceview.paste-clipboard</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_Delete</attribute>
+        <attribute name="action">sourceview.delete-selection</attribute>
+      </item>
+    </section>
+    <section id="ide-source-view-popup-menu-spellcheck-section">
+    </section>
+    <section id="ide-source-view-popup-menu-highlighting-section">
+      <submenu id="ide-source-view-popup-menu-highlighting-submenu">
+        <attribute name="label" translatable="yes">Highlighting</attribute>
+      </submenu>
+    </section>
+    <section id="ide-source-view-popup-menu-selection-section">
+      <submenu id="ide-source-view-popup-menu-selection-submenu">
+        <attribute name="label" translatable="yes">Selection</attribute>
+        <item>
+          <attribute name="label" translatable="yes">Select _All</attribute>
+          <attribute name="action">sourceview.select-all</attribute>
+          <attribute name="target" type="(b)">(true,)</attribute>
+        </item>
+        <item>
+          <attribute name="label" translatable="yes">Select _None</attribute>
+          <attribute name="action">sourceview.select-all</attribute>
+          <attribute name="target" type="(b)">(false,)</attribute>
+        </item>
+        <section id="ide-source-view-popup-menu-case-section">
+          <item>
+            <attribute name="label" translatable="yes">All _Upper Case</attribute>
+            <attribute name="action">sourceview.change-case</attribute>
+            <attribute name="target" type="(u)">(1,)</attribute>
+          </item>
+          <item>
+            <attribute name="label" translatable="yes">All _Lower Case</attribute>
+            <attribute name="action">sourceview.change-case</attribute>
+            <attribute name="target" type="(u)">(0,)</attribute>
+          </item>
+          <item>
+            <attribute name="label" translatable="yes">_Invert Case</attribute>
+            <attribute name="action">sourceview.change-case</attribute>
+            <attribute name="target" type="(u)">(2,)</attribute>
+          </item>
+          <item>
+            <attribute name="label" translatable="yes">_Title Case</attribute>
+            <attribute name="action">sourceview.change-case</attribute>
+            <attribute name="target" type="(u)">(3,)</attribute>
+          </item>
+        </section>
+        <section id="ide-source-view-popup-menu-line-section">
+          <item>
+            <attribute name="label" translatable="yes">Join Lines</attribute>
+            <attribute name="action">sourceview.join-lines</attribute>
+          </item>
+          <item>
+            <attribute name="label" translatable="yes">Sort Lines</attribute>
+            <attribute name="action">sourceview.sort</attribute>
+            <attribute name="target" type="(bb)">(false,false)</attribute>
+          </item>
+        </section>
+      </submenu>
+    </section>
+    <section id="ide-source-view-popup-menu-zoom-section">
+      <submenu id="ide-source-view-popup-menu-zoom-section-submenu">
+        <attribute name="label" translatable="yes">Zoom</attribute>
+        <item>
+          <attribute name="label" translatable="yes">Zoom _In</attribute>
+          <attribute name="action">sourceview.increase-font-size</attribute>
+          <attribute name="accel">&lt;control&gt;plus</attribute>
+        </item>
+        <item>
+          <attribute name="label" translatable="yes">Zoom _Out</attribute>
+          <attribute name="action">sourceview.decrease-font-size</attribute>
+            <attribute name="accel">&lt;control&gt;minus</attribute>
+        </item>
+        <section id="ide-source-view-popup-menu-zoom-section-submenu-reset">
+          <item>
+            <attribute name="label" translatable="yes">Reset</attribute>
+            <attribute name="action">sourceview.reset-font-size</attribute>
+            <attribute name="accel">&lt;control&gt;0</attribute>
+          </item>
+        </section>
+      </submenu>
+    </section>
+  </menu>
+</interface>
diff --git a/src/libide/sourceview/ide-completion-context.c b/src/libide/sourceview/ide-completion-context.c
new file mode 100644
index 000000000..6fdbb117a
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-context.c
@@ -0,0 +1,1092 @@
+/* ide-completion-context.c
+ *
+ * Copyright 2018-2019 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 "ide-completion-context"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-completion.h"
+#include "ide-completion-context.h"
+#include "ide-completion-private.h"
+#include "ide-completion-proposal.h"
+#include "ide-completion-provider.h"
+
+struct _IdeCompletionContext
+{
+  GObject parent_instance;
+
+  IdeCompletion *completion;
+
+  GArray *providers;
+
+  GtkTextMark *begin_mark;
+  GtkTextMark *end_mark;
+
+  IdeCompletionActivation activation;
+
+  guint busy : 1;
+  guint has_populated : 1;
+  guint empty : 1;
+};
+
+typedef struct
+{
+  IdeCompletionProvider *provider;
+  GCancellable          *cancellable;
+  GListModel            *results;
+  GError                *error;
+  gulong                 items_changed_handler;
+} ProviderInfo;
+
+typedef struct
+{
+  guint n_active;
+} CompleteTaskData;
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeCompletionContext, ide_completion_context, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+  PROP_0,
+  PROP_BUSY,
+  PROP_COMPLETION,
+  PROP_EMPTY,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+static GQuark provider_quark;
+
+static void
+clear_provider_info (gpointer data)
+{
+  ProviderInfo *info = data;
+
+  if (info->items_changed_handler != 0)
+    {
+      g_signal_handler_disconnect (info->results, info->items_changed_handler);
+      info->items_changed_handler = 0;
+    }
+
+  g_clear_object (&info->provider);
+  g_clear_object (&info->cancellable);
+  g_clear_object (&info->results);
+  g_clear_error (&info->error);
+}
+
+static gint
+compare_provider_info (gconstpointer a,
+                       gconstpointer b,
+                       gpointer      user_data)
+{
+  IdeCompletionContext *self = user_data;
+  const ProviderInfo *info_a = a;
+  const ProviderInfo *info_b = b;
+
+  return ide_completion_provider_get_priority (info_a->provider, self) -
+         ide_completion_provider_get_priority (info_b->provider, self);
+}
+
+static void
+complete_task_data_free (gpointer data)
+{
+  CompleteTaskData *task_data = data;
+
+  g_slice_free (CompleteTaskData, task_data);
+}
+
+static void
+ide_completion_context_update_empty (IdeCompletionContext *self)
+{
+  gboolean empty = TRUE;
+
+  g_assert (IDE_IS_COMPLETION_CONTEXT (self));
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      const ProviderInfo *info = &g_array_index (self->providers, ProviderInfo, i);
+
+      if (info->results != NULL && g_list_model_get_n_items (info->results) > 0)
+        {
+          empty = FALSE;
+          break;
+        }
+    }
+
+  if (self->empty != empty)
+    {
+      self->empty = empty;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EMPTY]);
+    }
+}
+
+static void
+ide_completion_context_mark_failed (IdeCompletionContext  *self,
+                                    IdeCompletionProvider *provider,
+                                    const GError          *error)
+{
+  g_assert (IDE_IS_COMPLETION_CONTEXT (self));
+  g_assert (IDE_IS_COMPLETION_PROVIDER (provider));
+  g_assert (error != NULL);
+
+  if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) ||
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+    return;
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      ProviderInfo *info = &g_array_index (self->providers, ProviderInfo, i);
+
+      if (info->provider == provider)
+        {
+          if (error != info->error)
+            {
+              g_clear_error (&info->error);
+              info->error = g_error_copy (error);
+            }
+          break;
+        }
+    }
+}
+
+static void
+ide_completion_context_dispose (GObject *object)
+{
+  IdeCompletionContext *self = (IdeCompletionContext *)object;
+
+  g_clear_pointer (&self->providers, g_array_unref);
+  g_clear_object (&self->completion);
+
+  if (self->begin_mark != NULL)
+    {
+      gtk_text_buffer_delete_mark (gtk_text_mark_get_buffer (self->begin_mark), self->begin_mark);
+      g_clear_object (&self->begin_mark);
+    }
+
+  if (self->end_mark != NULL)
+    {
+      gtk_text_buffer_delete_mark (gtk_text_mark_get_buffer (self->end_mark), self->end_mark);
+      g_clear_object (&self->end_mark);
+    }
+
+  G_OBJECT_CLASS (ide_completion_context_parent_class)->dispose (object);
+}
+
+static void
+ide_completion_context_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  IdeCompletionContext *self = IDE_COMPLETION_CONTEXT (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUSY:
+      g_value_set_boolean (value, ide_completion_context_get_busy (self));
+      break;
+
+    case PROP_COMPLETION:
+      g_value_set_object (value, ide_completion_context_get_completion (self));
+      break;
+
+    case PROP_EMPTY:
+      g_value_set_boolean (value, ide_completion_context_is_empty (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_context_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  IdeCompletionContext *self = IDE_COMPLETION_CONTEXT (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMPLETION:
+      self->completion = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_context_class_init (IdeCompletionContextClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_completion_context_dispose;
+  object_class->get_property = ide_completion_context_get_property;
+  object_class->set_property = ide_completion_context_set_property;
+
+  /**
+   * IdeCompletionContext:busy:
+   *
+   * The "busy" property is %TRUE while the completion context is
+   * populating completion proposals.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_BUSY] =
+    g_param_spec_boolean ("busy",
+                          "Busy",
+                          "Is the completion context busy populating",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeCompletionContext:empty:
+   *
+   * The "empty" property is %TRUE when there are no results.
+   *
+   * It will be notified when the first result is added or the last
+   * result is removed.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_EMPTY] =
+    g_param_spec_boolean ("empty",
+                          "Empty",
+                          "If the context has no results",
+                          TRUE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeCompletionContext:completion:
+   *
+   * The "completion" is the #IdeCompletion that was used to create the context.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_COMPLETION] =
+    g_param_spec_object ("completion",
+                         "Completion",
+                         "Completion",
+                         IDE_TYPE_COMPLETION,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  provider_quark = g_quark_from_static_string ("IDE_COMPLETION_PROPOSAL_PROVIDER");
+}
+
+static void
+ide_completion_context_init (IdeCompletionContext *self)
+{
+  self->empty = TRUE;
+
+  self->providers = g_array_new (FALSE, FALSE, sizeof (ProviderInfo));
+  g_array_set_clear_func (self->providers, clear_provider_info);
+}
+
+void
+_ide_completion_context_add_provider (IdeCompletionContext  *self,
+                                      IdeCompletionProvider *provider)
+{
+  ProviderInfo info = {0};
+
+  g_return_if_fail (IDE_IS_COMPLETION_CONTEXT (self));
+  g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (provider));
+  g_return_if_fail (self->has_populated == FALSE);
+
+  info.provider = g_object_ref (provider);
+  info.cancellable = g_cancellable_new ();
+  info.results = NULL;
+
+  g_array_append_val (self->providers, info);
+  g_array_sort_with_data (self->providers, compare_provider_info, self);
+}
+
+void
+_ide_completion_context_remove_provider (IdeCompletionContext  *self,
+                                         IdeCompletionProvider *provider)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_CONTEXT (self));
+  g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (provider));
+  g_return_if_fail (self->has_populated == FALSE);
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      const ProviderInfo *info = &g_array_index (self->providers, ProviderInfo, i);
+
+      if (info->provider == provider)
+        {
+          g_array_remove_index (self->providers, i);
+          return;
+        }
+    }
+
+  g_warning ("No such provider <%s %p> in context",
+             G_OBJECT_TYPE_NAME (provider), provider);
+}
+
+static void
+ide_completion_context_items_changed_cb (IdeCompletionContext  *self,
+                                         guint                  position,
+                                         guint                  removed,
+                                         guint                  added,
+                                         GListModel            *results)
+{
+  guint real_position = 0;
+
+  g_assert (IDE_IS_COMPLETION_CONTEXT (self));
+  g_assert (G_IS_LIST_MODEL (results));
+
+  if (removed == 0 && added == 0)
+    return;
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      ProviderInfo *info = &g_array_index (self->providers, ProviderInfo, i);
+
+      if (info->results == results)
+        {
+          g_list_model_items_changed (G_LIST_MODEL (self),
+                                      real_position + position,
+                                      removed,
+                                      added);
+          break;
+        }
+
+      if (info->results != NULL)
+        real_position += g_list_model_get_n_items (info->results);
+    }
+
+  ide_completion_context_update_empty (self);
+}
+
+/**
+ * ide_completion_context_set_proposals_for_provider:
+ * @self: an #IdeCompletionContext
+ * @provider: an #IdeCompletionProvider
+ * @results: (nullable): a #GListModel or %NULL
+ *
+ * This function allows providers to update their results for a context
+ * outside of a call to ide_completion_provider_populate_async(). This
+ * can be used to immediately return results for a provider while it does
+ * additional asynchronous work. Doing so will allow the completions to
+ * update while the operation is in progress.
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_context_set_proposals_for_provider (IdeCompletionContext  *self,
+                                                   IdeCompletionProvider *provider,
+                                                   GListModel            *results)
+{
+  guint position = 0;
+
+  g_assert (IDE_IS_COMPLETION_CONTEXT (self));
+  g_assert (IDE_IS_COMPLETION_PROVIDER (provider));
+  g_assert (!results || G_IS_LIST_MODEL (results));
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      ProviderInfo *info = &g_array_index (self->providers, ProviderInfo, i);
+
+      if (info->provider == provider)
+        {
+          guint n_removed = 0;
+          guint n_added = 0;
+
+          if (info->results == results)
+            return;
+
+          if (info->results != NULL)
+            n_removed = g_list_model_get_n_items (info->results);
+
+          if (results != NULL)
+            n_added = g_list_model_get_n_items (results);
+
+          if (info->items_changed_handler != 0)
+            {
+              g_signal_handler_disconnect (info->results, info->items_changed_handler);
+              info->items_changed_handler = 0;
+            }
+
+          g_set_object (&info->results, results);
+
+          if (info->results != NULL)
+            info->items_changed_handler =
+              g_signal_connect_object (info->results,
+                                       "items-changed",
+                                       G_CALLBACK (ide_completion_context_items_changed_cb),
+                                       self,
+                                       G_CONNECT_SWAPPED);
+
+          g_list_model_items_changed (G_LIST_MODEL (self), position, n_removed, n_added);
+
+          break;
+        }
+
+      if (info->results != NULL)
+        position += g_list_model_get_n_items (info->results);
+    }
+
+  ide_completion_context_update_empty (self);
+}
+
+static void
+ide_completion_context_populate_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  IdeCompletionProvider *provider = (IdeCompletionProvider *)object;
+  IdeCompletionContext *self;
+  CompleteTaskData *task_data;
+  g_autoptr(GListModel) results = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_COMPLETION_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_COMPLETION_CONTEXT (self));
+
+  task_data = ide_task_get_task_data (task);
+  g_assert (task_data != NULL);
+
+  if (!(results = ide_completion_provider_populate_finish (provider, result, &error)))
+    ide_completion_context_mark_failed (self, provider, error);
+  else
+    ide_completion_context_set_proposals_for_provider (self, provider, results);
+
+  task_data->n_active--;
+
+  ide_completion_context_update_empty (self);
+
+  if (task_data->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_completion_context_notify_complete_cb (IdeCompletionContext *self,
+                                           GParamSpec           *pspec,
+                                           IdeTask                *task)
+{
+  g_assert (IDE_IS_COMPLETION_CONTEXT (self));
+  g_assert (IDE_IS_TASK (task));
+
+  self->busy = FALSE;
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+}
+
+/**
+ * _ide_completion_context_complete_async:
+ * @self: a #IdeCompletionContext
+ * @activation: how we are being activated
+ * @iter: a #GtkTextIter
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: (nullable): a callback or %NULL
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously requests that the completion context load proposals
+ * from the registered providers.
+ *
+ * Since: 3.32
+ */
+void
+_ide_completion_context_complete_async (IdeCompletionContext    *self,
+                                        IdeCompletionActivation  activation,
+                                        const GtkTextIter       *begin,
+                                        const GtkTextIter       *end,
+                                        GCancellable            *cancellable,
+                                        GAsyncReadyCallback      callback,
+                                        gpointer                 user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  CompleteTaskData *task_data;
+  GtkTextBuffer *buffer;
+  guint n_items;
+
+  g_return_if_fail (IDE_IS_COMPLETION_CONTEXT (self));
+  g_return_if_fail (self->has_populated == FALSE);
+  g_return_if_fail (self->begin_mark == NULL);
+  g_return_if_fail (self->end_mark == NULL);
+  g_return_if_fail (begin != NULL);
+  g_return_if_fail (end != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  self->activation = activation;
+  self->has_populated = TRUE;
+  self->busy = TRUE;
+
+  buffer = ide_completion_get_buffer (self->completion);
+
+  self->begin_mark = gtk_text_buffer_create_mark (buffer, NULL, begin, TRUE);
+  g_object_ref (self->begin_mark);
+
+  self->end_mark = gtk_text_buffer_create_mark (buffer, NULL, end, FALSE);
+  g_object_ref (self->end_mark);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, _ide_completion_context_complete_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  task_data = g_slice_new0 (CompleteTaskData);
+  task_data->n_active = self->providers->len;
+  ide_task_set_task_data (task, task_data, complete_task_data_free);
+
+  /* Always notify of busy completion, whether we fail or not */
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_completion_context_notify_complete_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      const ProviderInfo *info = &g_array_index (self->providers, ProviderInfo, i);
+
+      dzl_cancellable_chain (info->cancellable, cancellable);
+      ide_completion_provider_populate_async (info->provider,
+                                              self,
+                                              info->cancellable,
+                                              ide_completion_context_populate_cb,
+                                              g_object_ref (task));
+    }
+
+  /* Providers may adjust their position based on our new marks */
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self));
+  g_array_sort_with_data (self->providers, compare_provider_info, self);
+  g_list_model_items_changed (G_LIST_MODEL (self), 0, n_items, n_items);
+
+  if (task_data->n_active == 0)
+      ide_task_return_boolean (task, TRUE);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUSY]);
+}
+
+/**
+ * _ide_completion_context_complete_finish:
+ * @self: an #IdeCompletionContext
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to populate proposals.
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set
+ *
+ * Since: 3.32
+ */
+gboolean
+_ide_completion_context_complete_finish (IdeCompletionContext  *self,
+                                         GAsyncResult          *result,
+                                         GError               **error)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * ide_completion_context_get_busy:
+ *
+ * Gets the "busy" property. This is set to %TRUE while the completion
+ * context is actively fetching proposals from the #IdeCompletionProvider
+ * that were registered with ide_completion_context_add_provider().
+ *
+ * Returns: %TRUE if the context is busy
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_context_get_busy (IdeCompletionContext *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), FALSE);
+
+  return self->busy;
+}
+
+static GType
+ide_completion_context_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_COMPLETION_PROPOSAL;
+}
+
+static guint
+ide_completion_context_get_n_items (GListModel *model)
+{
+  IdeCompletionContext *self = (IdeCompletionContext *)model;
+  guint count = 0;
+
+  g_assert (IDE_IS_COMPLETION_CONTEXT (self));
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      const ProviderInfo *info = &g_array_index (self->providers, ProviderInfo, i);
+
+      if (info->results != NULL)
+        count += g_list_model_get_n_items (info->results);
+    }
+
+  return count;
+}
+
+gboolean
+ide_completion_context_get_item_full (IdeCompletionContext   *self,
+                                      guint                   position,
+                                      IdeCompletionProvider **provider,
+                                      IdeCompletionProposal **proposal)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), FALSE);
+
+  if (provider != NULL)
+    *provider = NULL;
+
+  if (proposal != NULL)
+    *proposal = NULL;
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      const ProviderInfo *info = &g_array_index (self->providers, ProviderInfo, i);
+      guint n_items;
+
+      if (info->results == NULL)
+        continue;
+
+      n_items = g_list_model_get_n_items (info->results);
+
+      if (position >= n_items)
+        {
+          position -= n_items;
+          continue;
+        }
+
+      if (provider != NULL)
+        *provider = g_object_ref (info->provider);
+
+      if (proposal != NULL)
+        *proposal = g_list_model_get_item (info->results, position);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static gpointer
+ide_completion_context_get_item (GListModel *model,
+                                 guint       position)
+{
+  IdeCompletionContext *self = (IdeCompletionContext *)model;
+  g_autoptr(IdeCompletionProposal) proposal = NULL;
+
+  g_assert (IDE_IS_COMPLETION_CONTEXT (self));
+
+  if (ide_completion_context_get_item_full (self, position, NULL, &proposal))
+    return g_steal_pointer (&proposal);
+
+  return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = ide_completion_context_get_item_type;
+  iface->get_item = ide_completion_context_get_item;
+  iface->get_n_items = ide_completion_context_get_n_items;
+}
+
+/**
+ * ide_completion_context_get_bounds:
+ * @self: an #IdeCompletionContext
+ * @begin: (out) (optional): a #GtkTextIter
+ * @end: (out) (optional): a #GtkTextIter
+ *
+ * Gets the bounds for the completion, which is the beginning of the
+ * current word (taking break characters into account) to the current
+ * insertion cursor.
+ *
+ * If @begin is non-%NULL, it will be set to the start position of the
+ * current word being completed.
+ *
+ * If @end is non-%NULL, it will be set to the insertion cursor for the
+ * current word being completed.
+ *
+ * Returns: %TRUE if the marks are still valid and @begin or @end was set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_context_get_bounds (IdeCompletionContext *self,
+                                   GtkTextIter          *begin,
+                                   GtkTextIter          *end)
+{
+  GtkTextBuffer *buffer;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), FALSE);
+  g_return_val_if_fail (self->completion != NULL, FALSE);
+  g_return_val_if_fail (begin != NULL || end != NULL, FALSE);
+
+  buffer = ide_completion_get_buffer (self->completion);
+
+  g_return_val_if_fail (buffer != NULL, FALSE);
+
+  if (begin != NULL)
+    memset (begin, 0, sizeof *begin);
+
+  if (end != NULL)
+    memset (end, 0, sizeof *end);
+
+  if (self->begin_mark == NULL)
+    {
+      /* Try to give some sort of valid iter */
+      gtk_text_buffer_get_selection_bounds (buffer, begin, end);
+      return FALSE;
+    }
+
+  g_assert (GTK_IS_TEXT_MARK (self->begin_mark));
+  g_assert (GTK_IS_TEXT_MARK (self->end_mark));
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+
+  if (begin != NULL)
+    gtk_text_buffer_get_iter_at_mark (buffer, begin, self->begin_mark);
+
+  if (end != NULL)
+    gtk_text_buffer_get_iter_at_mark (buffer, end, self->end_mark);
+
+  return TRUE;
+}
+
+/**
+ * ide_completion_context_get_completion:
+ * @self: an #IdeCompletionContext
+ *
+ * Gets the #IdeCompletion that created the context.
+ *
+ * Returns: (transfer none) (nullable): an #IdeCompletion or %NULL
+ *
+ * Since: 3.32
+ */
+IdeCompletion *
+ide_completion_context_get_completion (IdeCompletionContext *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), NULL);
+
+  return self->completion;
+}
+
+IdeCompletionContext *
+_ide_completion_context_new (IdeCompletion *completion)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION (completion), NULL);
+
+  return g_object_new (IDE_TYPE_COMPLETION_CONTEXT,
+                       "completion", completion,
+                       NULL);
+}
+
+/**
+ * ide_completion_context_is_empty:
+ * @self: a #IdeCompletionContext
+ *
+ * Checks if any proposals have been provided to the context.
+ *
+ * Out of convenience, this function will return %TRUE if @self is %NULL.
+ *
+ * Returns: %TRUE if there are no proposals in the context
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_context_is_empty (IdeCompletionContext *self)
+{
+  g_return_val_if_fail (!self || IDE_IS_COMPLETION_CONTEXT (self), FALSE);
+
+  return self ? self->empty : TRUE;
+}
+
+/**
+ * ide_completion_context_get_start_iter:
+ * @self: a #IdeCompletionContext
+ * @iter: (out): a location for a #GtkTextIter
+ *
+ * Gets the iter for the start of the completion.
+ *
+ * Returns:
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_context_get_start_iter (IdeCompletionContext *self,
+                                       GtkTextIter          *iter)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), FALSE);
+  g_return_val_if_fail (self->completion != NULL, FALSE);
+  g_return_val_if_fail (iter != NULL, FALSE);
+
+  if (self->begin_mark != NULL)
+    {
+      GtkTextBuffer *buffer = gtk_text_mark_get_buffer (self->begin_mark);
+      gtk_text_buffer_get_iter_at_mark (buffer, iter, self->begin_mark);
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_completion_context_get_word:
+ * @self: a #IdeCompletionContext
+ *
+ * Gets the word that is being completed up to the position of the insert mark.
+ *
+ * Returns: (transfer full): a string containing the current word
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_completion_context_get_word (IdeCompletionContext *self)
+{
+  GtkTextIter begin, end;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), NULL);
+
+  ide_completion_context_get_bounds (self, &begin, &end);
+  return gtk_text_iter_get_slice (&begin, &end);
+}
+
+gboolean
+_ide_completion_context_can_refilter (IdeCompletionContext *self,
+                                      const GtkTextIter    *begin,
+                                      const GtkTextIter    *end)
+{
+  GtkTextIter old_begin;
+  GtkTextIter old_end;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), FALSE);
+  g_return_val_if_fail (begin != NULL, FALSE);
+  g_return_val_if_fail (end != NULL, FALSE);
+
+  ide_completion_context_get_bounds (self, &old_begin, &old_end);
+
+  if (gtk_text_iter_equal (&old_begin, begin))
+    {
+      /*
+       * TODO: We can probably get smarter about this by asking all of
+       * the providers if they can refilter the new word (and only reload
+       * the data for those that cannot.
+       *
+       * Also, we might want to deal with that by copying the context
+       * into a new context and query using that.
+       */
+      if (gtk_text_iter_compare (&old_end, end) <= 0)
+        {
+          GtkTextBuffer *buffer = gtk_text_iter_get_buffer (begin);
+
+          gtk_text_buffer_move_mark (buffer, self->begin_mark, begin);
+          gtk_text_buffer_move_mark (buffer, self->end_mark, end);
+
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_completion_context_get_buffer:
+ * @self: an #IdeCompletionContext
+ *
+ * Gets the underlying buffer used by the context.
+ *
+ * This is a convenience function to get the buffer via the #IdeCompletion
+ * property.
+ *
+ * Returns: (transfer none) (nullable): a #GtkTextBuffer or %NULL
+ *
+ * Since: 3.32
+ */
+GtkTextBuffer *
+ide_completion_context_get_buffer (IdeCompletionContext *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), NULL);
+
+  if (self->completion != NULL)
+    return ide_completion_get_buffer (self->completion);
+
+  return NULL;
+}
+
+/**
+ * ide_completion_context_get_view:
+ * @self: a #IdeCompletionContext
+ *
+ * Gets the text view for the context.
+ *
+ * Returns: (nullable) (transfer none): a #GtkTextView or %NULL
+ *
+ * Since: 3.32
+ */
+GtkTextView *
+ide_completion_context_get_view (IdeCompletionContext *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), NULL);
+
+  if (self->completion != NULL)
+    return GTK_TEXT_VIEW (ide_completion_get_view (self->completion));
+
+  return NULL;
+}
+
+void
+_ide_completion_context_refilter (IdeCompletionContext *self)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_CONTEXT (self));
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      const ProviderInfo *info = &g_array_index (self->providers, ProviderInfo, i);
+
+      if (info->error != NULL)
+        continue;
+
+      if (info->results == NULL)
+        continue;
+
+      ide_completion_provider_refilter (info->provider, self, info->results);
+    }
+}
+
+gboolean
+_ide_completion_context_iter_invalidates (IdeCompletionContext *self,
+                                          const GtkTextIter    *iter)
+{
+  GtkTextIter begin, end;
+  GtkTextBuffer *buffer;
+
+  g_assert (!self || IDE_IS_COMPLETION_CONTEXT (self));
+  g_assert (iter != NULL);
+
+  if (self == NULL)
+    return FALSE;
+
+  buffer = gtk_text_iter_get_buffer (iter);
+
+  gtk_text_buffer_get_iter_at_mark (buffer, &begin, self->begin_mark);
+  gtk_text_buffer_get_iter_at_mark (buffer, &end, self->end_mark);
+
+  return gtk_text_iter_compare (&begin, iter) <= 0 &&
+         gtk_text_iter_compare (&end, iter) >= 0;
+}
+
+/**
+ * ide_completion_context_get_line_text:
+ * @self: a #IdeCompletionContext
+ *
+ * This is a convenience helper to get the line text up until the insertion
+ * cursor for the current completion.
+ *
+ * Returns: a newly allocated string
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_completion_context_get_line_text (IdeCompletionContext *self)
+{
+  GtkTextIter begin, end;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), NULL);
+
+  ide_completion_context_get_bounds (self, &begin, &end);
+  gtk_text_iter_set_line_offset (&begin, 0);
+  return gtk_text_iter_get_slice (&begin, &end);
+}
+
+/**
+ * ide_completion_context_get_language:
+ * @self: a #IdeCompletionContext
+ *
+ * Gets the language identifier which can be useful for providers that support
+ * multiple languages.
+ *
+ * Returns: (nullable): a language identifier or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_completion_context_get_language (IdeCompletionContext *self)
+{
+  GtkTextBuffer *buffer;
+  GtkSourceLanguage *language;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), NULL);
+
+  if (!(buffer = ide_completion_context_get_buffer (self)))
+    return NULL;
+
+  if (!(language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer))))
+    return NULL;
+
+  return gtk_source_language_get_id (language);
+}
+
+/**
+ * ide_completion_context_is_language:
+ * @self: a #IdeCompletionContext
+ *
+ * Helper to check the language of the underlying buffer.
+ *
+ * Returns: %TRUE if @language matches; otherwise %FALSE.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_context_is_language (IdeCompletionContext *self,
+                                    const gchar          *language)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), FALSE);
+
+  return g_strcmp0 (language, ide_completion_context_get_language (self)) == 0;
+}
+
+/**
+ * ide_completion_context_get_activation:
+ * @self: a #IdeCompletionContext
+ *
+ * Gets the mode for which the context was activated.
+ *
+ * Since: 3.32
+ */
+IdeCompletionActivation
+ide_completion_context_get_activation (IdeCompletionContext *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (self), 0);
+
+  return self->activation;
+}
diff --git a/src/libide/sourceview/ide-completion-context.h b/src/libide/sourceview/ide-completion-context.h
new file mode 100644
index 000000000..40110c192
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-context.h
@@ -0,0 +1,76 @@
+/* ide-completion-context.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+#include "ide-completion-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPLETION_CONTEXT (ide_completion_context_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeCompletionContext, ide_completion_context, IDE, COMPLETION_CONTEXT, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeCompletionActivation  ide_completion_context_get_activation             (IdeCompletionContext   *self);
+IDE_AVAILABLE_IN_3_32
+const gchar             *ide_completion_context_get_language               (IdeCompletionContext   *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                 ide_completion_context_is_language                (IdeCompletionContext   *self,
+                                                                            const gchar            
*language);
+IDE_AVAILABLE_IN_3_32
+GtkTextBuffer           *ide_completion_context_get_buffer                 (IdeCompletionContext   *self);
+IDE_AVAILABLE_IN_3_32
+GtkTextView             *ide_completion_context_get_view                   (IdeCompletionContext   *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                 ide_completion_context_get_busy                   (IdeCompletionContext   *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                 ide_completion_context_is_empty                   (IdeCompletionContext   *self);
+IDE_AVAILABLE_IN_3_32
+void                     ide_completion_context_set_proposals_for_provider (IdeCompletionContext   *self,
+                                                                            IdeCompletionProvider  *provider,
+                                                                            GListModel             *results);
+IDE_AVAILABLE_IN_3_32
+IdeCompletion           *ide_completion_context_get_completion             (IdeCompletionContext   *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                 ide_completion_context_get_bounds                 (IdeCompletionContext   *self,
+                                                                            GtkTextIter            *begin,
+                                                                            GtkTextIter            *end);
+IDE_AVAILABLE_IN_3_32
+gboolean                 ide_completion_context_get_start_iter             (IdeCompletionContext   *self,
+                                                                            GtkTextIter            *iter);
+IDE_AVAILABLE_IN_3_32
+gchar                   *ide_completion_context_get_word                   (IdeCompletionContext   *self);
+IDE_AVAILABLE_IN_3_32
+gchar                   *ide_completion_context_get_line_text              (IdeCompletionContext   *self);
+IDE_AVAILABLE_IN_3_32
+gboolean                 ide_completion_context_get_item_full              (IdeCompletionContext   *self,
+                                                                            guint                   position,
+                                                                            IdeCompletionProvider **provider,
+                                                                            IdeCompletionProposal 
**proposal);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-completion-display.c b/src/libide/sourceview/ide-completion-display.c
new file mode 100644
index 000000000..946909d22
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-display.c
@@ -0,0 +1,96 @@
+/* ide-completion-display.c
+ *
+ * Copyright 2018-2019 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 "ide-completion-display"
+
+#include "config.h"
+
+#include "ide-completion-context.h"
+#include "ide-completion-display.h"
+#include "ide-completion-private.h"
+#include "ide-source-view.h"
+
+G_DEFINE_INTERFACE (IdeCompletionDisplay, ide_completion_display, GTK_TYPE_WIDGET)
+
+static void
+ide_completion_display_default_init (IdeCompletionDisplayInterface *iface)
+{
+}
+
+void
+ide_completion_display_set_context (IdeCompletionDisplay *self,
+                                    IdeCompletionContext *context)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_DISPLAY (self));
+  g_return_if_fail (!context || IDE_IS_COMPLETION_CONTEXT (context));
+
+  IDE_COMPLETION_DISPLAY_GET_IFACE (self)->set_context (self, context);
+}
+
+gboolean
+ide_completion_display_key_press_event (IdeCompletionDisplay *self,
+                                        const GdkEventKey    *key)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_DISPLAY (self), FALSE);
+  g_return_val_if_fail (key!= NULL, FALSE);
+
+  return IDE_COMPLETION_DISPLAY_GET_IFACE (self)->key_press_event (self, key);
+}
+
+void
+ide_completion_display_set_n_rows (IdeCompletionDisplay *self,
+                                   guint                 n_rows)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_DISPLAY (self));
+  g_return_if_fail (n_rows > 0);
+  g_return_if_fail (n_rows <= 32);
+
+  IDE_COMPLETION_DISPLAY_GET_IFACE (self)->set_n_rows (self, n_rows);
+}
+
+void
+ide_completion_display_attach (IdeCompletionDisplay *self,
+                               GtkSourceView        *view)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_DISPLAY (self));
+  g_return_if_fail (IDE_IS_SOURCE_VIEW (view));
+
+  IDE_COMPLETION_DISPLAY_GET_IFACE (self)->attach (self, view);
+}
+
+void
+ide_completion_display_move_cursor (IdeCompletionDisplay *self,
+                                    GtkMovementStep       step,
+                                    gint                  count)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_DISPLAY (self));
+
+  IDE_COMPLETION_DISPLAY_GET_IFACE (self)->move_cursor (self, step, count);
+}
+
+void
+_ide_completion_display_set_font_desc (IdeCompletionDisplay       *self,
+                                       const PangoFontDescription *font_desc)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_DISPLAY (self));
+
+  if (IDE_COMPLETION_DISPLAY_GET_IFACE (self)->set_font_desc)
+    IDE_COMPLETION_DISPLAY_GET_IFACE (self)->set_font_desc (self, font_desc);
+}
diff --git a/src/libide/sourceview/ide-completion-display.h b/src/libide/sourceview/ide-completion-display.h
new file mode 100644
index 000000000..80da7a893
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-display.h
@@ -0,0 +1,74 @@
+/* ide-completion-display.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <gtksourceview/gtksource.h>
+
+#include "ide-completion-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPLETION_DISPLAY (ide_completion_display_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeCompletionDisplay, ide_completion_display, IDE, COMPLETION_DISPLAY, GtkWidget)
+
+struct _IdeCompletionDisplayInterface
+{
+  GTypeInterface parent_iface;
+
+  void     (*set_context)     (IdeCompletionDisplay       *self,
+                               IdeCompletionContext       *context);
+  gboolean (*key_press_event) (IdeCompletionDisplay       *self,
+                               const GdkEventKey          *key);
+  void     (*attach)          (IdeCompletionDisplay       *self,
+                               GtkSourceView              *view);
+  void     (*set_font_desc)   (IdeCompletionDisplay       *self,
+                               const PangoFontDescription *font_desc);
+  void     (*set_n_rows)      (IdeCompletionDisplay       *self,
+                               guint                       n_rows);
+  void     (*move_cursor)     (IdeCompletionDisplay       *self,
+                               GtkMovementStep             step,
+                               gint                        count);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_completion_display_attach          (IdeCompletionDisplay *self,
+                                                 GtkSourceView        *view);
+IDE_AVAILABLE_IN_3_32
+void     ide_completion_display_set_context     (IdeCompletionDisplay *self,
+                                                 IdeCompletionContext *context);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_completion_display_key_press_event (IdeCompletionDisplay *self,
+                                                 const GdkEventKey    *key);
+IDE_AVAILABLE_IN_3_32
+void     ide_completion_display_set_n_rows      (IdeCompletionDisplay *self,
+                                                 guint                 n_rows);
+IDE_AVAILABLE_IN_3_32
+void     ide_completion_display_move_cursor     (IdeCompletionDisplay *self,
+                                                 GtkMovementStep       step,
+                                                 gint                  count);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-completion-list-box-row.c 
b/src/libide/sourceview/ide-completion-list-box-row.c
new file mode 100644
index 000000000..31b4fd7fa
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-list-box-row.c
@@ -0,0 +1,369 @@
+/* ide-completion-list-box-row.c
+ *
+ * Copyright 2018-2019 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 "ide-completion-list-box-row"
+
+#include "config.h"
+
+#include "ide-completion-list-box-row.h"
+#include "ide-completion-private.h"
+
+struct _IdeCompletionListBoxRow
+{
+  GtkListBoxRow          parent_instance;
+
+  IdeCompletionProposal *proposal;
+
+  GtkBox                *box;
+  GtkImage              *image;
+  GtkLabel              *left;
+  GtkLabel              *center;
+  GtkLabel              *right;
+};
+
+enum {
+  PROP_0,
+  PROP_PROPOSAL,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeCompletionListBoxRow, ide_completion_list_box_row, GTK_TYPE_LIST_BOX_ROW)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_completion_list_box_row_finalize (GObject *object)
+{
+  IdeCompletionListBoxRow *self = (IdeCompletionListBoxRow *)object;
+
+  g_clear_object (&self->proposal);
+
+  G_OBJECT_CLASS (ide_completion_list_box_row_parent_class)->finalize (object);
+}
+
+static void
+ide_completion_list_box_row_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  IdeCompletionListBoxRow *self = IDE_COMPLETION_LIST_BOX_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROPOSAL:
+      g_value_set_object (value, ide_completion_list_box_row_get_proposal (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_list_box_row_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  IdeCompletionListBoxRow *self = IDE_COMPLETION_LIST_BOX_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROPOSAL:
+      ide_completion_list_box_row_set_proposal (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_list_box_row_class_init (IdeCompletionListBoxRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_completion_list_box_row_finalize;
+  object_class->get_property = ide_completion_list_box_row_get_property;
+  object_class->set_property = ide_completion_list_box_row_set_property;
+
+  /**
+   * IdeCompletionListBoxRow:proposal:
+   *
+   * The proposal to display in the list box row.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_PROPOSAL] =
+    g_param_spec_object ("proposal",
+                         "Proposal",
+                         "The proposal to be displayed",
+                         IDE_TYPE_COMPLETION_PROPOSAL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-sourceview/ui/ide-completion-list-box-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeCompletionListBoxRow, box);
+  gtk_widget_class_bind_template_child (widget_class, IdeCompletionListBoxRow, image);
+  gtk_widget_class_bind_template_child (widget_class, IdeCompletionListBoxRow, left);
+  gtk_widget_class_bind_template_child (widget_class, IdeCompletionListBoxRow, center);
+  gtk_widget_class_bind_template_child (widget_class, IdeCompletionListBoxRow, right);
+}
+
+static void
+ide_completion_list_box_row_init (IdeCompletionListBoxRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget *
+ide_completion_list_box_row_new (void)
+{
+  return g_object_new (IDE_TYPE_COMPLETION_LIST_BOX_ROW, NULL);
+}
+
+/**
+ * ide_completion_list_box_row_get_proposal:
+ * @self: a #IdeCompletionListBoxRow
+ *
+ * Gets the proposal viewed by the row.
+ *
+ * Returns: (transfer none) (nullable): an #IdeCompletionProposal or %NULL
+ *
+ * Since: 3.32
+ */
+IdeCompletionProposal *
+ide_completion_list_box_row_get_proposal (IdeCompletionListBoxRow *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (self), NULL);
+
+  return self->proposal;
+}
+
+/**
+ * ide_completion_list_box_row_set_proposal:
+ * @self: a #IdeCompletionListBoxRow
+ * @proposal: an #IdeCompletionProposal
+ *
+ * Sets the proposal to display in the row.
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_list_box_row_set_proposal (IdeCompletionListBoxRow *self,
+                                          IdeCompletionProposal   *proposal)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (self));
+  g_return_if_fail (!proposal || IDE_IS_COMPLETION_PROPOSAL (proposal));
+
+  if (g_set_object (&self->proposal, proposal))
+    {
+      if (proposal == NULL)
+        {
+          gtk_label_set_label (self->left, NULL);
+          gtk_label_set_label (self->center, NULL);
+          gtk_label_set_label (self->right, NULL);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROPOSAL]);
+    }
+}
+
+/**
+ * ide_completion_list_box_row_set_left:
+ * @self: a #IdeCompletionListBoxRow
+ * @left: (nullable): text for the left column
+ *
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_list_box_row_set_left (IdeCompletionListBoxRow *self,
+                                      const gchar             *left)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (self));
+
+  gtk_label_set_label (self->left, left);
+}
+
+/**
+ * ide_completion_list_box_row_set_left_markup:
+ * @self: a #IdeCompletionListBoxRow
+ * @left_markup: (nullable): markup for the left column
+ *
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_list_box_row_set_left_markup (IdeCompletionListBoxRow *self,
+                                             const gchar             *left_markup)
+{
+  g_autofree gchar *adjusted = NULL;
+
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (self));
+
+  /*
+   * HACK: For some reason labels ending in a <span fgalpha=xxx> span
+   *       cause fgalpha to effect external pango contexts and i have
+   *       no idea how/why that is happening.
+   */
+  if (left_markup != NULL && g_str_has_suffix (left_markup, "</span>"))
+    left_markup = adjusted = g_strdup_printf ("%s ", left_markup);
+
+  gtk_label_set_label (self->left, left_markup);
+  gtk_label_set_use_markup (self->left, TRUE);
+}
+
+/**
+ * ide_completion_list_box_row_set_center:
+ * @self: a #IdeCompletionListBoxRow
+ * @center: (nullable): text for the center column
+ *
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_list_box_row_set_center (IdeCompletionListBoxRow *self,
+                                        const gchar             *center)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (self));
+
+  gtk_label_set_use_markup (self->center, FALSE);
+  gtk_label_set_label (self->center, center);
+}
+
+/**
+ * ide_completion_list_box_row_set_center_markup:
+ * @self: a #IdeCompletionListBoxRow
+ * @center_markup: (nullable): markup for the center column
+ *
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_list_box_row_set_center_markup (IdeCompletionListBoxRow *self,
+                                               const gchar             *center_markup)
+{
+  g_autofree gchar *adjusted = NULL;
+
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (self));
+
+  /*
+   * HACK: For some reason labels ending in a <span fgalpha=xxx> span
+   *       cause fgalpha to effect external pango contexts and i have
+   *       no idea how/why that is happening.
+   */
+  if (center_markup != NULL && g_str_has_suffix (center_markup, "</span>"))
+    center_markup = adjusted = g_strdup_printf ("%s ", center_markup);
+
+  gtk_label_set_label (self->center, center_markup);
+  gtk_label_set_use_markup (self->center, TRUE);
+}
+
+/**
+ * ide_completion_list_box_row_set_right:
+ * @self: a #IdeCompletionListBoxRow
+ * @right: (nullable): text for the right column
+ *
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_list_box_row_set_right (IdeCompletionListBoxRow *self,
+                                       const gchar             *right)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (self));
+
+  gtk_label_set_label (self->right, right);
+}
+
+/**
+ * ide_completion_list_box_row_set_icon_name:
+ * @self: a #IdeCompletionListBoxRow
+ * @icon_name: (nullable): an icon-name or %NULL
+ *
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_list_box_row_set_icon_name (IdeCompletionListBoxRow *self,
+                                           const gchar             *icon_name)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (self));
+
+  g_object_set (self->image,
+                "icon-name", icon_name,
+                NULL);
+}
+
+void
+_ide_completion_list_box_row_attach (IdeCompletionListBoxRow *self,
+                                     GtkSizeGroup            *left,
+                                     GtkSizeGroup            *center,
+                                     GtkSizeGroup            *right)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (self));
+
+  gtk_size_group_add_widget (left, GTK_WIDGET (self->left));
+  gtk_size_group_add_widget (center, GTK_WIDGET (self->center));
+  gtk_size_group_add_widget (right, GTK_WIDGET (self->right));
+}
+
+gint
+_ide_completion_list_box_row_get_x_offset (IdeCompletionListBoxRow *self,
+                                           GtkWidget               *toplevel)
+{
+  GtkStyleContext *style_context;
+  GtkBorder margin;
+  GtkStateFlags flags;
+  gint min, nat;
+  gint x = 0;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (self), 0);
+  g_return_val_if_fail (GTK_IS_WIDGET (toplevel), 0);
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self->image));
+  flags = gtk_style_context_get_state (style_context);
+  gtk_style_context_get_margin (style_context, flags, &margin);
+  gtk_widget_get_preferred_width (GTK_WIDGET (self->image), &min, &nat);
+  x += nat + margin.left + margin.right;
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self->left));
+  flags = gtk_style_context_get_state (style_context);
+  gtk_style_context_get_margin (style_context, flags, &margin);
+  gtk_widget_get_preferred_width (GTK_WIDGET (self->left), &min, &nat);
+  x += nat + margin.left + margin.right;
+
+  return x;
+}
+
+void
+_ide_completion_list_box_row_set_attrs (IdeCompletionListBoxRow *self,
+                                        PangoAttrList           *attrs)
+{
+  g_assert (IDE_IS_COMPLETION_LIST_BOX_ROW (self));
+
+  gtk_label_set_attributes (self->left, attrs);
+  gtk_label_set_attributes (self->center, attrs);
+  gtk_label_set_attributes (self->right, attrs);
+}
diff --git a/src/libide/sourceview/ide-completion-list-box-row.h 
b/src/libide/sourceview/ide-completion-list-box-row.h
new file mode 100644
index 000000000..b945df155
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-list-box-row.h
@@ -0,0 +1,64 @@
+/* ide-completion-list-box-row.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+#include "ide-completion-proposal.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPLETION_LIST_BOX_ROW (ide_completion_list_box_row_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeCompletionListBoxRow, ide_completion_list_box_row, IDE, COMPLETION_LIST_BOX_ROW, 
GtkListBoxRow)
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget             *ide_completion_list_box_row_new               (void);
+IDE_AVAILABLE_IN_3_32
+IdeCompletionProposal *ide_completion_list_box_row_get_proposal      (IdeCompletionListBoxRow *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_completion_list_box_row_set_proposal      (IdeCompletionListBoxRow *self,
+                                                                      IdeCompletionProposal   *proposal);
+IDE_AVAILABLE_IN_3_32
+void                   ide_completion_list_box_row_set_icon_name     (IdeCompletionListBoxRow *self,
+                                                                      const gchar             *icon_name);
+IDE_AVAILABLE_IN_3_32
+void                   ide_completion_list_box_row_set_left          (IdeCompletionListBoxRow *self,
+                                                                      const gchar             *left);
+IDE_AVAILABLE_IN_3_32
+void                   ide_completion_list_box_row_set_left_markup   (IdeCompletionListBoxRow *self,
+                                                                      const gchar             *left_markup);
+IDE_AVAILABLE_IN_3_32
+void                   ide_completion_list_box_row_set_right         (IdeCompletionListBoxRow *self,
+                                                                      const gchar             *right);
+IDE_AVAILABLE_IN_3_32
+void                   ide_completion_list_box_row_set_center        (IdeCompletionListBoxRow *self,
+                                                                      const gchar             *center);
+IDE_AVAILABLE_IN_3_32
+void                   ide_completion_list_box_row_set_center_markup (IdeCompletionListBoxRow *self,
+                                                                      const gchar             
*center_markup);
+
+G_END_DECLS
diff --git a/src/libide/completion/ide-completion-list-box-row.ui 
b/src/libide/sourceview/ide-completion-list-box-row.ui
similarity index 100%
rename from src/libide/completion/ide-completion-list-box-row.ui
rename to src/libide/sourceview/ide-completion-list-box-row.ui
diff --git a/src/libide/sourceview/ide-completion-list-box.c b/src/libide/sourceview/ide-completion-list-box.c
new file mode 100644
index 000000000..2d953fc58
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-list-box.c
@@ -0,0 +1,938 @@
+/* ide-completion-list-box.c
+ *
+ * Copyright 2018-2019 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 "ide-completion-list-box"
+
+#include "config.h"
+
+#include "ide-completion-context.h"
+#include "ide-completion-list-box.h"
+#include "ide-completion-list-box-row.h"
+#include "ide-completion-private.h"
+#include "ide-completion-proposal.h"
+#include "ide-completion-provider.h"
+
+struct _IdeCompletionListBox
+{
+  DzlBin parent_instance;
+
+  /* The box containing the rows. */
+  GtkBox *box;
+
+  /* The event box for button press events */
+  GtkEventBox *events;
+
+  /* Font stylign for rows */
+  PangoAttrList *font_attrs;
+
+  /*
+   * The completion context that is being displayed.
+   */
+  IdeCompletionContext *context;
+
+  /*
+   * The handler for IdeCompletionContecxt::items-chaged which should
+   * be disconnected when no longer needed.
+   */
+  gulong items_changed_handler;
+
+  /*
+   * The number of rows we expect to have visible to the user.
+   */
+  guint n_rows;
+
+  /*
+   * The currently selected index within the result set. Signed to
+   * ensure our math in various places allows going negative to catch
+   * lower edge.
+   */
+  gint selected;
+
+  /*
+   * This is set whenever we make a change that requires updating the
+   * row content. We delay the update until the next frame callback so
+   * that we only update once right before we draw the frame. This helps
+   * reduce duplicate work when reacting to ::items-changed in the model.
+   */
+  guint queued_update;
+
+  /*
+   * These size groups are used to keep each portion of the proposal row
+   * aligned with each other. Since we only have a fixed number of visible
+   * rows, the overhead here is negligable by introducing the size cycle.
+   */
+  GtkSizeGroup *left_size_group;
+  GtkSizeGroup *center_size_group;
+  GtkSizeGroup *right_size_group;
+
+  /*
+   * The adjustments for scrolling the GtkScrollable.
+   */
+  GtkAdjustment *hadjustment;
+  GtkAdjustment *vadjustment;
+
+  /*
+   * Gesture to handle button press/touch events.
+   */
+  GtkGesture *multipress_gesture;
+};
+
+typedef struct
+{
+  IdeCompletionListBox *self;
+  IdeCompletionContext *context;
+  guint n_items;
+  guint position;
+  guint selected;
+} UpdateState;
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_PROPOSAL,
+  PROP_N_ROWS,
+  PROP_HADJUSTMENT,
+  PROP_HSCROLL_POLICY,
+  PROP_VADJUSTMENT,
+  PROP_VSCROLL_POLICY,
+  N_PROPS
+};
+
+enum {
+  REPOSITION,
+  N_SIGNALS
+};
+
+static void ide_completion_list_box_queue_update (IdeCompletionListBox *self);
+
+G_DEFINE_TYPE_WITH_CODE (IdeCompletionListBox, ide_completion_list_box, DZL_TYPE_BIN,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static guint
+ide_completion_list_box_get_offset (IdeCompletionListBox *self)
+{
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+
+  return gtk_adjustment_get_value (self->vadjustment);
+}
+
+static void
+ide_completion_list_box_set_offset (IdeCompletionListBox *self,
+                                    guint                 offset)
+{
+  gdouble value = offset;
+  gdouble page_size;
+  gdouble upper;
+  gdouble lower;
+
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+
+  lower = gtk_adjustment_get_lower (self->vadjustment);
+  upper = gtk_adjustment_get_upper (self->vadjustment);
+  page_size = gtk_adjustment_get_page_size (self->vadjustment);
+
+  if (value > (upper - page_size))
+    value = upper - page_size;
+
+  if (value < lower)
+    value = lower;
+
+  gtk_adjustment_set_value (self->vadjustment, value);
+}
+
+static void
+ide_completion_list_box_value_changed (IdeCompletionListBox *self,
+                                       GtkAdjustment        *vadj)
+{
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+  g_assert (GTK_IS_ADJUSTMENT (vadj));
+
+  ide_completion_list_box_queue_update (self);
+}
+
+static void
+ide_completion_list_box_set_hadjustment (IdeCompletionListBox *self,
+                                         GtkAdjustment        *hadjustment)
+{
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+  g_assert (!hadjustment || GTK_IS_ADJUSTMENT (hadjustment));
+
+  if (g_set_object (&self->hadjustment, hadjustment))
+    ide_completion_list_box_queue_update (self);
+}
+
+static void
+ide_completion_list_box_set_vadjustment (IdeCompletionListBox *self,
+                                         GtkAdjustment        *vadjustment)
+{
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+  g_assert (!vadjustment || GTK_IS_ADJUSTMENT (vadjustment));
+
+  if (self->vadjustment == vadjustment)
+    return;
+
+  if (self->vadjustment)
+    {
+      g_signal_handlers_disconnect_by_func (self->vadjustment,
+                                            G_CALLBACK (ide_completion_list_box_value_changed),
+                                            self);
+      g_clear_object (&self->vadjustment);
+    }
+
+  if (vadjustment)
+    {
+      self->vadjustment = g_object_ref (vadjustment);
+
+      gtk_adjustment_set_lower (self->vadjustment, 0);
+      gtk_adjustment_set_upper (self->vadjustment, 0);
+      gtk_adjustment_set_value (self->vadjustment, 0);
+      gtk_adjustment_set_step_increment (self->vadjustment, 1);
+      gtk_adjustment_set_page_size (self->vadjustment, self->n_rows);
+      gtk_adjustment_set_page_increment (self->vadjustment, self->n_rows);
+
+      g_signal_connect_object (self->vadjustment,
+                               "value-changed",
+                               G_CALLBACK (ide_completion_list_box_value_changed),
+                               self,
+                               G_CONNECT_SWAPPED);
+    }
+
+  ide_completion_list_box_queue_update (self);
+}
+
+static void
+ide_completion_list_box_add (GtkContainer *container,
+                             GtkWidget    *widget)
+{
+  IdeCompletionListBox *self = (IdeCompletionListBox *)container;
+
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if (IDE_IS_COMPLETION_LIST_BOX_ROW (widget))
+    gtk_container_add (GTK_CONTAINER (self->box), widget);
+  else
+    GTK_CONTAINER_CLASS (ide_completion_list_box_parent_class)->add (container, widget);
+}
+
+static guint
+get_row_at_y (IdeCompletionListBox *self,
+              gdouble               y)
+{
+  GtkAllocation alloc;
+  guint offset;
+  guint n_items;
+
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+  g_assert (G_IS_LIST_MODEL (self->context));
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  offset = ide_completion_list_box_get_offset (self);
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->context));
+  n_items = MAX (1, MIN (self->n_rows, n_items));
+
+  return offset + (y / (alloc.height / n_items));
+}
+
+static void
+multipress_gesture_pressed (GtkGestureMultiPress *gesture,
+                            guint                 n_press,
+                            gdouble               x,
+                            gdouble               y,
+                            IdeCompletionListBox *self)
+{
+  g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+
+  if (self->context == NULL)
+    return;
+
+  self->selected = get_row_at_y (self, y);
+  ide_completion_list_box_queue_update (self);
+}
+
+static void
+multipress_gesture_released (GtkGestureMultiPress    *gesture,
+                             guint                    n_press,
+                             gdouble                  x,
+                             gdouble                  y,
+                             IdeCompletionListBoxRow *self)
+{
+  g_assert (GTK_IS_GESTURE_MULTI_PRESS (gesture));
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+
+}
+
+static void
+ide_completion_list_box_constructed (GObject *object)
+{
+  IdeCompletionListBox *self = (IdeCompletionListBox *)object;
+
+  G_OBJECT_CLASS (ide_completion_list_box_parent_class)->constructed (object);
+
+  if (self->hadjustment == NULL)
+    self->hadjustment = gtk_adjustment_new (0, 0, 0, 0, 0, 0);
+
+  if (self->vadjustment == NULL)
+    self->vadjustment = gtk_adjustment_new (0, 0, 0, 0, 0, 0);
+
+  gtk_adjustment_set_lower (self->hadjustment, 0);
+  gtk_adjustment_set_upper (self->hadjustment, 0);
+  gtk_adjustment_set_value (self->hadjustment, 0);
+
+  ide_completion_list_box_queue_update (self);
+}
+
+static void
+ide_completion_list_box_finalize (GObject *object)
+{
+  IdeCompletionListBox *self = (IdeCompletionListBox *)object;
+
+  g_clear_object (&self->multipress_gesture);
+  g_clear_object (&self->left_size_group);
+  g_clear_object (&self->center_size_group);
+  g_clear_object (&self->right_size_group);
+  g_clear_object (&self->hadjustment);
+  g_clear_object (&self->vadjustment);
+  g_clear_pointer (&self->font_attrs, pango_attr_list_unref);
+
+  G_OBJECT_CLASS (ide_completion_list_box_parent_class)->finalize (object);
+}
+
+static void
+ide_completion_list_box_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeCompletionListBox *self = IDE_COMPLETION_LIST_BOX (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, ide_completion_list_box_get_context (self));
+      break;
+
+    case PROP_PROPOSAL:
+      g_value_take_object (value, ide_completion_list_box_get_proposal (self));
+      break;
+
+    case PROP_N_ROWS:
+      g_value_set_uint (value, ide_completion_list_box_get_n_rows (self));
+      break;
+
+    case PROP_HADJUSTMENT:
+      g_value_set_object (value, self->hadjustment);
+      break;
+
+    case PROP_VADJUSTMENT:
+      g_value_set_object (value, self->vadjustment);
+      break;
+
+    case PROP_HSCROLL_POLICY:
+      g_value_set_enum (value, GTK_SCROLL_NATURAL);
+      break;
+
+    case PROP_VSCROLL_POLICY:
+      g_value_set_enum (value, GTK_SCROLL_NATURAL);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_list_box_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeCompletionListBox *self = IDE_COMPLETION_LIST_BOX (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      ide_completion_list_box_set_context (self, g_value_get_object (value));
+      break;
+
+    case PROP_N_ROWS:
+      ide_completion_list_box_set_n_rows (self, g_value_get_uint (value));
+      break;
+
+    case PROP_HADJUSTMENT:
+      ide_completion_list_box_set_hadjustment (self, g_value_get_object (value));
+      break;
+
+    case PROP_VADJUSTMENT:
+      ide_completion_list_box_set_vadjustment (self, g_value_get_object (value));
+      break;
+
+    case PROP_HSCROLL_POLICY:
+      /* Do nothing */
+      break;
+
+    case PROP_VSCROLL_POLICY:
+      /* Do nothing */
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_list_box_class_init (IdeCompletionListBoxClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->constructed = ide_completion_list_box_constructed;
+  object_class->finalize = ide_completion_list_box_finalize;
+  object_class->get_property = ide_completion_list_box_get_property;
+  object_class->set_property = ide_completion_list_box_set_property;
+
+  container_class->add = ide_completion_list_box_add;
+
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The context being displayed",
+                         IDE_TYPE_COMPLETION_CONTEXT,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HADJUSTMENT] =
+    g_param_spec_object ("hadjustment", NULL, NULL,
+                         GTK_TYPE_ADJUSTMENT,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HSCROLL_POLICY] =
+    g_param_spec_enum ("hscroll-policy", NULL, NULL,
+                       GTK_TYPE_SCROLLABLE_POLICY,
+                       GTK_SCROLL_NATURAL,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_VADJUSTMENT] =
+    g_param_spec_object ("vadjustment", NULL, NULL,
+                         GTK_TYPE_ADJUSTMENT,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_VSCROLL_POLICY] =
+    g_param_spec_enum ("vscroll-policy", NULL, NULL,
+                       GTK_TYPE_SCROLLABLE_POLICY,
+                       GTK_SCROLL_NATURAL,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PROPOSAL] =
+    g_param_spec_object ("proposal",
+                         "Proposal",
+                         "The proposal that is currently selected",
+                         IDE_TYPE_COMPLETION_PROPOSAL,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_N_ROWS] =
+    g_param_spec_uint ("n-rows",
+                       "N Rows",
+                       "The number of visible rows",
+                       1, 32, 5,
+                       (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_EXPLICIT_NOTIFY | 
G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [REPOSITION] =
+    g_signal_new_class_handler ("reposition",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                NULL, NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [REPOSITION],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+
+  gtk_widget_class_set_css_name (widget_class, "list");
+}
+
+static void
+ide_completion_list_box_init (IdeCompletionListBox *self)
+{
+  self->events = g_object_new (GTK_TYPE_EVENT_BOX,
+                               "visible", TRUE,
+                               NULL);
+  gtk_widget_add_events (GTK_WIDGET (self->events), GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK);
+  g_signal_connect (self->events,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->events);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->events));
+
+  self->box = g_object_new (GTK_TYPE_BOX,
+                            "orientation", GTK_ORIENTATION_VERTICAL,
+                            "visible", TRUE,
+                            NULL);
+  g_signal_connect (self->box,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->box);
+  gtk_container_add (GTK_CONTAINER (self->events), GTK_WIDGET (self->box));
+
+  self->left_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+  self->center_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+  self->right_size_group = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
+
+  self->multipress_gesture = gtk_gesture_multi_press_new (GTK_WIDGET (self->events));
+  gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (self->multipress_gesture), 
GTK_PHASE_BUBBLE);
+  gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (self->multipress_gesture), FALSE);
+  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (self->multipress_gesture), GDK_BUTTON_PRIMARY);
+  g_signal_connect_object (self->multipress_gesture, "pressed",
+                           G_CALLBACK (multipress_gesture_pressed), self, 0);
+  g_signal_connect_object (self->multipress_gesture, "released",
+                           G_CALLBACK (multipress_gesture_released), self, 0);
+}
+
+static void
+ide_completion_list_box_update_row_cb (GtkWidget *widget,
+                                       gpointer   user_data)
+{
+  g_autoptr(IdeCompletionProposal) proposal = NULL;
+  g_autoptr(IdeCompletionProvider) provider = NULL;
+  UpdateState *state = user_data;
+
+  g_assert (IDE_IS_COMPLETION_LIST_BOX_ROW (widget));
+  g_assert (state != NULL);
+
+  if (state->position == state->selected)
+    gtk_widget_set_state_flags (widget, GTK_STATE_FLAG_SELECTED, FALSE);
+  else
+    gtk_widget_unset_state_flags (widget, GTK_STATE_FLAG_SELECTED);
+
+  if (state->context != NULL && state->position < state->n_items)
+    ide_completion_context_get_item_full (state->context, state->position, &provider, &proposal);
+
+  ide_completion_list_box_row_set_proposal (IDE_COMPLETION_LIST_BOX_ROW (widget), proposal);
+
+  if (provider && proposal)
+    {
+      g_autofree gchar *typed_text = NULL;
+      GtkTextIter begin, end;
+
+      if (ide_completion_context_get_bounds (state->context, &begin, &end))
+        typed_text = gtk_text_iter_get_slice (&begin, &end);
+
+      ide_completion_provider_display_proposal (provider,
+                                                IDE_COMPLETION_LIST_BOX_ROW (widget),
+                                                state->context,
+                                                typed_text,
+                                                proposal);
+    }
+
+  gtk_widget_set_visible (widget, proposal != NULL);
+
+  state->position++;
+}
+
+static gboolean
+ide_completion_list_box_update_cb (GtkWidget     *widget,
+                                   GdkFrameClock *frame_clock,
+                                   gpointer       user_data)
+{
+  IdeCompletionListBox *self = (IdeCompletionListBox *)widget;
+  UpdateState state = {0};
+
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+  g_assert (GDK_IS_FRAME_CLOCK (frame_clock));
+
+  state.self = self;
+  state.context = self->context;
+  state.position = ide_completion_list_box_get_offset (self);
+  state.selected = self->selected;
+
+  if (self->context != NULL)
+    state.n_items = g_list_model_get_n_items (G_LIST_MODEL (self->context));
+
+  state.position = MIN (state.position, MAX (state.n_items, self->n_rows) - self->n_rows);
+  state.selected = MIN (self->selected, state.n_items ? state.n_items - 1 : 0);
+
+  if (gtk_adjustment_get_upper (self->vadjustment) != state.n_items)
+    gtk_adjustment_set_upper (self->vadjustment, state.n_items);
+
+  gtk_container_foreach (GTK_CONTAINER (self->box),
+                         ide_completion_list_box_update_row_cb,
+                         &state);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROPOSAL]);
+
+  g_signal_emit (self, signals [REPOSITION], 0);
+
+  /* Do this last so that we block any follow-up queue_updates */
+  self->queued_update = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_completion_list_box_queue_update (IdeCompletionListBox *self)
+{
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+
+  if (self->queued_update == 0)
+    {
+      self->queued_update = gtk_widget_add_tick_callback (GTK_WIDGET (self),
+                                                          ide_completion_list_box_update_cb,
+                                                          NULL, NULL);
+      gtk_widget_queue_resize (GTK_WIDGET (self));
+    }
+}
+
+GtkWidget *
+ide_completion_list_box_new (void)
+{
+  return g_object_new (IDE_TYPE_COMPLETION_LIST_BOX, NULL);
+}
+
+guint
+ide_completion_list_box_get_n_rows (IdeCompletionListBox *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), 0);
+
+  return self->n_rows;
+}
+
+void
+ide_completion_list_box_set_n_rows (IdeCompletionListBox *self,
+                                    guint                 n_rows)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX (self));
+  g_return_if_fail (n_rows > 0);
+  g_return_if_fail (n_rows <= 32);
+
+  if (n_rows != self->n_rows)
+    {
+      gtk_container_foreach (GTK_CONTAINER (self->box),
+                             (GtkCallback)gtk_widget_destroy,
+                             NULL);
+
+      self->n_rows = n_rows;
+
+      if (self->vadjustment != NULL)
+        gtk_adjustment_set_page_size (self->vadjustment, n_rows);
+
+      for (guint i = 0; i < n_rows; i++)
+        {
+          GtkWidget *row = ide_completion_list_box_row_new ();
+
+          _ide_completion_list_box_row_attach (IDE_COMPLETION_LIST_BOX_ROW (row),
+                                               self->left_size_group,
+                                               self->center_size_group,
+                                               self->right_size_group);
+          _ide_completion_list_box_row_set_attrs (IDE_COMPLETION_LIST_BOX_ROW (row),
+                                                  self->font_attrs);
+          gtk_container_add (GTK_CONTAINER (self), row);
+        }
+
+      ide_completion_list_box_queue_update (self);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_N_ROWS]);
+    }
+}
+
+/**
+ * ide_completion_list_box_get_proposal:
+ * @self: a #IdeCompletionListBox
+ *
+ * Gets the currently selected proposal, or %NULL if no proposal is selected
+ *
+ * Returns: (nullable) (transfer full): a #IdeCompletionProposal or %NULL
+ *
+ * Since: 3.32
+ */
+IdeCompletionProposal *
+ide_completion_list_box_get_proposal (IdeCompletionListBox *self)
+{
+  IdeCompletionProposal *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), NULL);
+
+  if (self->context != NULL &&
+      self->selected < g_list_model_get_n_items (G_LIST_MODEL (self->context)))
+    ret = g_list_model_get_item (G_LIST_MODEL (self->context), self->selected);
+
+  g_return_val_if_fail (!ret || IDE_IS_COMPLETION_PROPOSAL (ret), NULL);
+
+  return ret;
+}
+
+/**
+ * ide_completion_list_box_get_selected:
+ * @self: an #IdeCompletionListBox
+ * @provider: (out) (transfer full) (optional): a location for the provider
+ * @proposal: (out) (transfer full) (optional): a location for the proposal
+ *
+ * Gets the selected item if there is any.
+ *
+ * If there is a selection, %TRUE is returned and @provider and @proposal
+ * are set to the selected provider/proposal.
+ *
+ * Returns: %TRUE if there is a selection
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_list_box_get_selected (IdeCompletionListBox   *self,
+                                      IdeCompletionProvider **provider,
+                                      IdeCompletionProposal **proposal)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), FALSE);
+
+  if (self->context != NULL)
+    {
+      guint n_items = g_list_model_get_n_items (G_LIST_MODEL (self->context));
+
+      if (n_items > 0)
+        {
+          guint selected = MIN (self->selected, n_items - 1);
+          ide_completion_context_get_item_full (self->context, selected, provider, proposal);
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_completion_list_box_get_context:
+ * @self: a #IdeCompletionListBox
+ *
+ * Gets the context that is being displayed in the list box.
+ *
+ * Returns: (transfer none) (nullable): an #IdeCompletionContext or %NULL
+ *
+ * Since: 3.32
+ */
+IdeCompletionContext *
+ide_completion_list_box_get_context (IdeCompletionListBox *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), NULL);
+
+  return self->context;
+}
+
+static void
+ide_completion_list_box_items_changed_cb (IdeCompletionListBox *self,
+                                          guint                 position,
+                                          guint                 removed,
+                                          guint                 added,
+                                          GListModel           *model)
+{
+  guint offset;
+
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (self));
+  g_assert (G_IS_LIST_MODEL (model));
+
+  offset = ide_completion_list_box_get_offset (self);
+
+  /* Skip widget resize if results are not visible */
+  if (position >= offset + self->n_rows)
+    return;
+
+  ide_completion_list_box_queue_update (self);
+}
+
+/**
+ * ide_completion_list_box_set_context:
+ * @self: a #IdeCompletionListBox
+ * @context: the #IdeCompletionContext
+ *
+ * Sets the context to be displayed.
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_list_box_set_context (IdeCompletionListBox *self,
+                                     IdeCompletionContext *context)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX (self));
+  g_return_if_fail (!context || IDE_IS_COMPLETION_CONTEXT (context));
+
+  if (self->context == context)
+    return;
+
+  if (self->context != NULL && self->items_changed_handler != 0)
+    {
+      g_signal_handler_disconnect (self->context, self->items_changed_handler);
+      self->items_changed_handler = 0;
+    }
+
+  g_set_object (&self->context, context);
+
+  if (self->context != NULL)
+    self->items_changed_handler =
+      g_signal_connect_object (self->context,
+                               "items-changed",
+                               G_CALLBACK (ide_completion_list_box_items_changed_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+  self->selected = 0;
+  gtk_adjustment_set_value (self->vadjustment, 0);
+
+  ide_completion_list_box_queue_update (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]);
+}
+
+static void
+get_first_cb (GtkWidget *widget,
+              gpointer   user_data)
+{
+  GtkWidget **row = user_data;
+
+  if (*row == NULL)
+    *row = widget;
+}
+
+IdeCompletionListBoxRow *
+_ide_completion_list_box_get_first (IdeCompletionListBox *self)
+{
+  IdeCompletionListBoxRow *row = NULL;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), NULL);
+
+  gtk_container_foreach (GTK_CONTAINER (self->box), get_first_cb, &row);
+
+  return row;
+}
+
+void
+ide_completion_list_box_move_cursor (IdeCompletionListBox *self,
+                                     GtkMovementStep       step,
+                                     gint                  direction)
+{
+  gint n_items;
+  gint offset;
+
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX (self));
+
+  if (self->context == NULL || direction == 0)
+    return;
+
+  if (!(n_items = g_list_model_get_n_items (G_LIST_MODEL (self->context))))
+    return;
+
+  /* n_items is signed so that we can do negative comparison */
+  if (n_items < 0)
+    return;
+
+  if (step == GTK_MOVEMENT_BUFFER_ENDS)
+    {
+      if (direction > 0)
+        {
+          ide_completion_list_box_set_offset (self, n_items);
+          self->selected = n_items - 1;
+        }
+      else
+        {
+          ide_completion_list_box_set_offset (self, 0);
+          self->selected = 0;
+        }
+
+      ide_completion_list_box_queue_update (self);
+
+      return;
+    }
+
+  if (direction < 0 && self->selected == 0)
+    return;
+
+  if (direction > 0 && self->selected == n_items - 1)
+    return;
+
+  if (step == GTK_MOVEMENT_PAGES)
+    direction *= self->n_rows;
+
+  if ((self->selected + direction) > n_items)
+    self->selected = n_items - 1;
+  else if ((self->selected + direction) < 0)
+    self->selected = 0;
+  else
+    self->selected += direction;
+
+  offset = ide_completion_list_box_get_offset (self);
+
+  if (self->selected < offset)
+    ide_completion_list_box_set_offset (self, self->selected);
+  else if (self->selected >= (offset + self->n_rows))
+    ide_completion_list_box_set_offset (self, self->selected - self->n_rows + 1);
+
+  ide_completion_list_box_queue_update (self);
+}
+
+gboolean
+_ide_completion_list_box_key_activates (IdeCompletionListBox *self,
+                                        const GdkEventKey    *key)
+{
+  g_autoptr(IdeCompletionProvider) provider = NULL;
+  g_autoptr(IdeCompletionProposal) proposal = NULL;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_LIST_BOX (self), FALSE);
+  g_return_val_if_fail (key != NULL, FALSE);
+
+  if (ide_completion_list_box_get_selected (self, &provider, &proposal))
+    {
+      if (ide_completion_provider_key_activates (provider, proposal, key))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+update_font_desc (GtkWidget *widget,
+                  gpointer   user_data)
+{
+  PangoAttrList *attrs = user_data;
+
+  if (IDE_IS_COMPLETION_LIST_BOX_ROW (widget))
+    _ide_completion_list_box_row_set_attrs (IDE_COMPLETION_LIST_BOX_ROW (widget), attrs);
+}
+
+void
+_ide_completion_list_box_set_font_desc (IdeCompletionListBox       *self,
+                                        const PangoFontDescription *font_desc)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX (self));
+
+  g_clear_pointer (&self->font_attrs, pango_attr_list_unref);
+
+  if (font_desc)
+    {
+      self->font_attrs = pango_attr_list_new ();
+      if (font_desc)
+        pango_attr_list_insert (self->font_attrs, pango_attr_font_desc_new (font_desc));
+    }
+
+  gtk_container_foreach (GTK_CONTAINER (self->box), update_font_desc, self->font_attrs);
+}
diff --git a/src/libide/sourceview/ide-completion-list-box.h b/src/libide/sourceview/ide-completion-list-box.h
new file mode 100644
index 000000000..193ffa806
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-list-box.h
@@ -0,0 +1,56 @@
+/* ide-completion-list-box.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+#include <gtk/gtk.h>
+
+#include "ide-completion-context.h"
+#include "ide-completion-proposal.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPLETION_LIST_BOX (ide_completion_list_box_get_type())
+
+
+G_DECLARE_FINAL_TYPE (IdeCompletionListBox, ide_completion_list_box, IDE, COMPLETION_LIST_BOX, DzlBin)
+
+
+GtkWidget             *ide_completion_list_box_new          (void);
+IdeCompletionContext  *ide_completion_list_box_get_context  (IdeCompletionListBox   *self);
+void                   ide_completion_list_box_set_context  (IdeCompletionListBox   *self,
+                                                             IdeCompletionContext   *context);
+guint                  ide_completion_list_box_get_n_rows   (IdeCompletionListBox   *self);
+void                   ide_completion_list_box_set_n_rows   (IdeCompletionListBox   *self,
+                                                             guint                   n_rows);
+IdeCompletionProposal *ide_completion_list_box_get_proposal (IdeCompletionListBox   *self);
+gboolean               ide_completion_list_box_get_selected (IdeCompletionListBox   *self,
+                                                             IdeCompletionProvider **provider,
+                                                             IdeCompletionProposal **proposal);
+void                   ide_completion_list_box_move_cursor  (IdeCompletionListBox   *self,
+                                                             GtkMovementStep         step,
+                                                             gint                    direction);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-completion-overlay.c b/src/libide/sourceview/ide-completion-overlay.c
new file mode 100644
index 000000000..038737734
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-overlay.c
@@ -0,0 +1,330 @@
+/* ide-completion-overlay.c
+ *
+ * Copyright 2018-2019 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 "ide-completion-overlay"
+
+#include "config.h"
+
+#include "ide-completion-display.h"
+#include "ide-completion-overlay.h"
+#include "ide-completion-private.h"
+#include "ide-completion-proposal.h"
+#include "ide-completion-provider.h"
+#include "ide-completion-view.h"
+
+struct _IdeCompletionOverlay
+{
+  DzlBin             parent_instance;
+  IdeCompletionView *view;
+};
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  N_PROPS
+};
+
+static void completion_display_iface_init (IdeCompletionDisplayInterface *);
+
+G_DEFINE_TYPE_WITH_CODE (IdeCompletionOverlay, ide_completion_overlay, GTK_TYPE_BIN,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_COMPLETION_DISPLAY,
+                                                completion_display_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_completion_overlay_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  IdeCompletionOverlay *self = IDE_COMPLETION_OVERLAY (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, ide_completion_view_get_context (self->view));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_overlay_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  IdeCompletionOverlay *self = IDE_COMPLETION_OVERLAY (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      ide_completion_view_set_context (self->view, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_overlay_class_init (IdeCompletionOverlayClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_completion_overlay_get_property;
+  object_class->set_property = ide_completion_overlay_set_property;
+
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The context to be displayed",
+                         IDE_TYPE_COMPLETION_CONTEXT,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "completionoverlay");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-sourceview/ui/ide-completion-overlay.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeCompletionOverlay, view);
+
+  g_type_ensure (IDE_TYPE_COMPLETION_VIEW);
+}
+
+static void
+ide_completion_overlay_init (IdeCompletionOverlay *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_widget_set_can_focus (GTK_WIDGET (self), FALSE);
+
+  g_signal_connect_swapped (self->view,
+                            "reposition",
+                            G_CALLBACK (gtk_widget_queue_resize),
+                            self);
+}
+
+IdeCompletionOverlay *
+_ide_completion_overlay_new (void)
+{
+  return g_object_new (IDE_TYPE_COMPLETION_OVERLAY, NULL);
+}
+
+static gboolean
+ide_completion_overlay_get_child_position_cb (IdeCompletionOverlay *self,
+                                              GtkWidget            *widget,
+                                              GdkRectangle         *out_rect,
+                                              GtkOverlay           *overlay)
+{
+  IdeCompletionContext *context;
+  GtkStyleContext *style_context;
+  GdkRectangle begin_rect, end_rect, rect;
+  GtkTextIter begin, end;
+  GtkBorder border;
+  GtkAllocation alloc;
+  GtkStateFlags flags;
+  GtkRequisition min, nat;
+  GtkTextView *view;
+  gint x_offset = 0;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_OVERLAY (self), FALSE);
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE);
+  g_return_val_if_fail (GTK_IS_OVERLAY (overlay), FALSE);
+  g_return_val_if_fail (out_rect != NULL, FALSE);
+
+  if (widget != GTK_WIDGET (self))
+    return FALSE;
+
+  if (!(context = ide_completion_view_get_context (self->view)))
+    return FALSE;
+
+  gtk_widget_get_allocation (GTK_WIDGET (overlay), &alloc);
+
+  view = ide_completion_context_get_view (context);
+
+  gtk_widget_get_preferred_size (widget, &min, &nat);
+
+  ide_completion_context_get_bounds (context, &begin, &end);
+
+  gtk_text_view_get_iter_location (view, &begin, &begin_rect);
+  gtk_text_view_get_iter_location (view, &end, &end_rect);
+  gtk_text_view_buffer_to_window_coords (view,
+                                         GTK_TEXT_WINDOW_WIDGET,
+                                         begin_rect.x, begin_rect.y,
+                                         &begin_rect.x, &begin_rect.y);
+  gtk_text_view_buffer_to_window_coords (view,
+                                         GTK_TEXT_WINDOW_WIDGET,
+                                         end_rect.x, end_rect.y,
+                                         &end_rect.x, &end_rect.y);
+  gdk_rectangle_union (&begin_rect, &end_rect, &rect);
+  gtk_widget_translate_coordinates (GTK_WIDGET (view), GTK_WIDGET (overlay),
+                                    rect.x, rect.y,
+                                    &rect.x, &rect.y);
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self->view));
+  flags = gtk_style_context_get_state (style_context);
+  gtk_style_context_get_margin (style_context, flags, &border);
+
+  x_offset = _ide_completion_view_get_x_offset (self->view);
+
+/* TODO: Figure out where 11 is coming from */
+#define EXTRA_SHIFT 11
+  x_offset -= EXTRA_SHIFT;
+
+  out_rect->x = rect.x - x_offset - border.left;
+  out_rect->y = rect.y + rect.height;
+  out_rect->height = nat.height;
+  out_rect->width = nat.width;
+
+  /*
+   * If we can keep the position in place by using the minimum size (or
+   * larger up to the overlay bounds), then prefer to do that before shifting.
+   */
+  if (out_rect->x + out_rect->width > alloc.width)
+    {
+      if (out_rect->x + min.width <= alloc.width)
+        out_rect->width = alloc.width - out_rect->x;
+      else
+        out_rect->width = alloc.width - min.width;
+    }
+
+  if (out_rect->x < 0)
+    {
+      out_rect->x = 0;
+      if (out_rect->width > alloc.width)
+        out_rect->width = alloc.width;
+    }
+
+  if (out_rect->y + out_rect->height > alloc.height)
+    out_rect->y = rect.y - out_rect->height;
+
+#if 0
+  g_print ("Position: %d,%d  %dx%d\n",
+           out_rect->x,
+           out_rect->y,
+           out_rect->width,
+           out_rect->height);
+#endif
+
+  return TRUE;
+}
+
+static void
+ide_completion_overlay_attach (IdeCompletionDisplay *display,
+                               GtkSourceView        *view)
+{
+  IdeCompletionOverlay *self = (IdeCompletionOverlay *)display;
+  GtkOverlay *overlay = NULL;
+  GtkWidget *widget = (GtkWidget *)view;
+
+  g_assert (IDE_IS_COMPLETION_OVERLAY (self));
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  while ((widget = gtk_widget_get_ancestor (widget, GTK_TYPE_OVERLAY)))
+    {
+      overlay = GTK_OVERLAY (widget);
+      widget = gtk_widget_get_parent (widget);
+    }
+
+  if (overlay == NULL)
+    {
+      g_critical ("IdeCompletion requires a GtkOverlay to attach the completion "
+                  "window due to resize restrictions in windowing systems");
+      return;
+    }
+
+  gtk_overlay_add_overlay (overlay, GTK_WIDGET (self));
+
+  g_signal_connect_object (overlay,
+                           "get-child-position",
+                           G_CALLBACK (ide_completion_overlay_get_child_position_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+ide_completion_overlay_set_n_rows (IdeCompletionDisplay *display,
+                                   guint                 n_rows)
+{
+  g_assert (IDE_IS_COMPLETION_OVERLAY (display));
+  g_assert (n_rows > 0);
+  g_assert (n_rows <= 32);
+
+  _ide_completion_view_set_n_rows (IDE_COMPLETION_OVERLAY (display)->view, n_rows);
+}
+
+static gboolean
+ide_completion_overlay_key_press_event (IdeCompletionDisplay *display,
+                                        const GdkEventKey    *event)
+{
+  IdeCompletionOverlay *self = (IdeCompletionOverlay *)display;
+
+  g_assert (IDE_IS_COMPLETION_OVERLAY (self));
+  g_assert (event != NULL);
+
+  return _ide_completion_view_handle_key_press (self->view, event);
+}
+
+static void
+ide_completion_overlay_set_context (IdeCompletionDisplay *display,
+                                    IdeCompletionContext *context)
+{
+  IdeCompletionOverlay *self = (IdeCompletionOverlay *)display;
+
+  g_return_if_fail (IDE_IS_COMPLETION_OVERLAY (self));
+  g_return_if_fail (!context || IDE_IS_COMPLETION_CONTEXT (context));
+
+  ide_completion_view_set_context (self->view, context);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]);
+}
+
+static void
+ide_completion_overlay_move_cursor (IdeCompletionDisplay *display,
+                                    GtkMovementStep       step,
+                                    gint                  count)
+{
+  g_assert (IDE_IS_COMPLETION_OVERLAY (display));
+
+  _ide_completion_view_move_cursor (IDE_COMPLETION_OVERLAY (display)->view, step, count);
+}
+
+static void
+ide_completion_overlay_set_font_desc (IdeCompletionDisplay       *display,
+                                      const PangoFontDescription *font_desc)
+{
+  g_assert (IDE_IS_COMPLETION_OVERLAY (display));
+
+  _ide_completion_view_set_font_desc (IDE_COMPLETION_OVERLAY (display)->view, font_desc);
+}
+
+static void
+completion_display_iface_init (IdeCompletionDisplayInterface *iface)
+{
+  iface->set_context = ide_completion_overlay_set_context;
+  iface->attach = ide_completion_overlay_attach;
+  iface->key_press_event = ide_completion_overlay_key_press_event;
+  iface->set_n_rows = ide_completion_overlay_set_n_rows;
+  iface->move_cursor = ide_completion_overlay_move_cursor;
+  iface->set_font_desc = ide_completion_overlay_set_font_desc;
+}
diff --git a/src/libide/sourceview/ide-completion-overlay.h b/src/libide/sourceview/ide-completion-overlay.h
new file mode 100644
index 000000000..3c476ae9a
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-overlay.h
@@ -0,0 +1,37 @@
+/* ide-completion-overlay.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+
+#include "ide-completion-context.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPLETION_OVERLAY (ide_completion_overlay_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeCompletionOverlay, ide_completion_overlay, IDE, COMPLETION_OVERLAY, DzlBin)
+
+G_END_DECLS
diff --git a/src/libide/completion/ide-completion-overlay.ui b/src/libide/sourceview/ide-completion-overlay.ui
similarity index 100%
rename from src/libide/completion/ide-completion-overlay.ui
rename to src/libide/sourceview/ide-completion-overlay.ui
diff --git a/src/libide/sourceview/ide-completion-private.h b/src/libide/sourceview/ide-completion-private.h
new file mode 100644
index 000000000..12b3761ad
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-private.h
@@ -0,0 +1,96 @@
+/* ide-completion-private.h
+ *
+ * Copyright 2018-2019 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 "ide-completion-types.h"
+#include "ide-source-view.h"
+
+G_BEGIN_DECLS
+
+typedef struct _IdeCompletionListBox    IdeCompletionListBox;
+typedef struct _IdeCompletionListBoxRow IdeCompletionListBoxRow;
+typedef struct _IdeCompletionOverlay    IdeCompletionOverlay;
+typedef struct _IdeCompletionView       IdeCompletionView;
+typedef struct _IdeCompletionWindow     IdeCompletionWindow;
+
+IdeCompletionWindow     *_ide_completion_window_new                (GtkWidget                   *view);
+void                     _ide_completion_view_set_font_desc        (IdeCompletionView           *self,
+                                                                    const PangoFontDescription  *font_desc);
+void                     _ide_completion_view_set_n_rows           (IdeCompletionView           *self,
+                                                                    guint                        n_rows);
+gint                     _ide_completion_view_get_x_offset         (IdeCompletionView           *self);
+gboolean                 _ide_completion_view_handle_key_press     (IdeCompletionView           *self,
+                                                                    const GdkEventKey           *event);
+void                     _ide_completion_view_move_cursor          (IdeCompletionView           *self,
+                                                                    GtkMovementStep              step,
+                                                                    gint                         count);
+IdeCompletion           *_ide_completion_new                       (GtkSourceView               *view);
+void                     _ide_completion_set_font_description      (IdeCompletion               *self,
+                                                                    const PangoFontDescription  *font_desc);
+void                     _ide_completion_set_language_id           (IdeCompletion               *self,
+                                                                    const gchar                 
*language_id);
+void                     _ide_completion_activate                  (IdeCompletion               *self,
+                                                                    IdeCompletionContext        *context,
+                                                                    IdeCompletionProvider       *provider,
+                                                                    IdeCompletionProposal       *proposal);
+IdeCompletionContext    *_ide_completion_context_new               (IdeCompletion               *completion);
+gboolean                 _ide_completion_context_iter_invalidates  (IdeCompletionContext        *self,
+                                                                    const GtkTextIter           *iter);
+void                     _ide_completion_context_add_provider      (IdeCompletionContext        *self,
+                                                                    IdeCompletionProvider       *provider);
+void                     _ide_completion_context_remove_provider   (IdeCompletionContext        *self,
+                                                                    IdeCompletionProvider       *provider);
+gboolean                 _ide_completion_context_can_refilter      (IdeCompletionContext        *self,
+                                                                    const GtkTextIter           *begin,
+                                                                    const GtkTextIter           *end);
+void                     _ide_completion_context_refilter          (IdeCompletionContext        *self);
+void                     _ide_completion_context_complete_async    (IdeCompletionContext        *self,
+                                                                    IdeCompletionActivation      activation,
+                                                                    const GtkTextIter           *begin,
+                                                                    const GtkTextIter           *end,
+                                                                    GCancellable                *cancellable,
+                                                                    GAsyncReadyCallback          callback,
+                                                                    gpointer                     user_data);
+gboolean                 _ide_completion_context_complete_finish   (IdeCompletionContext        *self,
+                                                                    GAsyncResult                *result,
+                                                                    GError                     **error);
+void                     _ide_completion_display_set_font_desc     (IdeCompletionDisplay        *self,
+                                                                    const PangoFontDescription  *font_desc);
+gboolean                 _ide_completion_list_box_key_activates    (IdeCompletionListBox        *self,
+                                                                    const GdkEventKey           *key);
+void                     _ide_completion_list_box_set_font_desc    (IdeCompletionListBox        *self,
+                                                                    const PangoFontDescription  *font_desc);
+IdeCompletionListBoxRow *_ide_completion_list_box_get_first        (IdeCompletionListBox        *self);
+void                     _ide_completion_list_box_row_attach       (IdeCompletionListBoxRow     *self,
+                                                                    GtkSizeGroup                *left,
+                                                                    GtkSizeGroup                *center,
+                                                                    GtkSizeGroup                *right);
+void                     _ide_completion_list_box_row_set_attrs    (IdeCompletionListBoxRow     *self,
+                                                                    PangoAttrList               *attrs);
+gint                     _ide_completion_list_box_row_get_x_offset (IdeCompletionListBoxRow     *self,
+                                                                    GtkWidget                   *toplevel);
+IdeCompletionOverlay    *_ide_completion_overlay_new               (void);
+void                     _ide_completion_proposal_display          (IdeCompletionProposal       *self,
+                                                                    IdeCompletionListBoxRow     *row);
+void                     _ide_completion_provider_load             (IdeCompletionProvider       *self,
+                                                                    IdeContext                  *context);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-completion-proposal.c b/src/libide/sourceview/ide-completion-proposal.c
new file mode 100644
index 000000000..bfd6d2c72
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-proposal.c
@@ -0,0 +1,32 @@
+/* ide-completion-proposal.c
+ *
+ * Copyright 2018-2019 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 "ide-completion-proposal"
+
+#include "config.h"
+
+#include "ide-completion-proposal.h"
+
+G_DEFINE_INTERFACE (IdeCompletionProposal, ide_completion_proposal, G_TYPE_OBJECT)
+
+static void
+ide_completion_proposal_default_init (IdeCompletionProposalInterface *iface)
+{
+}
diff --git a/src/libide/sourceview/ide-completion-proposal.h b/src/libide/sourceview/ide-completion-proposal.h
new file mode 100644
index 000000000..c687e94f4
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-proposal.h
@@ -0,0 +1,41 @@
+/* ide-completion-proposal.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPLETION_PROPOSAL (ide_completion_proposal_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeCompletionProposal, ide_completion_proposal, IDE, COMPLETION_PROPOSAL, GObject)
+
+struct _IdeCompletionProposalInterface
+{
+  GTypeInterface parent_iface;
+};
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-completion-provider.c b/src/libide/sourceview/ide-completion-provider.c
new file mode 100644
index 000000000..918f1be0d
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-provider.c
@@ -0,0 +1,350 @@
+/* ide-completion-provider.c
+ *
+ * Copyright 2018-2019 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 "ide-completion-provider"
+
+#include "config.h"
+
+#include "ide-completion-context.h"
+#include "ide-completion-private.h"
+#include "ide-completion-proposal.h"
+#include "ide-completion-provider.h"
+#include "ide-completion-list-box-row.h"
+
+G_DEFINE_INTERFACE (IdeCompletionProvider, ide_completion_provider, G_TYPE_OBJECT)
+
+static void
+ide_completion_provider_default_init (IdeCompletionProviderInterface *iface)
+{
+}
+
+/**
+ * ide_completion_provider_get_icon:
+ * @self: an #IdeCompletionProvider
+ *
+ * Gets the #GIcon to represent this provider. This may be used in UI
+ * to allow the user to filter the results to only those of this
+ * completion provider.
+ *
+ * Returns: (transfer full) (nullable): a #GIcon or %NULL.
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_completion_provider_get_icon (IdeCompletionProvider *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_PROVIDER (self), NULL);
+
+  if (IDE_COMPLETION_PROVIDER_GET_IFACE (self)->get_icon)
+    return IDE_COMPLETION_PROVIDER_GET_IFACE (self)->get_icon (self);
+
+  return NULL;
+}
+
+/**
+ * ide_completion_provider_get_priority:
+ * @self: an #IdeCompletionProvider
+ * @context: an #IdeCompletionContext
+ *
+ * Gets the priority for the completion provider.
+ *
+ * This value is used to group all of the providers proposals together
+ * when displayed, with relation to other providers.
+ *
+ * The @context is provided as some providers may want to lower their
+ * priority based on the position of the completion.
+ *
+ * Returns: an integer specific to the provider
+ *
+ * Since: 3.32
+ */
+gint
+ide_completion_provider_get_priority (IdeCompletionProvider *self,
+                                      IdeCompletionContext  *context)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_PROVIDER (self), 0);
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (context), 0);
+
+  if (IDE_COMPLETION_PROVIDER_GET_IFACE (self)->get_priority)
+    return IDE_COMPLETION_PROVIDER_GET_IFACE (self)->get_priority (self, context);
+
+  return 0;
+}
+
+/**
+ * ide_completion_provider_get_title:
+ * @self: an #IdeCompletionProvider
+ *
+ * Gets the title for the provider. This may be used in UI to give
+ * the user context about the type of results that are displayed.
+ *
+ * Returns: (transfer full) (nullable): a string or %NULL
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_completion_provider_get_title (IdeCompletionProvider *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_PROVIDER (self), NULL);
+
+  if (IDE_COMPLETION_PROVIDER_GET_IFACE (self)->get_title)
+    return IDE_COMPLETION_PROVIDER_GET_IFACE (self)->get_title (self);
+
+  return NULL;
+}
+
+/**
+ * ide_completion_provider_populate_async:
+ * @self: an #IdeCompletionProvider
+ * @context: the completion context
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @callback: (nullable) (scope async) (closure user_data): a #GAsyncReadyCallback
+ *   or %NULL. Called when the provider has completed loading proposals.
+ * @user_data: closure data for @callback
+ *
+ * Asynchronously requests the provider populate the contents.
+ *
+ * For completion providers that can provide intermediate results immediately,
+ * use ide_completion_context_set_proposals_for_provider() to notify of results
+ * while the async operation is in progress.
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_provider_populate_async (IdeCompletionProvider  *self,
+                                        IdeCompletionContext   *context,
+                                        GCancellable           *cancellable,
+                                        GAsyncReadyCallback     callback,
+                                        gpointer                user_data)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (self));
+  g_return_if_fail (IDE_IS_COMPLETION_CONTEXT (context));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_COMPLETION_PROVIDER_GET_IFACE (self)->populate_async (self, context, cancellable, callback, user_data);
+}
+
+/**
+ * ide_completion_provider_populate_finish:
+ * @self: an #IdeCompletionProvider
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a GError, or %NULL
+ *
+ * Returns: (transfer full): a #GListModel of #IdeCompletionProposal
+ *
+ * Since: 3.32
+ */
+GListModel *
+ide_completion_provider_populate_finish (IdeCompletionProvider  *self,
+                                         GAsyncResult           *result,
+                                         GError                **error)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_PROVIDER (self), NULL);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
+
+  return IDE_COMPLETION_PROVIDER_GET_IFACE (self)->populate_finish (self, result, error);
+}
+
+void
+ide_completion_provider_activate_poposal (IdeCompletionProvider *self,
+                                          IdeCompletionContext  *context,
+                                          IdeCompletionProposal *proposal,
+                                          const GdkEventKey     *key)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (self));
+  g_return_if_fail (IDE_IS_COMPLETION_CONTEXT (context));
+  g_return_if_fail (IDE_IS_COMPLETION_PROPOSAL (proposal));
+
+  if (IDE_COMPLETION_PROVIDER_GET_IFACE (self)->activate_proposal)
+    IDE_COMPLETION_PROVIDER_GET_IFACE (self)->activate_proposal (self, context, proposal, key);
+  else
+    g_critical ("%s does not implement activate_proposal()!", G_OBJECT_TYPE_NAME (self));
+}
+
+/**
+ * ide_completion_provider_refilter:
+ * @self: an #IdeCompletionProvider
+ * @context: an #IdeCompletionContext
+ * @proposals: a #GListModel of results previously provided to the context
+ *
+ * This requests that the completion provider refilter the results based on
+ * changes to the #IdeCompletionContext, such as additional text typed by the
+ * user. If the provider can refine the results, then the provider should do
+ * so and return %TRUE.
+ *
+ * Otherwise, %FALSE is returned and the context will request a new set of
+ * completion results.
+ *
+ * Returns: %TRUE if refiltered; otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_provider_refilter (IdeCompletionProvider *self,
+                                  IdeCompletionContext  *context,
+                                  GListModel            *proposals)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_PROVIDER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_COMPLETION_CONTEXT (context), FALSE);
+  g_return_val_if_fail (G_IS_LIST_MODEL (proposals), FALSE);
+
+  if (IDE_COMPLETION_PROVIDER_GET_IFACE (self)->refilter)
+    return IDE_COMPLETION_PROVIDER_GET_IFACE (self)->refilter (self, context, proposals);
+
+  return FALSE;
+}
+
+/**
+ * ide_completion_provider_is_trigger:
+ * @self: an #IdeCompletionProvider
+ * @iter: the current insertion point
+ * @ch: the character that was just inserted
+ *
+ * Completion providers may want to trigger that the completion window is
+ * displayed upon insertion of a particular character. For example, a C
+ * indenter might want to trigger after -> or . is inserted.
+ *
+ * @ch is set to the character that was just inserted. If you need something
+ * more complex, copy @iter and move it backwards twice to check the character
+ * previous to @ch.
+ *
+ * Returns: %TRUE to request that the completion window is displayed.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_provider_is_trigger (IdeCompletionProvider *self,
+                                    const GtkTextIter     *iter,
+                                    gunichar               ch)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_PROVIDER (self), FALSE);
+  g_return_val_if_fail (iter != NULL, FALSE);
+
+  if (IDE_COMPLETION_PROVIDER_GET_IFACE (self)->is_trigger)
+    return IDE_COMPLETION_PROVIDER_GET_IFACE (self)->is_trigger (self, iter, ch);
+
+  return FALSE;
+}
+
+/**
+ * ide_completion_provider_key_activates:
+ * @self: a #IdeCompletionProvider
+ * @proposal: an #IdeCompletionProposal created by the provider
+ * @key: the #GdkEventKey for the current keyboard event
+ *
+ * This function is called to ask the provider if the key-press event should
+ * force activation of the proposal. This is useful for languages where you
+ * might want to activate the completion from a language-specific character.
+ *
+ * For example, in C, you might want to use period (.) to activate the
+ * completion and insert either (.) or (->) based on the type.
+ *
+ * Returns: %TRUE if the proposal should be activated.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_provider_key_activates (IdeCompletionProvider *self,
+                                       IdeCompletionProposal *proposal,
+                                       const GdkEventKey     *key)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_PROVIDER (self), FALSE);
+  g_return_val_if_fail (IDE_IS_COMPLETION_PROPOSAL (proposal), FALSE);
+  g_return_val_if_fail (key != NULL, FALSE);
+
+  if (IDE_COMPLETION_PROVIDER_GET_IFACE (self)->key_activates)
+    return IDE_COMPLETION_PROVIDER_GET_IFACE (self)->key_activates (self, proposal, key);
+
+  return FALSE;
+}
+
+void
+_ide_completion_provider_load (IdeCompletionProvider *self,
+                               IdeContext            *context)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  if (IDE_COMPLETION_PROVIDER_GET_IFACE (self)->load)
+    IDE_COMPLETION_PROVIDER_GET_IFACE (self)->load (self, context);
+}
+
+/**
+ * ide_completion_provider_display_proposal:
+ * @self: a #IdeCompletionProvider
+ * @row: an #IdeCompletionListBoxRow
+ * @context: an #IdeCompletionContext
+ * @typed_text: (nullable): the typed text for the proposal
+ * @proposal: an #IdeCompletionProposal
+ *
+ * Requests that the provider update @row with values from @proposal.
+ *
+ * The design rational about having this operation part of the
+ * #IdeCompletionProvider interface (as opposed to the #IdeCompletionProposal
+ * interface) is that it allows for some optimizations and code simplification
+ * on behalf of completion providers.
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_provider_display_proposal (IdeCompletionProvider   *self,
+                                          IdeCompletionListBoxRow *row,
+                                          IdeCompletionContext    *context,
+                                          const gchar             *typed_text,
+                                          IdeCompletionProposal   *proposal)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (self));
+  g_return_if_fail (IDE_IS_COMPLETION_LIST_BOX_ROW (row));
+  g_return_if_fail (IDE_IS_COMPLETION_CONTEXT (context));
+  g_return_if_fail (IDE_IS_COMPLETION_PROPOSAL (proposal));
+
+  if (typed_text == NULL)
+    typed_text = "";
+
+  if (IDE_COMPLETION_PROVIDER_GET_IFACE (self)->display_proposal)
+    IDE_COMPLETION_PROVIDER_GET_IFACE (self)->display_proposal (self, row, context, typed_text, proposal);
+}
+
+/**
+ * ide_completion_provider_get_comment:
+ * @self: an #IdeCompletionProvider
+ * @proposal: an #IdeCompletionProposal
+ *
+ * If the completion proposal has a comment, the provider should return
+ * a newly allocated string containing it.
+ *
+ * This is displayed at the bottom of the completion window.
+ *
+ * Returns: (transfer full) (nullable): A new string or %NULL
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_completion_provider_get_comment (IdeCompletionProvider *self,
+                                     IdeCompletionProposal *proposal)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_PROVIDER (self), NULL);
+  g_return_val_if_fail (IDE_IS_COMPLETION_PROPOSAL (proposal), NULL);
+
+  if (IDE_COMPLETION_PROVIDER_GET_IFACE (self)->get_comment)
+    return IDE_COMPLETION_PROVIDER_GET_IFACE (self)->get_comment (self, proposal);
+
+  return NULL;
+}
diff --git a/src/libide/sourceview/ide-completion-provider.h b/src/libide/sourceview/ide-completion-provider.h
new file mode 100644
index 000000000..8067f456f
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-provider.h
@@ -0,0 +1,122 @@
+/* ide-completion-provider.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+#include "ide-completion-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPLETION_PROVIDER (ide_completion_provider_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeCompletionProvider, ide_completion_provider, IDE, COMPLETION_PROVIDER, GObject)
+
+struct _IdeCompletionProviderInterface
+{
+  GTypeInterface parent;
+
+  void        (*load)              (IdeCompletionProvider    *self,
+                                    IdeContext               *context);
+  GIcon      *(*get_icon)          (IdeCompletionProvider    *self);
+  gint        (*get_priority)      (IdeCompletionProvider    *self,
+                                    IdeCompletionContext     *context);
+  gchar      *(*get_title)         (IdeCompletionProvider    *self);
+  void        (*populate_async)    (IdeCompletionProvider    *self,
+                                    IdeCompletionContext     *context,
+                                    GCancellable             *cancellable,
+                                    GAsyncReadyCallback       callback,
+                                    gpointer                  user_data);
+  GListModel *(*populate_finish)   (IdeCompletionProvider    *self,
+                                    GAsyncResult             *result,
+                                    GError                  **error);
+  void        (*display_proposal)  (IdeCompletionProvider    *self,
+                                    IdeCompletionListBoxRow  *row,
+                                    IdeCompletionContext     *context,
+                                    const gchar              *typed_text,
+                                    IdeCompletionProposal    *proposal);
+  void        (*activate_proposal) (IdeCompletionProvider    *self,
+                                    IdeCompletionContext     *context,
+                                    IdeCompletionProposal    *proposal,
+                                    const GdkEventKey        *key);
+  gboolean    (*refilter)          (IdeCompletionProvider    *self,
+                                    IdeCompletionContext     *context,
+                                    GListModel               *proposals);
+  gboolean    (*is_trigger)        (IdeCompletionProvider    *self,
+                                    const GtkTextIter        *iter,
+                                    gunichar                  ch);
+  gboolean    (*key_activates)     (IdeCompletionProvider    *self,
+                                    IdeCompletionProposal    *proposal,
+                                    const GdkEventKey        *key);
+  gchar      *(*get_comment)       (IdeCompletionProvider    *self,
+                                    IdeCompletionProposal    *proposal);
+};
+
+IDE_AVAILABLE_IN_3_32
+GIcon      *ide_completion_provider_get_icon         (IdeCompletionProvider    *self);
+IDE_AVAILABLE_IN_3_32
+gint        ide_completion_provider_get_priority     (IdeCompletionProvider    *self,
+                                                      IdeCompletionContext     *context);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_completion_provider_get_title        (IdeCompletionProvider    *self);
+IDE_AVAILABLE_IN_3_32
+void        ide_completion_provider_populate_async   (IdeCompletionProvider    *self,
+                                                      IdeCompletionContext     *context,
+                                                      GCancellable             *cancellable,
+                                                      GAsyncReadyCallback       callback,
+                                                      gpointer                  user_data);
+IDE_AVAILABLE_IN_3_32
+GListModel *ide_completion_provider_populate_finish  (IdeCompletionProvider    *self,
+                                                      GAsyncResult             *result,
+                                                      GError                  **error);
+IDE_AVAILABLE_IN_3_32
+void        ide_completion_provider_display_proposal (IdeCompletionProvider    *self,
+                                                      IdeCompletionListBoxRow  *row,
+                                                      IdeCompletionContext     *context,
+                                                      const gchar              *typed_text,
+                                                      IdeCompletionProposal    *proposal);
+IDE_AVAILABLE_IN_3_32
+void        ide_completion_provider_activate_poposal (IdeCompletionProvider    *self,
+                                                      IdeCompletionContext     *context,
+                                                      IdeCompletionProposal    *proposal,
+                                                      const GdkEventKey        *key);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_completion_provider_refilter         (IdeCompletionProvider    *self,
+                                                      IdeCompletionContext     *context,
+                                                      GListModel               *proposals);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_completion_provider_is_trigger       (IdeCompletionProvider    *self,
+                                                      const GtkTextIter        *iter,
+                                                      gunichar                  ch);
+IDE_AVAILABLE_IN_3_32
+gboolean    ide_completion_provider_key_activates    (IdeCompletionProvider    *self,
+                                                      IdeCompletionProposal    *proposal,
+                                                      const GdkEventKey        *key);
+IDE_AVAILABLE_IN_3_32
+gchar      *ide_completion_provider_get_comment      (IdeCompletionProvider    *self,
+                                                      IdeCompletionProposal    *proposal);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-completion-types.h b/src/libide/sourceview/ide-completion-types.h
new file mode 100644
index 000000000..14554ed39
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-types.h
@@ -0,0 +1,52 @@
+/* ide-completion-types.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+typedef struct _IdeCompletion           IdeCompletion;
+typedef struct _IdeCompletionContext    IdeCompletionContext;
+typedef struct _IdeCompletionDisplay    IdeCompletionDisplay;
+typedef struct _IdeCompletionProposal   IdeCompletionProposal;
+typedef struct _IdeCompletionProvider   IdeCompletionProvider;
+
+typedef enum
+{
+  IDE_COMPLETION_INTERACTIVE,
+  IDE_COMPLETION_USER_REQUESTED,
+  IDE_COMPLETION_TRIGGERED,
+} IdeCompletionActivation;
+
+typedef enum
+{
+  IDE_COMPLETION_COLUMN_ICON,
+  IDE_COMPLETION_COLUMN_LEFT_OF,
+  IDE_COMPLETION_COLUMN_TYPED_TEXT,
+  IDE_COMPLETION_COLUMN_RIGHT_OF,
+} IdeCompletionColumn;
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-completion-view.c b/src/libide/sourceview/ide-completion-view.c
new file mode 100644
index 000000000..ec10b634a
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-view.c
@@ -0,0 +1,443 @@
+/* ide-completion-view.c
+ *
+ * Copyright 2018-2019 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 "ide-completion-view"
+
+#include "config.h"
+
+#include "ide-completion.h"
+#include "ide-completion-context.h"
+#include "ide-completion-list-box.h"
+#include "ide-completion-private.h"
+#include "ide-completion-proposal.h"
+#include "ide-completion-provider.h"
+#include "ide-completion-view.h"
+
+struct _IdeCompletionView
+{
+  DzlBin                parent_instance;
+  IdeCompletionContext *context;
+  IdeCompletionListBox *list_box;
+  GtkLabel             *details;
+};
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_PROPOSAL,
+  N_PROPS
+};
+
+enum {
+  ACTIVATE,
+  MOVE_CURSOR,
+  REPOSITION,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE (IdeCompletionView, ide_completion_view, DZL_TYPE_BIN)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+ide_completion_view_real_activate (IdeCompletionView *self)
+{
+  g_autoptr(IdeCompletionProvider) provider = NULL;
+  g_autoptr(IdeCompletionProposal) proposal = NULL;
+  IdeCompletion *completion;
+
+  g_assert (IDE_IS_COMPLETION_VIEW (self));
+
+  if (self->context == NULL ||
+      !gtk_widget_get_visible (GTK_WIDGET (self)) ||
+      !(completion = ide_completion_context_get_completion (self->context)) ||
+      !ide_completion_list_box_get_selected (self->list_box, &provider, &proposal))
+    return;
+
+  _ide_completion_activate (completion, self->context, provider, proposal);
+}
+
+static void
+ide_completion_view_real_move_cursor (IdeCompletionView *self,
+                                      GtkMovementStep    step,
+                                      gint               direction)
+{
+  g_assert (IDE_IS_COMPLETION_VIEW (self));
+
+  if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+    return;
+
+  ide_completion_list_box_move_cursor (self->list_box, step, direction);
+}
+
+static void
+on_notify_proposal_cb (IdeCompletionView    *self,
+                       GParamSpec           *pspec,
+                       IdeCompletionListBox *list_box)
+{
+  g_autoptr(IdeCompletionProposal) proposal = NULL;
+  g_autoptr(IdeCompletionProvider) provider = NULL;
+  g_autofree gchar *comment = NULL;
+
+  g_assert (IDE_IS_COMPLETION_VIEW (self));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (list_box));
+
+  if (ide_completion_list_box_get_selected (list_box, &provider, &proposal))
+    comment = ide_completion_provider_get_comment (provider, proposal);
+
+  gtk_label_set_label (self->details, comment);
+  gtk_widget_set_visible (GTK_WIDGET (self->details), comment && *comment);
+}
+
+static void
+ide_completion_view_notify_proposal_cb (IdeCompletionListBox *list_box,
+                                        GParamSpec           *pspec,
+                                        IdeCompletionView    *view)
+{
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (list_box));
+  g_assert (IDE_IS_COMPLETION_VIEW (view));
+
+  g_object_notify_by_pspec (G_OBJECT (view), properties [PROP_PROPOSAL]);
+}
+
+static void
+ide_completion_view_reposition_cb (IdeCompletionListBox *list_box,
+                                   IdeCompletionView    *view)
+{
+  g_assert (IDE_IS_COMPLETION_VIEW (view));
+  g_assert (IDE_IS_COMPLETION_LIST_BOX (list_box));
+
+  g_signal_emit (view, signals [REPOSITION], 0);
+}
+
+static void
+ide_completion_view_finalize (GObject *object)
+{
+  IdeCompletionView *self = (IdeCompletionView *)object;
+
+  g_clear_object (&self->context);
+
+  G_OBJECT_CLASS (ide_completion_view_parent_class)->finalize (object);
+}
+
+static void
+ide_completion_view_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  IdeCompletionView *self = IDE_COMPLETION_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, ide_completion_view_get_context (self));
+      break;
+
+    case PROP_PROPOSAL:
+      g_value_take_object (value, ide_completion_list_box_get_proposal (self->list_box));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_view_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  IdeCompletionView *self = IDE_COMPLETION_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      ide_completion_view_set_context (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_view_class_init (IdeCompletionViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkBindingSet *binding_set = gtk_binding_set_by_class (klass);
+
+  object_class->finalize = ide_completion_view_finalize;
+  object_class->get_property = ide_completion_view_get_property;
+  object_class->set_property = ide_completion_view_set_property;
+
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The context to display in the view",
+                         IDE_TYPE_COMPLETION_CONTEXT,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_PROPOSAL] =
+    g_param_spec_object ("proposal",
+                         "Proposal",
+                         "The selected proposal",
+                         IDE_TYPE_COMPLETION_PROPOSAL,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeCompletionOverlay::move-cursor:
+   * @self: an #IdeCompletionOverlay
+   * @direction: the amount to move and in what direction
+   *
+   * Make @direction positive to move forward, negative to move backwards
+   *
+   * Since: 3.32
+   */
+  signals [MOVE_CURSOR] =
+    g_signal_new_class_handler ("move-cursor",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (ide_completion_view_real_move_cursor),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE, 2, GTK_TYPE_MOVEMENT_STEP, G_TYPE_INT);
+
+  /**
+   * IdeCompletionOverlay::activate:
+   * @self: an #IdeCompletionOverlay
+   *
+   * Activates the selected item in the completion window.
+   *
+   * Since: 3.32
+   */
+  signals [ACTIVATE] =
+    g_signal_new_class_handler ("activate",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (ide_completion_view_real_activate),
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [ACTIVATE],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+
+  /**
+   * IdeCompletionView::reposition:
+   *
+   * Signal used to request the the container reposition itself due
+   * to changes in the underlying list.
+   *
+   * Since: 3.32
+   */
+  signals [REPOSITION] =
+    g_signal_new_class_handler ("reposition",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                NULL, NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [REPOSITION],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+
+  widget_class->activate_signal = signals [ACTIVATE];
+
+  gtk_widget_class_set_css_name (widget_class, "completionview");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-sourceview/ui/ide-completion-view.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeCompletionView, details);
+  gtk_widget_class_bind_template_child (widget_class, IdeCompletionView, list_box);
+  gtk_widget_class_bind_template_callback (widget_class, on_notify_proposal_cb);
+
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Down, 0, "move-cursor", 2,
+                                GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_DISPLAY_LINES,
+                                G_TYPE_INT, 1);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Up, 0, "move-cursor", 2,
+                                GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_DISPLAY_LINES,
+                                G_TYPE_INT, -1);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Page_Down, 0, "move-cursor", 2,
+                                GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES,
+                                G_TYPE_INT, 1);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Page_Down, 0, "move-cursor", 2,
+                                GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES,
+                                G_TYPE_INT, 1);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Page_Up, 0, "move-cursor", 2,
+                                GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES,
+                                G_TYPE_INT, -1);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Page_Up, 0, "move-cursor", 2,
+                                GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES,
+                                G_TYPE_INT, -1);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Home, GDK_CONTROL_MASK, "move-cursor", 2,
+                                GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_BUFFER_ENDS,
+                                G_TYPE_INT, -1);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_End, GDK_CONTROL_MASK, "move-cursor", 2,
+                                GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_BUFFER_ENDS,
+                                G_TYPE_INT, 1);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Page_Up, GDK_CONTROL_MASK, "move-cursor", 2,
+                                GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES,
+                                G_TYPE_INT, -5);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Page_Down, GDK_CONTROL_MASK, "move-cursor", 2,
+                                GTK_TYPE_MOVEMENT_STEP, GTK_MOVEMENT_PAGES,
+                                G_TYPE_INT, 5);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Return, 0, "activate", 0);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Tab, 0, "activate", 0);
+
+  g_type_ensure (IDE_TYPE_COMPLETION_LIST_BOX);
+}
+
+static void
+ide_completion_view_init (IdeCompletionView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect (self->list_box,
+                    "notify::proposal",
+                    G_CALLBACK (ide_completion_view_notify_proposal_cb),
+                    self);
+  g_signal_connect (self->list_box,
+                    "reposition",
+                    G_CALLBACK (ide_completion_view_reposition_cb),
+                    self);
+}
+
+/**
+ * ide_completion_view_get_context:
+ * @self: a #IdeCompletionView
+ *
+ * Gets the #IdeCompletionView:context property.
+ *
+ * Returns: (transfer none) (nullable): an #IdeCompletionContext or %NULL
+ *
+ * Since: 3.32
+ */
+IdeCompletionContext *
+ide_completion_view_get_context (IdeCompletionView *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_VIEW (self), NULL);
+
+  return self->context;
+}
+
+/**
+ * ide_completion_view_set_context:
+ * @self: a #IdeCompletionView
+ *
+ * Sets the #IdeCompletionContext to be visualized.
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_view_set_context (IdeCompletionView    *self,
+                                 IdeCompletionContext *context)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_VIEW (self));
+  g_return_if_fail (!context || IDE_IS_COMPLETION_CONTEXT (context));
+
+  if (g_set_object (&self->context, context))
+    {
+      ide_completion_list_box_set_context (self->list_box, context);
+      gtk_widget_queue_resize (GTK_WIDGET (self));
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]);
+    }
+}
+
+void
+_ide_completion_view_set_n_rows (IdeCompletionView *self,
+                                 guint              n_rows)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_VIEW (self));
+  g_return_if_fail (n_rows > 0);
+  g_return_if_fail (n_rows <= 32);
+
+  ide_completion_list_box_set_n_rows (self->list_box, n_rows);
+}
+
+gint
+_ide_completion_view_get_x_offset (IdeCompletionView *self)
+{
+  IdeCompletionListBoxRow *first;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_VIEW (self), 0);
+
+  if ((first = _ide_completion_list_box_get_first (self->list_box)))
+    return _ide_completion_list_box_row_get_x_offset (first, GTK_WIDGET (self));
+
+  return 0;
+}
+
+gboolean
+_ide_completion_view_handle_key_press (IdeCompletionView *self,
+                                       const GdkEventKey *event)
+{
+  GtkBindingSet *binding_set;
+  GtkTextView *view;
+
+  g_return_val_if_fail (IDE_IS_COMPLETION_VIEW (self), GDK_EVENT_PROPAGATE);
+  g_return_val_if_fail (event != NULL, GDK_EVENT_PROPAGATE);
+
+  /*
+   * If we have a snippet active, we don't want to activate with tab since
+   * that could advance the snippet (and should take precedence).
+   */
+  if (self->context != NULL &&
+      event->keyval == GDK_KEY_Tab &&
+      (view = ide_completion_context_get_view (self->context)) &&
+      ide_source_view_has_snippet (IDE_SOURCE_VIEW (view)))
+    return FALSE;
+
+  /* The key-press might cause the proposal to activate as well as insert some
+   * extra data. For example, a C completion provider might convert '.' to '->'
+   * after inserting the completion.
+   */
+  if (_ide_completion_list_box_key_activates (self->list_box, event))
+    {
+      gtk_widget_activate (GTK_WIDGET (self));
+      return GDK_EVENT_STOP;
+    }
+
+  binding_set = gtk_binding_set_by_class (G_OBJECT_GET_CLASS (self));
+
+  return gtk_binding_set_activate (binding_set, event->keyval, event->state, G_OBJECT (self));
+}
+
+void
+_ide_completion_view_move_cursor (IdeCompletionView *self,
+                                  GtkMovementStep    step,
+                                  gint               count)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_VIEW (self));
+
+  g_signal_emit (self, signals [MOVE_CURSOR], 0, step, count);
+}
+
+void
+_ide_completion_view_set_font_desc (IdeCompletionView          *self,
+                                    const PangoFontDescription *font_desc)
+{
+  g_assert (IDE_IS_COMPLETION_VIEW (self));
+
+  _ide_completion_list_box_set_font_desc (self->list_box, font_desc);
+}
diff --git a/src/libide/sourceview/ide-completion-view.h b/src/libide/sourceview/ide-completion-view.h
new file mode 100644
index 000000000..40ad652c0
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-view.h
@@ -0,0 +1,41 @@
+/* ide-completion-view.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <dazzle.h>
+
+#include "ide-completion-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPLETION_VIEW (ide_completion_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeCompletionView, ide_completion_view, IDE, COMPLETION_VIEW, DzlBin)
+
+IdeCompletionContext *ide_completion_view_get_context (IdeCompletionView    *self);
+void                  ide_completion_view_set_context (IdeCompletionView    *self,
+                                                       IdeCompletionContext *context);
+
+G_END_DECLS
diff --git a/src/libide/completion/ide-completion-view.ui b/src/libide/sourceview/ide-completion-view.ui
similarity index 100%
rename from src/libide/completion/ide-completion-view.ui
rename to src/libide/sourceview/ide-completion-view.ui
diff --git a/src/libide/sourceview/ide-completion-window.c b/src/libide/sourceview/ide-completion-window.c
new file mode 100644
index 000000000..b4882dc5d
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-window.c
@@ -0,0 +1,361 @@
+/* ide-completion-window.c
+ *
+ * Copyright 2018-2019 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 "ide-completion-window"
+
+#include "config.h"
+
+#include "ide-completion.h"
+#include "ide-completion-context.h"
+#include "ide-completion-display.h"
+#include "ide-completion-window.h"
+#include "ide-completion-private.h"
+#include "ide-completion-proposal.h"
+#include "ide-completion-provider.h"
+#include "ide-completion-view.h"
+
+struct _IdeCompletionWindow
+{
+  GtkWindow          parent_instance;
+  IdeCompletionView *view;
+};
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  N_PROPS
+};
+
+extern gpointer *gdk__private__                (void);
+static void      completion_display_iface_init (IdeCompletionDisplayInterface *);
+
+G_DEFINE_TYPE_WITH_CODE (IdeCompletionWindow, ide_completion_window, GTK_TYPE_WINDOW,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_COMPLETION_DISPLAY,
+                                                completion_display_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static gboolean
+ide_completion_window_reposition (IdeCompletionWindow *self)
+{
+  IdeCompletionContext *context;
+  GtkRequisition min, nat;
+  IdeCompletion *completion;
+  GdkRectangle rect;
+  GdkRectangle begin_rect, end_rect;
+  GtkSourceView *view;
+  GtkTextIter begin, end;
+  GtkWidget *toplevel;
+  GdkWindow *window;
+  gint x_offset = 0;
+
+  g_assert (IDE_IS_COMPLETION_WINDOW (self));
+
+  context = ide_completion_view_get_context (self->view);
+
+  if (context == NULL)
+    return FALSE;
+
+  if (!(completion = ide_completion_context_get_completion (context)))
+    return FALSE;
+
+  if (!(view = ide_completion_get_view (completion)))
+    return FALSE;
+
+  if (!ide_completion_context_get_bounds (context, &begin, &end))
+    return FALSE;
+
+  if (!(toplevel = gtk_widget_get_ancestor (GTK_WIDGET (view), GTK_TYPE_WINDOW)))
+    return FALSE;
+
+  gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), &begin, &begin_rect);
+  gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), &end, &end_rect);
+  gtk_text_view_buffer_to_window_coords (GTK_TEXT_VIEW (view),
+                                         GTK_TEXT_WINDOW_WIDGET,
+                                         begin_rect.x, begin_rect.y,
+                                         &begin_rect.x, &begin_rect.y);
+  gtk_text_view_buffer_to_window_coords (GTK_TEXT_VIEW (view),
+                                         GTK_TEXT_WINDOW_WIDGET,
+                                         end_rect.x, end_rect.y,
+                                         &end_rect.x, &end_rect.y);
+  gdk_rectangle_union (&begin_rect, &end_rect, &rect);
+  gtk_widget_translate_coordinates (GTK_WIDGET (view), toplevel,
+                                    rect.x, rect.y,
+                                    &rect.x, &rect.y);
+
+  if (!gtk_widget_get_realized (GTK_WIDGET (self)))
+    gtk_widget_realize (GTK_WIDGET (self));
+
+  gtk_widget_get_preferred_size (GTK_WIDGET (self), &min, &nat);
+
+  window = gtk_widget_get_window (GTK_WIDGET (self));
+
+  x_offset = _ide_completion_view_get_x_offset (self->view);
+
+#if 0
+  g_print ("Target: %d,%d %dx%d (%d)\n",
+           rect.x, rect.y, rect.width, rect.height, x_offset);
+#endif
+
+/* TODO: figure out where this comes from */
+#define EXTRA_SPACE 9
+
+  gdk_window_move_to_rect (window,
+                           &rect,
+                           GDK_GRAVITY_SOUTH_WEST,
+                           GDK_GRAVITY_NORTH_WEST,
+                           GDK_ANCHOR_FLIP_Y | GDK_ANCHOR_RESIZE_X,
+                           -x_offset + EXTRA_SPACE,
+                           0);
+
+  return TRUE;
+}
+
+static void
+ide_completion_window_real_show (GtkWidget *widget)
+{
+  IdeCompletionWindow *self = (IdeCompletionWindow *)widget;
+
+  g_assert (IDE_IS_COMPLETION_WINDOW (self));
+
+  ide_completion_window_reposition (self);
+
+  GTK_WIDGET_CLASS (ide_completion_window_parent_class)->show (widget);
+}
+
+static void
+ide_completion_window_real_realize (GtkWidget *widget)
+{
+  IdeCompletionWindow *self = (IdeCompletionWindow *)widget;
+  GdkScreen *screen;
+  GdkVisual *visual;
+
+  g_assert (IDE_IS_COMPLETION_WINDOW (self));
+
+  screen = gtk_widget_get_screen (widget);
+  visual = gdk_screen_get_rgba_visual (screen);
+
+  if (visual != NULL)
+    gtk_widget_set_visual (widget, visual);
+
+  GTK_WIDGET_CLASS (ide_completion_window_parent_class)->realize (widget);
+
+  ide_completion_window_reposition (self);
+}
+
+static void
+ide_completion_window_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  IdeCompletionWindow *self = IDE_COMPLETION_WINDOW (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, ide_completion_window_get_context (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_window_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  IdeCompletionWindow *self = IDE_COMPLETION_WINDOW (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      ide_completion_window_set_context (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_window_class_init (IdeCompletionWindowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_completion_window_get_property;
+  object_class->set_property = ide_completion_window_set_property;
+
+  widget_class->show = ide_completion_window_real_show;
+  widget_class->realize = ide_completion_window_real_realize;
+
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The completion context to display results for",
+                         IDE_TYPE_COMPLETION_CONTEXT,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "completionwindow");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-sourceview/ui/ide-completion-window.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeCompletionWindow, view);
+
+  g_type_ensure (IDE_TYPE_COMPLETION_VIEW);
+}
+
+static void
+ide_completion_window_init (IdeCompletionWindow *self)
+{
+  gtk_window_set_type_hint (GTK_WINDOW (self), GDK_WINDOW_TYPE_HINT_COMBO);
+  gtk_window_set_skip_pager_hint (GTK_WINDOW (self), TRUE);
+  gtk_window_set_skip_taskbar_hint (GTK_WINDOW (self), TRUE);
+  gtk_window_set_decorated (GTK_WINDOW (self), FALSE);
+  gtk_window_set_resizable (GTK_WINDOW (self), FALSE);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_swapped (self->view,
+                            "reposition",
+                            G_CALLBACK (ide_completion_window_reposition),
+                            self);
+}
+
+IdeCompletionWindow *
+_ide_completion_window_new (GtkWidget *view)
+{
+  GtkWidget *toplevel;
+
+  toplevel = gtk_widget_get_ancestor (view, GTK_TYPE_WINDOW);
+
+  return g_object_new (IDE_TYPE_COMPLETION_WINDOW,
+                       "destroy-with-parent", TRUE,
+                       "modal", FALSE,
+                       "transient-for", toplevel,
+                       "type", GTK_WINDOW_POPUP,
+                       NULL);
+}
+
+/**
+ * ide_completion_window_get_context:
+ * @self: a #IdeCompletionWindow
+ *
+ * Gets the context that is being displayed in the window, or %NULL.
+ *
+ * Returns: (transfer none) (nullable): an #IdeCompletionContext or %NULL
+ *
+ * Since: 3.32
+ */
+IdeCompletionContext *
+ide_completion_window_get_context (IdeCompletionWindow *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION_WINDOW (self), NULL);
+
+  return ide_completion_view_get_context (self->view);
+}
+
+/**
+ * ide_completion_window_set_context:
+ * @self: a #IdeCompletionWindow
+ *
+ * Sets the context to be displayed in the window.
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_window_set_context (IdeCompletionWindow  *self,
+                                   IdeCompletionContext *context)
+{
+  g_return_if_fail (IDE_IS_COMPLETION_WINDOW (self));
+  g_return_if_fail (!context || IDE_IS_COMPLETION_CONTEXT (context));
+
+  ide_completion_view_set_context (self->view, context);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CONTEXT]);
+}
+
+static gboolean
+ide_completion_window_key_press_event (IdeCompletionDisplay *display,
+                                       const GdkEventKey    *event)
+{
+  g_assert (IDE_IS_COMPLETION_WINDOW (display));
+  g_assert (event != NULL);
+
+  return _ide_completion_view_handle_key_press (IDE_COMPLETION_WINDOW (display)->view, event);
+}
+
+static void
+ide_completion_window_set_n_rows (IdeCompletionDisplay *display,
+                                  guint                 n_rows)
+{
+  g_assert (IDE_IS_COMPLETION_WINDOW (display));
+  g_assert (n_rows > 0);
+  g_assert (n_rows <= 32);
+
+  _ide_completion_view_set_n_rows (IDE_COMPLETION_WINDOW (display)->view, n_rows);
+}
+
+static void
+ide_completion_window_attach (IdeCompletionDisplay *display,
+                              GtkSourceView        *view)
+{
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_COMPLETION_WINDOW (display));
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  if ((toplevel = gtk_widget_get_ancestor (GTK_WIDGET (view), GTK_TYPE_WINDOW)))
+    gtk_window_set_transient_for (GTK_WINDOW (display), GTK_WINDOW (toplevel));
+}
+
+
+static void
+ide_completion_window_move_cursor (IdeCompletionDisplay *display,
+                                   GtkMovementStep       step,
+                                   gint                  count)
+{
+  g_assert (IDE_IS_COMPLETION_WINDOW (display));
+
+  _ide_completion_view_move_cursor (IDE_COMPLETION_WINDOW (display)->view, step, count);
+}
+
+static void
+ide_completion_window_set_font_desc (IdeCompletionDisplay       *display,
+                                     const PangoFontDescription *font_desc)
+{
+  g_assert (IDE_IS_COMPLETION_WINDOW (display));
+
+  _ide_completion_view_set_font_desc (IDE_COMPLETION_WINDOW (display)->view, font_desc);
+}
+
+static void
+completion_display_iface_init (IdeCompletionDisplayInterface *iface)
+{
+  iface->set_context = (gpointer)ide_completion_window_set_context;
+  iface->set_n_rows = ide_completion_window_set_n_rows;
+  iface->attach = ide_completion_window_attach;
+  iface->key_press_event = ide_completion_window_key_press_event;
+  iface->move_cursor = ide_completion_window_move_cursor;
+  iface->set_font_desc = ide_completion_window_set_font_desc;
+}
diff --git a/src/libide/sourceview/ide-completion-window.h b/src/libide/sourceview/ide-completion-window.h
new file mode 100644
index 000000000..c5f04d153
--- /dev/null
+++ b/src/libide/sourceview/ide-completion-window.h
@@ -0,0 +1,40 @@
+/* ide-completion-window.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPLETION_WINDOW (ide_completion_window_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeCompletionWindow, ide_completion_window, IDE, COMPLETION_WINDOW, GtkWindow)
+
+IdeCompletionContext *ide_completion_window_get_context (IdeCompletionWindow  *self);
+void                  ide_completion_window_set_context (IdeCompletionWindow  *self,
+                                                         IdeCompletionContext *context);
+
+G_END_DECLS
diff --git a/src/libide/completion/ide-completion-window.ui b/src/libide/sourceview/ide-completion-window.ui
similarity index 100%
rename from src/libide/completion/ide-completion-window.ui
rename to src/libide/sourceview/ide-completion-window.ui
diff --git a/src/libide/sourceview/ide-completion.c b/src/libide/sourceview/ide-completion.c
new file mode 100644
index 000000000..5b9e5dce0
--- /dev/null
+++ b/src/libide/sourceview/ide-completion.c
@@ -0,0 +1,1787 @@
+/* ide-completion.c
+ *
+ * Copyright 2018-2019 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 "ide-completion"
+
+#include "config.h"
+
+#include <gtk/gtk.h>
+#include <dazzle.h>
+#include <gtksourceview/gtksource.h>
+#include <libide-code.h>
+#include <libide-plugins.h>
+#include <libpeas/peas.h>
+#include <string.h>
+
+#ifdef GDK_WINDOWING_WAYLAND
+# include <gdk/gdkwayland.h>
+#endif
+
+#include "ide-completion.h"
+#include "ide-completion-context.h"
+#include "ide-completion-display.h"
+#include "ide-completion-overlay.h"
+#include "ide-completion-private.h"
+#include "ide-completion-proposal.h"
+#include "ide-completion-provider.h"
+
+#define DEFAULT_N_ROWS 5
+
+struct _IdeCompletion
+{
+  GObject parent_instance;
+
+  /*
+   * The GtkSourceView that we are providing results for. This can be used by
+   * providers to get a reference.
+   */
+  GtkSourceView *view;
+
+  /*
+   * A cancellable that we'll monitor to cancel anything that is currently in
+   * flight. This is reset to a new GCancellable after each time
+   * g_cancellable_cancel() is called.
+   */
+  GCancellable *cancellable;
+
+  /*
+   * Our extension manager to get providers that were registered by plugins.
+   * We handle extension-added/extension-removed and add the results to the
+   * @providers array so that we can allow manual adding of providers too.
+   */
+  IdeExtensionSetAdapter *addins;
+
+  /*
+   * An array of providers that have been registered. These will be queried
+   * when input is provided for completion.
+   */
+  GPtrArray *providers;
+
+  /*
+   * If we are currently performing a completion, the context is stored here.
+   * It will be cleared as soon as it's no longer valid to (re)display.
+   */
+  IdeCompletionContext *context;
+
+  /*
+   * The signal group is used to track changes to the context while it is our
+   * current context. That includes handling notification of the first result
+   * so that we can show the window, etc.
+   */
+  DzlSignalGroup *context_signals;
+
+  /*
+   * Signals to changes in the underlying GtkTextBuffer that we use to
+   * determine where and how we can do completion.
+   */
+  DzlSignalGroup *buffer_signals;
+
+  /*
+   * We need to track various events on the view to ensure that we don't
+   * activate at incorrect times.
+   */
+  DzlSignalGroup *view_signals;
+
+  /*
+   * The display for results. This may use a different implementation based on
+   * the windowing system available to work around restrictions. For example,
+   * on wayland or quartz we'd use a toplevel GtkOverlay to draw into where as
+   * on Xorg we might just use an native window since we have more flexibility
+   * in Move/Resize there.
+   */
+  IdeCompletionDisplay *display;
+
+  /*
+   * Our current event while processing so that we can get access to it
+   * from a callback back into the completion instance.
+   */
+  const GdkEventKey *current_event;
+
+  /*
+   * Our cached font description to apply to views.
+   */
+  PangoFontDescription *font_desc;
+
+  /*
+   * If we have a queued update to refilter after deletions, this will be
+   * set to the GSource id.
+   */
+  guint queued_update;
+
+  /*
+   * This value is incremented/decremented based on if we need to suppress
+   * visibility of the completion window (and avoid doing queries).
+   */
+  guint block_count;
+
+  /* Re-entrancy protection for ide_completion_show(). */
+  guint showing;
+
+  /*
+   * The number of rows to display. This is propagated to the window if/when
+   * the window is created.
+   */
+  guint n_rows;
+
+  /* If we're currently being displayed */
+  guint shown : 1;
+
+  /* If we have a completion actively in play */
+  guint waiting_for_results : 1;
+
+  /* If we should refilter after the in-flight context completes */
+  guint needs_refilter : 1;
+};
+
+G_DEFINE_TYPE (IdeCompletion, ide_completion, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_BUFFER,
+  PROP_N_ROWS,
+  PROP_VIEW,
+  N_PROPS
+};
+
+enum {
+  ACTIVATE,
+  PROVIDER_ADDED,
+  PROVIDER_REMOVED,
+  SHOW,
+  HIDE,
+  N_SIGNALS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static gboolean
+ide_completion_is_blocked (IdeCompletion *self)
+{
+  GtkTextBuffer *buffer;
+
+  g_assert (IDE_IS_COMPLETION (self));
+
+  return self->block_count > 0 ||
+         self->view == NULL ||
+         self->providers->len == 0 ||
+         !gtk_widget_get_visible (GTK_WIDGET (self->view)) ||
+         !gtk_widget_has_focus (GTK_WIDGET (self->view)) ||
+         !(buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view))) ||
+         gtk_text_buffer_get_has_selection (buffer) ||
+         !GTK_SOURCE_IS_VIEW (self->view) ||
+         !ide_source_view_is_processing_key (IDE_SOURCE_VIEW (self->view));
+}
+
+static void
+ide_completion_complete_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeCompletionContext *context = (IdeCompletionContext *)object;
+  g_autoptr(IdeCompletion) self = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeCompletionDisplay *display;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION_CONTEXT (context));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_COMPLETION (self));
+
+  if (self->context == context)
+    self->waiting_for_results = FALSE;
+
+  if (!_ide_completion_context_complete_finish (context, result, &error))
+    {
+      g_debug ("%s", error->message);
+      IDE_EXIT;
+    }
+
+  if (context != self->context)
+    IDE_EXIT;
+
+  if (self->needs_refilter)
+    {
+      /*
+       * At this point, we've gotten our new results for the context. But we had
+       * new content come in since we fired that request. So we need to ask the
+       * providers to further reduce the list based on updated query text.
+       */
+      self->needs_refilter = FALSE;
+      _ide_completion_context_refilter (context);
+    }
+
+  display = ide_completion_get_display (self);
+
+  if (!ide_completion_context_is_empty (context))
+    gtk_widget_show (GTK_WIDGET (display));
+  else
+    gtk_widget_hide (GTK_WIDGET (display));
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_set_context (IdeCompletion        *self,
+                            IdeCompletionContext *context)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (!context || IDE_IS_COMPLETION_CONTEXT (context));
+
+  if (g_set_object (&self->context, context))
+    dzl_signal_group_set_target (self->context_signals, context);
+
+  IDE_EXIT;
+}
+
+static inline gboolean
+is_symbol_char (gunichar ch)
+{
+  return ch == '_' || g_unichar_isalnum (ch);
+}
+
+static gboolean
+ide_completion_compute_bounds (IdeCompletion *self,
+                               GtkTextIter   *begin,
+                               GtkTextIter   *end)
+{
+  GtkTextBuffer *buffer;
+  GtkTextMark *insert;
+  gunichar ch = 0;
+
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view));
+  insert = gtk_text_buffer_get_insert (buffer);
+  gtk_text_buffer_get_iter_at_mark (buffer, end, insert);
+
+  *begin = *end;
+
+  do
+    {
+      if (!gtk_text_iter_backward_char (begin))
+        break;
+      ch = gtk_text_iter_get_char (begin);
+    }
+  while (is_symbol_char (ch));
+
+  if (ch && !is_symbol_char (ch))
+    gtk_text_iter_forward_char (begin);
+
+  if (GTK_SOURCE_IS_BUFFER (buffer))
+    {
+      GtkSourceBuffer *gsb = GTK_SOURCE_BUFFER (buffer);
+
+      if (gtk_source_buffer_iter_has_context_class (gsb, begin, "comment") ||
+          gtk_source_buffer_iter_has_context_class (gsb, begin, "string") ||
+          gtk_source_buffer_iter_has_context_class (gsb, end, "comment") ||
+          gtk_source_buffer_iter_has_context_class (gsb, end, "string"))
+        return FALSE;
+    }
+
+  return !gtk_text_iter_equal (begin, end);
+}
+
+static void
+ide_completion_start (IdeCompletion           *self,
+                      IdeCompletionActivation  activation)
+{
+  g_autoptr(IdeCompletionContext) context = NULL;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (self->context == NULL);
+
+  dzl_clear_source (&self->queued_update);
+
+  if (!ide_completion_compute_bounds (self, &begin, &end))
+    {
+      if (activation == IDE_COMPLETION_INTERACTIVE)
+        IDE_EXIT;
+      begin = end;
+    }
+
+  context = _ide_completion_context_new (self);
+  for (guint i = 0; i < self->providers->len; i++)
+    _ide_completion_context_add_provider (context, g_ptr_array_index (self->providers, i));
+  ide_completion_set_context (self, context);
+
+  self->waiting_for_results = TRUE;
+  self->needs_refilter = FALSE;
+
+  _ide_completion_context_complete_async (context,
+                                          activation,
+                                          &begin,
+                                          &end,
+                                          self->cancellable,
+                                          ide_completion_complete_cb,
+                                          g_object_ref (self));
+
+  if (self->display != NULL)
+    {
+      ide_completion_display_set_context (self->display, context);
+
+      if (!ide_completion_context_is_empty (context))
+        gtk_widget_show (GTK_WIDGET (self->display));
+      else
+        gtk_widget_hide (GTK_WIDGET (self->display));
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_update (IdeCompletion           *self,
+                       IdeCompletionActivation  activation)
+{
+  GtkTextBuffer *buffer;
+  GtkTextMark *insert;
+  GtkTextIter begin;
+  GtkTextIter end;
+  GtkTextIter iter;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (self->context != NULL);
+  g_assert (IDE_IS_COMPLETION_CONTEXT (self->context));
+
+  /*
+   * First, find the boundary for the word we are trying to complete. We might
+   * be able to refine a previous query instead of making a new one which can
+   * save on a lot of backend work.
+   */
+  ide_completion_compute_bounds (self, &begin, &end);
+
+  if (_ide_completion_context_can_refilter (self->context, &begin, &end))
+    {
+      IdeCompletionDisplay *display = ide_completion_get_display (self);
+
+      /*
+       * Make sure we update providers that have already delivered results
+       * even though some of them won't be ready yet.
+       */
+      _ide_completion_context_refilter (self->context);
+
+      /*
+       * If we're waiting for the results still to come in, then just mark
+       * that we need to do post-processing rather than trying to refilter now.
+       */
+      if (self->waiting_for_results)
+        {
+          self->needs_refilter = TRUE;
+          IDE_EXIT;
+        }
+
+      if (!ide_completion_context_is_empty (self->context))
+        gtk_widget_show (GTK_WIDGET (display));
+      else
+        gtk_widget_hide (GTK_WIDGET (display));
+
+      IDE_EXIT;
+    }
+
+  if (!ide_completion_context_get_bounds (self->context, &begin, &end) ||
+      gtk_text_iter_equal (&begin, &end))
+    {
+      if (activation == IDE_COMPLETION_INTERACTIVE)
+        {
+          ide_completion_hide (self);
+          IDE_EXIT;
+        }
+
+      IDE_GOTO (reset);
+    }
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view));
+  insert = gtk_text_buffer_get_insert (buffer);
+  gtk_text_buffer_get_iter_at_mark (buffer, &iter, insert);
+
+  /*
+   * If our completion prefix bounds match the prefix that we looked
+   * at previously, we can possibly refilter the previous context instead
+   * of creating a new context.
+   */
+
+  /*
+   * The context uses GtkTextMark which should have been advanced as
+   * the user continued to type. So if @end matches @iter (our insert
+   * location), then we can possibly update the previous context by
+   * further refining the query to a subset of the result.
+   */
+  if (gtk_text_iter_equal (&iter, &end))
+    {
+      ide_completion_show (self);
+      IDE_EXIT;
+    }
+
+reset:
+  ide_completion_cancel (self);
+  ide_completion_start (self, activation);
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_real_hide (IdeCompletion *self)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION (self));
+
+  if (self->display != NULL)
+    gtk_widget_hide (GTK_WIDGET (self->display));
+
+  IDE_EXIT;
+}
+
+static IdeCompletionDisplay *
+ide_completion_create_display (IdeCompletion *self)
+{
+  GtkWidget *widget = GTK_WIDGET (self->view);
+  GdkDisplay *display = gtk_widget_get_display (widget);
+
+  if (FALSE) {}
+#ifdef GDK_WINDOWING_WAYLAND
+  else if (GDK_IS_WAYLAND_DISPLAY (display))
+    return IDE_COMPLETION_DISPLAY (_ide_completion_overlay_new ());
+#endif
+#ifdef GDK_WINDOWING_QUARTZ
+  /* Do string type check to avoid including obj-c header */
+  else if (g_strcmp0 ("GdkQuartzDisplay", G_OBJECT_TYPE_NAME (display)) == 0)
+    return IDE_COMPLETION_DISPLAY (_ide_completion_overlay_new ());
+#endif
+  else
+    return IDE_COMPLETION_DISPLAY (_ide_completion_window_new (widget));
+}
+
+static void
+ide_completion_real_show (IdeCompletion *self)
+{
+  IdeCompletionDisplay *display;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION (self));
+
+  display = ide_completion_get_display (self);
+
+  if (self->context == NULL)
+    ide_completion_start (self, IDE_COMPLETION_USER_REQUESTED);
+  else
+    ide_completion_update (self, IDE_COMPLETION_USER_REQUESTED);
+
+  ide_completion_display_set_context (display, self->context);
+
+  if (!ide_completion_context_is_empty (self->context))
+    gtk_widget_show (GTK_WIDGET (display));
+  else
+    gtk_widget_hide (GTK_WIDGET (display));
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_notify_context_empty_cb (IdeCompletion        *self,
+                                        GParamSpec           *pspec,
+                                        IdeCompletionContext *context)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_COMPLETION_CONTEXT (context));
+
+  if (context != self->context)
+    IDE_EXIT;
+
+  if (ide_completion_context_is_empty (context))
+    {
+      if (self->display != NULL)
+        gtk_widget_hide (GTK_WIDGET (self->display));
+    }
+  else
+    {
+      IdeCompletionDisplay *display = ide_completion_get_display (self);
+
+      gtk_widget_show (GTK_WIDGET (display));
+    }
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_completion_view_button_press_event_cb (IdeCompletion  *self,
+                                           GdkEventButton *event,
+                                           GtkSourceView  *view)
+{
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (event != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+  g_assert (self->view == view);
+
+  ide_completion_hide (self);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_completion_view_focus_out_event_cb (IdeCompletion *self,
+                                        GdkEventFocus *event,
+                                        GtkSourceView *view)
+{
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (event != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+  g_assert (self->view == view);
+
+  ide_completion_hide (self);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_completion_view_key_press_event_cb (IdeCompletion *self,
+                                        GdkEventKey   *event,
+                                        GtkSourceView *view)
+{
+  GtkBindingSet *binding_set;
+  gboolean ret = GDK_EVENT_PROPAGATE;
+
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (event != NULL);
+  g_assert (event->type == GDK_KEY_PRESS);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+  g_assert (self->view == view);
+
+  binding_set = gtk_binding_set_by_class (G_OBJECT_GET_CLASS (self));
+
+  self->current_event = event;
+
+  if (self->display != NULL &&
+      gtk_widget_get_visible (GTK_WIDGET (self->display)) &&
+      ide_completion_display_key_press_event (self->display, event))
+    ret = GDK_EVENT_STOP;
+
+  self->current_event = NULL;
+
+  if (ret == GDK_EVENT_PROPAGATE)
+    ret = gtk_binding_set_activate (binding_set, event->keyval, event->state, G_OBJECT (self));
+
+  return ret;
+}
+
+static void
+ide_completion_view_move_cursor_cb (IdeCompletion   *self,
+                                    GtkMovementStep  step,
+                                    gint             count,
+                                    gboolean         extend_selection,
+                                    GtkSourceView   *view)
+{
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  /* TODO: Should we keep the context alive while we begin a new one?
+   *       Or rather, how can we avoid the hide/show of the widget that
+   *       could result in flicker?
+   */
+
+  if (self->display != NULL &&
+      gtk_widget_get_visible (GTK_WIDGET (self->display)))
+    ide_completion_cancel (self);
+}
+
+static gboolean
+ide_completion_queued_update_cb (gpointer user_data)
+{
+  IdeCompletion *self = user_data;
+
+  g_assert (IDE_IS_COMPLETION (self));
+
+  self->queued_update = 0;
+
+  if (self->context != NULL)
+    ide_completion_update (self, IDE_COMPLETION_INTERACTIVE);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_completion_queue_update (IdeCompletion *self)
+{
+  g_assert (IDE_IS_COMPLETION (self));
+
+  dzl_clear_source (&self->queued_update);
+
+  /*
+   * We hit this code path when the user has deleted text. We want to
+   * introduce just a bit of delay so that deleting under heavy key
+   * repeat will not stall doing lots of refiltering.
+   */
+
+  self->queued_update =
+    gdk_threads_add_timeout_full (G_PRIORITY_LOW,
+                                  20,
+                                  ide_completion_queued_update_cb,
+                                  self,
+                                  NULL);
+}
+
+static void
+ide_completion_buffer_delete_range_after_cb (IdeCompletion *self,
+                                             GtkTextIter   *begin,
+                                             GtkTextIter   *end,
+                                             GtkTextBuffer *buffer)
+{
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (IDE_IS_SOURCE_VIEW (self->view));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+
+  if (self->context != NULL)
+    {
+      if (!ide_completion_is_blocked (self))
+        {
+          GtkTextIter b, e;
+
+          ide_completion_context_get_bounds (self->context, &b, &e);
+
+          /*
+           * If they just backspaced all of the text, then we want to just hide
+           * the completion window since that can get a bit intrusive.
+           */
+          if (gtk_text_iter_equal (&b, &e))
+            {
+              dzl_clear_source (&self->queued_update);
+              ide_completion_hide (self);
+              return;
+            }
+
+          ide_completion_queue_update (self);
+        }
+    }
+}
+
+static gboolean
+is_single_char (const gchar *text,
+                gint         len)
+{
+  if (len == 1)
+    return TRUE;
+  else if (len > 6)
+    return FALSE;
+  else
+    return g_utf8_strlen (text, len) == 1;
+}
+
+static void
+ide_completion_buffer_insert_text_after_cb (IdeCompletion *self,
+                                            GtkTextIter   *iter,
+                                            const gchar   *text,
+                                            gint           len,
+                                            GtkTextBuffer *buffer)
+{
+  IdeCompletionActivation activation = IDE_COMPLETION_INTERACTIVE;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (iter != NULL);
+  g_assert (text != NULL);
+  g_assert (len > 0);
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+
+  if (ide_buffer_get_loading (IDE_BUFFER (buffer)))
+    return;
+
+  dzl_clear_source (&self->queued_update);
+
+  if (ide_completion_is_blocked (self) || !is_single_char (text, len))
+    {
+      ide_completion_cancel (self);
+      return;
+    }
+
+  if (!ide_completion_compute_bounds (self, &begin, &end))
+    {
+      GtkTextIter cur = end;
+
+      if (gtk_text_iter_backward_char (&cur))
+        {
+          gunichar ch = gtk_text_iter_get_char (&cur);
+
+          for (guint i = 0; i < self->providers->len; i++)
+            {
+              IdeCompletionProvider *provider = g_ptr_array_index (self->providers, i);
+
+              if (ide_completion_provider_is_trigger (provider, &end, ch))
+                {
+                  /*
+                   * We got a trigger, but we failed to continue the bounds of a previous
+                   * completion. We need to cancel the previous completion (if any) first
+                   * and then try to start a new completion due to trigger.
+                   */
+                  ide_completion_cancel (self);
+                  activation = IDE_COMPLETION_TRIGGERED;
+                  goto do_completion;
+                }
+            }
+        }
+
+      ide_completion_cancel (self);
+      return;
+    }
+
+do_completion:
+
+  if (self->context == NULL)
+    ide_completion_start (self, activation);
+  else
+    ide_completion_update (self, activation);
+}
+
+static void
+ide_completion_buffer_mark_set_cb (IdeCompletion     *self,
+                                   const GtkTextIter *iter,
+                                   GtkTextMark       *mark,
+                                   GtkTextBuffer     *buffer)
+{
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (GTK_IS_TEXT_MARK (mark));
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+
+  if (mark != gtk_text_buffer_get_insert (buffer))
+    return;
+
+  if (_ide_completion_context_iter_invalidates (self->context, iter))
+    ide_completion_cancel (self);
+}
+
+static void
+ide_completion_set_view (IdeCompletion *self,
+                         GtkSourceView *view)
+{
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (!view || IDE_IS_SOURCE_VIEW (view));
+
+  if (view == NULL)
+    {
+      g_critical ("%s created without a view", G_OBJECT_TYPE_NAME (self));
+      return;
+    }
+
+  if (g_set_weak_pointer (&self->view, view))
+    {
+      dzl_signal_group_set_target (self->view_signals, view);
+      g_object_bind_property (view, "buffer",
+                              self->buffer_signals, "target",
+                              G_BINDING_SYNC_CREATE);
+    }
+}
+
+static void
+ide_completion_addins_extension_added_cb (IdeExtensionSetAdapter *adapter,
+                                          PeasPluginInfo         *plugin_info,
+                                          PeasExtension          *exten,
+                                          gpointer                user_data)
+{
+  IdeCompletionProvider *provider = (IdeCompletionProvider *)exten;
+  IdeCompletion *self = user_data;
+  GtkTextBuffer *buffer;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_COMPLETION_PROVIDER (provider));
+
+  if ((buffer = ide_completion_get_buffer (self)) && IDE_IS_BUFFER (buffer))
+    {
+      g_autoptr(IdeContext) context = ide_buffer_ref_context (IDE_BUFFER (buffer));
+      _ide_completion_provider_load (provider, context);
+    }
+
+  ide_completion_add_provider (self, provider);
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_addins_extension_removed_cb (IdeExtensionSetAdapter *adapter,
+                                            PeasPluginInfo         *plugin_info,
+                                            PeasExtension          *exten,
+                                            gpointer                user_data)
+{
+  IdeCompletionProvider *provider = (IdeCompletionProvider *)exten;
+  IdeCompletion *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_COMPLETION_PROVIDER (provider));
+
+  ide_completion_remove_provider (self, provider);
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_buffer_signals_bind_cb (IdeCompletion   *self,
+                                       GtkSourceBuffer *buffer,
+                                       DzlSignalGroup  *group)
+{
+  GtkSourceLanguage *language;
+  IdeObjectBox *box;
+  const gchar *language_id = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+  g_assert (DZL_IS_SIGNAL_GROUP (group));
+
+  if (!IDE_IS_BUFFER (buffer))
+    return;
+
+  if ((language = gtk_source_buffer_get_language (buffer)))
+    language_id = gtk_source_language_get_id (language);
+
+  box = ide_object_box_from_object (G_OBJECT (buffer));
+  self->addins = ide_extension_set_adapter_new (IDE_OBJECT (box),
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_COMPLETION_PROVIDER,
+                                                "Completion-Provider-Languages",
+                                                language_id);
+
+  g_signal_connect_object (self->addins,
+                           "extension-added",
+                           G_CALLBACK (ide_completion_addins_extension_added_cb),
+                           self, 0);
+  g_signal_connect_object (self->addins,
+                           "extension-removed",
+                           G_CALLBACK (ide_completion_addins_extension_removed_cb),
+                           self, 0);
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_completion_addins_extension_added_cb,
+                                     self);
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_buffer_signals_unbind_cb (IdeCompletion   *self,
+                                         DzlSignalGroup  *group)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (DZL_IS_SIGNAL_GROUP (group));
+
+  ide_clear_and_destroy_object (&self->addins);
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_buffer_notify_language_cb (IdeCompletion   *self,
+                                          GParamSpec      *pspec,
+                                          GtkSourceBuffer *buffer)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION (self));
+  g_assert (pspec != NULL);
+  g_assert (GTK_SOURCE_IS_BUFFER (buffer));
+
+  if (self->addins != NULL)
+    {
+      GtkSourceLanguage *language;
+      const gchar *language_id = NULL;
+
+      if ((language = gtk_source_buffer_get_language (buffer)))
+        language_id = gtk_source_language_get_id (language);
+
+      ide_extension_set_adapter_set_value (self->addins, language_id);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_dispose (GObject *object)
+{
+  IdeCompletion *self = (IdeCompletion *)object;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_COMPLETION (self));
+
+  if (self->display != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->display));
+
+  g_assert (self->display == NULL);
+
+  dzl_signal_group_set_target (self->context_signals, NULL);
+  dzl_signal_group_set_target (self->buffer_signals, NULL);
+  dzl_signal_group_set_target (self->view_signals, NULL);
+
+  g_clear_object (&self->context);
+  g_clear_object (&self->cancellable);
+
+  if (self->providers->len > 0)
+    g_ptr_array_remove_range (self->providers, 0, self->providers->len);
+
+  G_OBJECT_CLASS (ide_completion_parent_class)->dispose (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_finalize (GObject *object)
+{
+  IdeCompletion *self = (IdeCompletion *)object;
+
+  IDE_ENTRY;
+
+  dzl_clear_source (&self->queued_update);
+
+  g_clear_object (&self->cancellable);
+  ide_clear_and_destroy_object (&self->addins);
+  g_clear_object (&self->buffer_signals);
+  g_clear_object (&self->context_signals);
+  g_clear_object (&self->view_signals);
+  g_clear_object (&self->context);
+  g_clear_object (&self->cancellable);
+  g_clear_pointer (&self->providers, g_ptr_array_unref);
+  g_clear_pointer (&self->font_desc, pango_font_description_free);
+  g_clear_weak_pointer (&self->view);
+
+  G_OBJECT_CLASS (ide_completion_parent_class)->finalize (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_completion_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeCompletion *self = IDE_COMPLETION (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      g_value_set_object (value, ide_completion_get_buffer (self));
+      break;
+
+    case PROP_N_ROWS:
+      g_value_set_uint (value, ide_completion_get_n_rows (self));
+      break;
+
+    case PROP_VIEW:
+      g_value_set_object (value, self->view);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeCompletion *self = IDE_COMPLETION (object);
+
+  switch (prop_id)
+    {
+    case PROP_N_ROWS:
+      ide_completion_set_n_rows (self, g_value_get_uint (value));
+      break;
+
+    case PROP_VIEW:
+      ide_completion_set_view (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_completion_class_init (IdeCompletionClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkBindingSet *binding_set;
+
+  object_class->dispose = ide_completion_dispose;
+  object_class->finalize = ide_completion_finalize;
+  object_class->get_property = ide_completion_get_property;
+  object_class->set_property = ide_completion_set_property;
+
+  /**
+   * IdeCompletion:buffer:
+   *
+   * The #GtkTextBuffer for the #IdeCompletion:view.
+   * This is a convenience property for providers.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_BUFFER] =
+    g_param_spec_object ("buffer",
+                         "Buffer",
+                         "The buffer for the view",
+                         GTK_TYPE_TEXT_VIEW,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeCompletion:n-rows:
+   *
+   * The number of rows to display to the user.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_N_ROWS] =
+    g_param_spec_uint ("n-rows",
+                       "Number of Rows",
+                       "Number of rows to display to the user",
+                       1, 32, DEFAULT_N_ROWS,
+                       G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * IdeCompletion:view:
+   *
+   * The "view" property is the #GtkTextView for which this #IdeCompletion
+   * is providing completion features.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_VIEW] =
+    g_param_spec_object ("view",
+                         "View",
+                         "The text view for which to provide completion",
+                         GTK_SOURCE_TYPE_VIEW,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  /**
+   * IdeCompletion::provider-added:
+   * @self: an #ideCompletion
+   * @provider: an #IdeCompletionProvider
+   *
+   * The "provided-added" signal is emitted when a new provider is
+   * added to the completion.
+   *
+   * Since: 3.32
+   */
+  signals [PROVIDER_ADDED] =
+    g_signal_new ("provider-added",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_COMPLETION_PROVIDER);
+  g_signal_set_va_marshaller (signals [PROVIDER_ADDED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  /**
+   * IdeCompletion::provider-removed:
+   * @self: an #ideCompletion
+   * @provider: an #IdeCompletionProvider
+   *
+   * The "provided-removed" signal is emitted when a provider has
+   * been removed from the completion.
+   *
+   * Since: 3.32
+   */
+  signals [PROVIDER_REMOVED] =
+    g_signal_new ("provider-removed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL,
+                  g_cclosure_marshal_VOID__OBJECT,
+                  G_TYPE_NONE, 1, IDE_TYPE_COMPLETION_PROVIDER);
+  g_signal_set_va_marshaller (signals [PROVIDER_REMOVED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__OBJECTv);
+
+  /**
+   * IdeCompletion::hide:
+   * @self: an #IdeCompletion
+   *
+   * The "hide" signal is emitted when the completion window should
+   * be hidden.
+   *
+   * Since: 3.32
+   */
+  signals [HIDE] =
+    g_signal_new_class_handler ("hide",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (ide_completion_real_hide),
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [HIDE],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+
+  /**
+   * IdeCompletion::show:
+   * @self: an #IdeCompletion
+   *
+   * The "show" signal is emitted when the completion window should
+   * be shown.
+   *
+   * Since: 3.32
+   */
+  signals [SHOW] =
+    g_signal_new_class_handler ("show",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (ide_completion_real_show),
+                                NULL, NULL,
+                                g_cclosure_marshal_VOID__VOID,
+                                G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [SHOW],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+
+  binding_set = gtk_binding_set_by_class (klass);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_space, GDK_CONTROL_MASK, "show", 0);
+}
+
+static void
+ide_completion_init (IdeCompletion *self)
+{
+  self->cancellable = g_cancellable_new ();
+  self->providers = g_ptr_array_new_with_free_func (g_object_unref);
+  self->buffer_signals = dzl_signal_group_new (GTK_TYPE_TEXT_BUFFER);
+  self->context_signals = dzl_signal_group_new (IDE_TYPE_COMPLETION_CONTEXT);
+  self->view_signals = dzl_signal_group_new (GTK_SOURCE_TYPE_VIEW);
+  self->n_rows = DEFAULT_N_ROWS;
+
+  /*
+   * We want to be notified when the context switches from no results to
+   * having results (or vice-versa, when we've filtered to the point of
+   * no results).
+   */
+  dzl_signal_group_connect_object (self->context_signals,
+                                   "notify::empty",
+                                   G_CALLBACK (ide_completion_notify_context_empty_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  /*
+   * We need to know when the buffer inserts or deletes text so that we
+   * possibly start showing the results, or update our previous completion
+   * request.
+   */
+  g_signal_connect_object (self->buffer_signals,
+                           "bind",
+                           G_CALLBACK (ide_completion_buffer_signals_bind_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (self->buffer_signals,
+                           "unbind",
+                           G_CALLBACK (ide_completion_buffer_signals_unbind_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->buffer_signals,
+                                   "notify::language",
+                                   G_CALLBACK (ide_completion_buffer_notify_language_cb),
+                                   self,
+                                   G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->buffer_signals,
+                                   "delete-range",
+                                   G_CALLBACK (ide_completion_buffer_delete_range_after_cb),
+                                   self,
+                                   G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->buffer_signals,
+                                   "insert-text",
+                                   G_CALLBACK (ide_completion_buffer_insert_text_after_cb),
+                                   self,
+                                   G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->buffer_signals,
+                                   "mark-set",
+                                   G_CALLBACK (ide_completion_buffer_mark_set_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  /*
+   * We track some events on the view that owns our IdeCompletion instance so
+   * that we can hide the window when it definitely should not be displayed.
+   */
+  dzl_signal_group_connect_object (self->view_signals,
+                                   "button-press-event",
+                                   G_CALLBACK (ide_completion_view_button_press_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->view_signals,
+                                   "focus-out-event",
+                                   G_CALLBACK (ide_completion_view_focus_out_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->view_signals,
+                                   "key-press-event",
+                                   G_CALLBACK (ide_completion_view_key_press_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->view_signals,
+                                   "move-cursor",
+                                   G_CALLBACK (ide_completion_view_move_cursor_cb),
+                                   self,
+                                   G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->view_signals,
+                                   "paste-clipboard",
+                                   G_CALLBACK (ide_completion_block_interactive),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->view_signals,
+                                   "paste-clipboard",
+                                   G_CALLBACK (ide_completion_unblock_interactive),
+                                   self,
+                                   G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+}
+
+/**
+ * ide_completion_get_view:
+ * @self: a #IdeCompletion
+ *
+ * Returns: (transfer none): an #GtkSourceView
+ *
+ * Since: 3.32
+ */
+GtkSourceView *
+ide_completion_get_view (IdeCompletion *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION (self), NULL);
+
+  return self->view;
+}
+
+/**
+ * ide_completion_get_buffer:
+ * @self: a #IdeCompletion
+ *
+ * Returns: (transfer none): a #GtkTextBuffer
+ *
+ * Since: 3.32
+ */
+GtkTextBuffer *
+ide_completion_get_buffer (IdeCompletion *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION (self), NULL);
+
+  return gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->view));
+}
+
+IdeCompletion *
+_ide_completion_new (GtkSourceView *view)
+{
+  g_return_val_if_fail (IDE_IS_SOURCE_VIEW (view), NULL);
+
+  return g_object_new (IDE_TYPE_COMPLETION,
+                       "view", view,
+                       NULL);
+}
+
+/**
+ * ide_completion_add_provider:
+ * @self: an #IdeCompletion
+ * @provider: an #IdeCompletionProvider
+ *
+ * Adds an #IdeCompletionProvider to the list of providers to be queried
+ * for completion results.
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_add_provider (IdeCompletion         *self,
+                             IdeCompletionProvider *provider)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+  g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (provider));
+
+  g_ptr_array_add (self->providers, g_object_ref (provider));
+  g_signal_emit (self, signals [PROVIDER_ADDED], 0, provider);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_completion_remove_provider:
+ * @self: an #IdeCompletion
+ * @provider: an #IdeCompletionProvider
+ *
+ * Removes an #IdeCompletionProvider previously added with
+ * ide_completion_add_provider().
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_remove_provider (IdeCompletion         *self,
+                                IdeCompletionProvider *provider)
+{
+  g_autoptr(IdeCompletionProvider) hold = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+  g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (provider));
+
+  hold = g_object_ref (provider);
+
+  if (g_ptr_array_remove (self->providers, provider))
+    g_signal_emit (self, signals [PROVIDER_REMOVED], 0, hold);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_completion_show:
+ * @self: an #IdeCompletion
+ *
+ * Emits the "show" signal.
+ *
+ * When the "show" signal is emitted, the completion window will be
+ * displayed if there are any results available.
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_show (IdeCompletion *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+
+  if (ide_completion_is_blocked (self))
+    IDE_EXIT;
+
+  self->showing++;
+  if (self->showing == 1)
+    g_signal_emit (self, signals [SHOW], 0);
+  self->showing--;
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_completion_hide:
+ * @self: an #IdeCompletion
+ *
+ * Emits the "hide" signal.
+ *
+ * When the "hide" signal is emitted, the completion window will be
+ * dismissed.
+ *
+ * Since: 3.32
+ */
+void
+ide_completion_hide (IdeCompletion *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+
+  g_signal_emit (self, signals [HIDE], 0);
+
+  IDE_EXIT;
+}
+
+void
+ide_completion_cancel (IdeCompletion *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+
+  /* Nothing can re-use in-flight results now */
+  self->waiting_for_results = FALSE;
+  self->needs_refilter = FALSE;
+
+  if (self->context != NULL)
+    {
+      g_cancellable_cancel (self->cancellable);
+      g_clear_object (&self->cancellable);
+      ide_completion_set_context (self, NULL);
+
+      if (self->display != NULL)
+        {
+          ide_completion_display_set_context (self->display, NULL);
+          gtk_widget_hide (GTK_WIDGET (self->display));
+        }
+    }
+
+  IDE_EXIT;
+}
+
+void
+ide_completion_block_interactive (IdeCompletion *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+
+  self->block_count++;
+
+  ide_completion_cancel (self);
+
+  IDE_EXIT;
+}
+
+void
+ide_completion_unblock_interactive (IdeCompletion *self)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+
+  self->block_count--;
+
+  IDE_EXIT;
+}
+
+void
+ide_completion_set_n_rows (IdeCompletion *self,
+                           guint          n_rows)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+  g_return_if_fail (n_rows > 0);
+  g_return_if_fail (n_rows <= 32);
+
+  if (self->n_rows != n_rows)
+    {
+      self->n_rows = n_rows;
+      if (self->display != NULL)
+        ide_completion_display_set_n_rows (self->display, n_rows);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_N_ROWS]);
+    }
+
+  IDE_EXIT;
+}
+
+guint
+ide_completion_get_n_rows (IdeCompletion *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION (self), 0);
+  return self->n_rows;
+}
+
+void
+_ide_completion_activate (IdeCompletion         *self,
+                          IdeCompletionContext  *context,
+                          IdeCompletionProvider *provider,
+                          IdeCompletionProposal *proposal)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+  g_return_if_fail (IDE_IS_COMPLETION_CONTEXT (context));
+  g_return_if_fail (IDE_IS_COMPLETION_PROVIDER (provider));
+  g_return_if_fail (IDE_IS_COMPLETION_PROPOSAL (proposal));
+
+  self->block_count++;
+  ide_completion_provider_activate_poposal (provider, context, proposal, self->current_event);
+  self->block_count--;
+
+  IDE_EXIT;
+}
+
+void
+_ide_completion_set_language_id (IdeCompletion *self,
+                                 const gchar   *language_id)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+  g_return_if_fail (language_id != NULL);
+
+  ide_extension_set_adapter_set_value (self->addins, language_id);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_completion_is_visible:
+ * @self: a #IdeCompletion
+ *
+ * Checks if the completion display is visible.
+ *
+ * Returns: %TRUE if the display is visible
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_is_visible (IdeCompletion *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION (self), FALSE);
+
+  if (self->display != NULL)
+    return gtk_widget_get_visible (GTK_WIDGET (self->display));
+
+  return FALSE;
+}
+
+/**
+ * ide_completion_get_display:
+ * @self: a #IdeCompletion
+ *
+ * Gets the display for completion.
+ *
+ * Returns: (transfer none): an #IdeCompletionDisplay
+ *
+ * Since: 3.32
+ */
+IdeCompletionDisplay *
+ide_completion_get_display (IdeCompletion *self)
+{
+  g_return_val_if_fail (IDE_IS_COMPLETION (self), NULL);
+
+  if (self->display == NULL)
+    {
+      self->display = ide_completion_create_display (self);
+      g_signal_connect (self->display,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        &self->display);
+      ide_completion_display_set_n_rows (self->display, self->n_rows);
+      ide_completion_display_attach (self->display, self->view);
+      _ide_completion_display_set_font_desc (self->display, self->font_desc);
+      ide_completion_display_set_context (self->display, self->context);
+    }
+
+  return self->display;
+}
+
+void
+ide_completion_move_cursor (IdeCompletion   *self,
+                            GtkMovementStep  step,
+                            gint             direction)
+{
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+
+  if (self->display != NULL)
+    ide_completion_display_move_cursor (self->display, step, direction);
+
+  IDE_EXIT;
+}
+
+void
+_ide_completion_set_font_description (IdeCompletion              *self,
+                                      const PangoFontDescription *font_desc)
+{
+  g_return_if_fail (IDE_IS_COMPLETION (self));
+
+  if (font_desc != self->font_desc)
+    {
+      pango_font_description_free (self->font_desc);
+      self->font_desc = pango_font_description_copy (font_desc);
+
+      /*
+       * Work around issue where when a proposal provides "<b>markup</b>" and
+       * the weight is set in the font description, the <b> markup will not
+       * have it's weight respected. This seems to be happening because the
+       * weight mask is getting set in pango_font_description_from_string()
+       * even if the the value is set to normal. That matter is complicated
+       * because PangoAttrFontDesc and PangoAttrWeight will both have the
+       * same starting offset in the PangoLayout.
+       *
+       * https://bugzilla.gnome.org/show_bug.cgi?id=755968
+       */
+      if (PANGO_WEIGHT_NORMAL == pango_font_description_get_weight (self->font_desc))
+        pango_font_description_unset_fields (self->font_desc, PANGO_FONT_MASK_WEIGHT);
+
+      if (self->display != NULL)
+        _ide_completion_display_set_font_desc (self->display, font_desc);
+    }
+}
+
+/**
+ * ide_completion_fuzzy_match:
+ * @haystack: (nullable): the string to be searched.
+ * @casefold_needle: A g_utf8_casefold() version of the needle.
+ * @priority: (out) (allow-none): An optional location for the score of the match
+ *
+ * This helper function can do a fuzzy match for you giving a haystack and
+ * casefolded needle. Casefold your needle using g_utf8_casefold() before
+ * running the query.
+ *
+ * Score will be set with the score of the match upon success. Otherwise,
+ * it will be set to zero.
+ *
+ * Returns: %TRUE if @haystack matched @casefold_needle, otherwise %FALSE.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_completion_fuzzy_match (const gchar *haystack,
+                            const gchar *casefold_needle,
+                            guint       *priority)
+{
+  gint real_score = 0;
+
+  if (haystack == NULL || haystack[0] == 0)
+    return FALSE;
+
+  for (; *casefold_needle; casefold_needle = g_utf8_next_char (casefold_needle))
+    {
+      gunichar ch = g_utf8_get_char (casefold_needle);
+      gunichar chup = g_unichar_toupper (ch);
+      const gchar *tmp;
+      const gchar *downtmp;
+      const gchar *uptmp;
+
+      /*
+       * Note that the following code is not really correct. We want
+       * to be relatively fast here, but we also don't want to convert
+       * strings to casefolded versions for querying on each compare.
+       * So we use the casefold version and compare with upper. This
+       * works relatively well since we are usually dealing with ASCII
+       * for function names and symbols.
+       */
+
+      downtmp = strchr (haystack, ch);
+      uptmp = strchr (haystack, chup);
+
+      if (downtmp && uptmp)
+        tmp = MIN (downtmp, uptmp);
+      else if (downtmp)
+        tmp = downtmp;
+      else if (uptmp)
+        tmp = uptmp;
+      else
+        return FALSE;
+
+      /*
+       * Here we calculate the cost of this character into the score.
+       * If we matched exactly on the next character, the cost is ZERO.
+       * However, if we had to skip some characters, we have a cost
+       * of 2*distance to the character. This is necessary so that
+       * when we add the cost of the remaining haystack, strings which
+       * exhausted @casefold_needle score lower (higher priority) than
+       * strings which had to skip characters but matched the same
+       * number of characters in the string.
+       */
+      real_score += (tmp - haystack) * 2;
+
+      /* Add extra cost if we matched by using toupper */
+      if (*haystack == chup)
+        real_score += 1;
+
+      /*
+       * Now move past our matching character so we cannot match
+       * it a second time.
+       */
+      haystack = tmp + 1;
+    }
+
+  if (priority != NULL)
+    *priority = real_score + strlen (haystack);
+
+  return TRUE;
+}
+
+/**
+ * ide_completion_fuzzy_highlight:
+ * @haystack: the string to be highlighted
+ * @casefold_query: the typed-text used to highlight @haystack
+ *
+ * This will add &lt;b&gt; tags around matched characters in @haystack
+ * based on @casefold_query.
+ *
+ * Returns: a newly allocated string
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_completion_fuzzy_highlight (const gchar *haystack,
+                                const gchar *casefold_query)
+{
+  static const gchar *begin = "<b>";
+  static const gchar *end = "</b>";
+  GString *ret;
+  gunichar str_ch;
+  gunichar match_ch;
+  gboolean element_open = FALSE;
+
+  if (haystack == NULL || casefold_query == NULL)
+    return g_strdup (haystack);
+
+  ret = g_string_new (NULL);
+
+  for (; *haystack; haystack = g_utf8_next_char (haystack))
+    {
+      str_ch = g_utf8_get_char (haystack);
+      match_ch = g_utf8_get_char (casefold_query);
+
+      if ((str_ch == match_ch) || (g_unichar_tolower (str_ch) == g_unichar_tolower (match_ch)))
+        {
+          if (!element_open)
+            {
+              g_string_append (ret, begin);
+              element_open = TRUE;
+            }
+
+          g_string_append_unichar (ret, str_ch);
+
+          /* TODO: We could seek to the next char and append in a batch. */
+          casefold_query = g_utf8_next_char (casefold_query);
+        }
+      else
+        {
+          if (element_open)
+            {
+              g_string_append (ret, end);
+              element_open = FALSE;
+            }
+
+          g_string_append_unichar (ret, str_ch);
+        }
+    }
+
+  if (element_open)
+    g_string_append (ret, end);
+
+  return g_string_free (ret, FALSE);
+}
diff --git a/src/libide/sourceview/ide-completion.h b/src/libide/sourceview/ide-completion.h
new file mode 100644
index 000000000..585f9dd87
--- /dev/null
+++ b/src/libide/sourceview/ide-completion.h
@@ -0,0 +1,81 @@
+/* ide-completion.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <gtk/gtk.h>
+
+#include "ide-completion-types.h"
+#include "ide-source-view.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_COMPLETION (ide_completion_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeCompletion, ide_completion, IDE, COMPLETION, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeCompletionDisplay *ide_completion_get_display         (IdeCompletion *self);
+IDE_AVAILABLE_IN_3_32
+GtkSourceView        *ide_completion_get_view            (IdeCompletion         *self);
+IDE_AVAILABLE_IN_3_32
+GtkTextBuffer        *ide_completion_get_buffer          (IdeCompletion         *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_completion_block_interactive   (IdeCompletion         *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_completion_unblock_interactive (IdeCompletion         *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_completion_add_provider        (IdeCompletion         *self,
+                                                          IdeCompletionProvider *provider);
+IDE_AVAILABLE_IN_3_32
+void                  ide_completion_remove_provider     (IdeCompletion         *self,
+                                                          IdeCompletionProvider *provider);
+IDE_AVAILABLE_IN_3_32
+guint                 ide_completion_get_n_rows          (IdeCompletion         *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_completion_set_n_rows          (IdeCompletion         *self,
+                                                          guint                  n_rows);
+IDE_AVAILABLE_IN_3_32
+void                  ide_completion_hide                (IdeCompletion         *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_completion_show                (IdeCompletion         *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_completion_cancel              (IdeCompletion         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean              ide_completion_is_visible          (IdeCompletion         *self);
+IDE_AVAILABLE_IN_3_32
+void                  ide_completion_move_cursor         (IdeCompletion         *self,
+                                                          GtkMovementStep        step,
+                                                          gint                   direction);
+IDE_AVAILABLE_IN_3_32
+gboolean              ide_completion_fuzzy_match         (const gchar           *haystack,
+                                                          const gchar           *casefold_needle,
+                                                          guint                 *priority);
+IDE_AVAILABLE_IN_3_32
+gchar                *ide_completion_fuzzy_highlight     (const gchar           *haystack,
+                                                          const gchar           *casefold_query);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-cursor.c b/src/libide/sourceview/ide-cursor.c
index 0d966dde7..faa7411ba 100644
--- a/src/libide/sourceview/ide-cursor.c
+++ b/src/libide/sourceview/ide-cursor.c
@@ -14,6 +14,8 @@
  *
  * 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 "ide-cursor"
@@ -22,9 +24,9 @@
 
 #include <dazzle.h>
 
-#include "sourceview/ide-source-view.h"
-#include "sourceview/ide-cursor.h"
-#include "sourceview/ide-text-util.h"
+#include "ide-source-view.h"
+#include "ide-cursor.h"
+#include "ide-text-util.h"
 
 struct _IdeCursor
 {
diff --git a/src/libide/sourceview/ide-cursor.h b/src/libide/sourceview/ide-cursor.h
index f2d0e237c..7c1e4e6dc 100644
--- a/src/libide/sourceview/ide-cursor.h
+++ b/src/libide/sourceview/ide-cursor.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/libide/sourceview/ide-gutter.c b/src/libide/sourceview/ide-gutter.c
new file mode 100644
index 000000000..43529efef
--- /dev/null
+++ b/src/libide/sourceview/ide-gutter.c
@@ -0,0 +1,128 @@
+/* ide-gutter.c
+ *
+ * Copyright 2018-2019 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 "ide-gutter"
+
+#include "config.h"
+
+#include "ide-gutter.h"
+
+G_DEFINE_INTERFACE (IdeGutter, ide_gutter, G_TYPE_OBJECT)
+
+enum {
+  STYLE_CHANGED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+ide_gutter_default_init (IdeGutterInterface *iface)
+{
+  g_object_interface_install_property (iface,
+                                       g_param_spec_boolean ("show-line-changes",
+                                                             "Show Line Changes",
+                                                             "If line changes should be displayed",
+                                                             FALSE,
+                                                             (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
+
+  g_object_interface_install_property (iface,
+                                       g_param_spec_boolean ("show-line-diagnostics",
+                                                             "Show Line Diagnostics",
+                                                             "If line diagnostics should be displayed",
+                                                             FALSE,
+                                                             (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
+
+  g_object_interface_install_property (iface,
+                                       g_param_spec_boolean ("show-line-numbers",
+                                                             "Show Line Numbers",
+                                                             "If line numbers should be displayed",
+                                                             FALSE,
+                                                             (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
+
+  signals [STYLE_CHANGED] =
+    g_signal_new ("style-changed",
+                  G_TYPE_FROM_INTERFACE (iface),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeGutterInterface, style_changed),
+                  NULL,
+                  NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE, 0);
+}
+
+void
+ide_gutter_style_changed (IdeGutter *self)
+{
+  g_return_if_fail (IDE_IS_GUTTER (self));
+
+  g_signal_emit (self, signals [STYLE_CHANGED], 0);
+}
+
+gboolean
+ide_gutter_get_show_line_changes (IdeGutter *self)
+{
+  gboolean ret;
+  g_object_get (self, "show-line-changes", &ret, NULL);
+  return ret;
+}
+
+gboolean
+ide_gutter_get_show_line_numbers (IdeGutter *self)
+{
+  gboolean ret;
+  g_object_get (self, "show-line-numbers", &ret, NULL);
+  return ret;
+}
+
+gboolean
+ide_gutter_get_show_line_diagnostics (IdeGutter *self)
+{
+  gboolean ret;
+  g_object_get (self, "show-line-diagnostics", &ret, NULL);
+  return ret;
+}
+
+void
+ide_gutter_set_show_line_changes (IdeGutter *self,
+                                  gboolean   show_line_changes)
+{
+  g_return_if_fail (IDE_IS_GUTTER (self));
+
+  g_object_set (self, "show-line-changes", show_line_changes, NULL);
+}
+
+void
+ide_gutter_set_show_line_numbers (IdeGutter *self,
+                                  gboolean   show_line_numbers)
+{
+  g_return_if_fail (IDE_IS_GUTTER (self));
+
+  g_object_set (self, "show-line-numbers", show_line_numbers, NULL);
+}
+
+void
+ide_gutter_set_show_line_diagnostics (IdeGutter *self,
+                                      gboolean   show_line_diagnostics)
+{
+  g_return_if_fail (IDE_IS_GUTTER (self));
+
+  g_object_set (self, "show-line-diagnostics", show_line_diagnostics, NULL);
+}
diff --git a/src/libide/sourceview/ide-gutter.h b/src/libide/sourceview/ide-gutter.h
new file mode 100644
index 000000000..79a8ef918
--- /dev/null
+++ b/src/libide/sourceview/ide-gutter.h
@@ -0,0 +1,58 @@
+/* ide-gutter.h
+ *
+ * Copyright 2018-2019 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 <gtksourceview/gtksource.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_GUTTER (ide_gutter_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeGutter, ide_gutter, IDE, GUTTER, GObject)
+
+struct _IdeGutterInterface
+{
+  GTypeInterface parent_class;
+
+  void (*style_changed) (IdeGutter *self);
+};
+
+IDE_AVAILABLE_IN_3_32
+gboolean ide_gutter_get_show_line_changes     (IdeGutter *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_gutter_get_show_line_numbers     (IdeGutter *self);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_gutter_get_show_line_diagnostics (IdeGutter *self);
+IDE_AVAILABLE_IN_3_32
+void     ide_gutter_set_show_line_changes     (IdeGutter *self,
+                                               gboolean   show_line_changes);
+IDE_AVAILABLE_IN_3_32
+void     ide_gutter_set_show_line_numbers     (IdeGutter *self,
+                                               gboolean   show_line_numbers);
+IDE_AVAILABLE_IN_3_32
+void     ide_gutter_set_show_line_diagnostics (IdeGutter *self,
+                                               gboolean   show_line_diagnostics);
+IDE_AVAILABLE_IN_3_32
+void     ide_gutter_style_changed             (IdeGutter *self);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-hover-context-private.h 
b/src/libide/sourceview/ide-hover-context-private.h
new file mode 100644
index 000000000..2e3c9ce72
--- /dev/null
+++ b/src/libide/sourceview/ide-hover-context-private.h
@@ -0,0 +1,50 @@
+/* ide-hover-context-private.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+#include <libide-code.h>
+
+#include "ide-hover-context.h"
+#include "ide-hover-provider.h"
+
+G_BEGIN_DECLS
+
+typedef void (*IdeHoverContextForeach) (const gchar      *title,
+                                        IdeMarkedContent *content,
+                                        GtkWidget        *widget,
+                                        gpointer          user_data);
+
+void     _ide_hover_context_add_provider  (IdeHoverContext         *context,
+                                           IdeHoverProvider        *provider);
+void     _ide_hover_context_query_async   (IdeHoverContext         *self,
+                                           const GtkTextIter       *iter,
+                                           GCancellable            *cancellable,
+                                           GAsyncReadyCallback      callback,
+                                           gpointer                 user_data);
+gboolean _ide_hover_context_query_finish  (IdeHoverContext         *self,
+                                           GAsyncResult            *result,
+                                           GError                 **error);
+void     _ide_hover_context_foreach       (IdeHoverContext         *self,
+                                           IdeHoverContextForeach   foreach,
+                                           gpointer                 foreach_data);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-hover-context.c b/src/libide/sourceview/ide-hover-context.c
new file mode 100644
index 000000000..1cf6c5485
--- /dev/null
+++ b/src/libide/sourceview/ide-hover-context.c
@@ -0,0 +1,272 @@
+/* ide-hover-context.c
+ *
+ * Copyright 2018-2019 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 "ide-hover-context"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-hover-context.h"
+#include "ide-hover-context-private.h"
+#include "ide-hover-provider.h"
+
+struct _IdeHoverContext
+{
+  GObject    parent_instance;
+  GPtrArray *providers;
+  GArray    *content;
+};
+
+typedef struct
+{
+  gchar            *title;
+  IdeMarkedContent *content;
+  GtkWidget        *widget;
+  gint              priority;
+} Item;
+
+typedef struct
+{
+  guint active;
+} Query;
+
+G_DEFINE_TYPE (IdeHoverContext, ide_hover_context, G_TYPE_OBJECT)
+
+static void
+clear_item (Item *item)
+{
+  g_clear_pointer (&item->title, g_free);
+  g_clear_pointer (&item->content, ide_marked_content_unref);
+  g_clear_object (&item->widget);
+}
+
+static void
+ide_hover_context_dispose (GObject *object)
+{
+  IdeHoverContext *self = (IdeHoverContext *)object;
+
+  g_clear_pointer (&self->content, g_array_unref);
+  g_clear_pointer (&self->providers, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (ide_hover_context_parent_class)->dispose (object);
+}
+
+static void
+ide_hover_context_class_init (IdeHoverContextClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_hover_context_dispose;
+}
+
+static void
+ide_hover_context_init (IdeHoverContext *self)
+{
+  self->providers = g_ptr_array_new_with_free_func (g_object_unref);
+
+  self->content = g_array_new (FALSE, FALSE, sizeof (Item));
+  g_array_set_clear_func (self->content, (GDestroyNotify) clear_item);
+}
+
+static gint
+item_compare (gconstpointer a,
+              gconstpointer b)
+{
+  const Item *item_a = a;
+  const Item *item_b = b;
+
+  return item_a->priority - item_b->priority;
+}
+
+void
+ide_hover_context_add_content (IdeHoverContext  *self,
+                               gint              priority,
+                               const gchar      *title,
+                               IdeMarkedContent *content)
+{
+  Item item = {0};
+
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (self));
+  g_return_if_fail (content != NULL);
+
+  item.title = g_strdup (title);
+  item.content = ide_marked_content_ref (content);
+  item.widget = NULL;
+  item.priority = priority;
+
+  g_array_append_val (self->content, item);
+  g_array_sort (self->content, item_compare);
+}
+
+void
+ide_hover_context_add_widget (IdeHoverContext *self,
+                              gint             priority,
+                              const gchar     *title,
+                              GtkWidget       *widget)
+{
+  Item item = {0};
+
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (self));
+  g_return_if_fail (widget != NULL);
+
+  item.title = g_strdup (title);
+  item.content = NULL;
+  item.widget = g_object_ref_sink (widget);
+  item.priority = priority;
+
+  g_array_append_val (self->content, item);
+  g_array_sort (self->content, item_compare);
+}
+
+void
+_ide_hover_context_add_provider (IdeHoverContext  *self,
+                                 IdeHoverProvider *provider)
+{
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (self));
+  g_return_if_fail (IDE_IS_HOVER_PROVIDER (provider));
+
+  g_ptr_array_add (self->providers, g_object_ref (provider));
+}
+
+static void
+query_free (Query *q)
+{
+  g_slice_free (Query, q);
+}
+
+static void
+ide_hover_context_query_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeHoverProvider *provider = (IdeHoverProvider *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  Query *q;
+
+  g_assert (IDE_IS_HOVER_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  q = ide_task_get_task_data (task);
+  g_assert (q != NULL);
+  g_assert (q->active > 0);
+
+  if (!ide_hover_provider_hover_finish (provider, result, &error))
+    g_debug ("%s: %s", G_OBJECT_TYPE_NAME (provider), error->message);
+
+  q->active--;
+
+  if (q->active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+void
+_ide_hover_context_query_async (IdeHoverContext     *self,
+                                const GtkTextIter   *iter,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  Query *q;
+
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, _ide_hover_context_query_async);
+
+  q = g_slice_new0 (Query);
+  q->active = self->providers->len;
+  ide_task_set_task_data (task, q, query_free);
+
+  for (guint i = 0; i < self->providers->len; i++)
+    {
+      IdeHoverProvider *provider = g_ptr_array_index (self->providers, i);
+
+      ide_hover_provider_hover_async (provider,
+                                      self,
+                                      iter,
+                                      cancellable,
+                                      ide_hover_context_query_cb,
+                                      g_object_ref (task));
+    }
+
+  if (q->active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+/**
+ * ide_hover_context_query_finish:
+ * @self: an #IdeHoverContext
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes a request to query providers.
+ *
+ * Returns: %TRUE if successful, otherwise %FALSE and @error.
+ *
+ * Since: 3.32
+ */
+gboolean
+_ide_hover_context_query_finish (IdeHoverContext  *self,
+                                 GAsyncResult     *result,
+                                 GError          **error)
+{
+  g_return_val_if_fail (IDE_IS_HOVER_CONTEXT (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+gboolean
+ide_hover_context_has_content (IdeHoverContext *self)
+{
+  g_return_val_if_fail (IDE_IS_HOVER_CONTEXT (self), FALSE);
+
+  return self->content != NULL && self->content->len > 0;
+}
+
+void
+_ide_hover_context_foreach (IdeHoverContext        *self,
+                            IdeHoverContextForeach  foreach,
+                            gpointer                foreach_data)
+{
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (self));
+  g_return_if_fail (foreach != NULL);
+
+  if (self->content == NULL || self->content->len == 0)
+    return;
+
+  /* Iterate backwards to allow mutation */
+  for (guint i = self->content->len; i > 0; i--)
+    {
+      const Item *item = &g_array_index (self->content, Item, i - 1);
+
+      foreach (item->title, item->content, item->widget, foreach_data);
+
+      /* Widgets are consumed to prevent double use */
+      if (item->widget != NULL)
+        g_array_remove_index (self->content, i - 1);
+    }
+}
diff --git a/src/libide/sourceview/ide-hover-context.h b/src/libide/sourceview/ide-hover-context.h
new file mode 100644
index 000000000..0f42bec24
--- /dev/null
+++ b/src/libide/sourceview/ide-hover-context.h
@@ -0,0 +1,51 @@
+/* ide-hover-context.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+#include <libide-code.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HOVER_CONTEXT (ide_hover_context_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeHoverContext, ide_hover_context, IDE, HOVER_CONTEXT, GObject)
+
+IDE_AVAILABLE_IN_3_32
+void     ide_hover_context_add_content  (IdeHoverContext      *self,
+                                         gint                  priority,
+                                         const gchar          *title,
+                                         IdeMarkedContent     *content);
+IDE_AVAILABLE_IN_3_32
+void     ide_hover_context_add_widget   (IdeHoverContext      *self,
+                                         gint                  priority,
+                                         const gchar          *title,
+                                         GtkWidget            *widget);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_hover_context_has_content  (IdeHoverContext      *self);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-hover-popover-private.h 
b/src/libide/sourceview/ide-hover-popover-private.h
new file mode 100644
index 000000000..5171ef18c
--- /dev/null
+++ b/src/libide/sourceview/ide-hover-popover-private.h
@@ -0,0 +1,42 @@
+/* ide-hover-popover-private.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+
+#include "ide-hover-context.h"
+#include "ide-hover-provider.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HOVER_POPOVER (ide_hover_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeHoverPopover, ide_hover_popover, IDE, HOVER_POPOVER, GtkPopover)
+
+IdeHoverContext *_ide_hover_popover_get_context    (IdeHoverPopover    *self);
+void             _ide_hover_popover_add_provider   (IdeHoverPopover    *self,
+                                                    IdeHoverProvider   *provider);
+void             _ide_hover_popover_show           (IdeHoverPopover    *self);
+void             _ide_hover_popover_hide           (IdeHoverPopover    *self);
+void             _ide_hover_popover_set_hovered_at (IdeHoverPopover    *self,
+                                                    const GdkRectangle *hovered_at);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-hover-popover.c b/src/libide/sourceview/ide-hover-popover.c
new file mode 100644
index 000000000..c784e309a
--- /dev/null
+++ b/src/libide/sourceview/ide-hover-popover.c
@@ -0,0 +1,351 @@
+/* ide-hover-popover.c
+ *
+ * Copyright 2018-2019 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 "ide-hover-popover"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-code.h>
+#include <libide-gui.h>
+#include <string.h>
+
+#include "ide-hover-context-private.h"
+#include "ide-hover-popover-private.h"
+
+struct _IdeHoverPopover
+{
+  GtkPopover parent_instance;
+
+  /*
+   * A vertical box containing all of our marked content/widgets that
+   * were provided by the context.
+   */
+  GtkBox *box;
+
+  /*
+   * Our context to be observed. As items are added to the context,
+   * we add them to the popver (creating or re-using the widget) based
+   * on the kind of content.
+   */
+  IdeHoverContext *context;
+
+  /*
+   * This is our cancellable to cancel any in-flight requests to the
+   * hover providers when the popover is withdrawn. That could happen
+   * before we've even really been displayed to the user.
+   */
+  GCancellable *cancellable;
+
+  /*
+   * The position where the hover operation began, in buffer coordinates.
+   */
+  GdkRectangle hovered_at;
+
+  /*
+   * If we've had any providers added, so that we can short-circuit
+   * in that case without having to display the popover.
+   */
+  guint has_providers : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_HOVERED_AT,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeHoverPopover, ide_hover_popover, GTK_TYPE_POPOVER)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_hover_popover_add_content (const gchar      *title,
+                               IdeMarkedContent *content,
+                               GtkWidget        *widget,
+                               gpointer          user_data)
+{
+  IdeHoverPopover *self = user_data;
+  GtkBox *box;
+
+  g_assert (content != NULL || widget != NULL);
+  g_assert (!widget || GTK_IS_WIDGET (widget));
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "orientation", GTK_ORIENTATION_VERTICAL,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (self->box), GTK_WIDGET (box));
+
+  if (!dzl_str_empty0 (title))
+    {
+      GtkWidget *label;
+
+      label = g_object_new (GTK_TYPE_LABEL,
+                            "xalign", 0.0f,
+                            "label", title,
+                            "use-markup", FALSE,
+                            "visible", TRUE,
+                            NULL);
+      dzl_gtk_widget_add_style_class (label, "title");
+      gtk_container_add (GTK_CONTAINER (box), label);
+    }
+
+  if (content != NULL)
+    {
+      GtkWidget *view = ide_marked_view_new (content);
+
+      if (view != NULL)
+        {
+          gtk_container_add (GTK_CONTAINER (box), view);
+          gtk_widget_show (view);
+        }
+    }
+
+  if (widget != NULL)
+    {
+      gtk_container_add (GTK_CONTAINER (box), widget);
+      gtk_widget_show (widget);
+    }
+}
+
+static void
+ide_hover_popover_query_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeHoverContext *context = (IdeHoverContext *)object;
+  g_autoptr(IdeHoverPopover) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_HOVER_CONTEXT (context));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_HOVER_POPOVER (self));
+
+  if (!_ide_hover_context_query_finish (context, result, &error) ||
+      !ide_hover_context_has_content (context))
+    {
+      gtk_widget_destroy (GTK_WIDGET (self));
+      return;
+    }
+
+  _ide_hover_context_foreach (context,
+                              ide_hover_popover_add_content,
+                              self);
+
+  gtk_widget_show (GTK_WIDGET (self));
+}
+
+static void
+ide_hover_popover_get_preferred_height (GtkWidget *widget,
+                                        gint      *min_height,
+                                        gint      *nat_height)
+{
+  g_assert (IDE_IS_HOVER_POPOVER (widget));
+  g_assert (min_height != NULL);
+  g_assert (nat_height != NULL);
+
+  GTK_WIDGET_CLASS (ide_hover_popover_parent_class)->get_preferred_height (widget, min_height, nat_height);
+
+  /*
+   * If we have embedded webkit views, they can get some bogus size requests
+   * sometimes. So try to detect that and prevent giant popovers.
+   */
+
+  if (*nat_height > 1024)
+    *nat_height = *min_height;
+}
+
+void
+_ide_hover_popover_set_hovered_at (IdeHoverPopover    *self,
+                                   const GdkRectangle *hovered_at)
+{
+  g_assert (IDE_IS_HOVER_POPOVER (self));
+
+  if (hovered_at != NULL)
+    self->hovered_at = *hovered_at;
+  else
+    memset (&self->hovered_at, 0, sizeof self->hovered_at);
+}
+
+static void
+ide_hover_popover_destroy (GtkWidget *widget)
+{
+  IdeHoverPopover *self = (IdeHoverPopover *)widget;
+
+  g_cancellable_cancel (self->cancellable);
+
+  g_clear_object (&self->context);
+  g_clear_object (&self->cancellable);
+
+  GTK_WIDGET_CLASS (ide_hover_popover_parent_class)->destroy (widget);
+}
+
+static void
+ide_hover_popover_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeHoverPopover *self = IDE_HOVER_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, _ide_hover_popover_get_context (self));
+      break;
+
+    case PROP_HOVERED_AT:
+      g_value_set_boxed (value, &self->hovered_at);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_hover_popover_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeHoverPopover *self = IDE_HOVER_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_HOVERED_AT:
+      _ide_hover_popover_set_hovered_at (self, g_value_get_boxed (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_hover_popover_class_init (IdeHoverPopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_hover_popover_get_property;
+  object_class->set_property = ide_hover_popover_set_property;
+
+  widget_class->destroy = ide_hover_popover_destroy;
+  widget_class->get_preferred_height = ide_hover_popover_get_preferred_height;
+
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The hover context to display to the user",
+                         IDE_TYPE_HOVER_CONTEXT,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_HOVERED_AT] =
+    g_param_spec_boxed ("hovered-at",
+                         "Hovered At",
+                         "The position that the hover originated in buffer coordinates",
+                         GDK_TYPE_RECTANGLE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_hover_popover_init (IdeHoverPopover *self)
+{
+  GtkStyleContext *style_context;
+
+  self->context = g_object_new (IDE_TYPE_HOVER_CONTEXT, NULL);
+  self->cancellable = g_cancellable_new ();
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  gtk_style_context_add_class (style_context, "hoverer");
+
+  self->box = g_object_new (GTK_TYPE_BOX,
+                            "orientation", GTK_ORIENTATION_VERTICAL,
+                            "visible", TRUE,
+                            NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->box));
+}
+
+IdeHoverContext *
+_ide_hover_popover_get_context (IdeHoverPopover *self)
+{
+  g_return_val_if_fail (IDE_IS_HOVER_POPOVER (self), NULL);
+
+  return self->context;
+}
+
+void
+_ide_hover_popover_add_provider (IdeHoverPopover  *self,
+                                 IdeHoverProvider *provider)
+{
+  g_return_if_fail (IDE_IS_HOVER_POPOVER (self));
+  g_return_if_fail (IDE_IS_HOVER_PROVIDER (provider));
+
+  _ide_hover_context_add_provider (self->context, provider);
+
+  self->has_providers = TRUE;
+}
+
+void
+_ide_hover_popover_show (IdeHoverPopover *self)
+{
+  GtkWidget *view;
+
+  g_return_if_fail (IDE_IS_HOVER_POPOVER (self));
+  g_return_if_fail (self->context != NULL);
+
+  if (self->has_providers &&
+      !g_cancellable_is_cancelled (self->cancellable) &&
+      (view = gtk_popover_get_relative_to (GTK_POPOVER (self))) &&
+      GTK_IS_TEXT_VIEW (view))
+    {
+      GtkTextIter iter;
+
+      /* hovered_at is in buffer coordinates */
+      gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view),
+                                          &iter,
+                                          self->hovered_at.x,
+                                          self->hovered_at.y);
+
+      _ide_hover_context_query_async (self->context,
+                                      &iter,
+                                      self->cancellable,
+                                      ide_hover_popover_query_cb,
+                                      g_object_ref (self));
+
+      return;
+    }
+
+  /* Cancel this popover immediately, we have nothing to do */
+  gtk_widget_destroy (GTK_WIDGET (self));
+}
+
+void
+_ide_hover_popover_hide (IdeHoverPopover *self)
+{
+  g_return_if_fail (IDE_IS_HOVER_POPOVER (self));
+
+  gtk_widget_destroy (GTK_WIDGET (self));
+}
diff --git a/src/libide/sourceview/ide-hover-private.h b/src/libide/sourceview/ide-hover-private.h
new file mode 100644
index 000000000..3c80141ef
--- /dev/null
+++ b/src/libide/sourceview/ide-hover-private.h
@@ -0,0 +1,39 @@
+/* ide-hover-private.h
+ *
+ * Copyright 2018-2019 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 "ide-source-view.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HOVER (ide_hover_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeHover, ide_hover, IDE, HOVER, GObject)
+
+IdeHover *_ide_hover_new          (IdeSourceView     *view);
+void      _ide_hover_display      (IdeHover          *self,
+                                   const GtkTextIter *iter);
+void      _ide_hover_set_context  (IdeHover          *self,
+                                   IdeContext        *context);
+void      _ide_hover_set_language (IdeHover          *self,
+                                   const gchar       *language);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-hover-provider.c b/src/libide/sourceview/ide-hover-provider.c
new file mode 100644
index 000000000..079dc1007
--- /dev/null
+++ b/src/libide/sourceview/ide-hover-provider.c
@@ -0,0 +1,148 @@
+/* ide-hover-provider.c
+ *
+ * Copyright 2018-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-hover-provider"
+
+#include "config.h"
+
+#include "ide-hover-provider.h"
+#include "ide-source-view.h"
+
+G_DEFINE_INTERFACE (IdeHoverProvider, ide_hover_provider, G_TYPE_OBJECT)
+
+static void
+ide_hover_provider_real_hover_async (IdeHoverProvider    *self,
+                                     IdeHoverContext     *context,
+                                     const GtkTextIter   *location,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  g_task_report_new_error (self, callback, user_data,
+                           ide_hover_provider_real_hover_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Hovering not supported");
+}
+
+static gboolean
+ide_hover_provider_real_hover_finish (IdeHoverProvider  *self,
+                                      GAsyncResult      *result,
+                                      GError           **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_hover_provider_default_init (IdeHoverProviderInterface *iface)
+{
+  iface->hover_async = ide_hover_provider_real_hover_async;
+  iface->hover_finish = ide_hover_provider_real_hover_finish;
+}
+
+/**
+ * ide_hover_provider_load:
+ * @self: an #IdeHoverProvider
+ * @view: an #IdeSourceView
+ *
+ * This method is used to load an #IdeHoverProvider.
+ * Providers should perform any startup work from here.
+ *
+ * Since: 3.32
+ */
+void
+ide_hover_provider_load (IdeHoverProvider *self,
+                         IdeSourceView    *view)
+{
+  g_return_if_fail (IDE_IS_HOVER_PROVIDER (self));
+  g_return_if_fail (IDE_IS_SOURCE_VIEW (view));
+
+  if (IDE_HOVER_PROVIDER_GET_IFACE (self)->load)
+    IDE_HOVER_PROVIDER_GET_IFACE (self)->load (self, view);
+}
+
+/**
+ * ide_hover_provider_unload:
+ * @self: an #IdeHoverProvider
+ * @view: an #IdeSourceView
+ *
+ * This method is used to unload an #IdeHoverProvider.
+ * Providers should cleanup any state they've allocated.
+ *
+ * Since: 3.32
+ */
+void
+ide_hover_provider_unload (IdeHoverProvider *self,
+                           IdeSourceView    *view)
+{
+  g_return_if_fail (IDE_IS_HOVER_PROVIDER (self));
+  g_return_if_fail (IDE_IS_SOURCE_VIEW (view));
+
+  if (IDE_HOVER_PROVIDER_GET_IFACE (self)->unload)
+    IDE_HOVER_PROVIDER_GET_IFACE (self)->unload (self, view);
+}
+
+/**
+ * ide_hover_provider_hover_async:
+ * @self: an #IdeHoverProvider
+ * @location: a #GtkTextIter
+ * @cancellable: (nullable): a #GCancellable
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ *
+ * Since: 3.32
+ */
+void
+ide_hover_provider_hover_async (IdeHoverProvider    *self,
+                                IdeHoverContext     *context,
+                                const GtkTextIter   *location,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_HOVER_PROVIDER (self));
+  g_return_if_fail (IDE_IS_HOVER_CONTEXT (context));
+  g_return_if_fail (location != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_HOVER_PROVIDER_GET_IFACE (self)->hover_async (self, context, location, cancellable, callback, 
user_data);
+}
+
+/**
+ * ide_hover_provider_hover_finish:
+ * @self: an #IdeHoverProvider
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_hover_provider_hover_finish (IdeHoverProvider  *self,
+                                 GAsyncResult      *result,
+                                 GError           **error)
+{
+  g_return_val_if_fail (IDE_IS_HOVER_PROVIDER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_HOVER_PROVIDER_GET_IFACE (self)->hover_finish (self, result, error);
+}
diff --git a/src/libide/sourceview/ide-hover-provider.h b/src/libide/sourceview/ide-hover-provider.h
new file mode 100644
index 000000000..0f11e881c
--- /dev/null
+++ b/src/libide/sourceview/ide-hover-provider.h
@@ -0,0 +1,76 @@
+/* ide-hover-provider.h
+ *
+ * Copyright 2018-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+#include "ide-hover-context.h"
+#include "ide-source-view.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_HOVER_PROVIDER (ide_hover_provider_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeHoverProvider, ide_hover_provider, IDE, HOVER_PROVIDER, GObject)
+
+struct _IdeHoverProviderInterface
+{
+  GTypeInterface parent;
+
+  void     (*load)         (IdeHoverProvider     *self,
+                            IdeSourceView        *view);
+  void     (*unload)       (IdeHoverProvider     *self,
+                            IdeSourceView        *view);
+  void     (*hover_async)  (IdeHoverProvider     *self,
+                            IdeHoverContext      *context,
+                            const GtkTextIter    *location,
+                            GCancellable         *cancellable,
+                            GAsyncReadyCallback   callback,
+                            gpointer              user_data);
+  gboolean (*hover_finish) (IdeHoverProvider     *self,
+                            GAsyncResult         *result,
+                            GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_hover_provider_load         (IdeHoverProvider     *self,
+                                          IdeSourceView        *view);
+IDE_AVAILABLE_IN_3_32
+void     ide_hover_provider_unload       (IdeHoverProvider     *self,
+                                          IdeSourceView        *view);
+IDE_AVAILABLE_IN_3_32
+void     ide_hover_provider_hover_async  (IdeHoverProvider     *self,
+                                          IdeHoverContext      *context,
+                                          const GtkTextIter    *location,
+                                          GCancellable         *cancellable,
+                                          GAsyncReadyCallback   callback,
+                                          gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_hover_provider_hover_finish (IdeHoverProvider     *self,
+                                          GAsyncResult         *result,
+                                          GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-hover.c b/src/libide/sourceview/ide-hover.c
new file mode 100644
index 000000000..218fb9644
--- /dev/null
+++ b/src/libide/sourceview/ide-hover.c
@@ -0,0 +1,798 @@
+/* ide-hover.c
+ *
+ * Copyright 2018-2019 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 "ide-hover"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-code.h>
+#include <libide-plugins.h>
+#include <libpeas/peas.h>
+#include <string.h>
+
+#include "ide-hover-popover-private.h"
+#include "ide-hover-private.h"
+#include "ide-hover-provider.h"
+
+#define GRACE_X 20
+#define GRACE_Y 20
+#define MOTION_SETTLE_TIMEOUT_MSEC 500
+
+typedef enum
+{
+  IDE_HOVER_STATE_INITIAL,
+  IDE_HOVER_STATE_DISPLAY,
+  IDE_HOVER_STATE_IN_POPOVER,
+} IdeHoverState;
+
+struct _IdeHover
+{
+  GObject parent_instance;
+
+  /*
+   * Our signal group to handle the number of events on the textview so that
+   * we can update the hover provider and associated content.
+   */
+  DzlSignalGroup *signals;
+
+  /*
+   * Our plugins that can populate our IdeHoverContext with content to be
+   * displayed.
+   */
+  IdeExtensionSetAdapter *providers;
+
+  /*
+   * Our popover that will display content once the cursor has settled
+   * somewhere of importance.
+   */
+  IdeHoverPopover *popover;
+
+  /*
+   * Our last motion position, used to calculate where we should find
+   * our iter to display the popover.
+   */
+  gdouble motion_x;
+  gdouble motion_y;
+
+  /*
+   * Our state so that we can handle events in a sane manner without
+   * stomping all over things.
+   */
+  IdeHoverState state;
+
+  /*
+   * Our source which is continually delayed until the motion event has
+   * settled somewhere we can potentially display a popover.
+   */
+  guint delay_display_source;
+
+  /*
+   * We need to introduce some delay when we get a leave-notify-event
+   * because we might be entering the popover next.
+   */
+  guint dismiss_source;
+};
+
+static gboolean ide_hover_dismiss_cb (gpointer data);
+
+G_DEFINE_TYPE (IdeHover, ide_hover, G_TYPE_OBJECT)
+
+static void
+ide_hover_queue_dismiss (IdeHover *self)
+{
+  g_assert (IDE_IS_HOVER (self));
+
+  if (self->dismiss_source)
+    g_source_remove (self->dismiss_source);
+
+  /*
+   * Give ourselves just enough time to get the crossing event
+   * into the popover before we try to dismiss the popover.
+   */
+  self->dismiss_source =
+    gdk_threads_add_timeout_full (G_PRIORITY_HIGH,
+                                  10,
+                                  ide_hover_dismiss_cb,
+                                  self, NULL);
+}
+
+static void
+ide_hover_popover_closed_cb (IdeHover        *self,
+                             IdeHoverPopover *popover)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (IDE_IS_HOVER_POPOVER (popover));
+
+  self->state = IDE_HOVER_STATE_INITIAL;
+  gtk_widget_destroy (GTK_WIDGET (popover));
+  dzl_clear_source (&self->dismiss_source);
+  dzl_clear_source (&self->delay_display_source);
+
+  g_assert (self->popover == NULL);
+  g_assert (self->state == IDE_HOVER_STATE_INITIAL);
+  g_assert (self->dismiss_source == 0);
+  g_assert (self->delay_display_source == 0);
+}
+
+static gboolean
+ide_hover_popover_enter_notify_event_cb (IdeHover               *self,
+                                         const GdkEventCrossing *event,
+                                         IdeHoverPopover        *popover)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (IDE_IS_HOVER_POPOVER (popover));
+  g_assert (self->state == IDE_HOVER_STATE_DISPLAY);
+
+  self->state = IDE_HOVER_STATE_IN_POPOVER;
+
+  dzl_clear_source (&self->dismiss_source);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_hover_popover_leave_notify_event_cb (IdeHover               *self,
+                                         const GdkEventCrossing *event,
+                                         IdeHoverPopover        *popover)
+{
+  GtkWidget *child;
+
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (IDE_IS_HOVER_POPOVER (popover));
+
+  if (self->state == IDE_HOVER_STATE_IN_POPOVER)
+    self->state = IDE_HOVER_STATE_DISPLAY;
+
+  /* If the window that we are crossing into is not a descendant of our
+   * popover window, then we want to dismiss. This is rather annoying to
+   * track and suffers the same issue as with GtkNotebook tabs containing
+   * buttons (where it's possible to break the prelight state tracking).
+   *
+   * In future Gtk releases, we may be able to use GtkEventControllerMotion.
+   */
+
+  if ((child = gtk_bin_get_child (GTK_BIN (popover))))
+    {
+      GdkRectangle point = { event->x, event->y, 1, 1 };
+      GtkAllocation alloc;
+
+      gtk_widget_get_allocation (child, &alloc);
+
+      if (!dzl_cairo_rectangle_contains_rectangle (&alloc, &point))
+        ide_hover_queue_dismiss (self);
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_hover_providers_foreach_cb (IdeExtensionSetAdapter *set,
+                                PeasPluginInfo         *plugin_info,
+                                PeasExtension          *exten,
+                                gpointer                user_data)
+{
+  IdeHoverPopover *popover = user_data;
+  IdeHoverProvider *provider = (IdeHoverProvider *)exten;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_HOVER_PROVIDER (provider));
+  g_assert (IDE_IS_HOVER_POPOVER (popover));
+
+  _ide_hover_popover_add_provider (popover, provider);
+}
+
+static void
+ide_hover_popover_destroy_cb (IdeHover        *self,
+                              IdeHoverPopover *popover)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (IDE_IS_HOVER_POPOVER (popover));
+
+  self->popover = NULL;
+  self->state = IDE_HOVER_STATE_INITIAL;
+}
+
+static gboolean
+ide_hover_get_bounds (IdeHover    *self,
+                      GtkTextIter *begin,
+                      GtkTextIter *end,
+                      GtkTextIter *hover)
+{
+  GtkTextView *view;
+  GtkTextIter iter;
+  gint x, y;
+
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+  g_assert (hover != NULL);
+
+  memset (begin, 0, sizeof *begin);
+  memset (end, 0, sizeof *end);
+  memset (hover, 0, sizeof *hover);
+
+  if (!(view = dzl_signal_group_get_target (self->signals)))
+    return FALSE;
+
+  g_assert (GTK_IS_TEXT_VIEW (view));
+
+  gtk_text_view_window_to_buffer_coords (view,
+                                         GTK_TEXT_WINDOW_WIDGET,
+                                         self->motion_x,
+                                         self->motion_y,
+                                         &x, &y);
+
+  if (!gtk_text_view_get_iter_at_location (view, &iter, x, y))
+    return FALSE;
+
+  *hover = iter;
+
+  if (!_ide_source_iter_inside_word (&iter))
+    {
+      *begin = iter;
+      gtk_text_iter_set_line_offset (begin, 0);
+
+      *end = *begin;
+      gtk_text_iter_forward_to_line_end (end);
+
+      return TRUE;
+    }
+
+  if (!_ide_source_iter_starts_full_word (&iter))
+    _ide_source_iter_backward_full_word_start (&iter);
+
+  *begin = iter;
+  *end = iter;
+
+  _ide_source_iter_forward_full_word_end (end);
+
+  return TRUE;
+}
+
+static gboolean
+ide_hover_motion_timeout_cb (gpointer data)
+{
+  IdeHover *self = data;
+  IdeSourceView *view;
+  GdkRectangle rect;
+  GdkRectangle begin_rect;
+  GdkRectangle end_rect;
+  GdkRectangle hover_rect;
+  GtkTextIter begin;
+  GtkTextIter end;
+  GtkTextIter hover;
+
+  g_assert (IDE_IS_HOVER (self));
+
+  self->delay_display_source = 0;
+
+  if (!(view = dzl_signal_group_get_target (self->signals)))
+    return G_SOURCE_REMOVE;
+
+  /* Ignore signal if we're already processing */
+  if (self->state != IDE_HOVER_STATE_INITIAL)
+    return G_SOURCE_REMOVE;
+
+  /* Make sure we're over text */
+  if (!ide_hover_get_bounds (self, &begin, &end, &hover))
+    return G_SOURCE_REMOVE;
+
+  if (self->popover == NULL)
+    {
+      self->popover = g_object_new (IDE_TYPE_HOVER_POPOVER,
+                                    "modal", FALSE,
+                                    "position", GTK_POS_TOP,
+                                    "relative-to", view,
+                                    NULL);
+
+      g_signal_connect_object (self->popover,
+                               "destroy",
+                               G_CALLBACK (ide_hover_popover_destroy_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      g_signal_connect_object (self->popover,
+                               "closed",
+                               G_CALLBACK (ide_hover_popover_closed_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      g_signal_connect_object (self->popover,
+                               "enter-notify-event",
+                               G_CALLBACK (ide_hover_popover_enter_notify_event_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      g_signal_connect_object (self->popover,
+                               "leave-notify-event",
+                               G_CALLBACK (ide_hover_popover_leave_notify_event_cb),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+      if (self->providers != NULL)
+        ide_extension_set_adapter_foreach (self->providers,
+                                           ide_hover_providers_foreach_cb,
+                                           self->popover);
+    }
+
+  self->state = IDE_HOVER_STATE_DISPLAY;
+  gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), &begin, &begin_rect);
+  gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), &end, &end_rect);
+  gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), &hover, &hover_rect);
+  gdk_rectangle_union (&begin_rect, &end_rect, &rect);
+
+  gtk_text_view_buffer_to_window_coords (GTK_TEXT_VIEW (view),
+                                         GTK_TEXT_WINDOW_WIDGET,
+                                         rect.x, rect.y, &rect.x, &rect.y);
+
+  _ide_hover_popover_set_hovered_at (self->popover, &hover_rect);
+
+  if (gtk_text_iter_equal (&begin, &end) &&
+      gtk_text_iter_starts_line (&begin))
+    {
+      rect.width = 1;
+      gtk_popover_set_pointing_to (GTK_POPOVER (self->popover), &rect);
+      gtk_popover_set_position (GTK_POPOVER (self->popover), GTK_POS_RIGHT);
+    }
+  else
+    {
+      gtk_popover_set_pointing_to (GTK_POPOVER (self->popover), &rect);
+      gtk_popover_set_position (GTK_POPOVER (self->popover), GTK_POS_TOP);
+    }
+
+  _ide_hover_popover_show (self->popover);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+ide_hover_delay_display (IdeHover *self)
+{
+  g_assert (IDE_IS_HOVER (self));
+
+  if (self->delay_display_source)
+    g_source_remove (self->delay_display_source);
+
+  self->delay_display_source =
+    gdk_threads_add_timeout_full (G_PRIORITY_LOW,
+                                  MOTION_SETTLE_TIMEOUT_MSEC,
+                                  ide_hover_motion_timeout_cb,
+                                  self, NULL);
+}
+
+void
+_ide_hover_display (IdeHover          *self,
+                    const GtkTextIter *iter)
+{
+  IdeSourceView *view;
+  GdkRectangle rect;
+
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (iter != NULL);
+
+  if (self->state != IDE_HOVER_STATE_INITIAL)
+    return;
+
+  if (!(view = dzl_signal_group_get_target (self->signals)))
+    return;
+
+  g_assert (GTK_IS_TEXT_VIEW (view));
+
+  dzl_clear_source (&self->delay_display_source);
+
+  gtk_text_view_get_iter_location (GTK_TEXT_VIEW (view), iter, &rect);
+  gtk_text_view_buffer_to_window_coords (GTK_TEXT_VIEW (view),
+                                         GTK_TEXT_WINDOW_TEXT,
+                                         rect.x, rect.y,
+                                         &rect.x, &rect.y);
+
+  self->motion_x = rect.x;
+  self->motion_y = rect.y;
+
+  ide_hover_motion_timeout_cb (self);
+}
+
+static inline gboolean
+should_ignore_event (IdeSourceView  *view,
+                     GdkWindow      *event_window)
+{
+  GdkWindow *text_window;
+  GdkWindow *gutter_window;
+
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  text_window = gtk_text_view_get_window (GTK_TEXT_VIEW (view), GTK_TEXT_WINDOW_TEXT);
+  gutter_window = gtk_text_view_get_window (GTK_TEXT_VIEW (view), GTK_TEXT_WINDOW_LEFT);
+
+  return (event_window != text_window && event_window != gutter_window);
+}
+
+static gboolean
+ide_hover_key_press_event_cb (IdeHover          *self,
+                              const GdkEventKey *event,
+                              IdeSourceView     *view)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  if (self->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->popover));
+
+  dzl_clear_source (&self->delay_display_source);
+  dzl_clear_source (&self->dismiss_source);
+
+  g_assert (self->popover == NULL);
+  g_assert (self->state == IDE_HOVER_STATE_INITIAL);
+  g_assert (self->delay_display_source == 0);
+  g_assert (self->dismiss_source == 0);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_hover_enter_notify_event_cb (IdeHover               *self,
+                                 const GdkEventCrossing *event,
+                                 IdeSourceView          *view)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (event->type == GDK_ENTER_NOTIFY);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  if (should_ignore_event (view, event->window))
+    return GDK_EVENT_PROPAGATE;
+
+  dzl_clear_source (&self->dismiss_source);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_hover_dismiss_cb (gpointer data)
+{
+  IdeHover *self = data;
+
+  g_assert (IDE_IS_HOVER (self));
+
+  self->dismiss_source = 0;
+
+  switch (self->state)
+    {
+    case IDE_HOVER_STATE_DISPLAY:
+      g_assert (IDE_IS_HOVER_POPOVER (self->popover));
+
+      _ide_hover_popover_hide (self->popover);
+
+      g_assert (self->state == IDE_HOVER_STATE_INITIAL);
+      g_assert (self->popover == NULL);
+
+      break;
+
+    case IDE_HOVER_STATE_INITIAL:
+    case IDE_HOVER_STATE_IN_POPOVER:
+    default:
+      dzl_clear_source (&self->delay_display_source);
+      break;
+    }
+
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+ide_hover_leave_notify_event_cb (IdeHover               *self,
+                                 const GdkEventCrossing *event,
+                                 IdeSourceView          *view)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (event->type == GDK_LEAVE_NOTIFY);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  if (should_ignore_event (view, event->window))
+    return GDK_EVENT_PROPAGATE;
+
+  ide_hover_queue_dismiss (self);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_hover_scroll_event_cb (IdeHover             *self,
+                           const GdkEventScroll *event,
+                           IdeSourceView        *view)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+  g_assert (!self->popover || IDE_IS_HOVER_POPOVER (self->popover));
+
+  if (self->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->popover));
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+ide_hover_motion_notify_event_cb (IdeHover             *self,
+                                  const GdkEventMotion *event,
+                                  IdeSourceView        *view)
+{
+  GdkWindow *window;
+
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (event != NULL);
+  g_assert (event->type == GDK_MOTION_NOTIFY);
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  window = gtk_text_view_get_window (GTK_TEXT_VIEW (view), GTK_TEXT_WINDOW_LEFT);
+
+  if (window != NULL)
+    {
+      gint left_width = gdk_window_get_width (window);
+
+      self->motion_x = event->x + left_width;
+      self->motion_y = event->y;
+    }
+
+  /*
+   * If we have a popover displayed, get it's allocation so that
+   * we can detect if our x/y coordinate is outside the threshold
+   * of the rectangle + grace area. If so, we'll dismiss the popover
+   * immediately.
+   */
+
+  if (self->popover != NULL)
+    {
+      GtkAllocation alloc;
+      GdkRectangle pointing_to;
+
+      gtk_widget_get_allocation (GTK_WIDGET (self->popover), &alloc);
+      gtk_widget_translate_coordinates (GTK_WIDGET (self->popover),
+                                        GTK_WIDGET (view),
+                                        alloc.x, alloc.y,
+                                        &alloc.x, &alloc.y);
+      gtk_popover_get_pointing_to (GTK_POPOVER (self->popover), &pointing_to);
+
+      alloc.x -= GRACE_X;
+      alloc.width += GRACE_X * 2;
+      alloc.y -= GRACE_Y;
+      alloc.height += GRACE_Y * 2;
+
+      gdk_rectangle_union (&alloc, &pointing_to, &alloc);
+
+      if (event->x < alloc.x ||
+          event->x > (alloc.x + alloc.width) ||
+          event->y < alloc.y ||
+          event->y > (alloc.y + alloc.height))
+        {
+          _ide_hover_popover_hide (self->popover);
+
+          g_assert (self->popover == NULL);
+          g_assert (self->state == IDE_HOVER_STATE_INITIAL);
+        }
+    }
+
+  dzl_clear_source (&self->dismiss_source);
+
+  ide_hover_delay_display (self);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+ide_hover_destroy_cb (IdeHover      *self,
+                      IdeSourceView *view)
+{
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+  g_assert (!self->popover || IDE_IS_HOVER_POPOVER (self->popover));
+
+  dzl_clear_source (&self->delay_display_source);
+
+  if (self->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->popover));
+
+  g_assert (self->popover == NULL);
+  g_assert (self->delay_display_source == 0);
+}
+
+static void
+ide_hover_dispose (GObject *object)
+{
+  IdeHover *self = (IdeHover *)object;
+
+  ide_clear_and_destroy_object (&self->providers);
+
+  dzl_clear_source (&self->delay_display_source);
+  dzl_clear_source (&self->dismiss_source);
+  dzl_signal_group_set_target (self->signals, NULL);
+
+  if (self->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->popover));
+
+  G_OBJECT_CLASS (ide_hover_parent_class)->dispose (object);
+
+  g_assert (self->popover == NULL);
+  g_assert (self->delay_display_source == 0);
+  g_assert (self->dismiss_source == 0);
+}
+
+static void
+ide_hover_finalize (GObject *object)
+{
+  IdeHover *self = (IdeHover *)object;
+
+  g_clear_object (&self->signals);
+
+  g_assert (self->signals == NULL);
+  g_assert (self->popover == NULL);
+  g_assert (self->providers == NULL);
+
+  g_assert (self->delay_display_source == 0);
+  g_assert (self->dismiss_source == 0);
+
+  G_OBJECT_CLASS (ide_hover_parent_class)->finalize (object);
+}
+
+static void
+ide_hover_class_init (IdeHoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_hover_dispose;
+  object_class->finalize = ide_hover_finalize;
+}
+
+static void
+ide_hover_init (IdeHover *self)
+{
+  self->signals = dzl_signal_group_new (IDE_TYPE_SOURCE_VIEW);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "key-press-event",
+                                   G_CALLBACK (ide_hover_key_press_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "enter-notify-event",
+                                   G_CALLBACK (ide_hover_enter_notify_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "leave-notify-event",
+                                   G_CALLBACK (ide_hover_leave_notify_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "motion-notify-event",
+                                   G_CALLBACK (ide_hover_motion_notify_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "scroll-event",
+                                   G_CALLBACK (ide_hover_scroll_event_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->signals,
+                                   "destroy",
+                                   G_CALLBACK (ide_hover_destroy_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+}
+
+static void
+ide_hover_extension_added_cb (IdeExtensionSetAdapter *set,
+                              PeasPluginInfo         *plugin_info,
+                              PeasExtension          *exten,
+                              gpointer                user_data)
+{
+  IdeHover *self = user_data;
+  IdeHoverProvider *provider = (IdeHoverProvider *)exten;
+  IdeSourceView *view;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_HOVER_PROVIDER (provider));
+
+  view = dzl_signal_group_get_target (self->signals);
+  ide_hover_provider_load (provider, view);
+}
+
+static void
+ide_hover_extension_removed_cb (IdeExtensionSetAdapter *set,
+                                PeasPluginInfo         *plugin_info,
+                                PeasExtension          *exten,
+                                gpointer                user_data)
+{
+  IdeHover *self = user_data;
+  IdeHoverProvider *provider = (IdeHoverProvider *)exten;
+  IdeSourceView *view;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (IDE_IS_HOVER (self));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_HOVER_PROVIDER (provider));
+
+  view = dzl_signal_group_get_target (self->signals);
+  ide_hover_provider_unload (provider, view);
+}
+
+void
+_ide_hover_set_context (IdeHover   *self,
+                        IdeContext *context)
+{
+  g_return_if_fail (IDE_IS_HOVER (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  if (self->providers != NULL)
+    return;
+
+  self->providers = ide_extension_set_adapter_new (IDE_OBJECT (context),
+                                                   peas_engine_get_default (),
+                                                   IDE_TYPE_HOVER_PROVIDER,
+                                                   "Hover-Provider-Languages",
+                                                   NULL);
+
+  g_signal_connect_object (self->providers,
+                           "extension-added",
+                           G_CALLBACK (ide_hover_extension_added_cb),
+                           self, 0);
+
+  g_signal_connect_object (self->providers,
+                           "extension-removed",
+                           G_CALLBACK (ide_hover_extension_removed_cb),
+                           self, 0);
+
+  ide_extension_set_adapter_foreach (self->providers,
+                                     ide_hover_extension_added_cb,
+                                     self);
+}
+
+void
+_ide_hover_set_language (IdeHover    *self,
+                         const gchar *language)
+{
+  g_return_if_fail (IDE_IS_HOVER (self));
+
+  if (self->providers != NULL)
+    ide_extension_set_adapter_set_value (self->providers, language);
+}
+
+IdeHover *
+_ide_hover_new (IdeSourceView *view)
+{
+  IdeHover *self;
+
+  self = g_object_new (IDE_TYPE_HOVER, NULL);
+  dzl_signal_group_set_target (self->signals, view);
+
+  return self;
+}
diff --git a/src/libide/sourceview/ide-indenter.c b/src/libide/sourceview/ide-indenter.c
index ba931448e..6834952fd 100644
--- a/src/libide/sourceview/ide-indenter.c
+++ b/src/libide/sourceview/ide-indenter.c
@@ -1,6 +1,6 @@
 /* ide-indenter.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,16 +14,17 @@
  *
  * 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 "ide-indenter"
 
 #include "config.h"
 
-#include "ide-context.h"
-#include "ide-debug.h"
+#include <libide-code.h>
 
-#include "sourceview/ide-indenter.h"
+#include "ide-indenter.h"
 
 G_DEFINE_INTERFACE (IdeIndenter, ide_indenter, IDE_TYPE_OBJECT)
 
@@ -121,6 +122,8 @@ ide_indenter_mimic_source_view (GtkTextView *text_view,
  *
  * Returns: (nullable) (transfer full): A string containing the replacement
  *   text, or %NULL.
+ *
+ * Since: 3.32
  */
 gchar *
 ide_indenter_format (IdeIndenter *self,
@@ -155,6 +158,8 @@ ide_indenter_format (IdeIndenter *self,
  * the default indentation style of #GtkSourceView.
  *
  * Returns: %TRUE if @event should trigger an indentation request.
+ *
+ * Since: 3.32
  */
 gboolean
 ide_indenter_is_trigger (IdeIndenter *self,
diff --git a/src/libide/sourceview/ide-indenter.h b/src/libide/sourceview/ide-indenter.h
index 56c907cdb..226fab823 100644
--- a/src/libide/sourceview/ide-indenter.h
+++ b/src/libide/sourceview/ide-indenter.h
@@ -1,6 +1,6 @@
 /* ide-indenter.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,21 +14,24 @@
  *
  * 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 <gtk/gtk.h>
-
-#include "ide-version-macros.h"
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
 
-#include "ide-object.h"
+#include <gtk/gtk.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_INDENTER (ide_indenter_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_INTERFACE (IdeIndenter, ide_indenter, IDE, INDENTER, IdeObject)
 
 struct _IdeIndenterInterface
@@ -45,10 +48,10 @@ struct _IdeIndenterInterface
                             GdkEventKey   *event);
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean  ide_indenter_is_trigger (IdeIndenter *self,
                                    GdkEventKey *event);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gchar    *ide_indenter_format     (IdeIndenter *self,
                                    GtkTextView *text_view,
                                    GtkTextIter *begin,
diff --git a/src/libide/sourceview/ide-line-change-gutter-renderer.c 
b/src/libide/sourceview/ide-line-change-gutter-renderer.c
index 98f832310..a8904959c 100644
--- a/src/libide/sourceview/ide-line-change-gutter-renderer.c
+++ b/src/libide/sourceview/ide-line-change-gutter-renderer.c
@@ -1,6 +1,6 @@
 /* ide-line-change-gutter-renderer.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,25 +14,23 @@
  *
  * 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 "ide-line-change-gutter-renderer"
 
 #include "config.h"
 
-#include "ide-context.h"
+#include <libide-code.h>
 
-#include "buffers/ide-buffer.h"
-#include "files/ide-file.h"
-#include "sourceview/ide-line-change-gutter-renderer.h"
-#include "vcs/ide-vcs.h"
+#include "ide-line-change-gutter-renderer.h"
 
 #define DELETE_WIDTH  5.0
 #define DELETE_HEIGHT 8.0
 
-#if 0
-# define ARROW_TOWARDS_GUTTER
-#endif
+#define IS_LINE_CHANGE(i) ((i)->is_add || (i)->is_change || \
+                           (i)->is_delete || (i)->is_next_delete || (i)->is_prev_delete)
 
 struct _IdeLineChangeGutterRenderer
 {
@@ -44,33 +42,81 @@ struct _IdeLineChangeGutterRenderer
   GtkTextBuffer          *buffer;
   gulong                  buffer_notify_style_scheme;
 
-  GdkRGBA                 rgba_added;
-  GdkRGBA                 rgba_changed;
-  GdkRGBA                 rgba_removed;
+  GArray                 *lines;
+  guint                   begin_line;
 
-  guint                   show_line_deletions : 1;
+  struct {
+    GdkRGBA add;
+    GdkRGBA remove;
+    GdkRGBA change;
+  } changes;
 
   guint                   rgba_added_set : 1;
   guint                   rgba_changed_set : 1;
   guint                   rgba_removed_set : 1;
 };
 
+typedef struct
+{
+  /* The line is an addition to the buffer */
+  guint is_add : 1;
+
+  /* The line has changed in the buffer */
+  guint is_change : 1;
+
+  /* The line is part of a deleted range in the buffer */
+  guint is_delete : 1;
 
-G_DEFINE_TYPE (IdeLineChangeGutterRenderer,
-               ide_line_change_gutter_renderer,
-               GTK_SOURCE_TYPE_GUTTER_RENDERER)
+  /* The previous line was a delete */
+  guint is_prev_delete : 1;
 
-static GdkRGBA rgbaAdded;
-static GdkRGBA rgbaChanged;
-static GdkRGBA rgbaRemoved;
+  /* The next line is a delete */
+  guint is_next_delete : 1;
+} LineInfo;
 
 enum {
-  PROP_0,
-  PROP_SHOW_LINE_DELETIONS,
-  LAST_PROP
+  FOREGROUND,
+  BACKGROUND,
 };
 
-static GParamSpec *properties [LAST_PROP];
+G_DEFINE_TYPE (IdeLineChangeGutterRenderer, ide_line_change_gutter_renderer, GTK_SOURCE_TYPE_GUTTER_RENDERER)
+
+static gboolean
+get_style_rgba (GtkSourceStyleScheme *scheme,
+                const gchar          *style_name,
+                int                   type,
+                GdkRGBA              *rgba)
+{
+  GtkSourceStyle *style;
+
+  g_assert (!scheme || GTK_SOURCE_IS_STYLE_SCHEME (scheme));
+  g_assert (style_name != NULL);
+  g_assert (type == FOREGROUND || type == BACKGROUND);
+  g_assert (rgba != NULL);
+
+  memset (rgba, 0, sizeof *rgba);
+
+  if (scheme == NULL)
+    return FALSE;
+
+  if (NULL != (style = gtk_source_style_scheme_get_style (scheme, style_name)))
+    {
+      g_autofree gchar *str = NULL;
+      gboolean set = FALSE;
+
+      g_object_get (style,
+                    type ? "background" : "foreground", &str,
+                    type ? "background-set" : "foreground-set", &set,
+                    NULL);
+
+      if (str != NULL)
+        gdk_rgba_parse (rgba, str);
+
+      return set;
+    }
+
+  return FALSE;
+}
 
 static void
 disconnect_style_scheme (IdeLineChangeGutterRenderer *self)
@@ -109,70 +155,25 @@ disconnect_view (IdeLineChangeGutterRenderer *self)
 static void
 connect_style_scheme (IdeLineChangeGutterRenderer *self)
 {
-  GtkTextView *text_view;
+  GtkSourceStyleScheme *scheme;
   GtkTextBuffer *buffer;
-  GtkSourceStyleScheme *style_scheme;
-
-  text_view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (self));
-  buffer = gtk_text_view_get_buffer (text_view);
+  GtkTextView *view;
 
-  if (!GTK_SOURCE_IS_BUFFER (buffer))
+  if (!(view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (self))) ||
+      !(buffer = gtk_text_view_get_buffer (view)) ||
+      !GTK_SOURCE_IS_BUFFER (buffer))
     return;
 
-  style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
-
-  if (style_scheme)
-    {
-      GtkSourceStyle *style;
-
-      style = gtk_source_style_scheme_get_style (style_scheme, "gutter:added-line");
-
-      if (style)
-        {
-          g_autofree gchar *foreground = NULL;
-          gboolean foreground_set = 0;
-
-          g_object_get (style,
-                        "foreground-set", &foreground_set,
-                        "foreground", &foreground,
-                        NULL);
+  scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
 
-          if (foreground_set)
-            self->rgba_added_set = gdk_rgba_parse (&self->rgba_added, foreground);
-        }
+  if (!get_style_rgba (scheme, "gutter::added-line", FOREGROUND, &self->changes.add))
+    gdk_rgba_parse (&self->changes.add, "#8ae234");
 
-      style = gtk_source_style_scheme_get_style (style_scheme, "gutter:changed-line");
+  if (!get_style_rgba (scheme, "gutter::changed-line", FOREGROUND, &self->changes.change))
+    gdk_rgba_parse (&self->changes.change, "#fcaf3e");
 
-      if (style)
-        {
-          g_autofree gchar *foreground = NULL;
-          gboolean foreground_set = 0;
-
-          g_object_get (style,
-                        "foreground-set", &foreground_set,
-                        "foreground", &foreground,
-                        NULL);
-
-          if (foreground_set)
-            self->rgba_changed_set = gdk_rgba_parse (&self->rgba_changed, foreground);
-        }
-
-      style = gtk_source_style_scheme_get_style (style_scheme, "gutter:removed-line");
-
-      if (style)
-        {
-          g_autofree gchar *foreground = NULL;
-          gboolean foreground_set = 0;
-
-          g_object_get (style,
-                        "foreground-set", &foreground_set,
-                        "foreground", &foreground,
-                        NULL);
-
-          if (foreground_set)
-            self->rgba_removed_set = gdk_rgba_parse (&self->rgba_removed, foreground);
-        }
-    }
+  if (!get_style_rgba (scheme, "gutter::removed-line", FOREGROUND, &self->changes.remove))
+    gdk_rgba_parse (&self->changes.remove, "#ef2929");
 }
 
 static void
@@ -237,199 +238,187 @@ ide_line_change_gutter_renderer_notify_view (IdeLineChangeGutterRenderer *self)
 }
 
 static void
-ide_line_change_gutter_renderer_draw (GtkSourceGutterRenderer      *renderer,
-                                      cairo_t                      *cr,
-                                      GdkRectangle                 *bg_area,
-                                      GdkRectangle                 *cell_area,
-                                      GtkTextIter                  *begin,
-                                      GtkTextIter                  *end,
-                                      GtkSourceGutterRendererState  state)
+populate_changes_cb (guint               line,
+                     IdeBufferLineChange change,
+                     gpointer            user_data)
 {
-  IdeLineChangeGutterRenderer *self = (IdeLineChangeGutterRenderer *)renderer;
-  GdkRectangle cell_area_copy;
-  GtkTextBuffer *buffer;
-  GdkRGBA *rgba = NULL;
-  IdeBufferLineFlags flags;
-  IdeBufferLineFlags prev_flags = 0;
-  IdeBufferLineFlags next_flags;
-  guint lineno;
-  gint xpad;
+  LineInfo *info;
+  struct {
+    GArray *lines;
+    guint   begin_line;
+    guint   end_line;
+  } *state = user_data;
+  guint pos;
+
+  g_assert (line >= state->begin_line);
+  g_assert (line <= state->end_line);
+
+  pos = line - state->begin_line;
 
-  g_return_if_fail (IDE_IS_LINE_CHANGE_GUTTER_RENDERER (self));
-  g_return_if_fail (cr);
-  g_return_if_fail (bg_area);
-  g_return_if_fail (cell_area);
-  g_return_if_fail (begin);
-  g_return_if_fail (end);
+  info = &g_array_index (state->lines, LineInfo, pos);
+  info->is_add = !!(change & IDE_BUFFER_LINE_CHANGE_ADDED);
+  info->is_change = !!(change & IDE_BUFFER_LINE_CHANGE_CHANGED);
+  info->is_delete = !!(change & IDE_BUFFER_LINE_CHANGE_DELETED);
 
-  GTK_SOURCE_GUTTER_RENDERER_CLASS (ide_line_change_gutter_renderer_parent_class)->draw (renderer, cr, 
bg_area, cell_area, begin, end, state);
+  if (pos > 0)
+    {
+      LineInfo *last = &g_array_index (state->lines, LineInfo, pos - 1);
 
-  buffer = gtk_text_iter_get_buffer (begin);
+      info->is_prev_delete = last->is_delete;
+      last->is_next_delete = info->is_delete;
+    }
+}
 
-  if (!IDE_IS_BUFFER (buffer))
+static void
+ide_line_change_gutter_renderer_begin (GtkSourceGutterRenderer *renderer,
+                                       cairo_t                 *cr,
+                                       GdkRectangle            *bg_area,
+                                       GdkRectangle            *cell_area,
+                                       GtkTextIter             *begin,
+                                       GtkTextIter             *end)
+{
+  IdeLineChangeGutterRenderer *self = (IdeLineChangeGutterRenderer *)renderer;
+  IdeBufferChangeMonitor *monitor;
+  GtkTextBuffer *buffer;
+  GtkTextView *view;
+  struct {
+    GArray *lines;
+    guint   begin_line;
+    guint   end_line;
+  } state;
+
+  g_assert (IDE_IS_LINE_CHANGE_GUTTER_RENDERER (self));
+  g_assert (cr != NULL);
+  g_assert (bg_area != NULL);
+  g_assert (cell_area != NULL);
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+  if (!(view = gtk_source_gutter_renderer_get_view (renderer)) ||
+      !(buffer = gtk_text_view_get_buffer (view)) ||
+      !IDE_IS_BUFFER (buffer) ||
+      !(monitor = ide_buffer_get_change_monitor (IDE_BUFFER (buffer))))
     return;
 
-  lineno = gtk_text_iter_get_line (begin);
+  self->begin_line = state.begin_line = gtk_text_iter_get_line (begin);
+  state.end_line = gtk_text_iter_get_line (end);
+  state.lines = g_array_new (FALSE, TRUE, sizeof (LineInfo));
+  g_array_set_size (state.lines, state.end_line - state.begin_line + 1);
 
-  flags = ide_buffer_get_line_flags (IDE_BUFFER (buffer), lineno);
-  next_flags = ide_buffer_get_line_flags (IDE_BUFFER (buffer), lineno + 1);
-  if (lineno > 0)
-    prev_flags = ide_buffer_get_line_flags (IDE_BUFFER (buffer), lineno - 1);
+  ide_buffer_change_monitor_foreach_change (monitor,
+                                            state.begin_line,
+                                            state.end_line,
+                                            populate_changes_cb,
+                                            &state);
 
-  if ((flags & IDE_BUFFER_LINE_FLAGS_ADDED) != 0)
-    rgba = self->rgba_added_set ? &self->rgba_added : &rgbaAdded;
+  g_clear_pointer (&self->lines, g_array_unref);
+  self->lines = g_steal_pointer (&state.lines);
+}
 
-  if ((flags & IDE_BUFFER_LINE_FLAGS_CHANGED) != 0)
-    rgba = self->rgba_changed_set ? &self->rgba_changed : &rgbaChanged;
+static void
+ide_line_change_gutter_renderer_end (GtkSourceGutterRenderer *renderer)
+{
+  IdeLineChangeGutterRenderer *self = (IdeLineChangeGutterRenderer *)renderer;
 
-  if (rgba)
-    {
-      gdk_cairo_rectangle (cr, cell_area);
-      gdk_cairo_set_source_rgba (cr, rgba);
-      cairo_fill (cr);
-    }
+  g_assert (IDE_IS_LINE_CHANGE_GUTTER_RENDERER (self));
 
-  if (!self->show_line_deletions)
-    return;
+  //g_clear_pointer (&self->lines, g_array_unref);
+}
 
-  /*
-   * If we have xpad, we want to draw over it. So we'll just mutate
-   * the cell_area here.
-   */
-  g_object_get (self, "xpad", &xpad, NULL);
-  cell_area_copy = *cell_area;
-  cell_area->x += xpad;
+static void
+draw_line_change (IdeLineChangeGutterRenderer  *self,
+                  cairo_t                      *cr,
+                  GdkRectangle                 *area,
+                  LineInfo                     *info,
+                  GtkSourceGutterRendererState  state)
+{
+  g_assert (IDE_IS_LINE_CHANGE_GUTTER_RENDERER (self));
+  g_assert (cr != NULL);
+  g_assert (area != NULL);
 
   /*
-   * If the next line is a deletion, but we were not a deletion, then
-   * draw our half the deletion mark.
+   * Draw a simple line with the appropriate color from the style scheme
+   * based on the type of change we have.
    */
-  if (((next_flags & IDE_BUFFER_LINE_FLAGS_DELETED) != 0) &&
-      ((flags & IDE_BUFFER_LINE_FLAGS_DELETED) == 0))
+
+  if (info->is_add || info->is_change)
     {
-      rgba = self->rgba_removed_set ? &self->rgba_removed : &rgbaRemoved;
-      gdk_cairo_set_source_rgba (cr, rgba);
-
-#ifdef ARROW_TOWARDS_GUTTER
-      cairo_move_to (cr,
-                     cell_area->x + cell_area->width,
-                     cell_area->y + cell_area->height);
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width - DELETE_WIDTH,
-                     cell_area->y + cell_area->height);
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width,
-                     cell_area->y + cell_area->height - (DELETE_HEIGHT / 2));
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width,
-                     cell_area->y + cell_area->height);
-#else
-      cairo_move_to (cr,
-                     cell_area->x + cell_area->width,
-                     cell_area->y + cell_area->height);
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width - DELETE_WIDTH,
-                     cell_area->y + cell_area->height);
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width - DELETE_WIDTH,
-                     cell_area->y + cell_area->height - (DELETE_HEIGHT / 2));
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width,
-                     cell_area->y + cell_area->height);
-#endif
+      gdk_cairo_rectangle (cr, area);
+
+      if (info->is_add)
+        gdk_cairo_set_source_rgba (cr, &self->changes.add);
+      else
+        gdk_cairo_set_source_rgba (cr, &self->changes.change);
 
       cairo_fill (cr);
     }
 
-  /*
-   * If the previous line was not a deletion, and we have a deletion, then
-   * draw our half the deletion mark.
-   */
-  if (((prev_flags & IDE_BUFFER_LINE_FLAGS_DELETED) == 0) &&
-      ((flags & IDE_BUFFER_LINE_FLAGS_DELETED) != 0))
+  if (info->is_next_delete && !info->is_delete)
     {
-      rgba = self->rgba_removed_set ? &self->rgba_removed : &rgbaRemoved;
-      gdk_cairo_set_source_rgba (cr, rgba);
-
-#ifdef ARROW_TOWARDS_GUTTER
-      cairo_move_to (cr,
-                     cell_area->x + cell_area->width,
-                     cell_area->y);
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width,
-                     cell_area->y + (DELETE_HEIGHT / 2));
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width - DELETE_WIDTH,
-                     cell_area->y);
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width,
-                     cell_area->y);
-#else
-      cairo_move_to (cr,
-                     cell_area->x + cell_area->width,
-                     cell_area->y);
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width - DELETE_WIDTH,
-                     cell_area->y);
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width - DELETE_WIDTH,
-                     cell_area->y + (DELETE_HEIGHT / 2));
-      cairo_line_to (cr,
-                     cell_area->x + cell_area->width,
-                     cell_area->y);
-#endif
-
+      cairo_rectangle (cr,
+                       area->x,
+                       area->y + area->width / 2.0,
+                       area->width,
+                       area->height / 2.0);
+      gdk_cairo_set_source_rgba (cr, &self->changes.remove);
       cairo_fill (cr);
     }
 
-  *cell_area = cell_area_copy;
+  if (info->is_delete && !info->is_prev_delete)
+    {
+      cairo_rectangle (cr,
+                       area->x,
+                       area->y,
+                       area->width,
+                       area->height / 2.0);
+      gdk_cairo_set_source_rgba (cr, &self->changes.remove);
+      cairo_fill (cr);
+    }
 }
 
 static void
-ide_line_change_gutter_renderer_dispose (GObject *object)
+ide_line_change_gutter_renderer_draw (GtkSourceGutterRenderer      *renderer,
+                                      cairo_t                      *cr,
+                                      GdkRectangle                 *bg_area,
+                                      GdkRectangle                 *cell_area,
+                                      GtkTextIter                  *begin,
+                                      GtkTextIter                  *end,
+                                      GtkSourceGutterRendererState  state)
 {
-  disconnect_view (IDE_LINE_CHANGE_GUTTER_RENDERER (object));
+  IdeLineChangeGutterRenderer *self = (IdeLineChangeGutterRenderer *)renderer;
+  guint line;
 
-  G_OBJECT_CLASS (ide_line_change_gutter_renderer_parent_class)->dispose (object);
-}
+  g_assert (IDE_IS_LINE_CHANGE_GUTTER_RENDERER (self));
+  g_assert (cr != NULL);
+  g_assert (bg_area != NULL);
+  g_assert (cell_area != NULL);
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
 
-static void
-ide_line_change_gutter_renderer_get_property (GObject    *object,
-                                              guint       prop_id,
-                                              GValue     *value,
-                                              GParamSpec *pspec)
-{
-  IdeLineChangeGutterRenderer *self = IDE_LINE_CHANGE_GUTTER_RENDERER(object);
+  if (self->lines == NULL)
+    return;
+
+  line = gtk_text_iter_get_line (begin);
 
-  switch (prop_id)
+  if ((line - self->begin_line) < self->lines->len)
     {
-    case PROP_SHOW_LINE_DELETIONS:
-      g_value_set_boolean (value, self->show_line_deletions);
-      break;
+      LineInfo *info = &g_array_index (self->lines, LineInfo, line - self->begin_line);
 
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+      if (IS_LINE_CHANGE (info))
+        draw_line_change (self, cr, cell_area, info, state);
     }
 }
 
 static void
-ide_line_change_gutter_renderer_set_property (GObject      *object,
-                                              guint         prop_id,
-                                              const GValue *value,
-                                              GParamSpec   *pspec)
+ide_line_change_gutter_renderer_dispose (GObject *object)
 {
-  IdeLineChangeGutterRenderer *self = IDE_LINE_CHANGE_GUTTER_RENDERER(object);
+  IdeLineChangeGutterRenderer *self = (IdeLineChangeGutterRenderer *)object;
 
-  switch (prop_id)
-    {
-    case PROP_SHOW_LINE_DELETIONS:
-      self->show_line_deletions = g_value_get_boolean (value);
-      gtk_source_gutter_renderer_queue_draw (GTK_SOURCE_GUTTER_RENDERER (self));
-      break;
+  disconnect_view (IDE_LINE_CHANGE_GUTTER_RENDERER (object));
 
-    default:
-      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
-    }
+  g_clear_pointer (&self->lines, g_array_unref);
+
+  G_OBJECT_CLASS (ide_line_change_gutter_renderer_parent_class)->dispose (object);
 }
 
 static void
@@ -439,23 +428,10 @@ ide_line_change_gutter_renderer_class_init (IdeLineChangeGutterRendererClass *kl
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
   object_class->dispose = ide_line_change_gutter_renderer_dispose;
-  object_class->get_property = ide_line_change_gutter_renderer_get_property;
-  object_class->set_property = ide_line_change_gutter_renderer_set_property;
 
+  renderer_class->end = ide_line_change_gutter_renderer_end;
+  renderer_class->begin = ide_line_change_gutter_renderer_begin;
   renderer_class->draw = ide_line_change_gutter_renderer_draw;
-
-  properties [PROP_SHOW_LINE_DELETIONS] =
-    g_param_spec_boolean ("show-line-deletions",
-                          "Show Line Deletions",
-                          "If the deletion mark should be shown for deleted lines",
-                          FALSE,
-                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_properties (object_class, LAST_PROP, properties);
-
-  gdk_rgba_parse (&rgbaAdded, "#8ae234");
-  gdk_rgba_parse (&rgbaChanged, "#fcaf3e");
-  gdk_rgba_parse (&rgbaRemoved, "#ff0000");
 }
 
 static void
diff --git a/src/libide/sourceview/ide-line-change-gutter-renderer.h 
b/src/libide/sourceview/ide-line-change-gutter-renderer.h
index 4f8391842..244e6bb89 100644
--- a/src/libide/sourceview/ide-line-change-gutter-renderer.h
+++ b/src/libide/sourceview/ide-line-change-gutter-renderer.h
@@ -1,6 +1,6 @@
 /* ide-line-change-gutter-renderer.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,17 +14,20 @@
  *
  * 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 <gtksourceview/gtksource.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_LINE_CHANGE_GUTTER_RENDERER (ide_line_change_gutter_renderer_get_type())
 
-G_DECLARE_FINAL_TYPE (IdeLineChangeGutterRenderer, ide_line_change_gutter_renderer,
-                      IDE, LINE_CHANGE_GUTTER_RENDERER, GtkSourceGutterRenderer);
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeLineChangeGutterRenderer, ide_line_change_gutter_renderer, IDE, 
LINE_CHANGE_GUTTER_RENDERER, GtkSourceGutterRenderer)
 
 G_END_DECLS
diff --git a/src/libide/sourceview/ide-snippet-chunk.c b/src/libide/sourceview/ide-snippet-chunk.c
new file mode 100644
index 000000000..80f253c29
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet-chunk.c
@@ -0,0 +1,380 @@
+/* ide-snippet-chunk.c
+ *
+ * Copyright 2013-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-source-snippet"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-snippet-chunk.h"
+#include "ide-snippet-context.h"
+
+/**
+ * SECTION:ide-snippet-chunk
+ * @title: IdeSnippetChunk
+ * @short_description: An chunk of text within the source snippet
+ *
+ * The #IdeSnippetChunk represents a single chunk of text that
+ * may or may not be an edit point within the snippet. Chunks that are
+ * an edit point (also called a tab stop) have the
+ * #IdeSnippetChunk:tab-stop property set.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeSnippetChunk
+{
+  GObject            parent_instance;
+
+  IdeSnippetContext *context;
+  guint              context_changed_handler;
+  gint               tab_stop;
+  gchar             *spec;
+  gchar             *text;
+  guint              text_set : 1;
+};
+
+G_DEFINE_TYPE (IdeSnippetChunk, ide_snippet_chunk, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_SPEC,
+  PROP_TAB_STOP,
+  PROP_TEXT,
+  PROP_TEXT_SET,
+  LAST_PROP
+};
+
+static GParamSpec *properties[LAST_PROP];
+
+IdeSnippetChunk *
+ide_snippet_chunk_new (void)
+{
+  return g_object_new (IDE_TYPE_SNIPPET_CHUNK, NULL);
+}
+
+/**
+ * ide_snippet_chunk_copy:
+ *
+ * Copies the source snippet.
+ *
+ * Returns: (transfer full): An #IdeSnippetChunk.
+ *
+ * Since: 3.32
+ */
+IdeSnippetChunk *
+ide_snippet_chunk_copy (IdeSnippetChunk *chunk)
+{
+  IdeSnippetChunk *ret;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET_CHUNK (chunk), NULL);
+
+  ret = g_object_new (IDE_TYPE_SNIPPET_CHUNK,
+                      "spec", chunk->spec,
+                      "tab-stop", chunk->tab_stop,
+                      NULL);
+
+  return ret;
+}
+
+static void
+on_context_changed (IdeSnippetContext *context,
+                    IdeSnippetChunk   *chunk)
+{
+  gchar *text;
+
+  g_assert (IDE_IS_SNIPPET_CHUNK (chunk));
+  g_assert (IDE_IS_SNIPPET_CONTEXT (context));
+
+  if (!chunk->text_set)
+    {
+      text = ide_snippet_context_expand (context, chunk->spec);
+      ide_snippet_chunk_set_text (chunk, text);
+      g_free (text);
+    }
+}
+
+/**
+ * ide_snippet_chunk_get_context:
+ *
+ * Gets the context for the snippet insertion.
+ *
+ * Returns: (transfer none): An #IdeSnippetContext.
+ *
+ * Since: 3.32
+ */
+IdeSnippetContext *
+ide_snippet_chunk_get_context (IdeSnippetChunk *chunk)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET_CHUNK (chunk), NULL);
+
+  return chunk->context;
+}
+
+void
+ide_snippet_chunk_set_context (IdeSnippetChunk   *chunk,
+                               IdeSnippetContext *context)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CHUNK (chunk));
+  g_return_if_fail (!context || IDE_IS_SNIPPET_CONTEXT (context));
+
+  if (context != chunk->context)
+    {
+      if (chunk->context_changed_handler)
+        {
+          g_signal_handler_disconnect (chunk->context,
+                                       chunk->context_changed_handler);
+          chunk->context_changed_handler = 0;
+        }
+
+      g_clear_object (&chunk->context);
+
+      if (context != NULL)
+        {
+          chunk->context = g_object_ref (context);
+          chunk->context_changed_handler =
+            g_signal_connect_object (chunk->context,
+                                     "changed",
+                                     G_CALLBACK (on_context_changed),
+                                     chunk,
+                                     0);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (chunk), properties[PROP_CONTEXT]);
+    }
+}
+
+const gchar *
+ide_snippet_chunk_get_spec (IdeSnippetChunk *chunk)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET_CHUNK (chunk), NULL);
+  return chunk->spec;
+}
+
+void
+ide_snippet_chunk_set_spec (IdeSnippetChunk *chunk,
+                            const gchar     *spec)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CHUNK (chunk));
+
+  g_free (chunk->spec);
+  chunk->spec = g_strdup (spec);
+  g_object_notify_by_pspec (G_OBJECT (chunk), properties[PROP_SPEC]);
+}
+
+gint
+ide_snippet_chunk_get_tab_stop (IdeSnippetChunk *chunk)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET_CHUNK (chunk), 0);
+  return chunk->tab_stop;
+}
+
+void
+ide_snippet_chunk_set_tab_stop (IdeSnippetChunk *chunk,
+                                gint             tab_stop)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CHUNK (chunk));
+  chunk->tab_stop = tab_stop;
+  g_object_notify_by_pspec (G_OBJECT (chunk), properties[PROP_TAB_STOP]);
+}
+
+const gchar *
+ide_snippet_chunk_get_text (IdeSnippetChunk *chunk)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET_CHUNK (chunk), NULL);
+  return chunk->text ? chunk->text : "";
+}
+
+void
+ide_snippet_chunk_set_text (IdeSnippetChunk *chunk,
+                            const gchar     *text)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CHUNK (chunk));
+
+  if (chunk->text != text)
+    {
+      g_free (chunk->text);
+      chunk->text = g_strdup (text);
+      g_object_notify_by_pspec (G_OBJECT (chunk), properties[PROP_TEXT]);
+    }
+}
+
+gboolean
+ide_snippet_chunk_get_text_set (IdeSnippetChunk *chunk)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET_CHUNK (chunk), FALSE);
+
+  return chunk->text_set;
+}
+
+void
+ide_snippet_chunk_set_text_set (IdeSnippetChunk *chunk,
+                                gboolean         text_set)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CHUNK (chunk));
+
+  text_set = !!text_set;
+
+  if (chunk->text_set != text_set)
+    {
+      chunk->text_set = text_set;
+      g_object_notify_by_pspec (G_OBJECT (chunk), properties[PROP_TEXT_SET]);
+    }
+}
+
+static void
+ide_snippet_chunk_finalize (GObject *object)
+{
+  IdeSnippetChunk *chunk = (IdeSnippetChunk *)object;
+
+  g_clear_pointer (&chunk->spec, g_free);
+  g_clear_pointer (&chunk->text, g_free);
+  g_clear_object (&chunk->context);
+
+  G_OBJECT_CLASS (ide_snippet_chunk_parent_class)->finalize (object);
+}
+
+static void
+ide_snippet_chunk_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeSnippetChunk *chunk = IDE_SNIPPET_CHUNK (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_value_set_object (value, ide_snippet_chunk_get_context (chunk));
+      break;
+
+    case PROP_SPEC:
+      g_value_set_string (value, ide_snippet_chunk_get_spec (chunk));
+      break;
+
+    case PROP_TAB_STOP:
+      g_value_set_int (value, ide_snippet_chunk_get_tab_stop (chunk));
+      break;
+
+    case PROP_TEXT:
+      g_value_set_string (value, ide_snippet_chunk_get_text (chunk));
+      break;
+
+    case PROP_TEXT_SET:
+      g_value_set_boolean (value, ide_snippet_chunk_get_text_set (chunk));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_snippet_chunk_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeSnippetChunk *chunk = IDE_SNIPPET_CHUNK (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      ide_snippet_chunk_set_context (chunk, g_value_get_object (value));
+      break;
+
+    case PROP_TAB_STOP:
+      ide_snippet_chunk_set_tab_stop (chunk, g_value_get_int (value));
+      break;
+
+    case PROP_SPEC:
+      ide_snippet_chunk_set_spec (chunk, g_value_get_string (value));
+      break;
+
+    case PROP_TEXT:
+      ide_snippet_chunk_set_text (chunk, g_value_get_string (value));
+      break;
+
+    case PROP_TEXT_SET:
+      ide_snippet_chunk_set_text_set (chunk, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_snippet_chunk_class_init (IdeSnippetChunkClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_snippet_chunk_finalize;
+  object_class->get_property = ide_snippet_chunk_get_property;
+  object_class->set_property = ide_snippet_chunk_set_property;
+
+  properties[PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The snippet context.",
+                         IDE_TYPE_SNIPPET_CONTEXT,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_SPEC] =
+    g_param_spec_string ("spec",
+                         "Spec",
+                         "The specification to expand using the context.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_TAB_STOP] =
+    g_param_spec_int ("tab-stop",
+                      "Tab Stop",
+                      "The tab stop for the chunk.",
+                      -1,
+                      G_MAXINT,
+                      -1,
+                      (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_TEXT] =
+    g_param_spec_string ("text",
+                         "Text",
+                         "The text for the chunk.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_TEXT_SET] =
+    g_param_spec_boolean ("text-set",
+                          "Text Set",
+                          "If the text property has been manually set.",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_snippet_chunk_init (IdeSnippetChunk *chunk)
+{
+  chunk->tab_stop = -1;
+  chunk->spec = g_strdup ("");
+}
diff --git a/src/libide/sourceview/ide-snippet-chunk.h b/src/libide/sourceview/ide-snippet-chunk.h
new file mode 100644
index 000000000..a519f7b1f
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet-chunk.h
@@ -0,0 +1,68 @@
+/* ide-snippet-chunk.h
+ *
+ * Copyright 2013-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-snippet-context.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SNIPPET_CHUNK (ide_snippet_chunk_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeSnippetChunk, ide_snippet_chunk, IDE, SNIPPET_CHUNK, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeSnippetChunk   *ide_snippet_chunk_new          (void);
+IDE_AVAILABLE_IN_3_32
+IdeSnippetChunk   *ide_snippet_chunk_copy         (IdeSnippetChunk   *chunk);
+IDE_AVAILABLE_IN_3_32
+IdeSnippetContext *ide_snippet_chunk_get_context  (IdeSnippetChunk   *chunk);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_chunk_set_context  (IdeSnippetChunk   *chunk,
+                                                   IdeSnippetContext *context);
+IDE_AVAILABLE_IN_3_32
+const gchar       *ide_snippet_chunk_get_spec     (IdeSnippetChunk   *chunk);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_chunk_set_spec     (IdeSnippetChunk   *chunk,
+                                                   const gchar       *spec);
+IDE_AVAILABLE_IN_3_32
+gint               ide_snippet_chunk_get_tab_stop (IdeSnippetChunk   *chunk);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_chunk_set_tab_stop (IdeSnippetChunk   *chunk,
+                                                   gint              tab_stop);
+IDE_AVAILABLE_IN_3_32
+const gchar       *ide_snippet_chunk_get_text     (IdeSnippetChunk   *chunk);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_chunk_set_text     (IdeSnippetChunk   *chunk,
+                                                   const gchar       *text);
+IDE_AVAILABLE_IN_3_32
+gboolean           ide_snippet_chunk_get_text_set (IdeSnippetChunk   *chunk);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_chunk_set_text_set (IdeSnippetChunk   *chunk,
+                                                   gboolean          text_set);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-snippet-context.c b/src/libide/sourceview/ide-snippet-context.c
new file mode 100644
index 000000000..9e7a3fc8b
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet-context.c
@@ -0,0 +1,767 @@
+/* ide-snippet-context.c
+ *
+ * Copyright 2013-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-snippets-context"
+
+#include "config.h"
+
+#include <errno.h>
+#include <glib/gi18n.h>
+#include <stdlib.h>
+
+#include "ide-snippet-context.h"
+
+/**
+ * SECTION:ide-snippet-context
+ * @title: IdeSnippetContext
+ * @short_description: Context for expanding #IdeSnippetChunk
+ *
+ * This class is currently used primary as a hashtable. However, the longer
+ * term goal is to have it hold onto a GjsContext as well as other languages
+ * so that #IdeSnippetChunk can expand themselves by executing
+ * script within the context.
+ *
+ * The #IdeSnippet will build the context and then expand each of the
+ * chunks during the insertion/edit phase.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeSnippetContext
+{
+  GObject     parent_instance;
+
+  GHashTable *shared;
+  GHashTable *variables;
+  gchar      *line_prefix;
+  gint        tab_width;
+  guint       use_spaces : 1;
+};
+
+struct _IdeSnippetContextClass
+{
+  GObjectClass parent;
+};
+
+G_DEFINE_TYPE (IdeSnippetContext, ide_snippet_context, G_TYPE_OBJECT)
+
+enum {
+  CHANGED,
+  LAST_SIGNAL
+};
+
+typedef gchar *(*InputFilter) (const gchar *input);
+
+static GHashTable *filters;
+static guint signals[LAST_SIGNAL];
+
+IdeSnippetContext *
+ide_snippet_context_new (void)
+{
+  return g_object_new (IDE_TYPE_SNIPPET_CONTEXT, NULL);
+}
+
+void
+ide_snippet_context_dump (IdeSnippetContext *context)
+{
+  GHashTableIter iter;
+  gpointer key;
+  gpointer value;
+
+  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
+
+  g_hash_table_iter_init (&iter, context->variables);
+  while (g_hash_table_iter_next (&iter, &key, &value))
+    g_print (" %s=%s\n", (gchar *) key, (gchar *) value);
+}
+
+void
+ide_snippet_context_clear_variables (IdeSnippetContext *context)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
+
+  g_hash_table_remove_all (context->variables);
+}
+
+void
+ide_snippet_context_add_variable (IdeSnippetContext *context,
+                                  const gchar       *key,
+                                  const gchar       *value)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
+  g_return_if_fail (key);
+
+  g_hash_table_replace (context->variables, g_strdup (key), g_strdup (value));
+}
+
+void
+ide_snippet_context_add_shared_variable (IdeSnippetContext *context,
+                                         const gchar       *key,
+                                         const gchar       *value)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
+  g_return_if_fail (key);
+
+  g_hash_table_replace (context->shared, g_strdup (key), g_strdup (value));
+}
+
+const gchar *
+ide_snippet_context_get_variable (IdeSnippetContext *context,
+                                  const gchar       *key)
+{
+  const gchar *ret;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET_CONTEXT (context), NULL);
+
+  if (!(ret = g_hash_table_lookup (context->variables, key)))
+    ret = g_hash_table_lookup (context->shared, key);
+
+  return ret;
+}
+
+static gchar *
+filter_lower (const gchar *input)
+{
+  return g_utf8_strdown (input, -1);
+}
+
+static gchar *
+filter_upper (const gchar *input)
+{
+  return g_utf8_strup (input, -1);
+}
+
+static gchar *
+filter_capitalize (const gchar *input)
+{
+  gunichar c;
+  GString *str;
+
+  if (!*input)
+    return g_strdup ("");
+
+  c = g_utf8_get_char (input);
+
+  if (g_unichar_isupper (c))
+    return g_strdup (input);
+
+  str = g_string_new (NULL);
+  input = g_utf8_next_char (input);
+  g_string_append_unichar (str, g_unichar_toupper (c));
+  if (*input)
+    g_string_append (str, input);
+
+  return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_decapitalize (const gchar *input)
+{
+  gunichar c;
+  GString *str;
+
+  c = g_utf8_get_char (input);
+
+  if (g_unichar_islower (c))
+    return g_strdup (input);
+
+  str = g_string_new (NULL);
+  input = g_utf8_next_char (input);
+  g_string_append_unichar (str, g_unichar_tolower (c));
+  g_string_append (str, input);
+
+  return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_html (const gchar *input)
+{
+  gunichar c;
+  GString *str;
+
+  str = g_string_new (NULL);
+
+  for (; *input; input = g_utf8_next_char (input))
+    {
+      c = g_utf8_get_char (input);
+      switch (c)
+        {
+        case '<':
+          g_string_append_len (str, "&lt;", 4);
+          break;
+
+        case '>':
+          g_string_append_len (str, "&gt;", 4);
+          break;
+
+        default:
+          g_string_append_unichar (str, c);
+          break;
+        }
+    }
+
+  return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_camelize (const gchar *input)
+{
+  gboolean next_is_upper = TRUE;
+  gboolean skip = FALSE;
+  gunichar c;
+  GString *str;
+
+  if (!strchr (input, '_') && !strchr (input, ' ') && !strchr (input, '-'))
+    return filter_capitalize (input);
+
+  str = g_string_new (NULL);
+
+  for (; *input; input = g_utf8_next_char (input))
+    {
+      c = g_utf8_get_char (input);
+
+      switch (c)
+        {
+        case '_':
+        case '-':
+        case ' ':
+          next_is_upper = TRUE;
+          skip = TRUE;
+          break;
+
+        default:
+          break;
+        }
+
+      if (skip)
+        {
+          skip = FALSE;
+          continue;
+        }
+
+      if (next_is_upper)
+        {
+          c = g_unichar_toupper (c);
+          next_is_upper = FALSE;
+        }
+      else
+        c = g_unichar_tolower (c);
+
+      g_string_append_unichar (str, c);
+    }
+
+  return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_functify (const gchar *input)
+{
+  gunichar last = 0;
+  gunichar c;
+  gunichar n;
+  GString *str;
+
+  str = g_string_new (NULL);
+
+  for (; *input; input = g_utf8_next_char (input))
+    {
+      c = g_utf8_get_char (input);
+      n = g_utf8_get_char (g_utf8_next_char (input));
+
+      if (last)
+        {
+          if ((g_unichar_islower (last) && g_unichar_isupper (c)) ||
+              (g_unichar_isupper (c) && g_unichar_islower (n)))
+            g_string_append_c (str, '_');
+        }
+
+      if ((c == ' ') || (c == '-'))
+        c = '_';
+
+      g_string_append_unichar (str, g_unichar_tolower (c));
+
+      last = c;
+    }
+
+  return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_namespace (const gchar *input)
+{
+  gunichar last = 0;
+  gunichar c;
+  gunichar n;
+  GString *str;
+  gboolean first_is_lower = FALSE;
+
+  str = g_string_new (NULL);
+
+  for (; *input; input = g_utf8_next_char (input))
+    {
+      c = g_utf8_get_char (input);
+      n = g_utf8_get_char (g_utf8_next_char (input));
+
+      if (c == '_')
+        break;
+
+      if (last)
+        {
+          if ((g_unichar_islower (last) && g_unichar_isupper (c)) ||
+              (g_unichar_isupper (c) && g_unichar_islower (n)))
+            break;
+        }
+      else
+        first_is_lower = g_unichar_islower (c);
+
+      if ((c == ' ') || (c == '-'))
+        break;
+
+      g_string_append_unichar (str, c);
+
+      last = c;
+    }
+
+  if (first_is_lower)
+    {
+      gchar *ret;
+
+      ret = filter_capitalize (str->str);
+      g_string_free (str, TRUE);
+      return ret;
+    }
+
+  return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_class (const gchar *input)
+{
+  gchar *camel;
+  gchar *ns;
+  gchar *ret = NULL;
+
+  camel = filter_camelize (input);
+  ns = filter_namespace (input);
+
+  if (g_str_has_prefix (camel, ns))
+    ret = g_strdup (camel + strlen (ns));
+  else
+    {
+      ret = camel;
+      camel = NULL;
+    }
+
+  g_free (camel);
+  g_free (ns);
+
+  return ret;
+}
+
+static gchar *
+filter_instance (const gchar *input)
+{
+  const gchar *tmp;
+  gchar *funct = NULL;
+  gchar *ret;
+
+  if (!strchr (input, '_'))
+    {
+      funct = filter_functify (input);
+      input = funct;
+    }
+
+  if ((tmp = strrchr (input, '_')))
+    ret = g_strdup (tmp+1);
+  else
+    ret = g_strdup (input);
+
+  g_free (funct);
+
+  return ret;
+}
+
+static gchar *
+filter_space (const gchar *input)
+{
+  GString *str;
+
+  str = g_string_new (NULL);
+  for (; *input; input = g_utf8_next_char (input))
+    g_string_append_c (str, ' ');
+
+  return g_string_free (str, FALSE);
+}
+
+static gchar *
+filter_descend_path (const gchar *input)
+{
+   const char* pos = strchr (input, G_DIR_SEPARATOR);
+   if (pos == input)
+     {
+       input++;
+       pos = strchr (input, G_DIR_SEPARATOR);
+     }
+   if (pos)
+     {
+       pos++;
+       return g_strdup (pos);
+     }
+
+   return NULL;
+}
+
+static gchar *
+filter_stripsuffix (const gchar *input)
+{
+  const gchar *endpos;
+
+  g_return_val_if_fail (input, NULL);
+
+  endpos = strrchr (input, '.');
+  if (endpos)
+    return g_strndup (input, (endpos - input));
+
+  return g_strdup (input);
+}
+
+static gchar *
+filter_slash_to_dots (const gchar *input)
+{
+  gunichar c;
+  GString *str;
+
+  str = g_string_new (NULL);
+
+  for (; *input; input = g_utf8_next_char (input))
+    {
+      c = g_utf8_get_char (input);
+
+      if (c == G_DIR_SEPARATOR)
+        g_string_append_c (str, '.');
+      else
+        g_string_append_c (str, c);
+    }
+
+  return g_string_free (str, FALSE);
+}
+
+static gchar *
+apply_filter (gchar       *input,
+              const gchar *filter)
+{
+  InputFilter filter_func;
+  gchar *tmp;
+
+  filter_func = g_hash_table_lookup (filters, filter);
+  if (filter_func)
+    {
+      tmp = input;
+      input = filter_func (input);
+      g_free (tmp);
+    }
+
+  return input;
+}
+
+static gchar *
+apply_filters (GString     *str,
+               const gchar *filters_list)
+{
+  gchar **filter_names;
+  gchar *input = g_string_free (str, FALSE);
+  gint i;
+
+  filter_names = g_strsplit (filters_list, "|", 0);
+
+  for (i = 0; filter_names[i]; i++)
+    input = apply_filter (input, filter_names[i]);
+
+  g_strfreev (filter_names);
+
+  return input;
+}
+
+static gchar *
+scan_forward (const gchar  *input,
+              const gchar **endpos,
+              gunichar      needle)
+{
+  const gchar *begin = input;
+
+  for (; *input; input = g_utf8_next_char (input))
+    {
+      gunichar c = g_utf8_get_char (input);
+
+      if (c == needle)
+        {
+          *endpos = input;
+          return g_strndup (begin, (input - begin));
+        }
+    }
+
+  *endpos = NULL;
+
+  return NULL;
+}
+
+gchar *
+ide_snippet_context_expand (IdeSnippetContext *context,
+                            const gchar       *input)
+{
+  const gchar *expand;
+  gunichar c;
+  gboolean is_dynamic;
+  GString *str;
+  gchar key[12];
+  glong n;
+  gint i;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET_CONTEXT (context), NULL);
+  g_return_val_if_fail (input, NULL);
+
+  is_dynamic = (*input == '$');
+
+  str = g_string_new (NULL);
+
+  for (; *input; input = g_utf8_next_char (input))
+    {
+      c = g_utf8_get_char (input);
+      if (c == '\\')
+        {
+          input = g_utf8_next_char (input);
+          if (!*input)
+            break;
+          c = g_utf8_get_char (input);
+        }
+      else if (is_dynamic && c == '$')
+        {
+          input = g_utf8_next_char (input);
+          if (!*input)
+            break;
+          c = g_utf8_get_char (input);
+          if (g_unichar_isdigit (c))
+            {
+              errno = 0;
+              n = strtol (input, (gchar * *) &input, 10);
+              if (((n == LONG_MIN) || (n == LONG_MAX)) && errno == ERANGE)
+                break;
+              input--;
+              g_snprintf (key, sizeof key, "%ld", n);
+              key[sizeof key - 1] = '\0';
+              expand = ide_snippet_context_get_variable (context, key);
+              if (expand)
+                g_string_append (str, expand);
+              continue;
+            }
+          else
+            {
+              if (strchr (input, '|'))
+                {
+                  g_autofree gchar *lkey = NULL;
+
+                  lkey = g_strndup (input, strchr (input, '|') - input);
+                  expand = ide_snippet_context_get_variable (context, lkey);
+                  if (expand)
+                    {
+                      g_string_append (str, expand);
+                      input = strchr (input, '|') - 1;
+                    }
+                  else
+                    input += strlen (input) - 1;
+                }
+              else
+                {
+                  expand = ide_snippet_context_get_variable (context, input);
+                  if (expand)
+                    g_string_append (str, expand);
+                  else
+                    {
+                      g_string_append_c (str, '$');
+                      g_string_append (str, input);
+                    }
+                  input += strlen (input) - 1;
+                }
+              continue;
+            }
+        }
+      else if (is_dynamic && c == '|')
+        return apply_filters (str, input + 1);
+      else if (c == '`')
+        {
+          const gchar *endpos = NULL;
+          gchar *slice;
+
+          slice = scan_forward (input + 1, &endpos, '`');
+
+          if (slice)
+            {
+              gchar *expanded;
+
+              input = endpos;
+
+              expanded = ide_snippet_context_expand (context, slice);
+
+              g_string_append (str, expanded);
+
+              g_free (expanded);
+              g_free (slice);
+
+              continue;
+            }
+        }
+      else if (c == '\t')
+        {
+          if (context->use_spaces)
+            for (i = 0; i < context->tab_width; i++)
+              g_string_append_c (str, ' ');
+
+          else
+            g_string_append_c (str, '\t');
+          continue;
+        }
+      else if (c == '\n')
+        {
+          g_string_append_c (str, '\n');
+          if (context->line_prefix)
+            g_string_append (str, context->line_prefix);
+          continue;
+        }
+      g_string_append_unichar (str, c);
+    }
+
+  return g_string_free (str, FALSE);
+}
+
+void
+ide_snippet_context_set_tab_width (IdeSnippetContext *context,
+                                   gint               tab_width)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
+  context->tab_width = tab_width;
+}
+
+void
+ide_snippet_context_set_use_spaces (IdeSnippetContext *context,
+                                    gboolean           use_spaces)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
+  context->use_spaces = !!use_spaces;
+}
+
+void
+ide_snippet_context_set_line_prefix (IdeSnippetContext *context,
+                                     const gchar       *line_prefix)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
+  g_free (context->line_prefix);
+  context->line_prefix = g_strdup (line_prefix);
+}
+
+void
+ide_snippet_context_emit_changed (IdeSnippetContext *context)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_CONTEXT (context));
+  g_signal_emit (context, signals[CHANGED], 0);
+}
+
+static void
+ide_snippet_context_finalize (GObject *object)
+{
+  IdeSnippetContext *context = (IdeSnippetContext *)object;
+
+  g_clear_pointer (&context->shared, g_hash_table_unref);
+  g_clear_pointer (&context->variables, g_hash_table_unref);
+  g_clear_pointer (&context->line_prefix, g_free);
+
+  G_OBJECT_CLASS (ide_snippet_context_parent_class)->finalize (object);
+}
+
+static void
+ide_snippet_context_class_init (IdeSnippetContextClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_snippet_context_finalize;
+
+  signals[CHANGED] = g_signal_new ("changed",
+                                    IDE_TYPE_SNIPPET_CONTEXT,
+                                    G_SIGNAL_RUN_FIRST,
+                                    0,
+                                    NULL, NULL, NULL,
+                                    G_TYPE_NONE,
+                                    0);
+
+  filters = g_hash_table_new (g_str_hash, g_str_equal);
+  g_hash_table_insert (filters, (gpointer) "lower", filter_lower);
+  g_hash_table_insert (filters, (gpointer) "upper", filter_upper);
+  g_hash_table_insert (filters, (gpointer) "capitalize", filter_capitalize);
+  g_hash_table_insert (filters, (gpointer) "decapitalize", filter_decapitalize);
+  g_hash_table_insert (filters, (gpointer) "html", filter_html);
+  g_hash_table_insert (filters, (gpointer) "camelize", filter_camelize);
+  g_hash_table_insert (filters, (gpointer) "functify", filter_functify);
+  g_hash_table_insert (filters, (gpointer) "namespace", filter_namespace);
+  g_hash_table_insert (filters, (gpointer) "class", filter_class);
+  g_hash_table_insert (filters, (gpointer) "space", filter_space);
+  g_hash_table_insert (filters, (gpointer) "stripsuffix", filter_stripsuffix);
+  g_hash_table_insert (filters, (gpointer) "instance", filter_instance);
+  g_hash_table_insert (filters, (gpointer) "slash_to_dots", filter_slash_to_dots);
+  g_hash_table_insert (filters, (gpointer) "descend_path", filter_descend_path);
+}
+
+static void
+ide_snippet_context_init (IdeSnippetContext *context)
+{
+  GDateTime *dt;
+  gchar *str;
+
+  context->variables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+  context->shared = g_hash_table_new_full (g_str_hash,
+                                                 g_str_equal,
+                                                 g_free,
+                                                 g_free);
+
+#define ADD_VARIABLE(k, v) \
+  g_hash_table_insert (context->shared, g_strdup (k), g_strdup (v))
+
+  ADD_VARIABLE ("username", g_get_user_name ());
+  ADD_VARIABLE ("fullname", g_get_real_name ());
+  ADD_VARIABLE ("author", g_get_real_name ());
+
+  dt = g_date_time_new_now_local ();
+  str = g_date_time_format (dt, "%Y");
+  ADD_VARIABLE ("year", str);
+  g_free (str);
+  str = g_date_time_format (dt, "%b");
+  ADD_VARIABLE ("shortmonth", str);
+  g_free (str);
+  str = g_date_time_format (dt, "%d");
+  ADD_VARIABLE ("day", str);
+  g_free (str);
+  str = g_date_time_format (dt, "%a");
+  ADD_VARIABLE ("shortweekday", str);
+  g_free (str);
+  g_date_time_unref (dt);
+
+  ADD_VARIABLE ("email", "unknown domain org");
+
+#undef ADD_VARIABLE
+}
diff --git a/src/libide/sourceview/ide-snippet-context.h b/src/libide/sourceview/ide-snippet-context.h
new file mode 100644
index 000000000..55ee3943a
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet-context.h
@@ -0,0 +1,68 @@
+/* ide-snippet-context.h
+ *
+ * Copyright 2013-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SNIPPET_CONTEXT (ide_snippet_context_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeSnippetContext, ide_snippet_context, IDE, SNIPPET_CONTEXT, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeSnippetContext *ide_snippet_context_new                 (void);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_context_emit_changed        (IdeSnippetContext *context);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_context_clear_variables     (IdeSnippetContext *context);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_context_add_variable        (IdeSnippetContext *context,
+                                                            const gchar       *key,
+                                                            const gchar       *value);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_context_add_shared_variable (IdeSnippetContext *context,
+                                                            const gchar       *key,
+                                                            const gchar       *value);
+IDE_AVAILABLE_IN_3_32
+const gchar       *ide_snippet_context_get_variable        (IdeSnippetContext *context,
+                                                            const gchar       *key);
+IDE_AVAILABLE_IN_3_32
+gchar             *ide_snippet_context_expand              (IdeSnippetContext *context,
+                                                            const gchar       *input);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_context_set_tab_width       (IdeSnippetContext *context,
+                                                            gint               tab_size);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_context_set_use_spaces      (IdeSnippetContext *context,
+                                                            gboolean           use_spaces);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_context_set_line_prefix     (IdeSnippetContext *context,
+                                                            const gchar       *line_prefix);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_context_dump                (IdeSnippetContext *context);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-snippet-parser.c b/src/libide/sourceview/ide-snippet-parser.c
new file mode 100644
index 000000000..f094b9f1d
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet-parser.c
@@ -0,0 +1,725 @@
+/* ide-snippet-parser.c
+ *
+ * Copyright 2013-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-snippet-parser"
+
+#include "config.h"
+
+#include <errno.h>
+#include <glib/gi18n.h>
+#include <libide-io.h>
+#include <stdlib.h>
+
+#include "ide-snippet.h"
+#include "ide-snippet-chunk.h"
+#include "ide-snippet-parser.h"
+#include "ide-snippet-private.h"
+
+/**
+ * SECTION:ide-snippet-parser
+ * @title: IdeSnippetParser
+ * @short_description: A parser for Builder's snippet text format
+ *
+ * The #IdeSnippetParser can be used to parse ".snippets" formatted
+ * text files. This is generally only used internally by Builder, but can
+ * be used by plugins under certain situations.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeSnippetParser
+{
+  GObject  parent_instance;
+
+  GList   *snippets;
+
+  gint     lineno;
+  GList   *chunks;
+  GList   *scope;
+  gchar   *cur_name;
+  gchar   *cur_desc;
+  GString *cur_text;
+  GString *snippet_text;
+
+  GFile   *current_file;
+
+  guint    had_error : 1;
+};
+
+G_DEFINE_TYPE (IdeSnippetParser, ide_snippet_parser, G_TYPE_OBJECT)
+
+enum {
+  PARSING_ERROR,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+IdeSnippetParser *
+ide_snippet_parser_new (void)
+{
+  return g_object_new (IDE_TYPE_SNIPPET_PARSER, NULL);
+}
+
+static void
+ide_snippet_parser_flush_chunk (IdeSnippetParser *parser)
+{
+  IdeSnippetChunk *chunk;
+
+  if (parser->cur_text->len)
+    {
+      chunk = ide_snippet_chunk_new ();
+      ide_snippet_chunk_set_spec (chunk, parser->cur_text->str);
+      parser->chunks = g_list_append (parser->chunks, chunk);
+      g_string_truncate (parser->cur_text, 0);
+    }
+}
+
+static void
+ide_snippet_parser_store (IdeSnippetParser *parser)
+{
+  IdeSnippet *snippet;
+  GList *scope_iter;
+  GList *chunck_iter;
+
+  ide_snippet_parser_flush_chunk (parser);
+  for (scope_iter = parser->scope; scope_iter; scope_iter = scope_iter->next)
+    {
+      snippet = ide_snippet_new (parser->cur_name, scope_iter->data);
+      ide_snippet_set_description (snippet, parser->cur_desc);
+
+      for (chunck_iter = parser->chunks; chunck_iter; chunck_iter = chunck_iter->next)
+        {
+#if 0
+          g_printerr ("%s:  Tab: %02d  Link: %02d  Text: %s\n",
+                      parser->cur_name,
+                      ide_snippet_chunk_get_tab_stop (chunck_iter->data),
+                      ide_snippet_chunk_get_linked_chunk (chunck_iter->data),
+                      ide_snippet_chunk_get_text (chunck_iter->data));
+#endif
+          ide_snippet_add_chunk (snippet, chunck_iter->data);
+        }
+
+      parser->snippets = g_list_append (parser->snippets, snippet);
+    }
+}
+
+static void
+ide_snippet_parser_finish (IdeSnippetParser *parser)
+{
+  if (parser->cur_name)
+    ide_snippet_parser_store(parser);
+
+  g_clear_pointer (&parser->cur_name, g_free);
+
+  g_string_truncate (parser->cur_text, 0);
+  g_string_truncate (parser->snippet_text, 0);
+
+  g_list_foreach (parser->chunks, (GFunc) g_object_unref, NULL);
+  g_list_free (parser->chunks);
+  parser->chunks = NULL;
+
+  g_list_free_full (parser->scope, g_free);
+  parser->scope = NULL;
+
+  g_free (parser->cur_desc);
+  parser->cur_desc = NULL;
+}
+
+static void
+ide_snippet_parser_do_part_simple (IdeSnippetParser *parser,
+                                   const gchar      *line)
+{
+  g_string_append (parser->cur_text, line);
+}
+
+static void
+ide_snippet_parser_do_part_n (IdeSnippetParser *parser,
+                              gint              n,
+                              const gchar      *inner)
+{
+  IdeSnippetChunk *chunk;
+
+  g_return_if_fail (IDE_IS_SNIPPET_PARSER (parser));
+  g_return_if_fail (n >= -1);
+  g_return_if_fail (inner);
+
+  chunk = ide_snippet_chunk_new ();
+  ide_snippet_chunk_set_spec (chunk, n ? inner : "");
+  ide_snippet_chunk_set_tab_stop (chunk, n);
+  parser->chunks = g_list_append (parser->chunks, chunk);
+}
+
+static void
+ide_snippet_parser_do_part_linked (IdeSnippetParser *parser,
+                                   gint              n)
+{
+  IdeSnippetChunk *chunk;
+  gchar text[12];
+
+  chunk = ide_snippet_chunk_new ();
+  if (n)
+    {
+      g_snprintf (text, sizeof text, "$%d", n);
+      text[sizeof text - 1] = '\0';
+      ide_snippet_chunk_set_spec (chunk, text);
+    }
+  else
+    {
+      ide_snippet_chunk_set_spec (chunk, "");
+      ide_snippet_chunk_set_tab_stop (chunk, 0);
+    }
+  parser->chunks = g_list_append (parser->chunks, chunk);
+}
+
+static void
+ide_snippet_parser_do_part_named (IdeSnippetParser *parser,
+                                  const gchar      *name)
+{
+  IdeSnippetChunk *chunk;
+  gchar *spec;
+
+  chunk = ide_snippet_chunk_new ();
+  spec = g_strdup_printf ("$%s", name);
+  ide_snippet_chunk_set_spec (chunk, spec);
+  ide_snippet_chunk_set_tab_stop (chunk, -1);
+  parser->chunks = g_list_append (parser->chunks, chunk);
+  g_free (spec);
+}
+
+static gboolean
+parse_variable (const gchar  *line,
+                glong        *n,
+                gchar       **inner,
+                const gchar **endptr,
+                gchar       **name)
+{
+  gboolean has_inner = FALSE;
+  char *end = NULL;
+  gint brackets;
+  gint i;
+
+  *n = -1;
+  *inner = NULL;
+  *endptr = NULL;
+  *name = NULL;
+
+  g_assert (*line == '$');
+
+  line++;
+
+  *endptr = line;
+
+  if (!*line)
+    {
+      *endptr = NULL;
+      return FALSE;
+    }
+
+  if (*line == '{')
+    {
+      has_inner = TRUE;
+      line++;
+    }
+
+  if (g_ascii_isdigit (*line))
+    {
+      errno = 0;
+      *n = strtol (line, &end, 10);
+      if (((*n == LONG_MIN) || (*n == LONG_MAX)) && errno == ERANGE)
+        return FALSE;
+      else if (*n < 0)
+        return FALSE;
+      line = end;
+    }
+  else if (g_ascii_isalpha (*line))
+    {
+      const gchar *cur;
+
+      for (cur = line; *cur; cur++)
+        {
+          if (g_ascii_isalnum (*cur))
+            continue;
+          break;
+        }
+      *endptr = cur;
+      *name = g_strndup (line, cur - line);
+      *n = -2;
+      return TRUE;
+    }
+
+  if (has_inner)
+    {
+      if (*line == ':')
+        line++;
+
+      brackets = 1;
+
+      for (i = 0; line[i]; i++)
+        {
+          switch (line[i])
+            {
+            case '{':
+              brackets++;
+              break;
+
+            case '}':
+              brackets--;
+              break;
+
+            default:
+              break;
+            }
+
+          if (!brackets)
+            {
+              *inner = g_strndup (line, i);
+              *endptr = &line[i + 1];
+              return TRUE;
+            }
+        }
+
+      return FALSE;
+    }
+
+  *endptr = line;
+
+  return TRUE;
+}
+
+static void
+ide_snippet_parser_do_part (IdeSnippetParser *parser,
+                            const gchar      *line)
+{
+  const gchar *dollar;
+  gchar *str;
+  gchar *inner;
+  gchar *name;
+  glong n;
+
+  g_assert (line);
+  g_assert (*line == '\t');
+
+  line++;
+
+again:
+  if (!*line)
+    return;
+
+  if (!(dollar = strchr (line, '$')))
+    {
+      ide_snippet_parser_do_part_simple (parser, line);
+      return;
+    }
+
+  /*
+   * Parse up to the next $ as a simple.
+   * If it is $N or ${N} then it is a linked chunk w/o tabstop.
+   * If it is ${N:""} then it is a chunk w/ tabstop.
+   * If it is ${blah|upper} then it is a non-tab stop chunk performing
+   * some sort of of expansion.
+   */
+
+  g_assert (dollar >= line);
+
+  if (dollar != line)
+    {
+      str = g_strndup (line, (dollar - line));
+      ide_snippet_parser_do_part_simple (parser, str);
+      g_free (str);
+      line = dollar;
+    }
+
+parse_dollar:
+  inner = NULL;
+
+  if (!parse_variable (line, &n, &inner, &line, &name))
+    {
+      ide_snippet_parser_do_part_simple (parser, line);
+      return;
+    }
+
+#if 0
+  g_printerr ("Parse Variable: N=%d  inner=\"%s\"\n", n, inner);
+  g_printerr ("  Left over: \"%s\"\n", line);
+#endif
+
+  ide_snippet_parser_flush_chunk (parser);
+
+  if (inner)
+    {
+      ide_snippet_parser_do_part_n (parser, n, inner);
+      g_free (inner);
+      inner = NULL;
+    }
+  else if (n == -2 && name)
+    ide_snippet_parser_do_part_named (parser, name);
+  else
+    ide_snippet_parser_do_part_linked (parser, n);
+
+  g_free (name);
+
+  if (line)
+    {
+      if (*line == '$')
+        {
+          goto parse_dollar;
+        }
+      else
+        goto again;
+    }
+}
+
+static void
+ide_snippet_parser_do_snippet (IdeSnippetParser *parser,
+                               const gchar      *line)
+{
+  parser->cur_name = g_strstrip (g_strdup (&line[8]));
+}
+
+static void
+ide_snippet_parser_do_snippet_scope (IdeSnippetParser *parser,
+                                     const gchar      *line)
+{
+  gchar **scope_list;
+  GList *iter;
+  gint i;
+  gboolean add_scope;
+
+  scope_list = g_strsplit (&line[8], ",", -1);
+
+  for (i = 0; scope_list[i]; i++)
+    {
+      add_scope = TRUE;
+      for (iter = parser->scope; iter; iter = iter->next)
+        {
+          if (g_strcmp0 (iter->data, scope_list[i]) == 0)
+            {
+              add_scope = FALSE;
+              break;
+            }
+        }
+
+      if (add_scope)
+        parser->scope = g_list_append(parser->scope, g_strstrip (g_strdup (scope_list[i])));
+    }
+
+  g_strfreev(scope_list);
+}
+
+static void
+ide_snippet_parser_do_snippet_description (IdeSnippetParser *parser,
+                                           const gchar      *line)
+{
+  if (parser->cur_desc)
+    {
+      g_free(parser->cur_desc);
+      parser->cur_desc = NULL;
+    }
+
+  parser->cur_desc = g_strstrip (g_strdup (&line[7]));
+}
+
+static void
+ide_snippet_parser_feed_line (IdeSnippetParser *parser,
+                              const gchar      *basename,
+                              const gchar      *line)
+{
+  const gchar *orig = line;
+
+  g_assert (parser);
+  g_assert (basename);
+  g_assert (line);
+
+  parser->lineno++;
+
+  switch (*line)
+    {
+    case '\0':
+      if (parser->cur_name)
+        g_string_append_c (parser->cur_text, '\n');
+      break;
+
+    case '#':
+      break;
+
+    case '\t':
+      if (parser->cur_name)
+        {
+          GList *iter;
+          gboolean add_default_scope = TRUE;
+          for (iter = parser->scope; iter; iter = iter->next)
+            {
+              if (g_strcmp0(iter->data, basename) == 0)
+                {
+                  add_default_scope = FALSE;
+                  break;
+                }
+            }
+
+          if (add_default_scope)
+            parser->scope = g_list_append(parser->scope,
+                                        g_strstrip (g_strdup (basename)));
+
+          if (parser->cur_text->len || parser->chunks)
+            g_string_append_c (parser->cur_text, '\n');
+          ide_snippet_parser_do_part (parser, line);
+        }
+      break;
+
+    case 's':
+      if (g_str_has_prefix (line, "snippet"))
+        {
+          ide_snippet_parser_finish (parser);
+          ide_snippet_parser_do_snippet (parser, line);
+          break;
+        }
+
+    /* Fall through */
+    case '-':
+      if (parser->cur_text->len || parser->chunks)
+        {
+          ide_snippet_parser_store(parser);
+
+          g_string_truncate (parser->cur_text, 0);
+
+          g_list_foreach (parser->chunks, (GFunc) g_object_unref, NULL);
+          g_list_free (parser->chunks);
+          parser->chunks = NULL;
+
+          g_list_free_full(parser->scope, g_free);
+          parser->scope = NULL;
+        }
+
+      if (g_str_has_prefix(line, "- scope"))
+        {
+          ide_snippet_parser_do_snippet_scope (parser, line);
+          break;
+        }
+
+      if (g_str_has_prefix(line, "- desc"))
+        {
+          ide_snippet_parser_do_snippet_description (parser, line);
+          break;
+        }
+
+    /* Fall through */
+    default:
+      g_signal_emit (parser, signals [PARSING_ERROR], 0,
+                     parser->current_file, parser->lineno, line);
+      parser->had_error = TRUE;
+      break;
+    }
+
+  g_string_append (parser->snippet_text, orig);
+  g_string_append_c (parser->snippet_text, '\n');
+}
+
+gboolean
+ide_snippet_parser_load_from_file (IdeSnippetParser  *parser,
+                                   GFile             *file,
+                                   GError           **error)
+{
+  GFileInputStream *file_stream;
+  g_autoptr(GDataInputStream) data_stream = NULL;
+  GError *local_error = NULL;
+  gchar *line;
+  gchar *basename = NULL;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET_PARSER (parser), FALSE);
+  g_return_val_if_fail (G_IS_FILE (file), FALSE);
+
+  basename = g_file_get_basename (file);
+
+  if (basename)
+    {
+      if (strstr (basename, "."))
+        *strstr (basename, ".") = '\0';
+    }
+
+  file_stream = g_file_read (file, NULL, error);
+  if (!file_stream)
+    return FALSE;
+
+  data_stream = g_data_input_stream_new (G_INPUT_STREAM (file_stream));
+  g_object_unref (file_stream);
+
+  g_set_object (&parser->current_file, file);
+
+again:
+  if (parser->had_error)
+    {
+      /* TODO: Better error messages */
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_DATA,
+                   "%s:%d: invalid snippet",
+                   basename, parser->lineno);
+      return FALSE;
+    }
+
+  line = g_data_input_stream_read_line_utf8 (data_stream, NULL, NULL, &local_error);
+  if (!line && local_error)
+    {
+      g_propagate_error (error, local_error);
+      g_set_object (&parser->current_file, NULL);
+      return FALSE;
+    }
+  else if (line)
+    {
+      ide_snippet_parser_feed_line (parser, basename, line);
+      g_free (line);
+      goto again;
+    }
+
+  ide_snippet_parser_finish (parser);
+  g_free (basename);
+
+  g_set_object (&parser->current_file, NULL);
+
+  return TRUE;
+}
+
+gboolean
+ide_snippet_parser_load_from_data (IdeSnippetParser  *parser,
+                                   const gchar       *default_language,
+                                   const gchar       *data,
+                                   gssize             data_len,
+                                   GError           **error)
+{
+  IdeLineReader reader;
+  gchar *line;
+  gsize line_len;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET_PARSER (parser), FALSE);
+  g_return_val_if_fail (data != NULL, FALSE);
+
+  if (data_len < 0)
+    data_len = strlen (data);
+
+  ide_line_reader_init (&reader, (gchar *)data, data_len);
+
+  while ((line = ide_line_reader_next (&reader, &line_len)))
+    {
+      g_autofree gchar *copy = NULL;
+
+      if (parser->had_error)
+        {
+          g_set_error (error,
+                       G_IO_ERROR,
+                       G_IO_ERROR_INVALID_DATA,
+                       "<data>:%d: invalid snippet",
+                       parser->lineno);
+          return FALSE;
+        }
+
+      copy = g_strndup (line, line_len);
+      ide_snippet_parser_feed_line (parser, default_language, copy);
+    }
+
+  ide_snippet_parser_finish (parser);
+
+  return TRUE;
+}
+
+/**
+ * ide_snippet_parser_get_snippets:
+ * @parser: a #IdeSnippetParser
+ *
+ * Get the list of all the snippets loaded.
+ *
+ * Returns: (transfer none) (element-type Ide.Snippet): a #GList of #IdeSnippets items.
+ *
+ * Since: 3.32
+ */
+GList *
+ide_snippet_parser_get_snippets (IdeSnippetParser *parser)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET_PARSER (parser), NULL);
+  return parser->snippets;
+}
+
+static void
+ide_snippet_parser_finalize (GObject *object)
+{
+  IdeSnippetParser *self = IDE_SNIPPET_PARSER (object);
+
+  g_list_foreach (self->snippets, (GFunc) g_object_unref, NULL);
+  g_list_free (self->snippets);
+  self->snippets = NULL;
+
+  g_list_foreach (self->chunks, (GFunc) g_object_unref, NULL);
+  g_list_free (self->chunks);
+  self->chunks = NULL;
+
+  g_list_free_full(self->scope, g_free);
+  self->scope = NULL;
+
+  if (self->cur_text)
+    g_string_free (self->cur_text, TRUE);
+  self->cur_text = NULL;
+
+  if (self->snippet_text)
+    g_string_free (self->snippet_text, TRUE);
+  self->snippet_text = NULL;
+
+  g_free (self->cur_name);
+  self->cur_name = NULL;
+
+  if (self->cur_desc)
+    {
+      g_free (self->cur_desc);
+      self->cur_desc = NULL;
+    }
+
+  G_OBJECT_CLASS (ide_snippet_parser_parent_class)->finalize (object);
+}
+
+static void
+ide_snippet_parser_class_init (IdeSnippetParserClass *klass)
+{
+  GObjectClass *object_class;
+
+  object_class = G_OBJECT_CLASS (klass);
+  object_class->finalize = ide_snippet_parser_finalize;
+
+  signals [PARSING_ERROR] =
+    g_signal_new ("parsing-error",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL,
+                  NULL,
+                  G_TYPE_NONE,
+                  3,
+                  G_TYPE_FILE,
+                  G_TYPE_UINT,
+                  G_TYPE_STRING);
+}
+
+static void
+ide_snippet_parser_init (IdeSnippetParser *parser)
+{
+  parser->lineno = -1;
+  parser->cur_text = g_string_new (NULL);
+  parser->snippet_text = g_string_new (NULL);
+  parser->scope = NULL;
+  parser->cur_desc = NULL;
+}
diff --git a/src/libide/sourceview/ide-snippet-parser.h b/src/libide/sourceview/ide-snippet-parser.h
new file mode 100644
index 000000000..c45662273
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet-parser.h
@@ -0,0 +1,53 @@
+/* ide-snippet-parser.h
+ *
+ * Copyright 2013-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <gio/gio.h>
+
+#include "ide-version-macros.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SNIPPET_PARSER (ide_snippet_parser_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeSnippetParser, ide_snippet_parser, IDE, SNIPPET_PARSER, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeSnippetParser *ide_snippet_parser_new            (void);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_snippet_parser_load_from_data (IdeSnippetParser  *parser,
+                                                     const gchar       *defalut_language,
+                                                     const gchar       *data,
+                                                     gssize             data_len,
+                                                     GError           **error);
+IDE_AVAILABLE_IN_3_32
+gboolean          ide_snippet_parser_load_from_file (IdeSnippetParser  *parser,
+                                                     GFile             *file,
+                                                     GError           **error);
+IDE_AVAILABLE_IN_3_32
+GList            *ide_snippet_parser_get_snippets   (IdeSnippetParser  *parser);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-snippet-private.h b/src/libide/sourceview/ide-snippet-private.h
new file mode 100644
index 000000000..360258809
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet-private.h
@@ -0,0 +1,61 @@
+/* ide-snippet-private.h
+ *
+ * Copyright 2013-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <gtk/gtk.h>
+
+#include "ide-snippet.h"
+
+G_BEGIN_DECLS
+
+gboolean         ide_snippet_begin               (IdeSnippet    *self,
+                                                  GtkTextBuffer *buffer,
+                                                  GtkTextIter   *iter);
+void             ide_snippet_pause               (IdeSnippet    *self);
+void             ide_snippet_unpause             (IdeSnippet    *self);
+void             ide_snippet_finish              (IdeSnippet    *self);
+gboolean         ide_snippet_move_next           (IdeSnippet    *self);
+gboolean         ide_snippet_move_previous       (IdeSnippet    *self);
+void             ide_snippet_before_insert_text  (IdeSnippet    *self,
+                                                  GtkTextBuffer *buffer,
+                                                  GtkTextIter   *iter,
+                                                  gchar         *text,
+                                                  gint           len);
+void             ide_snippet_after_insert_text   (IdeSnippet    *self,
+                                                  GtkTextBuffer *buffer,
+                                                  GtkTextIter   *iter,
+                                                  gchar         *text,
+                                                  gint           len);
+void             ide_snippet_before_delete_range (IdeSnippet    *self,
+                                                  GtkTextBuffer *buffer,
+                                                  GtkTextIter   *begin,
+                                                  GtkTextIter   *end);
+void             ide_snippet_after_delete_range  (IdeSnippet    *self,
+                                                  GtkTextBuffer *buffer,
+                                                  GtkTextIter   *begin,
+                                                  GtkTextIter   *end);
+gboolean         ide_snippet_insert_set          (IdeSnippet    *self,
+                                                  GtkTextMark   *mark);
+void             ide_snippet_dump                (IdeSnippet    *self);
+GtkTextMark     *ide_snippet_get_mark_begin      (IdeSnippet    *self);
+GtkTextMark     *ide_snippet_get_mark_end        (IdeSnippet    *self);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-snippet-storage.c b/src/libide/sourceview/ide-snippet-storage.c
new file mode 100644
index 000000000..38ad6c774
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet-storage.c
@@ -0,0 +1,503 @@
+/* ide-snippet-storage.c
+ *
+ * Copyright 2018-2019 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 "ide-snippet-storage"
+
+#include "config.h"
+
+#include <libide-io.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "ide-snippet-storage.h"
+
+#define SNIPPETS_DIRECTORY "/org/gnome/builder/snippets/"
+
+/**
+ * SECTION:ide-snippet-storage
+ * @title: IdeSnippetStorage
+ * @short_description: storage and loading of snippets
+ *
+ * The #IdeSnippetStorage object manages parsing snippet files from disk.
+ * To avoid creating lots of small allocations, it delays parsing of
+ * snippets fully until necessary.
+ *
+ * To do this, mapped files are used and just enough information is
+ * extracted to describe the snippets. Then snippets are inflated and
+ * fully parsed when requested.
+ *
+ * In doing so, we can use #GStringChunk for the meta-data, and then only
+ * create all the small strings when we inflate the snippet and its chunks.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeSnippetStorage
+{
+  IdeObject     parent_instance;
+  GStringChunk *strings;
+  GArray       *infos;
+  GPtrArray    *bytes;
+
+  guint         loaded : 1;
+};
+
+typedef struct
+{
+  gchar *name;
+  gchar *desc;
+  gchar *scopes;
+  const gchar *beginptr;
+  const gchar *endptr;
+} LoadState;
+
+static void async_initable_iface_init (GAsyncInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeSnippetStorage, ide_snippet_storage, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init))
+
+static void
+ide_snippet_storage_finalize (GObject *object)
+{
+  IdeSnippetStorage *self = (IdeSnippetStorage *)object;
+
+  g_clear_pointer (&self->bytes, g_ptr_array_unref);
+  g_clear_pointer (&self->strings, g_string_chunk_free);
+  g_clear_pointer (&self->infos, g_array_unref);
+
+  G_OBJECT_CLASS (ide_snippet_storage_parent_class)->finalize (object);
+}
+
+static void
+ide_snippet_storage_class_init (IdeSnippetStorageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_snippet_storage_finalize;
+}
+
+static void
+ide_snippet_storage_init (IdeSnippetStorage *self)
+{
+  self->strings = g_string_chunk_new (4096);
+  self->infos = g_array_new (FALSE, FALSE, sizeof (IdeSnippetInfo));
+  self->bytes = g_ptr_array_new_with_free_func ((GDestroyNotify)g_bytes_unref);
+}
+
+IdeSnippetStorage *
+ide_snippet_storage_new (void)
+{
+  return g_object_new (IDE_TYPE_SNIPPET_STORAGE, NULL);
+}
+
+static gint
+snippet_info_compare (gconstpointer a,
+                      gconstpointer b)
+{
+  const IdeSnippetInfo *ai = a;
+  const IdeSnippetInfo *bi = b;
+  gint r;
+
+  if (!(r = g_strcmp0 (ai->lang, bi->lang)))
+    r = g_strcmp0 (ai->name, bi->name);
+
+  return r;
+}
+
+static gboolean
+str_starts_with (const gchar *str,
+                 gsize        len,
+                 const gchar *needle)
+{
+  gsize needle_len = strlen (needle);
+  if (len < needle_len)
+    return FALSE;
+  return strncmp (str, needle, needle_len) == 0;
+}
+
+static void
+flush_load_state (IdeSnippetStorage *self,
+                  const gchar       *default_scope,
+                  LoadState         *state)
+{
+  g_auto(GStrv) scopes = NULL;
+  IdeSnippetInfo info = {0};
+  gboolean needs_default = TRUE;
+
+  if (state->name == NULL)
+    goto cleanup;
+
+  g_assert (state->beginptr);
+  g_assert (state->endptr);
+  g_assert (state->endptr > state->beginptr);
+
+  if (state->scopes != NULL)
+    scopes = g_strsplit (state->scopes, ",", 0);
+
+  info.name = g_string_chunk_insert_const (self->strings, state->name);
+  if (state->desc)
+    info.desc = g_string_chunk_insert_const (self->strings, state->desc);
+
+  info.begin = state->beginptr;
+  info.len = state->endptr - state->beginptr;
+  info.default_lang = g_string_chunk_insert_const (self->strings, default_scope);
+
+  if (scopes != NULL)
+    {
+      for (guint i = 0; scopes[i] != NULL; i++)
+        {
+          g_strstrip (scopes[i]);
+          if (g_strcmp0 (scopes[i], default_scope) == 0)
+            needs_default = FALSE;
+          info.lang = g_string_chunk_insert_const (self->strings, scopes[i]);
+          g_array_append_val (self->infos, info);
+        }
+    }
+
+  if (needs_default && default_scope)
+    {
+      info.lang = g_string_chunk_insert_const (self->strings, default_scope);
+      g_array_append_val (self->infos, info);
+    }
+
+cleanup:
+  /* Leave name in-tact */
+  g_clear_pointer (&state->desc, g_free);
+  g_clear_pointer (&state->scopes, g_free);
+}
+
+void
+ide_snippet_storage_add (IdeSnippetStorage *self,
+                         const gchar       *default_scope,
+                         GBytes            *bytes)
+{
+  IdeLineReader reader;
+  LoadState state = {0};
+  const gchar *data;
+  const gchar *line;
+  gsize line_len;
+  gsize len;
+  gboolean found_data = FALSE;
+
+  g_return_if_fail (IDE_IS_SNIPPET_STORAGE (self));
+  g_return_if_fail (bytes != NULL);
+
+  g_ptr_array_add (self->bytes, g_bytes_ref (bytes));
+
+  data = g_bytes_get_data (bytes, &len);
+  state.beginptr = data;
+
+  ide_line_reader_init (&reader, (gchar *)data, len);
+
+#define COPY_AFTER(dst, str) \
+  G_STMT_START { \
+    g_free (state.dst); \
+    state.dst = g_strstrip(g_strndup(line + strlen(str), line_len - strlen(str))); \
+  } G_STMT_END
+
+  while ((line = ide_line_reader_next (&reader, &line_len)))
+    {
+      if (str_starts_with (line, line_len, "snippet "))
+        {
+          if (state.name && found_data)
+            flush_load_state (self, default_scope, &state);
+          state.beginptr = line;
+          COPY_AFTER (name, "snippet ");
+          found_data = FALSE;
+        }
+      else if (str_starts_with (line, line_len, "- desc "))
+        {
+          COPY_AFTER (desc, "- desc");
+        }
+      else if (str_starts_with (line, line_len, "- scope "))
+        {
+          /* We could have repeated scopes, so if we get a folloup -scope, we need
+           * to flush the previous and then update beginptr/endptr.
+           */
+          if (state.name && found_data)
+            flush_load_state (self, default_scope, &state);
+          COPY_AFTER (scopes, "- scope ");
+          found_data = FALSE;
+        }
+      else
+        {
+          found_data = TRUE;
+        }
+
+      state.endptr = line + line_len;
+    }
+
+#undef COPY_AFTER
+
+  flush_load_state (self, default_scope, &state);
+
+  g_array_sort (self->infos, snippet_info_compare);
+
+  g_clear_pointer (&state.name, g_free);
+  g_clear_pointer (&state.desc, g_free);
+  g_clear_pointer (&state.scopes, g_free);
+}
+
+/**
+ * ide_snippet_storage_foreach:
+ * @self: a #IdeSnippetStorage
+ * @foreach: (scope call): the closure to call for each info
+ * @user_data: closure data for @foreach
+ *
+ * This will call @foreach for every item that has been loaded.
+ *
+ * Since: 3.32
+ */
+void
+ide_snippet_storage_foreach (IdeSnippetStorage        *self,
+                             IdeSnippetStorageForeach  foreach,
+                             gpointer                  user_data)
+{
+  g_return_if_fail (IDE_IS_SNIPPET_STORAGE (self));
+  g_return_if_fail (foreach != NULL);
+
+  for (guint i = 0; i < self->infos->len; i++)
+    {
+      const IdeSnippetInfo *info = &g_array_index (self->infos, IdeSnippetInfo, i);
+
+      foreach (self, info, user_data);
+    }
+}
+
+static gint
+query_compare (gconstpointer a,
+               gconstpointer b)
+{
+  const IdeSnippetInfo *ai = a;
+  const IdeSnippetInfo *bi = b;
+  gboolean r;
+
+  if (!(r = g_strcmp0 (ai->lang, bi->lang)))
+    {
+      if (g_str_has_prefix (bi->name, ai->name))
+        return 0;
+      r = g_strcmp0 (ai->name, bi->name);
+    }
+
+  return r;
+}
+
+/**
+ * ide_snippet_storage_query:
+ * @self: a #IdeSnippetStorage
+ * @lang: language to query
+ * @prefix: (nullable): prefix for query
+ * @foreach: (scope call): the closure to call for each match
+ * @user_data: closure data for @foreach
+ *
+ * This will call @foreach for every info that matches the query. This is
+ * useful when building autocompletion lists based on word prefixes.
+ *
+ * Since: 3.32
+ */
+void
+ide_snippet_storage_query (IdeSnippetStorage        *self,
+                           const gchar              *lang,
+                           const gchar              *prefix,
+                           IdeSnippetStorageForeach  foreach,
+                           gpointer                  user_data)
+{
+  IdeSnippetInfo key = { 0 };
+  const IdeSnippetInfo *endptr;
+  const IdeSnippetInfo *base;
+
+  g_return_if_fail (IDE_IS_SNIPPET_STORAGE (self));
+  g_return_if_fail (lang != NULL);
+  g_return_if_fail (foreach != NULL);
+
+  if (self->infos->len == 0)
+    return;
+
+  if (prefix == NULL)
+    prefix = "";
+
+  key.lang = lang;
+  key.name = prefix;
+
+  base = bsearch (&key,
+                  self->infos->data,
+                  self->infos->len,
+                  sizeof (IdeSnippetInfo),
+                  query_compare);
+
+  if (base == NULL)
+    return;
+
+  while ((gpointer)base > (gpointer)self->infos->data)
+    {
+      const IdeSnippetInfo *prev = base - 1;
+
+      if (base->lang == prev->lang && g_str_has_prefix (prev->name, prefix))
+        base = prev;
+      else
+        break;
+    }
+
+  endptr = &g_array_index (self->infos, IdeSnippetInfo, self->infos->len);
+
+  for (; base < endptr; base++)
+    {
+      if (g_strcmp0 (base->lang, lang) != 0)
+        break;
+
+      if (!g_str_has_prefix (base->name, prefix))
+        break;
+
+      foreach (self, base, user_data);
+    }
+}
+
+static void
+ide_snippet_storage_init_async (GAsyncInitable      *initable,
+                                gint                 io_priority,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  IdeSnippetStorage *self = (IdeSnippetStorage *)initable;
+  g_autofree gchar *local = NULL;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GDir) dir = NULL;
+  g_autoptr(GError) error = NULL;
+  g_auto(GStrv) names = NULL;
+
+  g_return_if_fail (IDE_IS_SNIPPET_STORAGE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_snippet_storage_init_async);
+
+  if (self->loaded)
+    {
+      g_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  self->loaded = TRUE;
+
+  if (!(names = g_resources_enumerate_children (SNIPPETS_DIRECTORY,
+                                                G_RESOURCE_LOOKUP_FLAGS_NONE,
+                                                &error)))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  for (guint i = 0; names[i] != NULL; i++)
+    {
+      g_autofree gchar *path = g_build_filename (SNIPPETS_DIRECTORY, names[i], NULL);
+      g_autoptr(GBytes) bytes = g_resources_lookup_data (path, 0, NULL);
+      g_autofree gchar *base = NULL;
+      const gchar *dot;
+
+      if (bytes == NULL)
+        continue;
+
+      if ((dot = strrchr (names[i], '.')))
+        base = g_strndup (names[i], dot - names[i]);
+
+      ide_snippet_storage_add (self, base, bytes);
+    }
+
+  /* TODO: Do this async */
+
+  local = g_build_filename (g_get_user_config_dir (),
+                            "gnome-builder",
+                            "snippets",
+                            NULL);
+
+  if ((dir = g_dir_open (local, 0, NULL)))
+    {
+      const gchar *name;
+
+      while ((name = g_dir_read_name (dir)))
+        {
+          g_autofree gchar *path = g_build_filename (local, name, NULL);
+          g_autoptr(GMappedFile) mf = NULL;
+          g_autoptr(GBytes) bytes = NULL;
+          g_autofree gchar *base = NULL;
+          const gchar *dot;
+
+          if (!(mf = g_mapped_file_new (path, FALSE, &error)))
+            {
+              g_message ("%s", error->message);
+              g_clear_error (&error);
+              continue;
+            }
+
+          bytes = g_mapped_file_get_bytes (mf);
+
+          if ((dot = strrchr (name, '.')))
+            base = g_strndup (name, dot - name);
+
+          ide_snippet_storage_add (self, base, bytes);
+        }
+    }
+
+  g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_snippet_storage_init_finish (GAsyncInitable  *initable,
+                                 GAsyncResult    *result,
+                                 GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET_STORAGE (initable), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+async_initable_iface_init (GAsyncInitableIface *iface)
+{
+  iface->init_async = ide_snippet_storage_init_async;
+  iface->init_finish = ide_snippet_storage_init_finish;
+}
+
+/**
+ * ide_snippet_storage_from_context:
+ * @context: an #IdeContext
+ *
+ * Gets the snippet storage for the context.
+ *
+ * Returns: (transfer none): an #IdeSnippetStorage
+ *
+ * Since: 3.32
+ */
+IdeSnippetStorage *
+ide_snippet_storage_from_context (IdeContext *context)
+{
+  IdeSnippetStorage *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  /* Give back a borrowed reference instead of full */
+  ret = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_SNIPPET_STORAGE);
+  g_object_unref (ret);
+
+  return ret;
+}
diff --git a/src/libide/sourceview/ide-snippet-storage.h b/src/libide/sourceview/ide-snippet-storage.h
new file mode 100644
index 000000000..b3fde8d96
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet-storage.h
@@ -0,0 +1,73 @@
+/* ide-snippet-storage.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-snippet-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SNIPPET_STORAGE (ide_snippet_storage_get_type())
+
+typedef struct
+{
+  const gchar *lang;
+  const gchar *name;
+  const gchar *desc;
+
+  /*< private >*/
+  const gchar *default_lang;
+  const gchar *begin;
+  goffset      len;
+} IdeSnippetInfo;
+
+typedef void (*IdeSnippetStorageForeach) (IdeSnippetStorage    *self,
+                                          const IdeSnippetInfo *info,
+                                          gpointer              user_data);
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeSnippetStorage, ide_snippet_storage, IDE, SNIPPET_STORAGE, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeSnippetStorage *ide_snippet_storage_from_context (IdeContext *context);
+IDE_AVAILABLE_IN_3_32
+IdeSnippetStorage *ide_snippet_storage_new         (void);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_storage_add         (IdeSnippetStorage         *self,
+                                                    const gchar               *default_scope,
+                                                    GBytes                    *bytes);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_storage_foreach     (IdeSnippetStorage         *self,
+                                                    IdeSnippetStorageForeach   foreach,
+                                                    gpointer                   user_data);
+IDE_AVAILABLE_IN_3_32
+void               ide_snippet_storage_query       (IdeSnippetStorage         *self,
+                                                    const gchar               *lang,
+                                                    const gchar               *prefix,
+                                                    IdeSnippetStorageForeach   foreach,
+                                                    gpointer                   user_data);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-snippet-types.h b/src/libide/sourceview/ide-snippet-types.h
new file mode 100644
index 000000000..265edb633
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet-types.h
@@ -0,0 +1,37 @@
+/* ide-snippet-types.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+typedef struct _IdeSnippet IdeSnippet;
+typedef struct _IdeSnippetChunk IdeSnippetChunk;
+typedef struct _IdeSnippetParser IdeSnippetParser;
+typedef struct _IdeSnippetContext IdeSnippetContext;
+typedef struct _IdeSnippetStorage IdeSnippetStorage;
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-snippet.c b/src/libide/sourceview/ide-snippet.c
new file mode 100644
index 000000000..12e010d68
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet.c
@@ -0,0 +1,1359 @@
+/* ide-snippet.c
+ *
+ * Copyright 2013-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-snippet"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "ide-completion-proposal.h"
+#include "ide-snippet.h"
+#include "ide-snippet-private.h"
+#include "ide-snippet-chunk.h"
+#include "ide-snippet-context.h"
+
+/**
+ * SECTION:ide-snippet
+ * @title: IdeSnippet
+ * @short_description: A snippet to be inserted into a file
+ *
+ * The #IdeSnippet represents a single snippet that may be inserted
+ * into the #IdeSourceView.
+ *
+ * Since: 3.32
+ */
+
+#define TAG_SNIPPET_TAB_STOP "snippet::tab-stop"
+
+struct _IdeSnippet
+{
+  GObject                  parent_instance;
+
+  IdeSnippetContext       *snippet_context;
+  GtkTextBuffer           *buffer;
+  GPtrArray               *chunks;
+  GArray                  *runs;
+  GtkTextMark             *mark_begin;
+  GtkTextMark             *mark_end;
+  gchar                   *trigger;
+  const gchar             *language;
+  gchar                   *description;
+
+  gint                     tab_stop;
+  gint                     max_tab_stop;
+  gint                     current_chunk;
+
+  guint                    inserted : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_BUFFER,
+  PROP_DESCRIPTION,
+  PROP_LANGUAGE,
+  PROP_MARK_BEGIN,
+  PROP_MARK_END,
+  PROP_TAB_STOP,
+  PROP_TRIGGER,
+  LAST_PROP
+};
+
+G_DEFINE_TYPE (IdeSnippet, ide_snippet, G_TYPE_OBJECT)
+
+DZL_DEFINE_COUNTER (instances, "Snippets", "N Snippets", "Number of IdeSnippet instances.");
+
+static GParamSpec * properties[LAST_PROP];
+
+/**
+ * ide_snippet_new:
+ * @trigger: (nullable): the trigger word
+ * @language: (nullable): the source language
+ *
+ * Creates a new #IdeSnippet
+ *
+ * Returns: (transfer full): A new #IdeSnippet
+ *
+ * Since: 3.32
+ */
+IdeSnippet *
+ide_snippet_new (const gchar *trigger,
+                 const gchar *language)
+{
+  return g_object_new (IDE_TYPE_SNIPPET,
+                       "trigger", trigger,
+                       "language", language,
+                       NULL);
+}
+
+/**
+ * ide_snippet_copy:
+ * @self: an #IdeSnippet
+ *
+ * Does a deep copy of the snippet.
+ *
+ * Returns: (transfer full): An #IdeSnippet.
+ *
+ * Since: 3.32
+ */
+IdeSnippet *
+ide_snippet_copy (IdeSnippet *self)
+{
+  IdeSnippetChunk *chunk;
+  IdeSnippet *ret;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), NULL);
+
+  ret = g_object_new (IDE_TYPE_SNIPPET,
+                      "trigger", self->trigger,
+                      "language", self->language,
+                      "description", self->description,
+                      NULL);
+
+  for (guint i = 0; i < self->chunks->len; i++)
+    {
+      chunk = g_ptr_array_index (self->chunks, i);
+      chunk = ide_snippet_chunk_copy (chunk);
+      ide_snippet_add_chunk (ret, chunk);
+      g_object_unref (chunk);
+    }
+
+  return ret;
+}
+
+/**
+ * ide_snippet_get_tab_stop:
+ * @self: a #IdeSnippet
+ *
+ * Gets the current tab stop for the snippet. This is changed
+ * as the user Tab's through the edit points.
+ *
+ * Returns: The tab stop, or -1 if unset.
+ *
+ * Since: 3.32
+ */
+gint
+ide_snippet_get_tab_stop (IdeSnippet *self)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), -1);
+
+  return self->tab_stop;
+}
+
+/**
+ * ide_snippet_get_n_chunks:
+ * @self: a #IdeSnippet
+ *
+ * Gets the number of chunks in the snippet. Not all chunks
+ * are editable.
+ *
+ * Returns: The number of chunks.
+ *
+ * Since: 3.32
+ */
+guint
+ide_snippet_get_n_chunks (IdeSnippet *self)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), 0);
+
+  return self->chunks->len;
+}
+
+/**
+ * ide_snippet_get_nth_chunk:
+ * @self: an #IdeSnippet
+ * @n: the nth chunk to get
+ *
+ * Gets the chunk at @n.
+ *
+ * Returns: (transfer none): an #IdeSnippetChunk
+ *
+ * Since: 3.32
+ */
+IdeSnippetChunk *
+ide_snippet_get_nth_chunk (IdeSnippet *self,
+                           guint       n)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), 0);
+
+  if (n < self->chunks->len)
+    return g_ptr_array_index (self->chunks, n);
+
+  return NULL;
+}
+
+/**
+ * ide_snippet_get_trigger:
+ * @self: a #IdeSnippet
+ *
+ * Gets the trigger for the source snippet
+ *
+ * Returns: (nullable): A trigger if specified
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_snippet_get_trigger (IdeSnippet *self)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), NULL);
+
+  return self->trigger;
+}
+
+/**
+ * ide_snippet_set_trigger:
+ * @self: a #IdeSnippet
+ * @trigger: the trigger word
+ *
+ * Sets the trigger for the snippet.
+ *
+ * Since: 3.32
+ */
+void
+ide_snippet_set_trigger (IdeSnippet  *self,
+                         const gchar *trigger)
+{
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+
+  if (self->trigger != trigger)
+    {
+      g_free (self->trigger);
+      self->trigger = g_strdup (trigger);
+    }
+}
+
+/**
+ * ide_snippet_get_language:
+ * @self: a #IdeSnippet
+ *
+ * Gets the language used for the source snippet.
+ *
+ * The language identifier matches the #GtkSourceLanguage:id
+ * property.
+ *
+ * Returns: the language identifier
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_snippet_get_language (IdeSnippet *self)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), NULL);
+
+  return self->language;
+}
+
+/**
+ * ide_snippet_set_language:
+ * @self: a #IdeSnippet
+ *
+ * Sets the language identifier for the snippet.
+ *
+ * This should match the #GtkSourceLanguage:id identifier.
+ *
+ * Since: 3.32
+ */
+void
+ide_snippet_set_language (IdeSnippet  *self,
+                          const gchar *language)
+{
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+
+  language = g_intern_string (language);
+
+  if (self->language != language)
+    {
+      self->language = language;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_LANGUAGE]);
+    }
+}
+
+/**
+ * ide_snippet_get_description:
+ * @self: a #IdeSnippet
+ *
+ * Gets the description for the snippet.
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_snippet_get_description (IdeSnippet *self)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), NULL);
+
+  return self->description;
+}
+
+/**
+ * ide_snippet_set_description:
+ * @self: a #IdeSnippet
+ * @description: the snippet description
+ *
+ * Sets the description for the snippet.
+ *
+ * Since: 3.32
+ */
+void
+ide_snippet_set_description (IdeSnippet  *self,
+                             const gchar *description)
+{
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+
+  if (self->description != description)
+    {
+      g_free (self->description);
+      self->description = g_strdup (description);
+    }
+}
+
+static gint
+ide_snippet_get_offset (IdeSnippet  *self,
+                        GtkTextIter *iter)
+{
+  GtkTextIter begin;
+  gint ret;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), 0);
+  g_return_val_if_fail (iter, 0);
+
+  gtk_text_buffer_get_iter_at_mark (self->buffer, &begin, self->mark_begin);
+  ret = gtk_text_iter_get_offset (iter) - gtk_text_iter_get_offset (&begin);
+  ret = MAX (0, ret);
+
+  return ret;
+}
+
+static gint
+ide_snippet_get_index (IdeSnippet  *self,
+                       GtkTextIter *iter)
+{
+  gint offset;
+  gint run;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), 0);
+  g_return_val_if_fail (iter, 0);
+
+  offset = ide_snippet_get_offset (self, iter);
+
+  for (guint i = 0; i < self->runs->len; i++)
+    {
+      run = g_array_index (self->runs, gint, i);
+      offset -= run;
+      if (offset <= 0)
+        {
+          /*
+           * HACK: This is the central part of the hack by using offsets
+           *       instead of textmarks (which gives us lots of gravity grief).
+           *       We guess which snippet it is based on the current chunk.
+           */
+          if (self->current_chunk > -1 && (i + 1) == (guint)self->current_chunk)
+            return (i + 1);
+          return i;
+        }
+    }
+
+  return (self->runs->len - 1);
+}
+
+static gboolean
+ide_snippet_within_bounds (IdeSnippet  *self,
+                           GtkTextIter *iter)
+{
+  GtkTextIter begin;
+  GtkTextIter end;
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), FALSE);
+  g_return_val_if_fail (iter, FALSE);
+
+  gtk_text_buffer_get_iter_at_mark (self->buffer, &begin, self->mark_begin);
+  gtk_text_buffer_get_iter_at_mark (self->buffer, &end, self->mark_end);
+
+  ret = ((gtk_text_iter_compare (&begin, iter) <= 0) &&
+         (gtk_text_iter_compare (&end, iter) >= 0));
+
+  return ret;
+}
+
+gboolean
+ide_snippet_insert_set (IdeSnippet  *self,
+                        GtkTextMark *mark)
+{
+  GtkTextIter iter;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), FALSE);
+  g_return_val_if_fail (GTK_IS_TEXT_MARK (mark), FALSE);
+
+  gtk_text_buffer_get_iter_at_mark (self->buffer, &iter, mark);
+
+  if (!ide_snippet_within_bounds (self, &iter))
+    return FALSE;
+
+  self->current_chunk = ide_snippet_get_index (self, &iter);
+
+  return TRUE;
+}
+
+static void
+ide_snippet_get_nth_chunk_range (IdeSnippet  *self,
+                                 gint         n,
+                                 GtkTextIter *begin,
+                                 GtkTextIter *end)
+{
+  gint run;
+  gint i;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+  g_return_if_fail (n >= 0);
+  g_return_if_fail (begin);
+  g_return_if_fail (end);
+
+  gtk_text_buffer_get_iter_at_mark (self->buffer, begin, self->mark_begin);
+
+  for (i = 0; i < n; i++)
+    {
+      run = g_array_index (self->runs, gint, i);
+      gtk_text_iter_forward_chars (begin, run);
+    }
+
+  gtk_text_iter_assign (end, begin);
+  run = g_array_index (self->runs, gint, n);
+  gtk_text_iter_forward_chars (end, run);
+}
+
+void
+ide_snippet_get_chunk_range (IdeSnippet      *self,
+                             IdeSnippetChunk *chunk,
+                             GtkTextIter     *begin,
+                             GtkTextIter     *end)
+{
+  IdeSnippetChunk *item;
+  guint i;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+  g_return_if_fail (IDE_IS_SNIPPET_CHUNK (chunk));
+
+  for (i = 0; i < self->chunks->len; i++)
+    {
+      item = g_ptr_array_index (self->chunks, i);
+
+      if (item == chunk)
+        {
+          ide_snippet_get_nth_chunk_range (self, i, begin, end);
+          return;
+        }
+    }
+
+  g_warning ("Chunk does not belong to snippet.");
+}
+
+static void
+ide_snippet_select_chunk (IdeSnippet *self,
+                          gint        n)
+{
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+  g_return_if_fail (n >= 0);
+  g_return_if_fail ((guint)n < self->runs->len);
+
+  ide_snippet_get_nth_chunk_range (self, n, &begin, &end);
+
+  gtk_text_iter_order (&begin, &end);
+
+  IDE_TRACE_MSG ("Selecting chunk %d with range %d:%d to %d:%d (offset %d+%d)",
+                 n,
+                 gtk_text_iter_get_line (&begin) + 1,
+                 gtk_text_iter_get_line_offset (&begin) + 1,
+                 gtk_text_iter_get_line (&end) + 1,
+                 gtk_text_iter_get_line_offset (&end) + 1,
+                 gtk_text_iter_get_offset (&begin),
+                 gtk_text_iter_get_offset (&end) - gtk_text_iter_get_offset (&begin));
+
+  gtk_text_buffer_select_range (self->buffer, &begin, &end);
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    GtkTextIter set_begin;
+    GtkTextIter set_end;
+
+    gtk_text_buffer_get_selection_bounds (self->buffer, &set_begin, &set_end);
+
+    g_assert (gtk_text_iter_equal (&set_begin, &begin));
+    g_assert (gtk_text_iter_equal (&set_end, &end));
+  }
+#endif
+
+  self->current_chunk = n;
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_snippet_move_next (IdeSnippet *self)
+{
+  GtkTextIter iter;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), FALSE);
+
+  if (self->tab_stop > self->max_tab_stop)
+    IDE_RETURN (FALSE);
+
+  self->tab_stop++;
+
+  for (guint i = 0; i < self->chunks->len; i++)
+    {
+      IdeSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+
+      if (ide_snippet_chunk_get_tab_stop (chunk) == self->tab_stop)
+        {
+          ide_snippet_select_chunk (self, i);
+          IDE_RETURN (TRUE);
+        }
+    }
+
+  for (guint i = 0; i < self->chunks->len; i++)
+    {
+      IdeSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+
+      if (ide_snippet_chunk_get_tab_stop (chunk) == 0)
+        {
+          ide_snippet_select_chunk (self, i);
+          IDE_RETURN (FALSE);
+        }
+    }
+
+  IDE_TRACE_MSG ("No more tab stops, moving to end of snippet");
+
+  gtk_text_buffer_get_iter_at_mark (self->buffer, &iter, self->mark_end);
+  gtk_text_buffer_select_range (self->buffer, &iter, &iter);
+  self->current_chunk = self->chunks->len - 1;
+
+  IDE_RETURN (FALSE);
+}
+
+gboolean
+ide_snippet_move_previous (IdeSnippet *self)
+{
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), FALSE);
+
+  self->tab_stop = MAX (1, self->tab_stop - 1);
+
+  for (guint i = 0; i < self->chunks->len; i++)
+    {
+      IdeSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+
+      if (ide_snippet_chunk_get_tab_stop (chunk) == self->tab_stop)
+        {
+          ide_snippet_select_chunk (self, i);
+          IDE_RETURN (TRUE);
+        }
+    }
+
+  IDE_TRACE_MSG ("No previous tab stop to select, ignoring");
+
+  IDE_RETURN (FALSE);
+}
+
+static void
+ide_snippet_update_context (IdeSnippet *self)
+{
+  IdeSnippetContext *context;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+
+  if (self->chunks == NULL || self->chunks->len == 0)
+    IDE_EXIT;
+
+  context = ide_snippet_get_context (self);
+
+  ide_snippet_context_emit_changed (context);
+
+  for (guint i = 0; i < self->chunks->len; i++)
+    {
+      IdeSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+      gint tab_stop;
+
+      g_assert (IDE_IS_SNIPPET_CHUNK (chunk));
+
+      tab_stop = ide_snippet_chunk_get_tab_stop (chunk);
+
+      if (tab_stop > 0)
+        {
+          const gchar *text;
+
+          if (NULL != (text = ide_snippet_chunk_get_text (chunk)))
+            {
+              gchar key[12];
+
+              g_snprintf (key, sizeof key, "%d", tab_stop);
+              key[sizeof key - 1] = '\0';
+
+              ide_snippet_context_add_variable (context, key, text);
+            }
+        }
+    }
+
+  ide_snippet_context_emit_changed (context);
+
+  IDE_EXIT;
+}
+
+static void
+ide_snippet_clear_tags (IdeSnippet *self)
+{
+  g_assert (IDE_IS_SNIPPET (self));
+
+  if (self->mark_begin != NULL && self->mark_end != NULL)
+    {
+      GtkTextBuffer *buffer;
+      GtkTextIter begin;
+      GtkTextIter end;
+
+      buffer = gtk_text_mark_get_buffer (self->mark_begin);
+
+      gtk_text_buffer_get_iter_at_mark (buffer, &begin, self->mark_begin);
+      gtk_text_buffer_get_iter_at_mark (buffer, &end, self->mark_end);
+
+      gtk_text_buffer_remove_tag_by_name (buffer,
+                                          TAG_SNIPPET_TAB_STOP,
+                                          &begin, &end);
+    }
+}
+
+static void
+ide_snippet_update_tags (IdeSnippet *self)
+{
+  GtkTextBuffer *buffer;
+  guint i;
+
+  g_assert (IDE_IS_SNIPPET (self));
+
+  ide_snippet_clear_tags (self);
+
+  buffer = gtk_text_mark_get_buffer (self->mark_begin);
+
+  for (i = 0; i < self->chunks->len; i++)
+    {
+      IdeSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+      gint tab_stop = ide_snippet_chunk_get_tab_stop (chunk);
+
+      if (tab_stop >= 0)
+        {
+          GtkTextIter begin;
+          GtkTextIter end;
+
+          ide_snippet_get_chunk_range (self, chunk, &begin, &end);
+          gtk_text_buffer_apply_tag_by_name (buffer,
+                                             TAG_SNIPPET_TAB_STOP,
+                                             &begin, &end);
+        }
+    }
+}
+
+gboolean
+ide_snippet_begin (IdeSnippet    *self,
+                   GtkTextBuffer *buffer,
+                   GtkTextIter   *iter)
+{
+  IdeSnippetContext *context;
+  gboolean ret;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), FALSE);
+  g_return_val_if_fail (!self->buffer, FALSE);
+  g_return_val_if_fail (!self->mark_begin, FALSE);
+  g_return_val_if_fail (!self->mark_end, FALSE);
+  g_return_val_if_fail (GTK_IS_TEXT_BUFFER (buffer), FALSE);
+  g_return_val_if_fail (iter, FALSE);
+
+  self->inserted = TRUE;
+
+  context = ide_snippet_get_context (self);
+
+  ide_snippet_update_context (self);
+  ide_snippet_context_emit_changed (context);
+  ide_snippet_update_context (self);
+
+  self->buffer = g_object_ref (buffer);
+  self->mark_begin = gtk_text_buffer_create_mark (buffer, NULL, iter, TRUE);
+  g_object_add_weak_pointer (G_OBJECT (self->mark_begin),
+                             (gpointer *) &self->mark_begin);
+
+  gtk_text_buffer_begin_user_action (buffer);
+
+  for (guint i = 0; i < self->chunks->len; i++)
+    {
+      IdeSnippetChunk *chunk;
+      const gchar *text;
+
+      chunk = g_ptr_array_index (self->chunks, i);
+
+      if ((text = ide_snippet_chunk_get_text (chunk)))
+        {
+          gint len;
+
+          len = g_utf8_strlen (text, -1);
+          g_array_append_val (self->runs, len);
+          gtk_text_buffer_insert (buffer, iter, text, -1);
+        }
+    }
+
+  self->mark_end = gtk_text_buffer_create_mark (buffer, NULL, iter, FALSE);
+  g_object_add_weak_pointer (G_OBJECT (self->mark_end),
+                             (gpointer *) &self->mark_end);
+
+  g_object_ref (self->mark_begin);
+  g_object_ref (self->mark_end);
+
+  gtk_text_buffer_end_user_action (buffer);
+
+  ide_snippet_update_tags (self);
+
+  ret = ide_snippet_move_next (self);
+
+  return ret;
+}
+
+void
+ide_snippet_finish (IdeSnippet *self)
+{
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+
+  ide_snippet_clear_tags (self);
+
+  g_clear_object (&self->mark_begin);
+  g_clear_object (&self->mark_end);
+  g_clear_object (&self->buffer);
+}
+
+void
+ide_snippet_pause (IdeSnippet *self)
+{
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+}
+
+void
+ide_snippet_unpause (IdeSnippet *self)
+{
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+}
+
+void
+ide_snippet_add_chunk (IdeSnippet      *self,
+                       IdeSnippetChunk *chunk)
+{
+  gint tab_stop;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+  g_return_if_fail (IDE_IS_SNIPPET_CHUNK (chunk));
+  g_return_if_fail (!self->inserted);
+
+  g_ptr_array_add (self->chunks, g_object_ref (chunk));
+
+  ide_snippet_chunk_set_context (chunk, self->snippet_context);
+
+  tab_stop = ide_snippet_chunk_get_tab_stop (chunk);
+  self->max_tab_stop = MAX (self->max_tab_stop, tab_stop);
+}
+
+static gchar *
+ide_snippet_get_nth_text (IdeSnippet *self,
+                          gint        n)
+{
+  GtkTextIter iter;
+  GtkTextIter end;
+  gchar *ret;
+  gint i;
+
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), NULL);
+  g_return_val_if_fail (n >= 0, NULL);
+
+  gtk_text_buffer_get_iter_at_mark (self->buffer, &iter, self->mark_begin);
+
+  for (i = 0; i < n; i++)
+    gtk_text_iter_forward_chars (&iter, g_array_index (self->runs, gint, i));
+
+  gtk_text_iter_assign (&end, &iter);
+  gtk_text_iter_forward_chars (&end, g_array_index (self->runs, gint, n));
+
+  ret = gtk_text_buffer_get_text (self->buffer, &iter, &end, TRUE);
+
+  return ret;
+}
+
+static void
+ide_snippet_replace_chunk_text (IdeSnippet  *self,
+                                gint         n,
+                                const gchar *text)
+{
+  GtkTextIter begin;
+  GtkTextIter end;
+  gint diff = 0;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+  g_return_if_fail (n >= 0);
+  g_return_if_fail (text);
+
+  /*
+   * This replaces the text for the snippet. We insert new text before
+   * we delete the old text to ensure things are more stable as we
+   * manipulate the runs. Avoiding zero-length runs, even temporarily
+   * can be helpful.
+   */
+
+  ide_snippet_get_nth_chunk_range (self, n, &begin, &end);
+
+  if (!gtk_text_iter_equal (&begin, &end))
+    {
+      gtk_text_iter_order (&begin, &end);
+      diff = gtk_text_iter_get_offset (&end) - gtk_text_iter_get_offset (&begin);
+    }
+
+  g_array_index (self->runs, gint, n) += g_utf8_strlen (text, -1);
+  gtk_text_buffer_insert (self->buffer, &begin, text, -1);
+
+  /* At this point, begin should be updated to the end of where we inserted
+   * our new text. If `diff` is non-zero, then we need to remove those
+   * characters immediately after `begin`.
+   */
+  if (diff != 0)
+    {
+      end = begin;
+      gtk_text_iter_forward_chars (&end, diff);
+      g_array_index (self->runs, gint, n) -= diff;
+      gtk_text_buffer_delete (self->buffer, &begin, &end);
+    }
+}
+
+static void
+ide_snippet_rewrite_updated_chunks (IdeSnippet *self)
+{
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+
+  for (guint i = 0; i < self->chunks->len; i++)
+    {
+      IdeSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+      g_autofree gchar *real_text = NULL;
+      const gchar *text;
+
+      text = ide_snippet_chunk_get_text (chunk);
+      real_text = ide_snippet_get_nth_text (self, i);
+
+      if (!dzl_str_equal0 (text, real_text))
+        ide_snippet_replace_chunk_text (self, i, text);
+    }
+}
+
+void
+ide_snippet_before_insert_text (IdeSnippet    *self,
+                                GtkTextBuffer *buffer,
+                                GtkTextIter   *iter,
+                                gchar         *text,
+                                gint           len)
+{
+  gint utf8_len;
+  gint n;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+  g_return_if_fail (self->current_chunk >= 0);
+  g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+  g_return_if_fail (iter);
+
+  n = ide_snippet_get_index (self, iter);
+  utf8_len = g_utf8_strlen (text, len);
+  g_array_index (self->runs, gint, n) += utf8_len;
+
+#if 0
+  g_print ("I: ");
+  for (n = 0; n < self->runs->len; n++)
+    g_print ("%d ", g_array_index (self->runs, gint, n));
+  g_print ("\n");
+#endif
+
+  IDE_EXIT;
+}
+
+void
+ide_snippet_after_insert_text (IdeSnippet    *self,
+                               GtkTextBuffer *buffer,
+                               GtkTextIter   *iter,
+                               gchar         *text,
+                               gint           len)
+{
+  IdeSnippetChunk *chunk;
+  GtkTextMark *here;
+  gchar *new_text;
+  gint n;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+  g_return_if_fail (self->current_chunk >= 0);
+  g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+  g_return_if_fail (iter);
+
+  n = ide_snippet_get_index (self, iter);
+  chunk = g_ptr_array_index (self->chunks, n);
+  new_text = ide_snippet_get_nth_text (self, n);
+  ide_snippet_chunk_set_text (chunk, new_text);
+  ide_snippet_chunk_set_text_set (chunk, TRUE);
+  g_free (new_text);
+
+  here = gtk_text_buffer_create_mark (buffer, NULL, iter, TRUE);
+
+  ide_snippet_update_context (self);
+  ide_snippet_update_context (self);
+  ide_snippet_rewrite_updated_chunks (self);
+
+  gtk_text_buffer_get_iter_at_mark (buffer, iter, here);
+  gtk_text_buffer_delete_mark (buffer, here);
+
+  ide_snippet_update_tags (self);
+
+#if 0
+  ide_snippet_context_dump (self->snippet_context);
+#endif
+
+  IDE_EXIT;
+}
+
+void
+ide_snippet_before_delete_range (IdeSnippet    *self,
+                                 GtkTextBuffer *buffer,
+                                 GtkTextIter   *begin,
+                                 GtkTextIter   *end)
+{
+  gint *run;
+  gint len;
+  gint n;
+  gint i;
+  gint lower_bound = -1;
+  gint upper_bound = -1;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+  g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+  g_return_if_fail (begin);
+  g_return_if_fail (end);
+
+  len = gtk_text_iter_get_offset (end) - gtk_text_iter_get_offset (begin);
+
+  n = ide_snippet_get_index (self, begin);
+  if (n < 0)
+    IDE_EXIT;
+
+  self->current_chunk = n;
+
+  while (len != 0 && (guint)n < self->runs->len)
+    {
+      if (lower_bound == -1 || n < lower_bound)
+        lower_bound = n;
+      if (n > upper_bound)
+        upper_bound = n;
+      run = &g_array_index (self->runs, gint, n);
+      if (len > *run)
+        {
+          len -= *run;
+          *run = 0;
+          n++;
+          continue;
+        }
+      *run -= len;
+      break;
+    }
+
+  if (lower_bound == -1 || upper_bound == -1)
+    return;
+
+  for (i = lower_bound; i <= upper_bound; i++)
+    {
+      IdeSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+      g_autofree gchar *new_text = NULL;
+
+      new_text = ide_snippet_get_nth_text (self, i);
+      ide_snippet_chunk_set_text (chunk, new_text);
+      ide_snippet_chunk_set_text_set (chunk, TRUE);
+    }
+
+#if 0
+  g_print ("D: ");
+  for (n = 0; n < self->runs->len; n++)
+    g_print ("%d ", g_array_index (self->runs, gint, n));
+  g_print ("\n");
+#endif
+
+  IDE_EXIT;
+}
+
+void
+ide_snippet_after_delete_range (IdeSnippet    *self,
+                                GtkTextBuffer *buffer,
+                                GtkTextIter   *begin,
+                                GtkTextIter   *end)
+{
+  GtkTextMark *here;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+  g_return_if_fail (GTK_IS_TEXT_BUFFER (buffer));
+  g_return_if_fail (begin);
+  g_return_if_fail (end);
+
+  here = gtk_text_buffer_create_mark (buffer, NULL, begin, TRUE);
+
+  ide_snippet_update_context (self);
+  ide_snippet_update_context (self);
+  ide_snippet_rewrite_updated_chunks (self);
+
+  gtk_text_buffer_get_iter_at_mark (buffer, begin, here);
+  gtk_text_buffer_get_iter_at_mark (buffer, end, here);
+  gtk_text_buffer_delete_mark (buffer, here);
+
+  ide_snippet_update_tags (self);
+
+#if 0
+  ide_snippet_context_dump (self->snippet_context);
+#endif
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_snippet_get_mark_begin:
+ * @self: an #IdeSnippet
+ *
+ * Gets the begin text mark, which is only set when the snippet is
+ * actively being edited.
+ *
+ * Returns: (transfer none) (nullable): a #GtkTextMark or %NULL
+ *
+ * Since: 3.32
+ */
+GtkTextMark *
+ide_snippet_get_mark_begin (IdeSnippet *self)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), NULL);
+
+  return self->mark_begin;
+}
+
+/**
+ * ide_snippet_get_mark_end:
+ * @self: an #IdeSnippet
+ *
+ * Gets the end text mark, which is only set when the snippet is
+ * actively being edited.
+ *
+ * Returns: (transfer none) (nullable): a #GtkTextMark or %NULL
+ *
+ * Since: 3.32
+ */
+GtkTextMark *
+ide_snippet_get_mark_end (IdeSnippet *self)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), NULL);
+
+  return self->mark_end;
+}
+
+/**
+ * ide_snippet_get_context:
+ * @self: an #IdeSnippet
+ *
+ * Get's the context used for expanding the snippet.
+ *
+ * Returns: (nullable) (transfer none): an #IdeSnippetContext
+ *
+ * Since: 3.32
+ */
+IdeSnippetContext *
+ide_snippet_get_context (IdeSnippet *self)
+{
+  g_return_val_if_fail (IDE_IS_SNIPPET (self), NULL);
+
+  if (!self->snippet_context)
+    {
+      IdeSnippetChunk *chunk;
+      guint i;
+
+      self->snippet_context = ide_snippet_context_new ();
+
+      for (i = 0; i < self->chunks->len; i++)
+        {
+          chunk = g_ptr_array_index (self->chunks, i);
+          ide_snippet_chunk_set_context (chunk, self->snippet_context);
+        }
+    }
+
+  return self->snippet_context;
+}
+
+static void
+ide_snippet_dispose (GObject *object)
+{
+  IdeSnippet *self = (IdeSnippet *)object;
+
+  if (self->mark_begin)
+    {
+      g_object_remove_weak_pointer (G_OBJECT (self->mark_begin),
+                                    (gpointer *) &self->mark_begin);
+      gtk_text_buffer_delete_mark (self->buffer, self->mark_begin);
+      self->mark_begin = NULL;
+    }
+
+  if (self->mark_end)
+    {
+      g_object_remove_weak_pointer (G_OBJECT (self->mark_end),
+                                    (gpointer *) &self->mark_end);
+      gtk_text_buffer_delete_mark (self->buffer, self->mark_end);
+      self->mark_end = NULL;
+    }
+
+  g_clear_pointer (&self->runs, g_array_unref);
+  g_clear_pointer (&self->chunks, g_ptr_array_unref);
+
+  g_clear_object (&self->buffer);
+  g_clear_object (&self->snippet_context);
+
+  G_OBJECT_CLASS (ide_snippet_parent_class)->dispose (object);
+}
+
+static void
+ide_snippet_finalize (GObject *object)
+{
+  IdeSnippet *self = (IdeSnippet *)object;
+
+  g_clear_pointer (&self->description, g_free);
+  g_clear_pointer (&self->trigger, g_free);
+  g_clear_object (&self->buffer);
+
+  G_OBJECT_CLASS (ide_snippet_parent_class)->finalize (object);
+
+  DZL_COUNTER_DEC (instances);
+}
+
+static void
+ide_snippet_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  IdeSnippet *self = IDE_SNIPPET (object);
+
+  switch (prop_id)
+    {
+    case PROP_BUFFER:
+      g_value_set_object (value, self->buffer);
+      break;
+
+    case PROP_MARK_BEGIN:
+      g_value_set_object (value, self->mark_begin);
+      break;
+
+    case PROP_MARK_END:
+      g_value_set_object (value, self->mark_end);
+      break;
+
+    case PROP_TRIGGER:
+      g_value_set_string (value, self->trigger);
+      break;
+
+    case PROP_LANGUAGE:
+      g_value_set_string (value, self->language);
+      break;
+
+    case PROP_DESCRIPTION:
+      g_value_set_string (value, self->description);
+      break;
+
+    case PROP_TAB_STOP:
+      g_value_set_uint (value, self->tab_stop);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_snippet_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  IdeSnippet *self = IDE_SNIPPET (object);
+
+  switch (prop_id)
+    {
+    case PROP_TRIGGER:
+      ide_snippet_set_trigger (self, g_value_get_string (value));
+      break;
+
+    case PROP_LANGUAGE:
+      ide_snippet_set_language (self, g_value_get_string (value));
+      break;
+
+    case PROP_DESCRIPTION:
+      ide_snippet_set_description (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_snippet_class_init (IdeSnippetClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_snippet_dispose;
+  object_class->finalize = ide_snippet_finalize;
+  object_class->get_property = ide_snippet_get_property;
+  object_class->set_property = ide_snippet_set_property;
+
+  properties[PROP_BUFFER] =
+    g_param_spec_object ("buffer",
+                         "Buffer",
+                         "The GtkTextBuffer for the snippet.",
+                         GTK_TYPE_TEXT_BUFFER,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_MARK_BEGIN] =
+    g_param_spec_object ("mark-begin",
+                         "Mark Begin",
+                         "The beginning text mark.",
+                         GTK_TYPE_TEXT_MARK,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_MARK_END] =
+    g_param_spec_object ("mark-end",
+                         "Mark End",
+                         "The ending text mark.",
+                         GTK_TYPE_TEXT_MARK,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_TRIGGER] =
+    g_param_spec_string ("trigger",
+                         "Trigger",
+                         "The trigger for the snippet.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_LANGUAGE] =
+    g_param_spec_string ("language",
+                         "Language",
+                         "The language for the snippet.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_DESCRIPTION] =
+    g_param_spec_string ("description",
+                         "Description",
+                         "The description for the snippet.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties[PROP_TAB_STOP] =
+    g_param_spec_int ("tab-stop",
+                      "Tab Stop",
+                      "The current tab stop.",
+                      -1,
+                      G_MAXINT,
+                      -1,
+                      (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_snippet_init (IdeSnippet *self)
+{
+  DZL_COUNTER_INC (instances);
+
+  self->max_tab_stop = -1;
+  self->chunks = g_ptr_array_new_with_free_func (g_object_unref);
+  self->runs = g_array_new (FALSE, FALSE, sizeof (gint));
+}
+
+/**
+ * ide_snippet_dump:
+ * @self: a #IdeSnippet
+ *
+ * This is a debugging function to print information about a chunk to stderr.
+ * Plugin developers might use this to track down issues when using a snippet.
+ *
+ * Since: 3.32
+ */
+void
+ide_snippet_dump (IdeSnippet *self)
+{
+  guint offset = 0;
+
+  g_return_if_fail (IDE_IS_SNIPPET (self));
+
+  /* For debugging purposes */
+
+  g_printerr ("Snippet(trigger=%s, language=%s, tab_stop=%d, current_chunk=%d)\n",
+              self->trigger, self->language ?: "none", self->tab_stop, self->current_chunk);
+
+  g_assert (self->chunks->len == self->runs->len);
+
+  for (guint i = 0; i < self->chunks->len; i++)
+    {
+      IdeSnippetChunk *chunk = g_ptr_array_index (self->chunks, i);
+      g_autofree gchar *spec_escaped = NULL;
+      g_autofree gchar *text_escaped = NULL;
+      const gchar *spec;
+      const gchar *text;
+      gint run_length = g_array_index (self->runs, gint, i);
+
+      g_assert (IDE_IS_SNIPPET_CHUNK (chunk));
+
+      text = ide_snippet_chunk_get_text (chunk);
+      text_escaped = g_strescape (text, NULL);
+
+      spec = ide_snippet_chunk_get_spec (chunk);
+      spec_escaped = g_strescape (spec, NULL);
+
+      g_printerr ("  Chunk(nth=%d, tab_stop=%d, position=%d (%d), spec=%s, text=%s)\n",
+                  i,
+                  ide_snippet_chunk_get_tab_stop (chunk),
+                  offset, run_length,
+                  spec_escaped,
+                  text_escaped);
+
+      offset += run_length;
+    }
+}
diff --git a/src/libide/sourceview/ide-snippet.h b/src/libide/sourceview/ide-snippet.h
new file mode 100644
index 000000000..3a2844955
--- /dev/null
+++ b/src/libide/sourceview/ide-snippet.h
@@ -0,0 +1,77 @@
+/* ide-snippet.h
+ *
+ * Copyright 2013-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-snippet-chunk.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SNIPPET (ide_snippet_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeSnippet, ide_snippet, IDE, SNIPPET, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeSnippet              *ide_snippet_new              (const gchar     *trigger,
+                                                       const gchar     *language);
+IDE_AVAILABLE_IN_3_32
+IdeSnippet              *ide_snippet_copy             (IdeSnippet      *self);
+IDE_AVAILABLE_IN_3_32
+const gchar             *ide_snippet_get_trigger      (IdeSnippet      *self);
+IDE_AVAILABLE_IN_3_32
+void                     ide_snippet_set_trigger      (IdeSnippet      *self,
+                                                       const gchar     *trigger);
+IDE_AVAILABLE_IN_3_32
+const gchar             *ide_snippet_get_language     (IdeSnippet      *self);
+IDE_AVAILABLE_IN_3_32
+void                     ide_snippet_set_language     (IdeSnippet      *self,
+                                                       const gchar     *language);
+IDE_AVAILABLE_IN_3_32
+const gchar             *ide_snippet_get_description  (IdeSnippet      *self);
+IDE_AVAILABLE_IN_3_32
+void                     ide_snippet_set_description  (IdeSnippet      *self,
+                                                       const gchar     *description);
+IDE_AVAILABLE_IN_3_32
+void                     ide_snippet_add_chunk        (IdeSnippet      *self,
+                                                       IdeSnippetChunk *chunk);
+IDE_AVAILABLE_IN_3_32
+guint                    ide_snippet_get_n_chunks     (IdeSnippet      *self);
+IDE_AVAILABLE_IN_3_32
+gint                     ide_snippet_get_tab_stop     (IdeSnippet      *self);
+IDE_AVAILABLE_IN_3_32
+IdeSnippetChunk         *ide_snippet_get_nth_chunk    (IdeSnippet      *self,
+                                                       guint            n);
+IDE_AVAILABLE_IN_3_32
+void                     ide_snippet_get_chunk_range  (IdeSnippet      *self,
+                                                       IdeSnippetChunk *chunk,
+                                                       GtkTextIter     *begin,
+                                                       GtkTextIter     *end);
+IDE_AVAILABLE_IN_3_32
+IdeSnippetContext       *ide_snippet_get_context      (IdeSnippet      *self);
+
+G_END_DECLS
diff --git a/src/libide/sourceview/ide-source-search-context.c 
b/src/libide/sourceview/ide-source-search-context.c
index db37f19ce..1bd7a474d 100644
--- a/src/libide/sourceview/ide-source-search-context.c
+++ b/src/libide/sourceview/ide-source-search-context.c
@@ -1,6 +1,6 @@
 /* ide-source-search-context.c
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,14 +14,17 @@
  *
  * 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 "ide-source-search-context"
+
 #include "config.h"
 
-#define G_LOG_DOMAIN "ide-source-search-context"
+#include <libide-threading.h>
 
-#include "sourceview/ide-source-search-context.h"
-#include "threading/ide-task.h"
+#include "ide-source-search-context.h"
 
 typedef struct
 {
@@ -58,6 +61,8 @@ search_data_free (SearchData *sd)
  * and we can remove this.
  *
  * https://gitlab.gnome.org/GNOME/gtksourceview/issues/8
+ *
+ * Since: 3.32
  */
 void
 ide_source_search_context_backward_async (GtkSourceSearchContext *search,
@@ -111,7 +116,7 @@ ide_source_search_context_backward_async (GtkSourceSearchContext *search,
  * @has_wrapped_around: (out): a location to a boolean
  * @error: a location for a #GError
  *
- * Since: 3.30
+ * Since: 3.32
  */
 gboolean
 ide_source_search_context_backward_finish2 (GtkSourceSearchContext  *search,
diff --git a/src/libide/sourceview/ide-source-search-context.h 
b/src/libide/sourceview/ide-source-search-context.h
index 02983707a..6d83eff23 100644
--- a/src/libide/sourceview/ide-source-search-context.h
+++ b/src/libide/sourceview/ide-source-search-context.h
@@ -1,6 +1,6 @@
 /* ide-source-search-context.h
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,13 +14,18 @@
  *
  * 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 <gtksourceview/gtksource.h>
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <gtksourceview/gtksource.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
@@ -31,13 +36,13 @@ G_BEGIN_DECLS
  * https://gitlab.gnome.org/GNOME/gtksourceview/issues/8
  */
 
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void     ide_source_search_context_backward_async   (GtkSourceSearchContext  *search,
                                                      const GtkTextIter       *iter,
                                                      GCancellable            *cancellable,
                                                      GAsyncReadyCallback      callback,
                                                      gpointer                 user_data);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gboolean ide_source_search_context_backward_finish2 (GtkSourceSearchContext  *search,
                                                      GAsyncResult            *result,
                                                      GtkTextIter             *match_begin,
diff --git a/src/libide/sourceview/ide-source-view-capture.c b/src/libide/sourceview/ide-source-view-capture.c
index def7233a3..83ad8f69a 100644
--- a/src/libide/sourceview/ide-source-view-capture.c
+++ b/src/libide/sourceview/ide-source-view-capture.c
@@ -1,6 +1,6 @@
 /* ide-source-view-capture.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,13 +14,16 @@
  *
  * 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 "ide-source-view-capture"
 
 #include <glib/gi18n.h>
 
-#include "sourceview/ide-source-view-capture.h"
+#include "ide-source-view-capture.h"
+#include "ide-source-view-private.h"
 
 typedef struct
 {
diff --git a/src/libide/sourceview/ide-source-view-capture.h b/src/libide/sourceview/ide-source-view-capture.h
index d53f05402..c2b20b026 100644
--- a/src/libide/sourceview/ide-source-view-capture.h
+++ b/src/libide/sourceview/ide-source-view-capture.h
@@ -1,6 +1,6 @@
 /* ide-source-view-capture.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,20 +14,19 @@
  *
  * 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 "sourceview/ide-source-view.h"
+#include "ide-source-view.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_SOURCE_VIEW_CAPTURE (ide_source_view_capture_get_type())
 
-G_DECLARE_FINAL_TYPE (IdeSourceViewCapture,
-                      ide_source_view_capture,
-                      IDE, SOURCE_VIEW_CAPTURE,
-                      GObject)
+G_DECLARE_FINAL_TYPE (IdeSourceViewCapture, ide_source_view_capture, IDE, SOURCE_VIEW_CAPTURE, GObject)
 
 IdeSourceViewCapture *ide_source_view_capture_new             (IdeSourceView         *view,
                                                                const gchar           *mode_name,
diff --git a/src/libide/sourceview/ide-source-view-mode.c b/src/libide/sourceview/ide-source-view-mode.c
index e788241e4..fc9b2366f 100644
--- a/src/libide/sourceview/ide-source-view-mode.c
+++ b/src/libide/sourceview/ide-source-view-mode.c
@@ -1,7 +1,7 @@
 /* ide-source-view-mode.c
  *
  * Copyright 2015 Alexander Larsson <alexl redhat com>
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -15,6 +15,8 @@
  *
  * 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 "ide-source-view-mode"
@@ -22,12 +24,11 @@
 #include "config.h"
 
 #include <glib/gi18n.h>
+#include <libide-core.h>
 #include <string.h>
 
-#include "ide-debug.h"
-
-#include "sourceview/ide-source-view.h"
-#include "sourceview/ide-source-view-mode.h"
+#include "ide-source-view.h"
+#include "ide-source-view-mode.h"
 
 struct _IdeSourceViewMode
 {
diff --git a/src/libide/sourceview/ide-source-view-mode.h b/src/libide/sourceview/ide-source-view-mode.h
index 90827cb4d..a24565f89 100644
--- a/src/libide/sourceview/ide-source-view-mode.h
+++ b/src/libide/sourceview/ide-source-view-mode.h
@@ -1,6 +1,7 @@
 /* ide-source-view-mode.h
  *
  * Copyright 2015 Alexander Larsson <alexl redhat com>
+ * Copyright 2015-2019 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
@@ -14,14 +15,15 @@
  *
  * 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 <gtk/gtk.h>
 
-#include "ide-types.h"
-#include "sourceview/ide-source-view.h"
+#include "ide-source-view.h"
 
 G_BEGIN_DECLS
 
@@ -43,9 +45,9 @@ void                   ide_source_view_mode_set_has_selection            (IdeSou
                                                                           gboolean               
has_selection);
 IdeSourceViewMode     *_ide_source_view_mode_new                         (GtkWidget             *view,
                                                                           const char            *mode,
-                                                                          IdeSourceViewModeType  type) 
G_GNUC_INTERNAL;
+                                                                          IdeSourceViewModeType  type);
 gboolean               _ide_source_view_mode_do_event                    (IdeSourceViewMode     *mode,
                                                                           GdkEventKey           *event,
-                                                                          gboolean              *remove) 
G_GNUC_INTERNAL;
+                                                                          gboolean              *remove);
 
 G_END_DECLS
diff --git a/src/libide/sourceview/ide-source-view-movements.c 
b/src/libide/sourceview/ide-source-view-movements.c
index 127195271..6cbd44175 100644
--- a/src/libide/sourceview/ide-source-view-movements.c
+++ b/src/libide/sourceview/ide-source-view-movements.c
@@ -1,6 +1,6 @@
 /* ide-source-view-movements.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-source-view-movements"
@@ -23,12 +25,11 @@
 #include <dazzle.h>
 #include <string.h>
 
-#include "ide-enums.h"
-#include "ide-debug.h"
+#include <libide-code.h>
 
-#include "sourceview/ide-source-iter.h"
-#include "sourceview/ide-source-view-movements.h"
-#include "sourceview/ide-text-iter.h"
+#include "ide-source-view-enums.h"
+#include "ide-source-view-movements.h"
+#include "ide-source-view-private.h"
 
 #define ANCHOR_BEGIN   "SELECTION_ANCHOR_BEGIN"
 #define ANCHOR_END     "SELECTION_ANCHOR_END"
@@ -999,20 +1000,20 @@ match_char_with_depth (GtkTextIter      *iter,
       if (string_mode)
         {
           gtk_text_iter_set_line_offset (&limit, 0);
-          ret = _ide_text_iter_backward_find_char (iter, bracket_predicate, &state, &limit);
+          ret = ide_text_iter_backward_find_char (iter, bracket_predicate, &state, &limit);
         }
       else
-        ret = _ide_text_iter_backward_find_char (iter, bracket_predicate, &state, NULL);
+        ret = ide_text_iter_backward_find_char (iter, bracket_predicate, &state, NULL);
     }
   else
     {
       if (string_mode)
         {
           gtk_text_iter_forward_to_line_end (&limit);
-          ret = _ide_text_iter_forward_find_char (iter, bracket_predicate, &state, &limit);
+          ret = ide_text_iter_forward_find_char (iter, bracket_predicate, &state, &limit);
         }
       else
-        ret = _ide_text_iter_forward_find_char (iter, bracket_predicate, &state, NULL);
+        ret = ide_text_iter_forward_find_char (iter, bracket_predicate, &state, NULL);
     }
 
   if (ret && !is_exclusive)
@@ -1062,17 +1063,17 @@ macro_conditionals_qualify_iter (GtkTextIter *insert,
                                  GtkTextIter *cond_end,
                                  gboolean     include_str_bounds)
 {
-  if (_ide_text_iter_in_string (insert, "#ifdef", cond_start, cond_end, include_str_bounds))
+  if (ide_text_iter_in_string (insert, "#ifdef", cond_start, cond_end, include_str_bounds))
     return MACRO_COND_IFDEF;
-  else if (_ide_text_iter_in_string (insert, "#ifndef", cond_start, cond_end, include_str_bounds))
+  else if (ide_text_iter_in_string (insert, "#ifndef", cond_start, cond_end, include_str_bounds))
     return MACRO_COND_IFNDEF;
-  else if (_ide_text_iter_in_string (insert, "#if", cond_start, cond_end, include_str_bounds))
+  else if (ide_text_iter_in_string (insert, "#if", cond_start, cond_end, include_str_bounds))
     return MACRO_COND_IF;
-  else if (_ide_text_iter_in_string (insert, "#elif", cond_start, cond_end, include_str_bounds))
+  else if (ide_text_iter_in_string (insert, "#elif", cond_start, cond_end, include_str_bounds))
     return MACRO_COND_ELIF;
-  else if (_ide_text_iter_in_string (insert, "#else", cond_start, cond_end, include_str_bounds))
+  else if (ide_text_iter_in_string (insert, "#else", cond_start, cond_end, include_str_bounds))
     return MACRO_COND_ELSE;
-  else if (_ide_text_iter_in_string (insert, "#endif", cond_start, cond_end, include_str_bounds))
+  else if (ide_text_iter_in_string (insert, "#endif", cond_start, cond_end, include_str_bounds))
     return MACRO_COND_ENDIF;
   else
     return MACRO_COND_NONE;
@@ -1288,7 +1289,7 @@ match_comments (GtkTextIter *insert,
 
   if (comment_start && !gtk_text_iter_is_end (&cursor))
     {
-      if (_ide_text_iter_find_chars_forward (&cursor, NULL, NULL, "*/", FALSE))
+      if (ide_text_iter_find_chars_forward (&cursor, NULL, NULL, "*/", FALSE))
         {
           gtk_text_iter_forward_char (&cursor);
           *insert = cursor;
@@ -1298,7 +1299,7 @@ match_comments (GtkTextIter *insert,
     }
   else if (!comment_start && !gtk_text_iter_is_start (&cursor))
     {
-      if (_ide_text_iter_find_chars_backward (&cursor, NULL, NULL, "/*", FALSE))
+      if (ide_text_iter_find_chars_backward (&cursor, NULL, NULL, "/*", FALSE))
         {
           *insert = cursor;
 
@@ -1345,7 +1346,7 @@ ide_source_view_movements_match_special (Movement *mv)
   if (!vim_percent_predicate (&mv->insert, start_char, NULL))
     {
 loop:
-      if (_ide_text_iter_forward_find_char (&mv->insert, vim_percent_predicate, NULL, &limit))
+      if (ide_text_iter_forward_find_char (&mv->insert, vim_percent_predicate, NULL, &limit))
         start_char = gtk_text_iter_get_char (&mv->insert);
       else
         {
@@ -1500,7 +1501,7 @@ ide_source_view_movements_next_word_end (Movement *mv)
 
   copy = mv->insert;
 
-  _ide_text_iter_forward_word_end (&mv->insert, mv->newline_stop);
+  ide_text_iter_forward_word_end (&mv->insert, mv->newline_stop);
 
   /* prefer an empty line before word */
   text_iter_forward_to_empty_line (&copy, &mv->insert);
@@ -1518,7 +1519,7 @@ ide_source_view_movements_next_full_word_end (Movement *mv)
 
   copy = mv->insert;
 
-  _ide_text_iter_forward_WORD_end (&mv->insert, mv->newline_stop);
+  ide_text_iter_forward_WORD_end (&mv->insert, mv->newline_stop);
 
   /* prefer an empty line before word */
   text_iter_forward_to_empty_line (&copy, &mv->insert);
@@ -1536,7 +1537,7 @@ ide_source_view_movements_next_word_start (Movement *mv)
 
   copy = mv->insert;
 
-  _ide_text_iter_forward_word_start (&mv->insert, mv->newline_stop);
+  ide_text_iter_forward_word_start (&mv->insert, mv->newline_stop);
 
   /* prefer an empty line before word */
   text_iter_forward_to_empty_line (&copy, &mv->insert);
@@ -1554,7 +1555,7 @@ ide_source_view_movements_next_full_word_start (Movement *mv)
 
   copy = mv->insert;
 
-  _ide_text_iter_forward_WORD_start (&mv->insert, mv->newline_stop);
+  ide_text_iter_forward_WORD_start (&mv->insert, mv->newline_stop);
 
   /* prefer an empty line before word */
   text_iter_forward_to_empty_line (&copy, &mv->insert);
@@ -1572,7 +1573,7 @@ ide_source_view_movements_previous_word_start (Movement *mv)
 
   copy = mv->insert;
 
-  _ide_text_iter_backward_word_start (&mv->insert, mv->newline_stop);
+  ide_text_iter_backward_word_start (&mv->insert, mv->newline_stop);
 
   /*
    * Vim treats an empty line as a word.
@@ -1592,7 +1593,7 @@ ide_source_view_movements_previous_full_word_start (Movement *mv)
 
   copy = mv->insert;
 
-  _ide_text_iter_backward_WORD_start (&mv->insert, mv->newline_stop);
+  ide_text_iter_backward_WORD_start (&mv->insert, mv->newline_stop);
 
   /*
    * Vim treats an empty line as a word.
@@ -1612,7 +1613,7 @@ ide_source_view_movements_previous_word_end (Movement *mv)
 
   copy = mv->insert;
 
-  _ide_text_iter_backward_word_end (&mv->insert, mv->newline_stop);
+  ide_text_iter_backward_word_end (&mv->insert, mv->newline_stop);
 
   /*
    * Vim treats an empty line as a word.
@@ -1636,7 +1637,7 @@ ide_source_view_movements_previous_full_word_end (Movement *mv)
 
   copy = mv->insert;
 
-  _ide_text_iter_backward_WORD_end (&mv->insert, mv->newline_stop);
+  ide_text_iter_backward_WORD_end (&mv->insert, mv->newline_stop);
 
   /*
    * Vim treats an empty line as a word.
@@ -1656,7 +1657,7 @@ ide_source_view_movements_previous_full_word_end (Movement *mv)
 static void
 ide_source_view_movements_paragraph_start (Movement *mv)
 {
-  _ide_text_iter_backward_paragraph_start (&mv->insert);
+  ide_text_iter_backward_paragraph_start (&mv->insert);
 
   if (mv->exclusive)
     {
@@ -1671,7 +1672,7 @@ ide_source_view_movements_paragraph_start (Movement *mv)
 static void
 ide_source_view_movements_paragraph_end (Movement *mv)
 {
-  _ide_text_iter_forward_paragraph_end (&mv->insert);
+  ide_text_iter_forward_paragraph_end (&mv->insert);
 
   if (mv->exclusive)
     {
@@ -1692,13 +1693,13 @@ ide_source_view_movements_paragraph_end (Movement *mv)
 static void
 ide_source_view_movements_sentence_start (Movement *mv)
 {
-  _ide_text_iter_backward_sentence_start (&mv->insert);
+  ide_text_iter_backward_sentence_start (&mv->insert);
 }
 
 static void
 ide_source_view_movements_sentence_end (Movement *mv)
 {
-  _ide_text_iter_forward_sentence_end (&mv->insert);
+  ide_text_iter_forward_sentence_end (&mv->insert);
 }
 
 static void
@@ -2552,10 +2553,10 @@ find_html_tag (GtkTextIter      *iter,
   g_return_val_if_fail (direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT, NULL);
 
   if (direction == GTK_DIR_LEFT)
-    ret = _ide_text_iter_backward_find_char (iter, html_tag_predicate, GUINT_TO_POINTER ('<'), NULL);
+    ret = ide_text_iter_backward_find_char (iter, html_tag_predicate, GUINT_TO_POINTER ('<'), NULL);
   else
     ret = (gtk_text_iter_get_char (iter) == '<') ||
-          _ide_text_iter_forward_find_char (iter, html_tag_predicate, GUINT_TO_POINTER ('<'), NULL);
+          ide_text_iter_forward_find_char (iter, html_tag_predicate, GUINT_TO_POINTER ('<'), NULL);
 
   if (!ret)
     return NULL;
@@ -2590,11 +2591,11 @@ find_html_tag (GtkTextIter      *iter,
 
       return tag;
     }
-  else if (_ide_text_iter_find_chars_forward (&cursor, NULL, &end, "!--", TRUE))
+  else if (ide_text_iter_find_chars_forward (&cursor, NULL, &end, "!--", TRUE))
     {
       tag->kind = HTML_TAG_KIND_COMMENT;
       cursor = end;
-      if (_ide_text_iter_find_chars_forward (&cursor, NULL, &end, "-->", FALSE))
+      if (ide_text_iter_find_chars_forward (&cursor, NULL, &end, "-->", FALSE))
         {
           tag->end = end;
           if (direction == GTK_DIR_RIGHT)
@@ -2662,10 +2663,10 @@ static HtmlTag *
 find_non_matching_html_tag_at_left (GtkTextIter *cursor,
                                     gboolean     block_cursor)
 {
-  GQueue *stack;
-  HtmlTag *tag;
-  HtmlTag *last_closing_tag;
   GtkTextIter cursor_right;
+  HtmlTag *last_closing_tag = NULL;
+  HtmlTag *tag = NULL;
+  GQueue *stack = NULL;
 
   stack = g_queue_new ();
 
@@ -2764,10 +2765,11 @@ find_non_matching_html_tag_at_right (GtkTextIter *cursor,
       else if (tag->kind == HTML_TAG_KIND_ERROR)
         gtk_text_iter_forward_char (&cursor_left);
 
-      free_html_tag (tag);
+      g_clear_pointer (&tag, free_html_tag);
     }
 
   g_queue_free_full (stack, free_html_tag);
+  g_clear_pointer (&tag, free_html_tag);
 
   return tag;
 }
diff --git a/src/libide/sourceview/ide-source-view-movements.h 
b/src/libide/sourceview/ide-source-view-movements.h
index 3e6af5eba..bf51dbd2d 100644
--- a/src/libide/sourceview/ide-source-view-movements.h
+++ b/src/libide/sourceview/ide-source-view-movements.h
@@ -1,6 +1,6 @@
 /* ide-source-view-movements.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 "sourceview/ide-source-view.h"
+#include "ide-source-view.h"
 
 G_BEGIN_DECLS
 
@@ -32,14 +34,12 @@ void _ide_source_view_apply_movement (IdeSourceView         *source_view,
                                       gunichar               modifier,
                                       gunichar               search_char,
                                       guint                 *target_column);
-
 void _ide_source_view_select_inner   (IdeSourceView *self,
                                       gunichar       inner_left,
                                       gunichar       inner_right,
                                       gint           count,
                                       gboolean       exclusive,
                                       gboolean       string_mode);
-
 void _ide_source_view_select_tag     (IdeSourceView *self,
                                       gint           count,
                                       gboolean       exclusive);
diff --git a/src/libide/sourceview/ide-source-view-private.h b/src/libide/sourceview/ide-source-view-private.h
index f844849f3..6c9be3a64 100644
--- a/src/libide/sourceview/ide-source-view-private.h
+++ b/src/libide/sourceview/ide-source-view-private.h
@@ -1,6 +1,6 @@
 /* ide-source-view-private.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,14 +14,22 @@
  *
  * 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 "sourceview/ide-source-view.h"
+#include "ide-source-view.h"
 
 G_BEGIN_DECLS
 
-void _ide_source_view_init_shortcuts (IdeSourceView *self);
+void         _ide_source_view_init_shortcuts  (IdeSourceView *self);
+const gchar *_ide_source_view_get_mode_name   (IdeSourceView *self);
+void         _ide_source_view_set_count       (IdeSourceView *self,
+                                               gint           count);
+void         _ide_source_view_set_modifier    (IdeSourceView *self,
+                                               gunichar       modifier);
+GtkTextMark *_ide_source_view_get_scroll_mark (IdeSourceView *self);
 
 G_END_DECLS
diff --git a/src/libide/sourceview/ide-source-view-shortcuts.c 
b/src/libide/sourceview/ide-source-view-shortcuts.c
index 45dadc672..f9224b17b 100644
--- a/src/libide/sourceview/ide-source-view-shortcuts.c
+++ b/src/libide/sourceview/ide-source-view-shortcuts.c
@@ -1,6 +1,6 @@
 /* ide-source-view-shortcuts.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-source-view-shortcuts"
@@ -22,12 +24,8 @@
 
 #include <dazzle.h>
 
-#include "sourceview/ide-source-view.h"
-#include "sourceview/ide-source-view-private.h"
-
-/* static const DzlShortcutEntry source_view_shortcuts[] = { */
-/*   { NULL } */
-/* }; */
+#include "ide-source-view.h"
+#include "ide-source-view-private.h"
 
 void
 _ide_source_view_init_shortcuts (IdeSourceView *self)
@@ -43,9 +41,4 @@ _ide_source_view_init_shortcuts (IdeSourceView *self)
                                               "Escape",
                                               DZL_SHORTCUT_PHASE_BUBBLE,
                                               "reset", 0);
-
-  /* dzl_shortcut_manager_add_shortcut_entries (NULL, */
-  /*                                            source_view_shortcuts, */
-  /*                                            G_N_ELEMENTS (source_view_shortcuts), */
-  /*                                            GETTEXT_PACKAGE); */
 }
diff --git a/src/libide/sourceview/ide-source-view.c b/src/libide/sourceview/ide-source-view.c
index 7447f701f..82e8eb347 100644
--- a/src/libide/sourceview/ide-source-view.c
+++ b/src/libide/sourceview/ide-source-view.c
@@ -1,6 +1,6 @@
 /* ide-source-view.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-source-view"
@@ -23,51 +25,30 @@
 #include <cairo-gobject.h>
 #include <dazzle.h>
 #include <glib/gi18n.h>
+#include <libide-code.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
 #include <stdlib.h>
 #include <string.h>
 
-#include "ide-context.h"
-#include "ide-debug.h"
-#include "ide-enums.h"
-
-#include "application/ide-application.h"
-#include "buffers/ide-buffer-manager.h"
-#include "buffers/ide-buffer.h"
-#include "buffers/ide-buffer-private.h"
-#include "completion/ide-completion.h"
-#include "completion/ide-completion-private.h"
-#include "diagnostics/ide-diagnostic.h"
-#include "diagnostics/ide-fixit.h"
-#include "diagnostics/ide-source-location.h"
-#include "diagnostics/ide-source-range.h"
-#include "files/ide-file-settings.h"
-#include "files/ide-file.h"
-#include "hover/ide-hover-private.h"
-#include "plugins/ide-extension-adapter.h"
-#include "plugins/ide-extension-set-adapter.h"
-#include "rename/ide-rename-provider.h"
-#include "snippets/ide-snippet-chunk.h"
-#include "snippets/ide-snippet-context.h"
-#include "snippets/ide-snippet-private.h"
-#include "snippets/ide-snippet.h"
-#include "sourceview/ide-cursor.h"
-#include "sourceview/ide-indenter.h"
-#include "sourceview/ide-omni-gutter-renderer.h"
-#include "sourceview/ide-omni-gutter-renderer-private.h"
-#include "sourceview/ide-source-iter.h"
-#include "sourceview/ide-source-view-capture.h"
-#include "sourceview/ide-source-view-mode.h"
-#include "sourceview/ide-source-view-movements.h"
-#include "sourceview/ide-source-view-private.h"
-#include "sourceview/ide-source-view.h"
-#include "sourceview/ide-text-util.h"
-#include "symbols/ide-symbol.h"
-#include "symbols/ide-symbol-resolver.h"
-#include "util/ide-gtk.h"
-#include "vcs/ide-vcs.h"
-#include "workbench/ide-workbench-private.h"
-#include "threading/ide-task.h"
-#include "util/ide-glib.h"
+#include "ide-buffer-private.h"
+
+#include "ide-completion-private.h"
+#include "ide-completion.h"
+#include "ide-cursor.h"
+#include "ide-hover-private.h"
+#include "ide-indenter.h"
+#include "ide-snippet-chunk.h"
+#include "ide-snippet-context.h"
+#include "ide-snippet-private.h"
+#include "ide-snippet.h"
+#include "ide-source-view-capture.h"
+#include "ide-source-view-mode.h"
+#include "ide-source-view-movements.h"
+#include "ide-source-view-private.h"
+#include "ide-source-view.h"
+#include "ide-source-view-enums.h"
+#include "ide-text-util.h"
 
 #define INCLUDE_STATEMENTS "^#include[\\s]+[\\\"\\<][^\\s\\\"\\\'\\<\\>[:cntrl:]]+[\\\"\\>]"
 
@@ -112,7 +93,7 @@ typedef struct
   GQueue                      *snippets;
   DzlAnimation                *hadj_animation;
   DzlAnimation                *vadj_animation;
-  IdeOmniGutterRenderer       *omni_renderer;
+  IdeGutter                   *gutter;
 
   IdeCompletion               *completion;
   IdeHover                    *hover;
@@ -149,7 +130,7 @@ typedef struct
   guint                        delay_size_allocate_chainup;
   GtkAllocation                delay_size_allocation;
 
-  IdeSourceLocation           *definition_src_location;
+  IdeLocation                 *definition_src_location;
   GtkTextMark                 *definition_highlight_start_mark;
   GtkTextMark                 *definition_highlight_end_mark;
 
@@ -173,6 +154,9 @@ typedef struct
   guint                        snippet_completion : 1;
   guint                        waiting_for_capture : 1;
   guint                        waiting_for_symbol : 1;
+  guint                        show_line_changes : 1;
+  guint                        show_line_diagnostics : 1;
+  guint                        show_line_numbers : 1;
 } IdeSourceViewPrivate;
 
 typedef struct
@@ -185,7 +169,7 @@ typedef struct
 typedef struct
 {
   GPtrArray         *resolvers;
-  IdeSourceLocation *location;
+  IdeLocation *location;
 } FindReferencesTaskData;
 
 G_DEFINE_TYPE_WITH_PRIVATE (IdeSourceView, ide_source_view, GTK_SOURCE_TYPE_VIEW)
@@ -300,22 +284,38 @@ static gdouble     fontScale [LAST_FONT_SCALE] = {
   1.2, 1.44, 1.728, 2.48832,
 };
 
-static void ide_source_view_real_save_insert_mark    (IdeSourceView         *self);
-static void ide_source_view_real_restore_insert_mark (IdeSourceView         *self);
-static void ide_source_view_real_set_mode            (IdeSourceView         *self,
-                                                      const gchar           *name,
-                                                      IdeSourceViewModeType  type);
-static void ide_source_view_save_column              (IdeSourceView         *self);
-static void ide_source_view_maybe_overwrite          (IdeSourceView         *self,
-                                                      GtkTextIter           *iter,
-                                                      const gchar           *text,
-                                                      gint                   len);
+static gboolean ide_source_view_do_size_allocate_hack_cb (gpointer               data);
+static void     ide_source_view_real_save_insert_mark    (IdeSourceView         *self);
+static void     ide_source_view_real_restore_insert_mark (IdeSourceView         *self);
+static void     ide_source_view_real_set_mode            (IdeSourceView         *self,
+                                                          const gchar           *name,
+                                                          IdeSourceViewModeType  type);
+static void     ide_source_view_save_column              (IdeSourceView         *self);
+static void     ide_source_view_maybe_overwrite          (IdeSourceView         *self,
+                                                          GtkTextIter           *iter,
+                                                          const gchar           *text,
+                                                          gint                   len);
+
+static gpointer
+get_selection_owner (IdeSourceView *self)
+{
+  return g_object_get_data (G_OBJECT (gtk_widget_get_toplevel (GTK_WIDGET (self))),
+                            "IDE_SOURCE_VIEW_SELECTION_OWNER");
+}
+
+static void
+set_selection_owner (IdeSourceView *self,
+                     gpointer       tag)
+{
+  g_object_set_data (G_OBJECT (gtk_widget_get_toplevel (GTK_WIDGET (self))),
+                     "IDE_SOURCE_VIEW_SELECTION_OWNER", tag);
+}
 
 static void
 find_references_task_data_free (FindReferencesTaskData *data)
 {
   g_clear_pointer (&data->resolvers, g_ptr_array_unref);
-  g_clear_pointer (&data->location, ide_source_location_unref);
+  g_clear_object (&data->location);
   g_slice_free (FindReferencesTaskData, data);
 }
 
@@ -362,21 +362,6 @@ ide_source_view_set_interactive_completion (IdeSourceView *self,
     }
 }
 
-static void
-find_references_task_get_extension (IdeExtensionSetAdapter *set,
-                                    PeasPluginInfo         *plugin_info,
-                                    PeasExtension          *extension,
-                                    gpointer                user_data)
-{
-  FindReferencesTaskData *data = user_data;
-  IdeSymbolResolver *resolver = (IdeSymbolResolver *)extension;
-
-  g_assert (data != NULL);
-  g_assert (IDE_IS_SYMBOL_RESOLVER (resolver));
-
-  g_ptr_array_add (data->resolvers, g_object_ref (resolver));
-}
-
 static void
 definition_highlight_data_free (DefinitionHighlightData *data)
 {
@@ -779,7 +764,7 @@ ide_source_view_set_file_settings (IdeSourceView   *self,
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
 
   g_assert (IDE_IS_SOURCE_VIEW (self));
-  g_assert (IDE_IS_FILE_SETTINGS (file_settings));
+  g_assert (!file_settings || IDE_IS_FILE_SETTINGS (file_settings));
 
   if (file_settings != ide_source_view_get_file_settings (self))
     {
@@ -789,80 +774,14 @@ ide_source_view_set_file_settings (IdeSourceView   *self,
 }
 
 static void
-ide_source_view__file_load_settings_cb (GObject      *object,
-                                        GAsyncResult *result,
-                                        gpointer      user_data)
-{
-  g_autoptr(IdeSourceView) self = user_data;
-  g_autoptr(IdeFileSettings) file_settings = NULL;
-  g_autoptr(GError) error = NULL;
-  IdeFile *file = (IdeFile *)object;
-
-  g_assert (IDE_IS_FILE (file));
-  g_assert (IDE_IS_SOURCE_VIEW (self));
-
-  file_settings = ide_file_load_settings_finish (file, result, &error);
-
-  if (!file_settings)
-    {
-      g_message ("%s", error->message);
-      return;
-    }
-
-  ide_source_view_set_file_settings (self, file_settings);
-}
-
-static void
-ide_source_view_reload_file_settings (IdeSourceView *self)
-{
-  IdeBuffer *buffer;
-  IdeFile *file;
-
-  g_assert (IDE_IS_SOURCE_VIEW (self));
-  g_assert (IDE_IS_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (self))));
-
-  buffer = IDE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (self)));
-  file = ide_buffer_get_file (buffer);
-
-  ide_file_load_settings_async (file,
-                                NULL,
-                                ide_source_view__file_load_settings_cb,
-                                g_object_ref (self));
-}
-
-static void
-ide_source_view_reload_language (IdeSourceView *self)
-{
-  GtkSourceLanguage *language;
-  GtkTextBuffer *buffer;
-  IdeFile *file = NULL;
-
-  g_assert (IDE_IS_SOURCE_VIEW (self));
-
-  /*
-   * Update source language, etc.
-   */
-  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self));
-  file = ide_buffer_get_file (IDE_BUFFER (buffer));
-  language = ide_file_get_language (file);
-
-  g_assert (IDE_IS_BUFFER (buffer));
-  g_assert (IDE_IS_FILE (file));
-  g_assert (!language || GTK_SOURCE_IS_LANGUAGE (language));
-
-  gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (buffer), language);
-}
-
-static void
-ide_source_view__buffer_notify_file_cb (IdeSourceView *self,
-                                        GParamSpec    *pspec,
-                                        IdeBuffer     *buffer)
+ide_source_view__buffer_notify_file_settings_cb (IdeSourceView *self,
+                                                 GParamSpec    *pspec,
+                                                 IdeBuffer     *buffer)
 {
   g_assert (IDE_IS_SOURCE_VIEW (self));
   g_assert (IDE_IS_BUFFER (buffer));
 
-  ide_source_view_reload_language (self);
-  ide_source_view_reload_file_settings (self);
+  ide_source_view_set_file_settings (self, ide_buffer_get_file_settings (buffer));
 }
 
 static void
@@ -961,8 +880,8 @@ ide_source_view_rebuild_css (IdeSourceView *self)
       css = g_strdup_printf ("textview { %s }", str ?: "");
       gtk_css_provider_load_from_data (priv->css_provider, css, -1, NULL);
 
-      if (priv->omni_renderer != NULL)
-        _ide_omni_gutter_renderer_reset_font (priv->omni_renderer);
+      if (priv->gutter != NULL)
+        ide_gutter_style_changed (priv->gutter);
 
       if (priv->completion != NULL)
         _ide_completion_set_font_description (priv->completion, font_desc);
@@ -1188,31 +1107,29 @@ ide_source_view__buffer_notify_has_selection_cb (IdeSourceView *self,
 {
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
   gboolean has_selection;
-  IdeWorkbench *workbench = ide_widget_get_workbench (GTK_WIDGET (self));
 
   has_selection = gtk_text_buffer_get_has_selection (GTK_TEXT_BUFFER (buffer));
   ide_source_view_mode_set_has_selection (priv->mode, has_selection);
 
-  if (workbench == NULL)
-    return;
-
   if (has_selection)
-    ide_workbench_set_selection_owner (workbench, G_OBJECT (self));
-  else if (ide_workbench_get_selection_owner (workbench) == G_OBJECT (self))
-    ide_workbench_set_selection_owner (workbench, NULL);
+    set_selection_owner (self, G_OBJECT (self));
+  else if (get_selection_owner (self) == G_OBJECT (self))
+    set_selection_owner (self, NULL);
 }
 
 static void
 ide_source_view__buffer_line_flags_changed_cb (IdeSourceView *self,
                                                IdeBuffer     *buffer)
 {
+  GtkSourceGutter *gutter;
+
   IDE_ENTRY;
 
   g_assert (IDE_IS_SOURCE_VIEW (self));
   g_assert (IDE_IS_BUFFER (buffer));
 
-  gtk_source_gutter_queue_draw (gtk_source_view_get_gutter (GTK_SOURCE_VIEW (self),
-                                                            GTK_TEXT_WINDOW_LEFT));
+  gutter = gtk_source_view_get_gutter (GTK_SOURCE_VIEW (self), GTK_TEXT_WINDOW_LEFT);
+  gtk_source_gutter_queue_draw (gutter);
 
   IDE_EXIT;
 }
@@ -1278,7 +1195,7 @@ ide_source_view_reset_definition_highlight (IdeSourceView *self)
   g_assert (IDE_IS_SOURCE_VIEW (self));
 
   if (priv->definition_src_location)
-    g_clear_pointer (&priv->definition_src_location, ide_source_location_unref);
+    g_clear_object (&priv->definition_src_location);
 
   if (priv->buffer != NULL)
     {
@@ -1336,12 +1253,14 @@ ide_source_view_bind_buffer (IdeSourceView  *self,
                              DzlSignalGroup *group)
 {
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
+  g_autoptr(IdeContext) context = NULL;
   GtkTextMark *insert;
+  IdeObjectBox *box;
   GtkTextIter iter;
-  IdeContext *context;
 
   IDE_ENTRY;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_SOURCE_VIEW (self));
   g_assert (IDE_IS_BUFFER (buffer));
   g_assert (DZL_IS_SIGNAL_GROUP (group));
@@ -1358,11 +1277,13 @@ ide_source_view_bind_buffer (IdeSourceView  *self,
       priv->completion_blocked = TRUE;
     }
 
-  context = ide_buffer_get_context (buffer);
+  context = ide_buffer_ref_context (buffer);
 
   _ide_hover_set_context (priv->hover, context);
 
-  priv->indenter_adapter = ide_extension_adapter_new (context,
+  box = ide_object_box_from_object (G_OBJECT (buffer));
+
+  priv->indenter_adapter = ide_extension_adapter_new (IDE_OBJECT (box),
                                                       peas_engine_get_default (),
                                                       IDE_TYPE_INDENTER,
                                                       "Indenter-Languages",
@@ -1386,7 +1307,7 @@ ide_source_view_bind_buffer (IdeSourceView  *self,
   g_object_ref (priv->definition_highlight_end_mark);
 
   ide_source_view__buffer_notify_language_cb (self, NULL, buffer);
-  ide_source_view__buffer_notify_file_cb (self, NULL, buffer);
+  ide_source_view__buffer_notify_file_settings_cb (self, NULL, buffer);
   ide_source_view__buffer_notify_style_scheme_cb (self, NULL, buffer);
   ide_source_view__buffer__notify_can_redo (self, NULL, buffer);
   ide_source_view__buffer__notify_can_undo (self, NULL, buffer);
@@ -1426,7 +1347,7 @@ ide_source_view_unbind_buffer (IdeSourceView  *self,
       g_clear_object (&priv->cursor);
     }
 
-  g_clear_object (&priv->indenter_adapter);
+  ide_clear_and_destroy_object (&priv->indenter_adapter);
   g_clear_object (&priv->definition_highlight_start_mark);
   g_clear_object (&priv->definition_highlight_end_mark);
 
@@ -2297,9 +2218,9 @@ ide_source_view_process_press_on_definition (IdeSourceView  *self,
 
       if (gtk_text_iter_in_range (&iter, &definition_highlight_start, &definition_highlight_end))
         {
-          g_autoptr(IdeSourceLocation) src_location = NULL;
+          g_autoptr(IdeLocation) src_location = NULL;
 
-          src_location = ide_source_location_ref (priv->definition_src_location);
+          src_location = g_object_ref (priv->definition_src_location);
           ide_source_view_reset_definition_highlight (self);
           g_signal_emit (self, signals [FOCUS_LOCATION], 0, src_location);
         }
@@ -2445,7 +2366,7 @@ ide_source_view_get_definition_on_mouse_over_cb (GObject      *object,
   IdeBuffer *buffer = (IdeBuffer *)object;
   g_autoptr(IdeSymbol) symbol = NULL;
   g_autoptr(GError) error = NULL;
-  IdeSourceLocation *srcloc;
+  IdeLocation *srcloc;
   IdeSymbolKind kind;
 
   IDE_ENTRY;
@@ -2473,10 +2394,10 @@ ide_source_view_get_definition_on_mouse_over_cb (GObject      *object,
 
   kind = ide_symbol_get_kind (symbol);
 
-  srcloc = ide_symbol_get_definition_location (symbol);
+  srcloc = ide_symbol_get_location (symbol);
 
   if (srcloc == NULL)
-    srcloc = ide_symbol_get_declaration_location (symbol);
+    srcloc = ide_symbol_get_header_location (symbol);
 
   if (srcloc != NULL)
     {
@@ -2484,17 +2405,17 @@ ide_source_view_get_definition_on_mouse_over_cb (GObject      *object,
       GtkTextIter word_end;
 
       if (priv->definition_src_location != NULL && priv->definition_src_location != srcloc)
-        g_clear_pointer (&priv->definition_src_location, ide_source_location_unref);
+        g_clear_object (&priv->definition_src_location);
 
       if (priv->definition_src_location == NULL)
-        priv->definition_src_location = ide_source_location_ref (srcloc);
+        priv->definition_src_location = g_object_ref (srcloc);
 
       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer),
                                         &word_start, data->word_start_mark);
       gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer),
                                         &word_end, data->word_end_mark);
 
-      if (kind == IDE_SYMBOL_HEADER)
+      if (kind == IDE_SYMBOL_KIND_HEADER)
         {
           GtkTextIter line_start = word_start;
           GtkTextIter line_end = word_end;
@@ -3385,8 +3306,10 @@ ide_source_view_real_move_error (IdeSourceView    *self,
                                  GtkDirectionType  dir)
 {
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
+  IdeDiagnostics *diagnostics;
   GtkTextBuffer *buffer;
   GtkTextMark *insert;
+  GFile *file;
   GtkTextIter iter;
   gboolean wrap_around = TRUE;
   gboolean (*movement) (GtkTextIter *) = NULL;
@@ -3396,6 +3319,11 @@ ide_source_view_real_move_error (IdeSourceView    *self,
   if (!priv->buffer)
     return;
 
+  if (!(diagnostics = ide_buffer_get_diagnostics (priv->buffer)))
+    return;
+
+  file = ide_buffer_get_file (priv->buffer);
+
   if (dir == GTK_DIR_RIGHT)
     dir = GTK_DIR_DOWN;
   else if (dir == GTK_DIR_LEFT)
@@ -3422,12 +3350,11 @@ wrapped:
   while (movement (&iter))
     {
       IdeDiagnostic *diag;
+      guint line = gtk_text_iter_get_line (&iter);
 
-      diag = ide_buffer_get_diagnostic_at_iter (priv->buffer, &iter);
-
-      if (diag)
+      if ((diag = ide_diagnostics_get_diagnostic_at_line (diagnostics, file, line)))
         {
-          IdeSourceLocation *location;
+          IdeLocation *location;
 
           location = ide_diagnostic_get_location (diag);
 
@@ -3435,7 +3362,7 @@ wrapped:
             {
               guint line_offset;
 
-              line_offset = ide_source_location_get_line_offset (location);
+              line_offset = ide_location_get_line_offset (location);
               gtk_text_iter_set_line_offset (&iter, 0);
               for (; line_offset; line_offset--)
                 if (gtk_text_iter_ends_line (&iter) || !gtk_text_iter_forward_char (&iter))
@@ -3696,17 +3623,15 @@ ide_source_view_real_push_selection (IdeSourceView *self)
 }
 
 static void
-ide_source_view_real_push_snippet (IdeSourceView           *self,
+ide_source_view_real_push_snippet (IdeSourceView     *self,
                                    IdeSnippet        *snippet,
-                                   const GtkTextIter       *location)
+                                   const GtkTextIter *location)
 {
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
-  IdeContext *ide_context;
-  IdeSnippetContext *context;
-  IdeFile *file;
-  GFile *gfile = NULL;
   g_autoptr(GFile) gparentfile = NULL;
-
+  g_autoptr(IdeContext) ide_context = NULL;
+  IdeSnippetContext *context;
+  GFile *file = NULL;
 
   g_assert (IDE_IS_SOURCE_VIEW (self));
   g_assert (IDE_IS_SNIPPET (snippet));
@@ -3716,34 +3641,30 @@ ide_source_view_real_push_snippet (IdeSourceView           *self,
 
   if (priv->buffer != NULL)
     {
-      if ((file = ide_buffer_get_file (priv->buffer)) &&
-          (gfile = ide_file_get_file (file)))
+      if ((file = ide_buffer_get_file (priv->buffer)))
         {
           g_autofree gchar *name = NULL;
           g_autofree gchar *path = NULL;
           g_autofree gchar *dirname = NULL;
 
-          name = g_file_get_basename (gfile);
-          gparentfile = g_file_get_parent(gfile);
+          name = g_file_get_basename (file);
+          gparentfile = g_file_get_parent (file);
           dirname = g_file_get_path (gparentfile);
-          path = g_file_get_path (gfile);
+          path = g_file_get_path (file);
           ide_snippet_context_add_variable (context, "filename", name);
           ide_snippet_context_add_variable (context, "dirname", dirname);
           ide_snippet_context_add_variable (context, "path", path);
         }
 
-      if ((ide_context = ide_buffer_get_context (priv->buffer)))
+      if ((ide_context = ide_buffer_ref_context (priv->buffer)))
         {
-          IdeVcs *vcs;
-          IdeVcsConfig *vcs_config;
-          GFile *workdir;
+          g_autoptr(GFile) workdir = NULL;
 
-          vcs = ide_context_get_vcs (ide_context);
-          workdir = ide_vcs_get_working_directory (vcs);
-          if (workdir && gfile)
+          workdir = ide_context_ref_workdir (ide_context);
+          if (workdir && file)
             {
               g_autofree gchar *relative_path = NULL;
-              relative_path = g_file_get_relative_path (workdir, gfile);
+              relative_path = g_file_get_relative_path (workdir, file);
               ide_snippet_context_add_variable (context, "relative_path", relative_path);
             }
           if (workdir && gparentfile)
@@ -3752,32 +3673,6 @@ ide_source_view_real_push_snippet (IdeSourceView           *self,
               relative_dirname = g_file_get_relative_path (workdir, gparentfile);
               ide_snippet_context_add_variable (context, "relative_dirname", relative_dirname);
             }
-
-          if ((vcs_config = ide_vcs_get_config (vcs)))
-            {
-              GValue value = G_VALUE_INIT;
-
-              g_value_init (&value, G_TYPE_STRING);
-
-              ide_vcs_config_get_config (vcs_config, IDE_VCS_CONFIG_FULL_NAME, &value);
-
-              if (!dzl_str_empty0 (g_value_get_string (&value)))
-                {
-                  ide_snippet_context_add_shared_variable (context, "author", g_value_get_string (&value));
-                  ide_snippet_context_add_shared_variable (context, "fullname", g_value_get_string (&value));
-                  ide_snippet_context_add_shared_variable (context, "username", g_value_get_string (&value));
-                }
-
-              g_value_reset (&value);
-
-              ide_vcs_config_get_config (vcs_config, IDE_VCS_CONFIG_EMAIL, &value);
-
-              if (!dzl_str_empty0 (g_value_get_string (&value)))
-                ide_snippet_context_add_shared_variable (context, "email", g_value_get_string (&value));
-
-              g_value_unset (&value);
-              g_object_unref (vcs_config);
-            }
         }
     }
 }
@@ -3909,7 +3804,6 @@ ide_source_view_constructed (GObject *object)
 {
   IdeSourceView *self = (IdeSourceView *)object;
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
-  GtkSourceGutter *gutter;
 
   G_OBJECT_CLASS (ide_source_view_parent_class)->constructed (object);
 
@@ -3920,13 +3814,6 @@ ide_source_view_constructed (GObject *object)
   priv->definition_src_location = NULL;
   ide_source_view_reset_definition_highlight (self);
 
-  gutter = gtk_source_view_get_gutter (GTK_SOURCE_VIEW (self), GTK_TEXT_WINDOW_LEFT);
-  priv->omni_renderer = g_object_new (IDE_TYPE_OMNI_GUTTER_RENDERER,
-                                      "visible", TRUE,
-                                      NULL);
-  g_object_ref_sink (priv->omni_renderer);
-  gtk_source_gutter_insert (gutter, GTK_SOURCE_GUTTER_RENDERER (priv->omni_renderer), 0);
-
   priv->completion = _ide_completion_new (GTK_SOURCE_VIEW (self));
 
   /* Disable sourceview completion always */
@@ -4096,7 +3983,6 @@ ide_source_view_focus_in_event (GtkWidget     *widget,
 {
   IdeSourceView *self = (IdeSourceView *)widget;
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
-  IdeWorkbench *workbench;
   gboolean ret;
 
   g_assert (IDE_IS_SOURCE_VIEW (self));
@@ -4104,13 +3990,19 @@ ide_source_view_focus_in_event (GtkWidget     *widget,
   /* Restore the completion window now that we have regained focus. */
   unblock_interactive (self);
 
+  /* Force size allocation immediately if we have something queued. */
+  if (priv->delay_size_allocate_chainup)
+    {
+      g_source_remove (priv->delay_size_allocate_chainup);
+      ide_source_view_do_size_allocate_hack_cb (self);
+    }
+
   /*
    * Restore the insert mark, but ignore selections (since we cant ensure they
    * will stay looking selected, as the other frame could be a view into our
    * own buffer).
    */
-  workbench = ide_widget_get_workbench (GTK_WIDGET (widget));
-  if (!workbench || ide_workbench_get_selection_owner (workbench) != G_OBJECT (self))
+  if (get_selection_owner (self) != self)
     {
       priv->saved_selection_line = priv->saved_line;
       priv->saved_selection_line_column = priv->saved_line_column;
@@ -4219,7 +4111,7 @@ ide_source_view_goto_definition_symbol_cb (GObject      *object,
   g_autoptr(IdeSymbol) symbol = NULL;
   IdeBuffer *buffer = (IdeBuffer *)object;
   g_autoptr(GError) error = NULL;
-  IdeSourceLocation *srcloc;
+  IdeLocation *srcloc;
 
   IDE_ENTRY;
 
@@ -4234,20 +4126,20 @@ ide_source_view_goto_definition_symbol_cb (GObject      *object,
       IDE_EXIT;
     }
 
-  srcloc = ide_symbol_get_definition_location (symbol);
+  srcloc = ide_symbol_get_location (symbol);
 
   if (srcloc == NULL)
-    srcloc = ide_symbol_get_declaration_location (symbol);
+    srcloc = ide_symbol_get_header_location (symbol);
 
   if (srcloc != NULL)
     {
-      guint line = ide_source_location_get_line (srcloc);
-      guint line_offset = ide_source_location_get_line_offset (srcloc);
-      IdeFile *file = ide_source_location_get_file (srcloc);
-      IdeFile *our_file = ide_buffer_get_file (buffer);
+      guint line = ide_location_get_line (srcloc);
+      guint line_offset = ide_location_get_line_offset (srcloc);
+      GFile *file = ide_location_get_file (srcloc);
+      GFile *our_file = ide_buffer_get_file (buffer);
 
 #ifdef IDE_ENABLE_TRACE
-      const gchar *filename = ide_file_get_path (file);
+      const gchar *filename = g_file_peek_path (file);
 
       IDE_TRACE_MSG ("%s => %s +%u:%u",
                      ide_symbol_get_name (symbol),
@@ -4261,7 +4153,7 @@ ide_source_view_goto_definition_symbol_cb (GObject      *object,
        * If we are navigating within this file, just stay captive instead of
        * potentially allowing jumping to the file in another editor.
        */
-      if (ide_file_equal (file, our_file))
+      if (g_file_equal (file, our_file))
         {
           GtkTextIter iter;
 
@@ -4394,11 +4286,11 @@ ide_source_view_get_overwrite (IdeSourceView *self)
 
 static gchar *
 ide_source_view_get_fixit_label (IdeSourceView *self,
-                                 IdeFixit      *fixit)
+                                 IdeTextEdit      *fixit)
 {
-  IdeSourceLocation *begin_loc;
-  IdeSourceLocation *end_loc;
-  IdeSourceRange *range;
+  IdeLocation *begin_loc;
+  IdeLocation *end_loc;
+  IdeRange *range;
   GtkTextBuffer *buffer;
   GtkTextIter begin;
   GtkTextIter end;
@@ -4410,11 +4302,11 @@ ide_source_view_get_fixit_label (IdeSourceView *self,
   g_assert (IDE_IS_SOURCE_VIEW (self));
   g_assert (fixit != NULL);
 
-  range = ide_fixit_get_range (fixit);
+  range = ide_text_edit_get_range (fixit);
   if (range == NULL)
     goto cleanup;
 
-  new_text = g_strdup (ide_fixit_get_text (fixit));
+  new_text = g_strdup (ide_text_edit_get_text (fixit));
   if (new_text == NULL)
     goto cleanup;
 
@@ -4422,11 +4314,11 @@ ide_source_view_get_fixit_label (IdeSourceView *self,
   if (!IDE_IS_BUFFER (buffer))
     goto cleanup;
 
-  begin_loc = ide_source_range_get_begin (range);
-  end_loc = ide_source_range_get_end (range);
+  begin_loc = ide_range_get_begin (range);
+  end_loc = ide_range_get_end (range);
 
-  ide_buffer_get_iter_at_source_location (IDE_BUFFER (buffer), &begin, begin_loc);
-  ide_buffer_get_iter_at_source_location (IDE_BUFFER (buffer), &end, end_loc);
+  ide_buffer_get_iter_at_location (IDE_BUFFER (buffer), &begin, begin_loc);
+  ide_buffer_get_iter_at_location (IDE_BUFFER (buffer), &end, end_loc);
 
   old_text = gtk_text_iter_get_slice (&begin, &end);
 
@@ -4468,7 +4360,7 @@ static void
 ide_source_view__fixit_activate (IdeSourceView *self,
                                  GtkMenuItem   *menu_item)
 {
-  IdeFixit *fixit;
+  IdeTextEdit *fixit;
 
   g_assert (IDE_IS_SOURCE_VIEW (self));
   g_assert (GTK_IS_MENU_ITEM (menu_item));
@@ -4477,8 +4369,8 @@ ide_source_view__fixit_activate (IdeSourceView *self,
 
   if (fixit != NULL)
     {
-      IdeSourceLocation *srcloc;
-      IdeSourceRange *range;
+      IdeLocation *srcloc;
+      IdeRange *range;
       GtkTextBuffer *buffer;
       const gchar *text;
       GtkTextIter begin;
@@ -4488,14 +4380,14 @@ ide_source_view__fixit_activate (IdeSourceView *self,
       if (!IDE_IS_BUFFER (buffer))
         return;
 
-      text = ide_fixit_get_text (fixit);
-      range = ide_fixit_get_range (fixit);
+      text = ide_text_edit_get_text (fixit);
+      range = ide_text_edit_get_range (fixit);
 
-      srcloc = ide_source_range_get_begin (range);
-      ide_buffer_get_iter_at_source_location (IDE_BUFFER (buffer), &begin, srcloc);
+      srcloc = ide_range_get_begin (range);
+      ide_buffer_get_iter_at_location (IDE_BUFFER (buffer), &begin, srcloc);
 
-      srcloc = ide_source_range_get_end (range);
-      ide_buffer_get_iter_at_source_location (IDE_BUFFER (buffer), &end, srcloc);
+      srcloc = ide_range_get_end (range);
+      ide_buffer_get_iter_at_location (IDE_BUFFER (buffer), &end, srcloc);
 
       gtk_text_buffer_begin_user_action (buffer);
       gtk_text_buffer_delete (buffer, &begin, &end);
@@ -4510,13 +4402,14 @@ ide_source_view_real_populate_popup (GtkTextView *text_view,
 {
   IdeSourceView *self = (IdeSourceView *)text_view;
   GtkSeparatorMenuItem *sep;
+  IdeDiagnostics *diagnostics;
+  IdeDiagnostic *diagnostic = NULL;
   GtkTextBuffer *buffer;
   GtkMenuItem *menu_item;
   GtkTextMark *insert;
   GtkTextIter iter;
   GtkTextIter begin;
   GtkTextIter end;
-  IdeDiagnostic *diagnostic;
   GMenu *model;
 
   g_assert (GTK_IS_TEXT_VIEW (text_view));
@@ -4545,15 +4438,18 @@ ide_source_view_real_populate_popup (GtkTextView *text_view,
 
   /*
    * Check if we have a diagnostic at this position and if there are fixits associated with it.
-   * If so, display the "Apply Fixit" menu item with available fixits.
+   * If so, display the "Apply TextEdit" menu item with available fixits.
    */
-  diagnostic = ide_buffer_get_diagnostic_at_iter (IDE_BUFFER (buffer), &iter);
+  if ((diagnostics = ide_buffer_get_diagnostics (IDE_BUFFER (buffer))))
+    diagnostic = ide_diagnostics_get_diagnostic_at_line (diagnostics,
+                                                         ide_buffer_get_file (IDE_BUFFER (buffer)),
+                                                         gtk_text_iter_get_line (&iter));
 
   if (diagnostic != NULL)
     {
       guint num_fixits;
 
-      num_fixits = ide_diagnostic_get_num_fixits (diagnostic);
+      num_fixits = ide_diagnostic_get_n_fixits (diagnostic);
 
       if (num_fixits > 0)
         {
@@ -4577,7 +4473,7 @@ ide_source_view_real_populate_popup (GtkTextView *text_view,
 
           for (i = 0; i < num_fixits; i++)
             {
-              IdeFixit *fixit;
+              IdeTextEdit *fixit;
               gchar *label;
 
               fixit = ide_diagnostic_get_fixit (diagnostic, i);
@@ -4591,8 +4487,8 @@ ide_source_view_real_populate_popup (GtkTextView *text_view,
 
               g_object_set_data_full (G_OBJECT (menu_item),
                                       "IDE_FIXIT",
-                                      ide_fixit_ref (fixit),
-                                      (GDestroyNotify)ide_fixit_unref);
+                                      g_object_ref (fixit),
+                                      (GDestroyNotify)g_object_unref);
 
               g_signal_connect_object (menu_item,
                                        "activate",
@@ -4887,8 +4783,8 @@ ide_source_view_rename_edits_cb (GObject      *object,
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
   g_autoptr(GPtrArray) edits = NULL;
   g_autoptr(GError) error = NULL;
+  g_autoptr(IdeContext) context = NULL;
   IdeBufferManager *buffer_manager;
-  IdeContext *context;
 
   IDE_ENTRY;
 
@@ -4907,8 +4803,8 @@ ide_source_view_rename_edits_cb (GObject      *object,
 
   IDE_PTR_ARRAY_SET_FREE_FUNC (edits, g_object_unref);
 
-  context = ide_buffer_get_context (priv->buffer);
-  buffer_manager = ide_context_get_buffer_manager (context);
+  context = ide_buffer_ref_context (priv->buffer);
+  buffer_manager = ide_buffer_manager_from_context (context);
 
   ide_buffer_manager_apply_edits_async (buffer_manager,
                                         IDE_PTR_ARRAY_STEAL_FULL (&edits),
@@ -4925,7 +4821,7 @@ ide_source_view_rename_activate (IdeSourceView    *self,
                                  DzlSimplePopover *popover)
 {
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
-  g_autoptr(IdeSourceLocation) location = NULL;
+  g_autoptr(IdeLocation) location = NULL;
   IdeRenameProvider *provider;
 
   IDE_ENTRY;
@@ -4961,7 +4857,7 @@ ide_source_view_real_begin_rename (IdeSourceView *self)
 {
   IdeRenameProvider *provider;
   DzlSimplePopover *popover;
-  g_autofree gchar *uri = NULL;
+  g_autofree gchar *title = NULL;
   GtkTextBuffer *buffer;
   GtkTextMark *insert;
   GtkTextIter iter;
@@ -4981,14 +4877,14 @@ ide_source_view_real_begin_rename (IdeSourceView *self)
     }
 
   insert = gtk_text_buffer_get_insert (buffer);
-  uri = ide_buffer_get_uri (IDE_BUFFER (buffer));
+  title = ide_buffer_dup_title (IDE_BUFFER (buffer));
 
   gtk_text_buffer_get_iter_at_mark (buffer, &iter, insert);
 
-  IDE_TRACE_MSG ("Renaming symbol found at %s: %d:%d",
-                 uri,
-                 gtk_text_iter_get_line (&iter) + 1,
-                 gtk_text_iter_get_line_offset (&iter) + 1);
+  g_debug ("Renaming symbol from %s +%d:%d",
+           title,
+           gtk_text_iter_get_line (&iter) + 1,
+           gtk_text_iter_get_line_offset (&iter) + 1);
 
   gtk_text_buffer_select_range (buffer, &iter, &iter);
   gtk_text_view_get_iter_location (GTK_TEXT_VIEW (self), &iter, &loc);
@@ -5076,7 +4972,7 @@ ide_source_view_real_find_references_jump (IdeSourceView *self,
                                            GtkListBoxRow *row,
                                            GtkListBox    *list_box)
 {
-  IdeSourceLocation *location;
+  IdeLocation *location;
 
   IDE_ENTRY;
 
@@ -5084,7 +4980,7 @@ ide_source_view_real_find_references_jump (IdeSourceView *self,
   g_assert (GTK_IS_LIST_BOX_ROW (row));
   g_assert (GTK_IS_LIST_BOX (list_box));
 
-  location = g_object_get_data (G_OBJECT (row), "IDE_SOURCE_LOCATION");
+  location = g_object_get_data (G_OBJECT (row), "IDE_LOCATION");
 
   if (location != NULL)
     g_signal_emit (self, signals [FOCUS_LOCATION], 0, location);
@@ -5094,11 +4990,11 @@ ide_source_view_real_find_references_jump (IdeSourceView *self,
 
 static gboolean
 insert_mark_within_range (IdeBuffer      *buffer,
-                          IdeSourceRange *range)
+                          IdeRange *range)
 {
   GtkTextMark *insert = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (buffer));
-  IdeSourceLocation *begin = ide_source_range_get_begin (range);
-  IdeSourceLocation *end = ide_source_range_get_end (range);
+  IdeLocation *begin = ide_range_get_begin (range);
+  IdeLocation *end = ide_range_get_end (range);
   GtkTextIter iter;
   GtkTextIter begin_iter;
   GtkTextIter end_iter;
@@ -5107,8 +5003,8 @@ insert_mark_within_range (IdeBuffer      *buffer,
     return FALSE;
 
   gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &iter, insert);
-  ide_buffer_get_iter_at_source_location (buffer, &begin_iter, begin);
-  ide_buffer_get_iter_at_source_location (buffer, &end_iter, end);
+  ide_buffer_get_iter_at_location (buffer, &begin_iter, begin);
+  ide_buffer_get_iter_at_location (buffer, &end_iter, end);
 
   return gtk_text_iter_compare (&begin_iter, &iter) <= 0 &&
          gtk_text_iter_compare (&end_iter, &iter) >= 0;
@@ -5142,7 +5038,7 @@ ide_source_view_find_references_cb (GObject      *object,
 
   references = ide_symbol_resolver_find_references_finish (symbol_resolver, result, &error);
 
-  IDE_PTR_ARRAY_SET_FREE_FUNC (references, ide_source_range_unref);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (references, g_object_unref);
 
   self = ide_task_get_source_object (task);
   priv = ide_source_view_get_instance_private (self);
@@ -5166,6 +5062,7 @@ ide_source_view_find_references_cb (GObject      *object,
 
       ide_symbol_resolver_find_references_async (resolver,
                                                  data->location,
+                                                 ide_buffer_get_language_id (priv->buffer),
                                                  cancellable,
                                                  ide_source_view_find_references_cb,
                                                  g_steal_pointer (&task));
@@ -5208,29 +5105,27 @@ ide_source_view_find_references_cb (GObject      *object,
 
   if (references != NULL && references->len > 0)
     {
-      IdeContext *context = ide_buffer_get_context (priv->buffer);
-      IdeVcs *vcs = ide_context_get_vcs (context);
-      GFile *workdir = ide_vcs_get_working_directory (vcs);
+      g_autoptr(IdeContext) context = ide_buffer_ref_context (priv->buffer);
+      g_autoptr(GFile) workdir = ide_context_ref_workdir (context);
 
       for (guint i = 0; i < references->len; i++)
         {
-          IdeSourceRange *range = g_ptr_array_index (references, i);
-          IdeSourceLocation *begin = ide_source_range_get_begin (range);
-          IdeFile *file = ide_source_location_get_file (begin);
-          GFile *gfile = ide_file_get_file (file);
-          guint line = ide_source_location_get_line (begin);
-          guint line_offset = ide_source_location_get_line_offset (begin);
+          IdeRange *range = g_ptr_array_index (references, i);
+          IdeLocation *begin = ide_range_get_begin (range);
+          GFile *file = ide_location_get_file (begin);
+          guint line = ide_location_get_line (begin);
+          guint line_offset = ide_location_get_line_offset (begin);
           g_autofree gchar *name = NULL;
           g_autofree gchar *text = NULL;
           GtkListBoxRow *row;
           GtkLabel *label;
 
-          if (g_file_has_prefix (gfile, workdir))
-            name = g_file_get_relative_path (workdir, gfile);
-          else if (g_file_is_native (gfile))
-            name = g_file_get_path (gfile);
+          if (g_file_has_prefix (file, workdir))
+            name = g_file_get_relative_path (workdir, file);
+          else if (g_file_is_native (file))
+            name = g_file_get_path (file);
           else
-            name = g_file_get_uri (gfile);
+            name = g_file_get_uri (file);
 
           /* translators: %s is the filename, then line number, column number. <> are pango markup */
           text = g_strdup_printf (_("<b>%s</b> — <small>Line %u, Column %u</small>"),
@@ -5247,9 +5142,9 @@ ide_source_view_find_references_cb (GObject      *object,
                               "visible", TRUE,
                               NULL);
           g_object_set_data_full (G_OBJECT (row),
-                                  "IDE_SOURCE_LOCATION",
-                                  ide_source_location_ref (begin),
-                                  (GDestroyNotify)ide_source_location_unref);
+                                  "IDE_LOCATION",
+                                  g_object_ref (begin),
+                                  g_object_unref);
           gtk_container_add (GTK_CONTAINER (list_box), GTK_WIDGET (row));
 
           if (insert_mark_within_range (priv->buffer, range))
@@ -5285,9 +5180,8 @@ ide_source_view_real_find_references (IdeSourceView *self)
 {
   IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
   g_autoptr(IdeTask) task = NULL;
-  IdeExtensionSetAdapter *adapter;
+  g_autoptr(GPtrArray) resolvers = NULL;
   FindReferencesTaskData *data;
-  guint n_extensions;
   IdeSymbolResolver *resolver;
 
   IDE_ENTRY;
@@ -5297,29 +5191,26 @@ ide_source_view_real_find_references (IdeSourceView *self)
   task = ide_task_new (self, NULL, NULL, NULL);
   ide_task_set_source_tag (task, ide_source_view_real_find_references);
 
-  adapter = ide_buffer_get_symbol_resolvers (priv->buffer);
-  n_extensions = ide_extension_set_adapter_get_n_extensions (adapter);
+  resolvers = ide_buffer_get_symbol_resolvers (priv->buffer);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (resolvers, g_object_unref);
 
-  if (!n_extensions)
+  if (resolvers->len == 0)
     {
       g_debug ("No symbol resolver is available");
-
       IDE_EXIT;
     }
 
   data = g_slice_new0 (FindReferencesTaskData);
-  data->resolvers = g_ptr_array_new_with_free_func (g_object_unref);
+  data->resolvers = g_steal_pointer (&resolvers);
   data->location = ide_buffer_get_insert_location (priv->buffer);
   ide_task_set_task_data (task, data, find_references_task_data_free);
 
-  ide_extension_set_adapter_foreach_by_priority (adapter, find_references_task_get_extension, data);
-  g_assert (data->resolvers->len > 0);
-
   resolver = g_ptr_array_index (data->resolvers, data->resolvers->len - 1);
 
   /* Try each symbol resolver one by one to find references. */
   ide_symbol_resolver_find_references_async (resolver,
                                              data->location,
+                                             ide_buffer_get_language_id (priv->buffer),
                                              NULL,
                                              ide_source_view_find_references_cb,
                                              g_steal_pointer (&task));
@@ -5375,6 +5266,21 @@ ide_source_view_real_reset (IdeSourceView *self)
   g_signal_emit (self, signals [SET_MODE], 0, NULL, IDE_SOURCE_VIEW_MODE_TYPE_PERMANENT);
 }
 
+static void
+ide_source_view_destroy (GtkWidget *widget)
+{
+  IdeSourceView *self = (IdeSourceView *)widget;
+  IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
+
+  g_assert (IDE_IS_SOURCE_VIEW (self));
+
+  /* Ensure we release the buffer immediately */
+  if (priv->buffer_signals != NULL)
+    dzl_signal_group_set_target (priv->buffer_signals, NULL);
+
+  GTK_WIDGET_CLASS (ide_source_view_parent_class)->destroy (widget);
+}
+
 static void
 ide_source_view_dispose (GObject *object)
 {
@@ -5404,12 +5310,12 @@ ide_source_view_dispose (GObject *object)
   g_clear_object (&priv->hover);
   g_clear_object (&priv->completion);
   g_clear_object (&priv->capture);
-  g_clear_object (&priv->indenter_adapter);
+  ide_clear_and_destroy_object (&priv->indenter_adapter);
   g_clear_object (&priv->css_provider);
   g_clear_object (&priv->mode);
   g_clear_object (&priv->buffer_signals);
   g_clear_object (&priv->file_setting_bindings);
-  g_clear_object (&priv->omni_renderer);
+  g_clear_object (&priv->gutter);
 
   if (priv->command_str != NULL)
     {
@@ -5635,6 +5541,7 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
   widget_class->scroll_event = ide_source_view_scroll_event;
   widget_class->size_allocate = ide_source_view_size_allocate;
   widget_class->style_updated = ide_source_view_real_style_updated;
+  widget_class->destroy = ide_source_view_destroy;
 
   text_view_class->delete_from_cursor = ide_source_view_real_delete_from_cursor;
   text_view_class->draw_layer = ide_source_view_real_draw_layer;
@@ -5802,6 +5709,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    *
    * This also requires that IdeBuffer:highlight-diagnostics is set to %TRUE
    * to generate diagnostics.
+   *
+   * Since: 3.32
    */
   properties [PROP_SHOW_LINE_DIAGNOSTICS] =
     g_param_spec_boolean ("show-line-diagnostics",
@@ -5855,6 +5764,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    * to replay the sequence starting from the correct state.
    *
    * Pair this with an emission of #IdeSourceView::end-macro to complete the sequence.
+   *
+   * Since: 3.32
    */
   signals [BEGIN_MACRO] =
     g_signal_new ("begin-macro",
@@ -5872,6 +5783,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    * operation using the #IdeRenameProvider from the underlying buffer. The
    * cursor position will be used as the location when sending the request to
    * the provider.
+   *
+   * Since: 3.32
    */
   signals [BEGIN_RENAME] =
     g_signal_new ("begin-rename",
@@ -5919,6 +5832,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    * Pressing Escape or unfocusing the widget will break from this loop.
    *
    * Use of this signal is not recommended except in very specific cases.
+   *
+   * Since: 3.32
    */
   signals [CAPTURE_MODIFIER] =
     g_signal_new ("capture-modifier",
@@ -6037,6 +5952,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    * Complete a macro recording sequence. This may be called more times than is necessary,
    * since #IdeSourceView will only keep the most recent macro recording. This can be
    * helpful when implementing recording sequences such as in Vim.
+   *
+   * Since: 3.32
    */
   signals [END_MACRO] =
     g_signal_new ("end-macro",
@@ -6071,7 +5988,7 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
                   NULL, NULL, NULL,
                   G_TYPE_NONE,
                   1,
-                  IDE_TYPE_SOURCE_LOCATION);
+                  IDE_TYPE_LOCATION);
 
   signals [FORMAT_SELECTION] =
     g_signal_new_class_handler ("format-selection",
@@ -6125,6 +6042,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    * Inserts the current modifier character at the insert mark in the buffer.
    * If @use_count is %TRUE, then the character will be inserted
    * #IdeSourceView:count times.
+   *
+   * Since: 3.32
    */
   signals [INSERT_MODIFIER] =
     g_signal_new ("insert-modifier",
@@ -6170,6 +6089,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    * @dir: The direction to move.
    *
    * Moves to the next search result either forwards or backwards.
+   *
+   * Since: 3.32
    */
   signals [MOVE_ERROR] =
     g_signal_new ("move-error",
@@ -6213,6 +6134,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    *
    * Reselects a previousl selected range of text that was saved using
    * IdeSourceView::push-selection.
+   *
+   * Since: 3.32
    */
   signals [POP_SELECTION] =
     g_signal_new ("pop-selection",
@@ -6229,6 +6152,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    * @snippet: An #IdeSnippet.
    *
    * Pops the current snippet from the sourceview if there is one.
+   *
+   * Since: 3.32
    */
   signals [POP_SNIPPET] =
     g_signal_new_class_handler ("pop-snippet",
@@ -6245,6 +6170,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    * Saves the current selection away to be restored by a call to
    * IdeSourceView::pop-selection. You must pop the selection to keep
    * the selection stack in consistent order.
+   *
+   * Since: 3.32
    */
   signals [PUSH_SELECTION] =
     g_signal_new ("push-selection",
@@ -6263,6 +6190,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    *
    * Pushes @snippet onto the snippet stack at either @iter or the insertion
    * mark if @iter is not provided.
+   *
+   * Since: 3.32
    */
   signals [PUSH_SNIPPET] =
     g_signal_new_class_handler ("push-snippet",
@@ -6308,6 +6237,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    *
    * Replays the last series of captured events that were captured between calls
    * to #IdeSourceView::begin-macro and #IdeSourceView::end-macro.
+   *
+   * Since: 3.32
    */
   signals [REPLAY_MACRO] =
     g_signal_new ("replay-macro",
@@ -6335,7 +6266,7 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    * and various stateful settings of the sourceview. This is a good
    * signal to map to the "Escape" key.
    *
-   * Since: 3.26
+   * Since: 3.32
    */
   signals [RESET] =
     g_signal_new_class_handler ("reset",
@@ -6448,6 +6379,8 @@ ide_source_view_class_init (IdeSourceViewClass *klass)
    *
    * This signal is meant to be activated from keybindings to sort the currently selected lines.
    * The lines are sorted using qsort() and either strcmp() or strcasecmp().
+   *
+   * Since: 3.32
    */
   signals [SORT] =
     g_signal_new ("sort",
@@ -6541,6 +6474,9 @@ ide_source_view_init (IdeSourceView *self)
                                      0,
                                      NULL);
 
+  priv->show_line_numbers = TRUE;
+  priv->show_line_changes = TRUE;
+  priv->show_line_diagnostics = TRUE;
   priv->interactive_completion = TRUE;
   priv->target_line_column = 0;
   priv->snippets = g_queue_new ();
@@ -6592,8 +6528,8 @@ ide_source_view_init (IdeSourceView *self)
                                    self,
                                    G_CONNECT_SWAPPED);
   dzl_signal_group_connect_object (priv->buffer_signals,
-                                   "notify::file",
-                                   G_CALLBACK (ide_source_view__buffer_notify_file_cb),
+                                   "notify::file-settings",
+                                   G_CALLBACK (ide_source_view__buffer_notify_file_settings_cb),
                                    self,
                                    G_CONNECT_SWAPPED);
   dzl_signal_group_connect_object (priv->buffer_signals,
@@ -6676,6 +6612,8 @@ ide_source_view_get_font_desc (IdeSourceView *self)
  * account. You must free the result with pango_font_description_free().
  *
  * Returns: (transfer full): a #PangoFontDescription
+ *
+ * Since: 3.32
  */
 PangoFontDescription *
 ide_source_view_get_scaled_font_desc (IdeSourceView *self)
@@ -6740,7 +6678,7 @@ ide_source_view_get_show_line_changes (IdeSourceView *self)
 
   g_return_val_if_fail (IDE_IS_SOURCE_VIEW (self), FALSE);
 
-  return ide_omni_gutter_renderer_get_show_line_changes (priv->omni_renderer);
+  return priv->show_line_changes;
 }
 
 void
@@ -6751,8 +6689,13 @@ ide_source_view_set_show_line_changes (IdeSourceView *self,
 
   g_return_if_fail (IDE_IS_SOURCE_VIEW (self));
 
-  ide_omni_gutter_renderer_set_show_line_changes (priv->omni_renderer, show_line_changes);
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_LINE_CHANGES]);
+  priv->show_line_changes = !!show_line_changes;
+
+  if (priv->gutter)
+    {
+      ide_gutter_set_show_line_changes (priv->gutter, show_line_changes);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_LINE_CHANGES]);
+    }
 }
 
 gboolean
@@ -6762,7 +6705,7 @@ ide_source_view_get_show_line_diagnostics (IdeSourceView *self)
 
   g_return_val_if_fail (IDE_IS_SOURCE_VIEW (self), FALSE);
 
-  return ide_omni_gutter_renderer_get_show_line_diagnostics (priv->omni_renderer);
+  return priv->show_line_diagnostics;
 }
 
 void
@@ -6773,8 +6716,13 @@ ide_source_view_set_show_line_diagnostics (IdeSourceView *self,
 
   g_return_if_fail (IDE_IS_SOURCE_VIEW (self));
 
-  ide_omni_gutter_renderer_set_show_line_diagnostics (priv->omni_renderer, show_line_diagnostics);
-  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_LINE_DIAGNOSTICS]);
+  priv->show_line_diagnostics = !!show_line_diagnostics;
+
+  if (priv->gutter)
+    {
+      ide_gutter_set_show_line_diagnostics (priv->gutter, show_line_diagnostics);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_LINE_DIAGNOSTICS]);
+    }
 }
 
 gboolean
@@ -6959,6 +6907,8 @@ ide_source_view_clear_snippets (IdeSourceView *self)
  * @location: (allow-none): A location for the snippet or %NULL.
  *
  * Pushes a new snippet onto the source view.
+ *
+ * Since: 3.32
  */
 void
 ide_source_view_push_snippet (IdeSourceView     *self,
@@ -7094,6 +7044,8 @@ ide_source_view_jump (IdeSourceView     *self,
  * Gets the #IdeSourceView:scroll-offset property. This property contains the number of lines
  * that should be kept above or below the line containing the insertion cursor relative to the
  * top and bottom of the visible text window.
+ *
+ * Since: 3.32
  */
 guint
 ide_source_view_get_scroll_offset (IdeSourceView *self)
@@ -7110,6 +7062,8 @@ ide_source_view_get_scroll_offset (IdeSourceView *self)
  *
  * Sets the #IdeSourceView:scroll-offset property. See ide_source_view_get_scroll_offset() for
  * more information. Set to 0 to unset this property.
+ *
+ * Since: 3.32
  */
 void
 ide_source_view_set_scroll_offset (IdeSourceView *self,
@@ -7135,6 +7089,8 @@ ide_source_view_set_scroll_offset (IdeSourceView *self,
  * is similar to gtk_text_view_get_visible_area() except that it takes into account the
  * #IdeSourceView:scroll-offset property to ensure there is space above and below the
  * visible_rect.
+ *
+ * Since: 3.32
  */
 void
 ide_source_view_get_visible_rect (IdeSourceView *self,
@@ -7596,6 +7552,8 @@ ide_source_view_place_cursor_onscreen (IdeSourceView *self)
  * such as spaces vs tabs.
  *
  * Returns: (transfer none) (nullable): An #IdeFileSettings or %NULL.
+ *
+ * Since: 3.32
  */
 IdeFileSettings *
 ide_source_view_get_file_settings (IdeSourceView *self)
@@ -7727,6 +7685,8 @@ _ide_source_view_get_scroll_mark (IdeSourceView *self)
  * Gets the current snippet if there is one, otherwise %NULL.
  *
  * Returns: (transfer none) (nullable): An #IdeSnippet or %NULL.
+ *
+ * Since: 3.32
  */
 IdeSnippet *
 ide_source_view_get_current_snippet (IdeSourceView *self)
@@ -7745,7 +7705,7 @@ ide_source_view_get_show_line_numbers (IdeSourceView *self)
 
   g_return_val_if_fail (IDE_IS_SOURCE_VIEW (self), FALSE);
 
-  return ide_omni_gutter_renderer_get_show_line_numbers (priv->omni_renderer);
+  return priv->show_line_numbers;
 }
 
 void
@@ -7756,8 +7716,13 @@ ide_source_view_set_show_line_numbers (IdeSourceView *self,
 
   g_return_if_fail (IDE_IS_SOURCE_VIEW (self));
 
-  ide_omni_gutter_renderer_set_show_line_numbers (priv->omni_renderer, show_line_numbers);
-  g_object_notify (G_OBJECT (self), "show-line-numbers");
+  priv->show_line_numbers = !!show_line_numbers;
+
+  if (priv->gutter)
+    {
+      ide_gutter_set_show_line_numbers (priv->gutter, show_line_numbers);
+      g_object_notify (G_OBJECT (self), "show-line-numbers");
+    }
 }
 
 gboolean
@@ -7778,7 +7743,7 @@ ide_source_view_is_processing_key (IdeSourceView *self)
  *
  * Returns: (transfer none): an #IdeCompletion
  *
- * Since: 3.30
+ * Since: 3.32
  */
 IdeCompletion *
 ide_source_view_get_completion (IdeSourceView *self)
@@ -7798,7 +7763,7 @@ ide_source_view_get_completion (IdeSourceView *self)
  *
  * Returns: %TRUE if there is an active snippet.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 gboolean
 ide_source_view_has_snippet (IdeSourceView *self)
@@ -7809,3 +7774,56 @@ ide_source_view_has_snippet (IdeSourceView *self)
 
   return priv->snippets->length > 0;
 }
+
+/**
+ * ide_source_view_set_gutter:
+ * @self: a #IdeSourceView
+ * @gutter: an #IdeGutter
+ *
+ * Allows setting the gutter for the sourceview.
+ *
+ * Generally, this will always be #IdeOmniGutterRenderer. However, to avoid
+ * circular dependencies, an interface is used to allow plugins to set
+ * this object.
+ *
+ * Since: 3.32
+ */
+void
+ide_source_view_set_gutter (IdeSourceView *self,
+                            IdeGutter     *gutter)
+{
+  IdeSourceViewPrivate *priv = ide_source_view_get_instance_private (self);
+  GtkSourceGutter *left_gutter;
+
+  g_return_if_fail (IDE_IS_SOURCE_VIEW (self));
+  g_return_if_fail (!gutter || IDE_IS_GUTTER (gutter));
+  g_return_if_fail (!gutter || GTK_SOURCE_IS_GUTTER_RENDERER (gutter));
+
+  if (gutter == priv->gutter)
+    return;
+
+  left_gutter = gtk_source_view_get_gutter (GTK_SOURCE_VIEW (self),
+                                            GTK_TEXT_WINDOW_LEFT);
+
+  if (priv->gutter)
+    {
+      gtk_source_gutter_remove (left_gutter, GTK_SOURCE_GUTTER_RENDERER (priv->gutter));
+      g_clear_object (&priv->gutter);
+    }
+
+  if (gutter)
+    {
+      priv->gutter = g_object_ref_sink (gutter);
+      gtk_source_gutter_insert (left_gutter,
+                                GTK_SOURCE_GUTTER_RENDERER (gutter),
+                                0);
+      ide_gutter_set_show_line_numbers (priv->gutter, priv->show_line_numbers);
+      ide_gutter_set_show_line_changes (priv->gutter, priv->show_line_changes);
+      ide_gutter_set_show_line_diagnostics (priv->gutter, priv->show_line_diagnostics);
+      ide_gutter_style_changed (gutter);
+    }
+
+  g_object_notify (G_OBJECT (self), "show-line-changes");
+  g_object_notify (G_OBJECT (self), "show-line-diagnostics");
+  g_object_notify (G_OBJECT (self), "show-line-numbers");
+}
diff --git a/src/libide/sourceview/ide-source-view.h b/src/libide/sourceview/ide-source-view.h
index 71a4a4d88..763f2b162 100644
--- a/src/libide/sourceview/ide-source-view.h
+++ b/src/libide/sourceview/ide-source-view.h
@@ -1,6 +1,6 @@
 /* ide-source-view.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,22 +14,28 @@
  *
  * 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 <gtksourceview/gtksource.h>
+#if !defined (IDE_SOURCEVIEW_INSIDE) && !defined (IDE_SOURCEVIEW_COMPILATION)
+# error "Only <libide-sourceview.h> can be included directly."
+#endif
 
-#include "ide-types.h"
-#include "ide-version-macros.h"
+#include <gtksourceview/gtksource.h>
+#include <libide-code.h>
 
-#include "completion/ide-completion-types.h"
+#include "ide-completion-types.h"
+#include "ide-gutter.h"
+#include "ide-snippet-types.h"
 
 G_BEGIN_DECLS
 
-#define IDE_TYPE_SOURCE_VIEW  (ide_source_view_get_type())
+#define IDE_TYPE_SOURCE_VIEW (ide_source_view_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeSourceView, ide_source_view, IDE, SOURCE_VIEW, GtkSourceView)
 
 typedef enum
@@ -46,6 +52,8 @@ typedef enum
  * @IDE_SOURCE_VIEW_MODE_MODAL: Modal
  *
  * The type of keyboard mode.
+ *
+ * Since: 3.32
  */
 typedef enum
 {
@@ -60,6 +68,8 @@ typedef enum
  * @IDE_SOURCE_VIEW_THEATRIC_SHRINK: shrink from selection location.
  *
  * The style of theatric.
+ *
+ * Since: 3.32
  */
 
 typedef enum
@@ -145,6 +155,8 @@ typedef enum
  *
  * Some of these movements may be modified by using the modify-repeat action.
  * First adjust the repeat and then perform the "movement" action.
+ *
+ * Since: 3.32
  */
 typedef enum
 {
@@ -266,7 +278,7 @@ struct _IdeSourceViewClass
   void (*delete_selection)            (IdeSourceView           *self);
   void (*end_macro)                   (IdeSourceView           *self);
   void (*focus_location)              (IdeSourceView           *self,
-                                       IdeSourceLocation       *location);
+                                       IdeLocation       *location);
   void (*goto_definition)             (IdeSourceView           *self);
   void (*hide_completion)             (IdeSourceView           *self);
   void (*indent_selection)            (IdeSourceView           *self,
@@ -337,157 +349,135 @@ struct _IdeSourceViewClass
   void (*copy_clipboard_extended)     (IdeSourceView           *self);
 
   /*< private >*/
-  gpointer _reserved1;
-  gpointer _reserved2;
-  gpointer _reserved3;
-  gpointer _reserved4;
-  gpointer _reserved5;
-  gpointer _reserved6;
-  gpointer _reserved7;
-  gpointer _reserved8;
-  gpointer _reserved9;
-  gpointer _reserved10;
-  gpointer _reserved11;
-  gpointer _reserved12;
-  gpointer _reserved13;
-  gpointer _reserved14;
-  gpointer _reserved15;
-  gpointer _reserved16;
-  gpointer _reserved17;
-  gpointer _reserved18;
-  gpointer _reserved19;
-  gpointer _reserved20;
-  gpointer _reserved21;
-  gpointer _reserved22;
-  gpointer _reserved23;
+  gpointer _reserved[32];
 };
 
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_has_snippet               (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_clear_snippets            (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeSnippet                 *ide_source_view_get_current_snippet       (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 guint                       ide_source_view_get_visual_column         (IdeSourceView              *self,
                                                                        const GtkTextIter          *location);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_get_visual_position       (IdeSourceView              *self,
                                                                        guint                      *line,
                                                                        guint                      
*line_column);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gint                        ide_source_view_get_count                 (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeFileSettings            *ide_source_view_get_file_settings         (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const PangoFontDescription *ide_source_view_get_font_desc             (IdeSourceView              *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 PangoFontDescription       *ide_source_view_get_scaled_font_desc      (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_get_highlight_current_line(IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_get_insert_matching_brace (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_get_iter_at_visual_column (IdeSourceView              *self,
                                                                        guint                      column,
                                                                        GtkTextIter                *location);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar                *ide_source_view_get_mode_display_name     (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 const gchar                *ide_source_view_get_mode_name             (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_get_overwrite_braces      (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_get_overwrite             (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 guint                       ide_source_view_get_scroll_offset         (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_get_show_grid_lines       (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_get_show_line_changes     (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_get_show_line_diagnostics (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_get_show_line_numbers     (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_get_snippet_completion    (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_get_spell_checking        (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_get_visible_rect          (IdeSourceView              *self,
                                                                        GdkRectangle               
*visible_rect);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_jump                      (IdeSourceView              *self,
                                                                        const GtkTextIter          *from,
                                                                        const GtkTextIter          *to);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_pop_snippet               (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_push_snippet              (IdeSourceView              *self,
                                                                        IdeSnippet                 *snippet,
                                                                        const GtkTextIter          *location);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_rollback_search           (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_save_search               (IdeSourceView              *self,
                                                                        const gchar                
*search_text);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_count                 (IdeSourceView              *self,
                                                                        gint                        count);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_font_desc             (IdeSourceView              *self,
                                                                        const PangoFontDescription 
*font_desc);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_font_name             (IdeSourceView              *self,
                                                                        const gchar                
*font_name);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_highlight_current_line(IdeSourceView              *self,
                                                                        gboolean                    
highlight_current_line);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_insert_matching_brace (IdeSourceView              *self,
                                                                        gboolean                    
insert_matching_brace);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_misspelled_word       (IdeSourceView              *self,
                                                                        GtkTextIter                *start,
                                                                        GtkTextIter                *end);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_overwrite_braces      (IdeSourceView              *self,
                                                                        gboolean                    
overwrite_braces);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_scroll_offset         (IdeSourceView              *self,
                                                                        guint                       
scroll_offset);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_show_grid_lines       (IdeSourceView              *self,
                                                                        gboolean                    
show_grid_lines);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_show_line_changes     (IdeSourceView              *self,
                                                                        gboolean                    
show_line_changes);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_show_line_diagnostics (IdeSourceView              *self,
                                                                        gboolean                    
show_line_diagnostics);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_show_line_numbers     (IdeSourceView              *self,
                                                                        gboolean                    
show_line_numbers);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_snippet_completion    (IdeSourceView              *self,
                                                                        gboolean                    
snippet_completion);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_set_spell_checking        (IdeSourceView              *self,
                                                                        gboolean                    enable);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_move_mark_onscreen        (IdeSourceView              *self,
                                                                        GtkTextMark                *mark);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_place_cursor_onscreen     (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_clear_search              (IdeSourceView              *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_scroll_mark_onscreen      (IdeSourceView              *self,
                                                                        GtkTextMark                *mark,
                                                                        IdeSourceScrollAlign        use_align,
                                                                        gdouble                     alignx,
                                                                        gdouble                     aligny);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_scroll_to_mark            (IdeSourceView              *self,
                                                                        GtkTextMark                *mark,
                                                                        gdouble                     
within_margin,
@@ -495,7 +485,7 @@ void                        ide_source_view_scroll_to_mark            (IdeSource
                                                                        gdouble                     xalign,
                                                                        gdouble                     yalign,
                                                                        gboolean                    
animate_scroll);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_scroll_to_iter            (IdeSourceView              *self,
                                                                        const GtkTextIter          *iter,
                                                                        gdouble                     
within_margin,
@@ -503,18 +493,14 @@ void                        ide_source_view_scroll_to_iter            (IdeSource
                                                                        gdouble                     xalign,
                                                                        gdouble                     yalign,
                                                                        gboolean                    
animate_scroll);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                        ide_source_view_scroll_to_insert          (IdeSourceView              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 IdeCompletion              *ide_source_view_get_completion            (IdeSourceView              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gboolean                    ide_source_view_is_processing_key         (IdeSourceView              *self);
-
-const gchar                *_ide_source_view_get_mode_name            (IdeSourceView              *self) 
G_GNUC_INTERNAL;
-void                        _ide_source_view_set_count                (IdeSourceView              *self,
-                                                                       gint                        count) 
G_GNUC_INTERNAL;
-void                        _ide_source_view_set_modifier             (IdeSourceView              *self,
-                                                                       gunichar                    modifier) 
G_GNUC_INTERNAL;
-GtkTextMark                *_ide_source_view_get_scroll_mark          (IdeSourceView              *self) 
G_GNUC_INTERNAL;
+IDE_AVAILABLE_IN_3_32
+void                        ide_source_view_set_gutter                (IdeSourceView              *self,
+                                                                       IdeGutter                  *gutter);
 
 G_END_DECLS
diff --git a/src/libide/sourceview/ide-text-util.c b/src/libide/sourceview/ide-text-util.c
index 86776cd4b..3ec3c01ec 100644
--- a/src/libide/sourceview/ide-text-util.c
+++ b/src/libide/sourceview/ide-text-util.c
@@ -18,9 +18,11 @@
  *
  * 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
  */
 
-#include "sourceview/ide-text-util.h"
+#include "ide-text-util.h"
 
 void
 ide_text_util_delete_line (GtkTextView *text_view,
diff --git a/src/libide/sourceview/ide-text-util.h b/src/libide/sourceview/ide-text-util.h
index b1a26d6b6..f4fbc1930 100644
--- a/src/libide/sourceview/ide-text-util.h
+++ b/src/libide/sourceview/ide-text-util.h
@@ -1,6 +1,6 @@
 /* ide-text-util.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/libide/sourceview/libide-sourceview.gresource.xml 
b/src/libide/sourceview/libide-sourceview.gresource.xml
new file mode 100644
index 000000000..52cfc8638
--- /dev/null
+++ b/src/libide/sourceview/libide-sourceview.gresource.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/libide-sourceview">
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+  </gresource>
+  <gresource prefix="/org/gnome/libide-sourceview/ui">
+    <file preprocess="xml-stripblanks">ide-completion-list-box-row.ui</file>
+    <file preprocess="xml-stripblanks">ide-completion-overlay.ui</file>
+    <file preprocess="xml-stripblanks">ide-completion-view.ui</file>
+    <file preprocess="xml-stripblanks">ide-completion-window.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/sourceview/libide-sourceview.h b/src/libide/sourceview/libide-sourceview.h
new file mode 100644
index 000000000..d8345b963
--- /dev/null
+++ b/src/libide/sourceview/libide-sourceview.h
@@ -0,0 +1,53 @@
+/* libide-sourceview.h
+ *
+ * Copyright 2018-2019 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-code.h>
+
+G_BEGIN_DECLS
+
+#define IDE_SOURCEVIEW_INSIDE
+
+#include"ide-completion-context.h"
+#include"ide-completion-display.h"
+#include"ide-completion-proposal.h"
+#include"ide-completion-list-box-row.h"
+#include"ide-completion-provider.h"
+#include"ide-completion-types.h"
+#include"ide-completion.h"
+#include"ide-hover-context.h"
+#include"ide-hover-provider.h"
+#include"ide-line-change-gutter-renderer.h"
+#include"ide-gutter.h"
+#include"ide-indenter.h"
+#include"ide-snippet-chunk.h"
+#include"ide-snippet-context.h"
+#include"ide-snippet-parser.h"
+#include"ide-snippet-storage.h"
+#include"ide-snippet-types.h"
+#include"ide-snippet.h"
+#include"ide-source-search-context.h"
+#include"ide-source-view.h"
+#include"ide-text-util.h"
+
+#define IDE_SOURCEVIEW_INSIDE
+
+G_END_DECLS
diff --git a/src/libide/sourceview/meson.build b/src/libide/sourceview/meson.build
index 441203430..7d07a0b0c 100644
--- a/src/libide/sourceview/meson.build
+++ b/src/libide/sourceview/meson.build
@@ -1,46 +1,172 @@
-sourceview_headers = [
+libide_sourceview_header_dir = join_paths(libide_header_dir, 'sourceview')
+libide_sourceview_header_subdir = join_paths(libide_header_subdir, 'sourceview')
+libide_include_directories += include_directories('.')
+
+libide_sourceview_generated_sources = []
+libide_sourceview_generated_headers = []
+
+#
+# Public API Headers
+#
+
+libide_sourceview_private_headers = [
+  'ide-completion-list-box.h',
+  'ide-completion-overlay.h',
+  'ide-completion-private.h',
+  'ide-completion-view.h',
+  'ide-completion-window.h',
+  'ide-cursor.h',
+  'ide-hover-popover-private.h',
+  'ide-hover-private.h',
+  'ide-source-view-capture.h',
+  'ide-source-view-mode.h',
+  'ide-source-view-movements.h',
+  'ide-source-view-private.h',
+]
+
+libide_sourceview_public_headers = [
+  'ide-completion-context.h',
+  'ide-completion-display.h',
+  'ide-completion-list-box-row.h',
+  'ide-completion-proposal.h',
+  'ide-completion-provider.h',
+  'ide-completion-types.h',
+  'ide-completion.h',
+  'ide-gutter.h',
+  'ide-hover-context.h',
+  'ide-hover-provider.h',
   'ide-indenter.h',
-  'ide-language.h',
-  'ide-source-iter.h',
-  'ide-source-style-scheme.h',
-  'ide-source-view.h',
-  'ide-text-iter.h',
+  'ide-line-change-gutter-renderer.h',
+  'ide-snippet-chunk.h',
+  'ide-snippet-context.h',
+  'ide-snippet-parser.h',
+  'ide-snippet-storage.h',
+  'ide-snippet-types.h',
+  'ide-snippet.h',
   'ide-source-search-context.h',
+  'ide-source-view.h',
+  'ide-text-util.h',
+  'libide-sourceview.h',
 ]
 
-sourceview_sources = [
-  'ide-indenter.c',
-  'ide-language.c',
-  'ide-source-iter.c',
-  'ide-source-style-scheme.c',
-  'ide-source-view.c',
-  'ide-text-iter.c',
-  'ide-source-search-context.c',
+libide_sourceview_enum_headers = [
+  'ide-completion-types.h',
+  'ide-source-view.h',
 ]
 
-sourceview_private_sources = [
+install_headers(libide_sourceview_public_headers, subdir: libide_sourceview_header_subdir)
+
+#
+# Sources
+#
+
+libide_sourceview_private_sources = [
+  'ide-completion-list-box.c',
+  'ide-completion-overlay.c',
+  'ide-completion-view.c',
+  'ide-completion-window.c',
   'ide-cursor.c',
-  'ide-cursor.h',
-  'ide-omni-gutter-renderer.c',
-  'ide-omni-gutter-renderer.h',
+  'ide-hover.c',
+  'ide-hover-popover.c',
   'ide-line-change-gutter-renderer.c',
-  'ide-line-change-gutter-renderer.h',
   'ide-source-view-capture.c',
-  'ide-source-view-capture.h',
   'ide-source-view-mode.c',
-  'ide-source-view-mode.h',
   'ide-source-view-movements.c',
   'ide-source-view-shortcuts.c',
   'ide-text-util.c',
 ]
 
-sourceview_enums = [
-  'ide-source-view.h',
+libide_sourceview_public_sources = [
+  'ide-completion-proposal.c',
+  'ide-completion-provider.c',
+  'ide-completion-context.c',
+  'ide-completion-display.c',
+  'ide-completion-list-box-row.c',
+  'ide-completion.c',
+  'ide-gutter.c',
+  'ide-hover-context.c',
+  'ide-hover-provider.c',
+  'ide-indenter.c',
+  'ide-source-search-context.c',
+  'ide-source-view.c',
+  'ide-snippet.c',
+  'ide-snippet-chunk.c',
+  'ide-snippet-context.c',
+  'ide-snippet-parser.c',
+  'ide-snippet-storage.c',
+]
+
+#
+# Generated Resource Files
+#
+
+libide_sourceview_resources = gnome.compile_resources(
+  'ide-sourceview-resources',
+  'libide-sourceview.gresource.xml',
+  c_name: 'ide_sourceview',
+)
+libide_sourceview_generated_headers += [libide_sourceview_resources[1]]
+libide_sourceview_generated_sources += libide_sourceview_resources[0]
+
+#
+# Enum generation
+#
+
+libide_sourceview_enums = gnome.mkenums_simple('ide-source-view-enums',
+     body_prefix: '#include "config.h"',
+   header_prefix: '#include <libide-core.h>',
+       decorator: '_IDE_EXTERN',
+         sources: libide_sourceview_enum_headers,
+  install_header: true,
+     install_dir: libide_sourceview_header_dir,
+)
+libide_sourceview_generated_sources += [libide_sourceview_enums[0]]
+libide_sourceview_generated_headers += [libide_sourceview_enums[1]]
+
+
+#
+# Dependencies
+#
+
+libide_sourceview_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libgtksource_dep,
+  libdazzle_dep,
+
+  libide_core_dep,
+  libide_threading_dep,
+  libide_code_dep,
+  libide_plugins_dep,
+  libide_io_dep,
+  libide_gui_dep,
 ]
 
-libide_public_headers += files(sourceview_headers)
-libide_public_sources += files(sourceview_sources)
-libide_private_sources += files(sourceview_private_sources)
-libide_enum_headers += files(sourceview_enums)
+#
+# Library Definitions
+#
+
+libide_sourceview = static_library('ide-sourceview-' + libide_api_version,
+                                   libide_sourceview_public_sources,
+                                   libide_sourceview_private_sources,
+                                   libide_sourceview_generated_sources,
+                                   libide_sourceview_generated_headers,
+   dependencies: libide_sourceview_deps,
+         c_args: libide_args + release_args + ['-DIDE_SOURCEVIEW_COMPILATION'],
+)
+
+libide_sourceview_dep = declare_dependency(
+              sources: libide_sourceview_private_headers + libide_sourceview_generated_headers,
+         dependencies: libide_sourceview_deps,
+           link_whole: libide_sourceview,
+  include_directories: include_directories('.'),
+)
 
-install_headers(sourceview_headers, subdir: join_paths(libide_header_subdir, 'sourceview'))
+gnome_builder_public_sources += files(libide_sourceview_public_sources)
+gnome_builder_public_headers += files(libide_sourceview_public_headers)
+gnome_builder_private_sources += files(libide_sourceview_private_sources)
+gnome_builder_private_headers += files(libide_sourceview_private_headers)
+gnome_builder_generated_headers += libide_sourceview_generated_headers
+gnome_builder_generated_sources += libide_sourceview_generated_sources
+gnome_builder_include_subdirs += libide_sourceview_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-sourceview.h', '-DIDE_SOURCEVIEW_COMPILATION']
diff --git a/src/libide/terminal/gtk/menus.ui b/src/libide/terminal/gtk/menus.ui
new file mode 100644
index 000000000..b70d3ad94
--- /dev/null
+++ b/src/libide/terminal/gtk/menus.ui
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-terminal-workspace-menu">
+    <section id="ide-terminal-workspace-menu-close">
+      <item>
+        <attribute name="id">ide-terminal-workspace-menu-close</attribute>
+        <attribute name="label" translatable="yes">Close</attribute>
+        <attribute name="action">win.close</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/libide/terminal/ide-terminal-page-actions.c b/src/libide/terminal/ide-terminal-page-actions.c
new file mode 100644
index 000000000..b4f96b5af
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-page-actions.c
@@ -0,0 +1,335 @@
+/* gb-editor-view-actions.c
+ *
+ * Copyright 2015 Sebastien Lafargue <slafargue gnome org>
+ *
+ * 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 "ide-terminal-page"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+#include <libide-terminal.h>
+#include <string.h>
+
+#include "ide-terminal-page-actions.h"
+#include "ide-terminal-page-private.h"
+
+typedef struct
+{
+  VteTerminal    *terminal;
+  GFile          *file;
+  GOutputStream  *stream;
+  gchar          *buffer;
+} SaveTask;
+
+static void
+savetask_free (gpointer data)
+{
+  SaveTask *savetask = (SaveTask *)data;
+
+  if (savetask != NULL)
+    {
+      g_clear_object (&savetask->file);
+      g_clear_object (&savetask->stream);
+      g_clear_object (&savetask->terminal);
+      g_clear_pointer (&savetask->buffer, g_free);
+      g_slice_free (SaveTask, savetask);
+    }
+}
+
+static gboolean
+ide_terminal_page_actions_save_finish (IdeTerminalPage  *view,
+                                      GAsyncResult    *result,
+                                      GError         **error)
+{
+  IdeTask *task = (IdeTask *)result;
+
+  g_return_val_if_fail (ide_task_is_valid (result, view), FALSE);
+
+  g_return_val_if_fail (IDE_IS_TERMINAL_PAGE (view), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (task), FALSE);
+
+  return ide_task_propagate_boolean (task, error);
+}
+
+static void
+save_worker (IdeTask      *task,
+             gpointer      source_object,
+             gpointer      task_data,
+             GCancellable *cancellable)
+{
+  SaveTask *savetask = (SaveTask *)task_data;
+  g_autoptr(GError) error = NULL;
+  gboolean ret;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_TERMINAL_PAGE (source_object));
+  g_assert (savetask != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (savetask->buffer != NULL)
+    {
+      g_autoptr(GInputStream) input_stream = NULL;
+
+      input_stream = g_memory_input_stream_new_from_data (savetask->buffer, -1, NULL);
+      ret = g_output_stream_splice (G_OUTPUT_STREAM (savetask->stream),
+                                    G_INPUT_STREAM (input_stream),
+                                    G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
+                                    cancellable,
+                                    &error);
+    }
+  else
+    {
+      ret = vte_terminal_write_contents_sync (savetask->terminal,
+                                              G_OUTPUT_STREAM (savetask->stream),
+                                              VTE_WRITE_DEFAULT,
+                                              cancellable,
+                                              &error);
+    }
+
+  if (ret)
+    ide_task_return_boolean (task, TRUE);
+  else
+    ide_task_return_error (task, g_steal_pointer (&error));
+}
+
+static void
+ide_terminal_page_actions_save_async (IdeTerminalPage       *view,
+                                     VteTerminal          *terminal,
+                                     GFile                *file,
+                                     GAsyncReadyCallback   callback,
+                                     GCancellable         *cancellable,
+                                     gpointer              user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFileOutputStream) output_stream = NULL;
+  g_autoptr(GError) error = NULL;
+  SaveTask *savetask;
+
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (view, cancellable, callback, user_data);
+
+  output_stream = g_file_replace (file, NULL, FALSE, G_FILE_CREATE_REPLACE_DESTINATION, cancellable, &error);
+
+  if (output_stream != NULL)
+    {
+      savetask = g_slice_new0 (SaveTask);
+      savetask->file = g_object_ref (file);
+      savetask->stream = g_object_ref (G_OUTPUT_STREAM (output_stream));
+      savetask->terminal = g_object_ref (terminal);
+      savetask->buffer = g_steal_pointer (&view->selection_buffer);
+
+      ide_task_set_task_data (task, savetask, savetask_free);
+      save_worker (task, view, savetask, cancellable);
+    }
+  else
+    ide_task_return_error (task, g_steal_pointer (&error));
+}
+
+static void
+save_as_cb (GObject      *object,
+            GAsyncResult *result,
+            gpointer      user_data)
+{
+  IdeTask *task = (IdeTask *)result;
+  IdeTerminalPage *view = user_data;
+  SaveTask *savetask;
+  GFile *file;
+  GError *error = NULL;
+
+  savetask = ide_task_get_task_data (task);
+  file = g_object_ref (savetask->file);
+
+  if (!ide_terminal_page_actions_save_finish (view, result, &error))
+    {
+      g_object_unref (file);
+      g_warning ("%s", error->message);
+      g_clear_error (&error);
+    }
+  else
+    {
+      g_clear_object (&view->save_as_file_top);
+      view->save_as_file_top = file;
+    }
+}
+
+static GFile *
+get_last_focused_terminal_file (IdeTerminalPage *view)
+{
+  GFile *file = NULL;
+
+  if (G_IS_FILE (view->save_as_file_top))
+    file = view->save_as_file_top;
+
+  return file;
+}
+
+static VteTerminal *
+get_last_focused_terminal (IdeTerminalPage *view)
+{
+  return VTE_TERMINAL (view->terminal_top);
+}
+
+static gchar *
+gb_terminal_get_selected_text (IdeTerminalPage  *view,
+                               VteTerminal    **terminal_p)
+{
+  VteTerminal *terminal;
+  gchar *buf = NULL;
+
+  terminal = get_last_focused_terminal (view);
+  if (terminal_p != NULL)
+    *terminal_p = terminal;
+
+  if (vte_terminal_get_has_selection (terminal))
+    {
+      vte_terminal_copy_primary (terminal);
+      buf = gtk_clipboard_wait_for_text (gtk_clipboard_get (GDK_SELECTION_PRIMARY));
+    }
+
+  return buf;
+}
+
+static void
+save_as_response (GtkWidget *widget,
+                  gint       response,
+                  gpointer   user_data)
+{
+  g_autoptr(IdeTerminalPage) view = user_data;
+  g_autoptr(GFile) file = NULL;
+  GtkFileChooser *chooser = (GtkFileChooser *)widget;
+  VteTerminal *terminal;
+
+  g_assert (GTK_IS_FILE_CHOOSER (chooser));
+  g_assert (IDE_IS_TERMINAL_PAGE (view));
+
+  switch (response)
+    {
+    case GTK_RESPONSE_OK:
+      file = gtk_file_chooser_get_file (chooser);
+      terminal = get_last_focused_terminal (view);
+      ide_terminal_page_actions_save_async (view, terminal, file, save_as_cb, NULL, view);
+      break;
+
+    case GTK_RESPONSE_CANCEL:
+      g_free (view->selection_buffer);
+
+    default:
+      break;
+    }
+
+  gtk_widget_destroy (widget);
+}
+
+static void
+ide_terminal_page_actions_save_as (GSimpleAction *action,
+                                  GVariant      *param,
+                                  gpointer       user_data)
+{
+  IdeTerminalPage *view = user_data;
+  GtkWidget *suggested;
+  GtkWidget *toplevel;
+  GtkWidget *dialog;
+  GFile *file = NULL;
+
+  g_assert (IDE_IS_TERMINAL_PAGE (view));
+
+  /* We can't get this later because the dialog makes the terminal
+   * unfocused and thus resets the selection
+   */
+  view->selection_buffer = gb_terminal_get_selected_text (view, NULL);
+
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (view));
+  dialog = g_object_new (GTK_TYPE_FILE_CHOOSER_DIALOG,
+                         "action", GTK_FILE_CHOOSER_ACTION_SAVE,
+                         "do-overwrite-confirmation", TRUE,
+                         "local-only", FALSE,
+                         "modal", TRUE,
+                         "select-multiple", FALSE,
+                         "show-hidden", FALSE,
+                         "transient-for", toplevel,
+                         "title", _("Save Terminal Content As"),
+                         NULL);
+
+  file = get_last_focused_terminal_file (view);
+  if (file != NULL)
+    gtk_file_chooser_set_file (GTK_FILE_CHOOSER (dialog), file, NULL);
+
+  gtk_dialog_add_buttons (GTK_DIALOG (dialog),
+                          _("Cancel"), GTK_RESPONSE_CANCEL,
+                          _("Save"), GTK_RESPONSE_OK,
+                          NULL);
+  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK);
+
+  suggested = gtk_dialog_get_widget_for_response (GTK_DIALOG (dialog), GTK_RESPONSE_OK);
+  gtk_style_context_add_class (gtk_widget_get_style_context (suggested),
+                               GTK_STYLE_CLASS_SUGGESTED_ACTION);
+
+  g_signal_connect (dialog, "response", G_CALLBACK (save_as_response), g_object_ref (view));
+
+  gtk_window_present (GTK_WINDOW (dialog));
+}
+
+static void
+ide_terminal_page_actions_reset (GSimpleAction *action,
+                                GVariant      *param,
+                                gpointer       user_data)
+{
+  IdeTerminalPage *self = user_data;
+  VteTerminal *terminal;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_TERMINAL_PAGE (self));
+
+  terminal = get_last_focused_terminal (self);
+  vte_terminal_reset (terminal, TRUE, FALSE);
+}
+
+static void
+ide_terminal_page_actions_reset_and_clear (GSimpleAction *action,
+                                          GVariant      *param,
+                                          gpointer       user_data)
+{
+  IdeTerminalPage *self = user_data;
+  VteTerminal *terminal;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (IDE_IS_TERMINAL_PAGE (self));
+
+  terminal = get_last_focused_terminal (self);
+  vte_terminal_reset (terminal, TRUE, TRUE);
+}
+
+static GActionEntry IdeTerminalPageActions[] = {
+  { "save-as", ide_terminal_page_actions_save_as },
+  { "reset", ide_terminal_page_actions_reset },
+  { "reset-and-clear", ide_terminal_page_actions_reset_and_clear },
+};
+
+void
+ide_terminal_page_actions_init (IdeTerminalPage *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group), IdeTerminalPageActions,
+                                   G_N_ELEMENTS (IdeTerminalPageActions), self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "terminal-view", G_ACTION_GROUP (group));
+}
diff --git a/src/libide/terminal/ide-terminal-page-actions.h b/src/libide/terminal/ide-terminal-page-actions.h
new file mode 100644
index 000000000..925ed90d8
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-page-actions.h
@@ -0,0 +1,29 @@
+/* ide-terminal-page-actions.h
+ *
+ * opyright (C) 2015 Sebastien Lafargue <slafargue gnome org>
+ *
+ * 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 "ide-terminal-page.h"
+
+G_BEGIN_DECLS
+
+void ide_terminal_page_actions_init (IdeTerminalPage *self);
+
+G_END_DECLS
diff --git a/src/libide/terminal/ide-terminal-page-private.h b/src/libide/terminal/ide-terminal-page-private.h
new file mode 100644
index 000000000..8005602e7
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-page-private.h
@@ -0,0 +1,66 @@
+/* ide-terminal-page-private.h
+ *
+ * Copyright 2015 Sebastien Lafargue <slafargue gnome org>
+ *
+ * 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-foundry.h>
+#include <libide-gui.h>
+#include <libide-terminal.h>
+
+G_BEGIN_DECLS
+
+struct _IdeTerminalPage
+{
+  IdePage              parent_instance;
+
+  /*
+   * If we are spawning a process in a runtime instead of the
+   * host, then we will have a runtime pointer here.
+   */
+  IdeRuntime          *runtime;
+
+  GtkOverlay          *terminal_overlay_top;
+
+  GtkRevealer         *search_revealer_top;
+
+  IdeTerminal         *terminal_top;
+
+  GtkScrollbar        *top_scrollbar;
+
+  IdeTerminalSearch   *tsearch;
+
+  GFile               *save_as_file_top;
+
+  gchar               *selection_buffer;
+
+  gchar               *cwd;
+
+  VtePty              *pty;
+
+  gint64               last_respawn;
+
+  guint                manage_spawn : 1;
+  guint                top_has_spawned : 1;
+  guint                top_has_needs_attention : 1;
+  guint                run_on_host : 1;
+  guint                use_runner : 1;
+};
+
+G_END_DECLS
diff --git a/src/libide/terminal/ide-terminal-page.c b/src/libide/terminal/ide-terminal-page.c
new file mode 100644
index 000000000..27ba33298
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-page.c
@@ -0,0 +1,765 @@
+/* ide-terminal-page.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-terminal-page"
+
+#include "config.h"
+
+#include <fcntl.h>
+#include <glib/gi18n.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+#include <libide-terminal.h>
+#include <stdlib.h>
+#include <vte/vte.h>
+#include <unistd.h>
+
+#define PCRE2_CODE_UNIT_WIDTH 0
+#include <pcre2.h>
+
+#include "ide-terminal-page.h"
+#include "ide-terminal-page-private.h"
+#include "ide-terminal-page-actions.h"
+
+G_DEFINE_TYPE (IdeTerminalPage, ide_terminal_page, IDE_TYPE_PAGE)
+
+enum {
+  PROP_0,
+  PROP_CWD,
+  PROP_MANAGE_SPAWN,
+  PROP_PTY,
+  PROP_RUNTIME,
+  PROP_RUN_ON_HOST,
+  PROP_USE_RUNNER,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void ide_terminal_page_connect_terminal (IdeTerminalPage *self,
+                                                VteTerminal     *terminal);
+static void gbp_terminal_respawn               (IdeTerminalPage *self,
+                                                VteTerminal     *terminal);
+
+static gboolean
+shell_supports_login (const gchar *shell)
+{
+  g_autofree gchar *name = NULL;
+
+  /* Shells that support --login */
+  static const gchar *supported[] = {
+    "bash",
+  };
+
+  if (shell == NULL)
+    return FALSE;
+
+  if (!(name = g_path_get_basename (shell)))
+    return FALSE;
+
+  for (guint i = 0; i < G_N_ELEMENTS (supported); i++)
+    {
+      if (g_str_equal (name, supported[i]))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_terminal_page_wait_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  VteTerminal *terminal = user_data;
+  IdeTerminalPage *self;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SUBPROCESS (subprocess));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (VTE_IS_TERMINAL (terminal));
+
+  if (!ide_subprocess_wait_finish (subprocess, result, &error))
+    {
+      g_warning ("%s", error->message);
+      IDE_GOTO (failure);
+    }
+
+  self = (IdeTerminalPage *)gtk_widget_get_ancestor (GTK_WIDGET (terminal), IDE_TYPE_TERMINAL_PAGE);
+  if (self == NULL)
+    IDE_GOTO (failure);
+
+  if (!dzl_gtk_widget_action (GTK_WIDGET (self), "frame", "close-page", NULL))
+    {
+      if (!gtk_widget_in_destruction (GTK_WIDGET (terminal)))
+        gbp_terminal_respawn (self, terminal);
+    }
+
+failure:
+  g_clear_object (&terminal);
+
+  IDE_EXIT;
+}
+
+static void
+ide_terminal_page_run_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  IdeRunner *runner = (IdeRunner *)object;
+  VteTerminal *terminal = user_data;
+  IdeTerminalPage *self;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUNNER (runner));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (VTE_IS_TERMINAL (terminal));
+
+  if (!ide_runner_run_finish (runner, result, &error))
+    {
+      g_warning ("%s", error->message);
+      IDE_GOTO (failure);
+    }
+
+  self = (IdeTerminalPage *)gtk_widget_get_ancestor (GTK_WIDGET (terminal), IDE_TYPE_TERMINAL_PAGE);
+  if (self == NULL)
+    IDE_GOTO (failure);
+
+  if (!dzl_gtk_widget_action (GTK_WIDGET (self), "frame", "close-page", NULL))
+    {
+      if (!gtk_widget_in_destruction (GTK_WIDGET (terminal)))
+        gbp_terminal_respawn (self, terminal);
+    }
+
+failure:
+  ide_object_destroy (IDE_OBJECT (runner));
+  g_clear_object (&terminal);
+
+  IDE_EXIT;
+}
+
+static gboolean
+terminal_has_notification_signal (void)
+{
+  GQuark quark;
+  guint signal_id;
+
+  return g_signal_parse_name ("notification-received",
+                              VTE_TYPE_TERMINAL,
+                              &signal_id,
+                              &quark,
+                              FALSE);
+}
+
+static void
+gbp_terminal_respawn (IdeTerminalPage *self,
+                      VteTerminal     *terminal)
+{
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *workpath = NULL;
+  g_autofree gchar *shell = NULL;
+  IdeBuildPipeline *pipeline = NULL;
+  IdeWorkbench *workbench;
+  IdeContext *context;
+  VtePty *pty = NULL;
+  gint64 now;
+  int tty_fd = -1;
+  gint stdout_fd = -1;
+  gint stderr_fd = -1;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_TERMINAL_PAGE (self));
+
+  vte_terminal_reset (terminal, TRUE, TRUE);
+
+  if (!(workbench = ide_widget_get_workbench (GTK_WIDGET (self))))
+    IDE_EXIT;
+
+  /* Prevent flapping */
+  now = g_get_monotonic_time ();
+  if ((now - self->last_respawn) < (G_USEC_PER_SEC / 10))
+    IDE_EXIT;
+  self->last_respawn = now;
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  workdir = ide_context_ref_workdir (context);
+  workpath = g_file_get_path (workdir);
+
+  if (ide_workbench_has_project (workbench))
+    {
+      IdeBuildManager *build_manager;
+
+      build_manager = ide_build_manager_from_context (context);
+      pipeline = ide_build_manager_get_pipeline (build_manager);
+    }
+
+  shell = g_strdup (ide_get_user_shell ());
+
+  pty = vte_terminal_pty_new_sync (terminal,
+                                   VTE_PTY_DEFAULT | VTE_PTY_NO_LASTLOG | VTE_PTY_NO_UTMP | VTE_PTY_NO_WTMP,
+                                   NULL,
+                                   &error);
+  if (pty == NULL)
+    IDE_GOTO (cleanup);
+
+  vte_terminal_set_pty (terminal, pty);
+
+  if (-1 == (tty_fd = ide_vte_pty_create_slave (pty)))
+    IDE_GOTO (cleanup);
+
+  if (self->runtime != NULL &&
+      !ide_runtime_contains_program_in_path (self->runtime, shell, NULL))
+    {
+      g_free (shell);
+      shell = g_strdup ("/bin/bash");
+    }
+
+  /* they want to use the runner API, which means we spawn in the
+   * program mount namespace, etc.
+   */
+  if (self->runtime != NULL && self->use_runner)
+    {
+      g_autoptr(IdeSimpleBuildTarget) target = NULL;
+      g_autoptr(IdeRunner) runner = NULL;
+      const gchar *argv[] = { shell, NULL };
+
+
+      target = ide_simple_build_target_new (context);
+      ide_simple_build_target_set_argv (target, argv);
+      ide_simple_build_target_set_cwd (target, self->cwd ?: workpath);
+
+      runner = ide_runtime_create_runner (self->runtime, IDE_BUILD_TARGET (target));
+
+      if (runner != NULL)
+        {
+          IdeEnvironment *env = ide_runner_get_environment (runner);
+
+          /* set_tty() will dup() the fd */
+          ide_runner_set_tty (runner, tty_fd);
+
+          ide_environment_setenv (env, "TERM", "xterm-256color");
+          ide_environment_setenv (env, "INSIDE_GNOME_BUILDER", PACKAGE_VERSION);
+          ide_environment_setenv (env, "SHELL", shell);
+
+          if (pipeline != NULL)
+            {
+              ide_environment_setenv (env, "BUILDDIR", ide_build_pipeline_get_builddir (pipeline));
+              ide_environment_setenv (env, "SRCDIR", ide_build_pipeline_get_srcdir (pipeline));
+            }
+
+          ide_runner_run_async (runner,
+                                NULL,
+                                ide_terminal_page_run_cb,
+                                g_object_ref (terminal));
+          IDE_GOTO (cleanup);
+        }
+    }
+
+  /* dup() is safe as it will inherit O_CLOEXEC */
+  if (-1 == (stdout_fd = dup (tty_fd)) || -1 == (stderr_fd = dup (tty_fd)))
+    IDE_GOTO (cleanup);
+
+  if (self->runtime != NULL)
+    launcher = ide_runtime_create_launcher (self->runtime, NULL);
+
+  if (launcher == NULL)
+    launcher = ide_subprocess_launcher_new (0);
+
+  ide_subprocess_launcher_set_flags (launcher, 0);
+  ide_subprocess_launcher_set_run_on_host (launcher, self->run_on_host);
+  ide_subprocess_launcher_set_clear_env (launcher, FALSE);
+  ide_subprocess_launcher_push_argv (launcher, shell);
+  if (shell_supports_login (shell))
+    ide_subprocess_launcher_push_argv (launcher, "--login");
+  ide_subprocess_launcher_take_stdin_fd (launcher, tty_fd);
+  ide_subprocess_launcher_take_stdout_fd (launcher, stdout_fd);
+  ide_subprocess_launcher_take_stderr_fd (launcher, stderr_fd);
+  ide_subprocess_launcher_setenv (launcher, "TERM", "xterm-256color", TRUE);
+  ide_subprocess_launcher_setenv (launcher, "INSIDE_GNOME_BUILDER", PACKAGE_VERSION, TRUE);
+  ide_subprocess_launcher_setenv (launcher, "SHELL", shell, TRUE);
+
+  if (self->cwd != NULL)
+    ide_subprocess_launcher_set_cwd (launcher, self->cwd);
+  else
+    ide_subprocess_launcher_set_cwd (launcher, workpath);
+
+  if (pipeline != NULL)
+    {
+      ide_subprocess_launcher_setenv (launcher, "BUILDDIR", ide_build_pipeline_get_builddir (pipeline), 
TRUE);
+      ide_subprocess_launcher_setenv (launcher, "SRCDIR", ide_build_pipeline_get_srcdir (pipeline), TRUE);
+    }
+
+  tty_fd = -1;
+  stdout_fd = -1;
+  stderr_fd = -1;
+
+  if (NULL == (subprocess = ide_subprocess_launcher_spawn (launcher, NULL, &error)))
+    IDE_GOTO (cleanup);
+
+  ide_subprocess_wait_async (subprocess,
+                             NULL,
+                             ide_terminal_page_wait_cb,
+                             g_object_ref (terminal));
+
+cleanup:
+  if (tty_fd != -1)
+    close (tty_fd);
+
+  if (stdout_fd != -1)
+    close (stdout_fd);
+
+  if (stderr_fd != -1)
+    close (stderr_fd);
+
+  g_clear_object (&pty);
+
+  if (error != NULL)
+    g_warning ("%s", error->message);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_terminal_realize (GtkWidget *widget)
+{
+  IdeTerminalPage *self = (IdeTerminalPage *)widget;
+
+  g_assert (IDE_IS_TERMINAL_PAGE (self));
+
+  GTK_WIDGET_CLASS (ide_terminal_page_parent_class)->realize (widget);
+
+  if (self->manage_spawn && !self->top_has_spawned)
+    {
+      self->top_has_spawned = TRUE;
+      gbp_terminal_respawn (self, VTE_TERMINAL (self->terminal_top));
+    }
+
+  if (!self->manage_spawn && self->pty != NULL)
+    vte_terminal_set_pty (VTE_TERMINAL (self->terminal_top), self->pty);
+}
+
+static void
+gbp_terminal_get_preferred_width (GtkWidget *widget,
+                                  gint      *min_width,
+                                  gint      *nat_width)
+{
+  /*
+   * Since we are placing the terminal in a GtkStack, we need
+   * to fake the size a bit. Otherwise, GtkStack tries to keep the
+   * widget at its natural size (which prevents us from getting
+   * appropriate size requests.
+   */
+  GTK_WIDGET_CLASS (ide_terminal_page_parent_class)->get_preferred_width (widget, min_width, nat_width);
+  *nat_width = *min_width;
+}
+
+static void
+gbp_terminal_get_preferred_height (GtkWidget *widget,
+                                   gint      *min_height,
+                                   gint      *nat_height)
+{
+  /*
+   * Since we are placing the terminal in a GtkStack, we need
+   * to fake the size a bit. Otherwise, GtkStack tries to keep the
+   * widget at its natural size (which prevents us from getting
+   * appropriate size requests.
+   */
+  GTK_WIDGET_CLASS (ide_terminal_page_parent_class)->get_preferred_height (widget, min_height, nat_height);
+  *nat_height = *min_height;
+}
+
+static void
+gbp_terminal_set_needs_attention (IdeTerminalPage *self,
+                                  gboolean         needs_attention)
+{
+  GtkWidget *parent;
+
+  g_assert (IDE_IS_TERMINAL_PAGE (self));
+
+  parent = gtk_widget_get_parent (GTK_WIDGET (self));
+
+  if (GTK_IS_STACK (parent) &&
+      !gtk_widget_in_destruction (GTK_WIDGET (self)) &&
+      !gtk_widget_in_destruction (parent))
+    {
+      if (!gtk_widget_in_destruction (GTK_WIDGET (self->terminal_top)))
+        self->top_has_needs_attention = !!needs_attention;
+
+      gtk_container_child_set (GTK_CONTAINER (parent), GTK_WIDGET (self),
+                               "needs-attention", needs_attention,
+                               NULL);
+    }
+}
+
+static void
+notification_received_cb (VteTerminal     *terminal,
+                          const gchar     *summary,
+                          const gchar     *body,
+                          IdeTerminalPage *self)
+{
+  g_assert (VTE_IS_TERMINAL (terminal));
+  g_assert (IDE_IS_TERMINAL_PAGE (self));
+
+  if (!gtk_widget_has_focus (GTK_WIDGET (terminal)))
+    gbp_terminal_set_needs_attention (self, TRUE);
+}
+
+static gboolean
+focus_in_event_cb (VteTerminal     *terminal,
+                   GdkEvent        *event,
+                   IdeTerminalPage *self)
+{
+  g_assert (VTE_IS_TERMINAL (terminal));
+  g_assert (IDE_IS_TERMINAL_PAGE (self));
+
+  self->top_has_needs_attention = FALSE;
+  gbp_terminal_set_needs_attention (self, FALSE);
+  gtk_revealer_set_reveal_child (self->search_revealer_top, FALSE);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+window_title_changed_cb (VteTerminal     *terminal,
+                         IdeTerminalPage *self)
+{
+  const gchar *title;
+
+  g_assert (VTE_IS_TERMINAL (terminal));
+  g_assert (IDE_IS_TERMINAL_PAGE (self));
+
+  title = vte_terminal_get_window_title (VTE_TERMINAL (self->terminal_top));
+
+  if (title == NULL)
+    title = _("Untitled terminal");
+
+  ide_page_set_title (IDE_PAGE (self), title);
+}
+
+static void
+style_context_changed (GtkStyleContext *style_context,
+                       IdeTerminalPage *self)
+{
+  GtkStateFlags state;
+  GdkRGBA fg;
+  GdkRGBA bg;
+
+  g_assert (GTK_IS_STYLE_CONTEXT (style_context));
+  g_assert (IDE_IS_TERMINAL_PAGE (self));
+
+  state = gtk_style_context_get_state (style_context);
+
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+  gtk_style_context_get_color (style_context, state, &fg);
+  gtk_style_context_get_background_color (style_context, state, &bg);
+  G_GNUC_END_IGNORE_DEPRECATIONS;
+
+  if (bg.alpha == 0.0)
+    gdk_rgba_parse (&bg, "#f6f7f8");
+
+  ide_page_set_primary_color_fg (IDE_PAGE (self), &fg);
+  ide_page_set_primary_color_bg (IDE_PAGE (self), &bg);
+}
+
+static IdePage *
+gbp_terminal_create_split (IdePage *page)
+{
+  g_assert (IDE_IS_TERMINAL_PAGE (page));
+
+  return g_object_new (IDE_TYPE_TERMINAL_PAGE,
+                       "visible", TRUE,
+                       NULL);
+}
+
+static void
+gbp_terminal_grab_focus (GtkWidget *widget)
+{
+  IdeTerminalPage *self = (IdeTerminalPage *)widget;
+
+  g_assert (IDE_IS_TERMINAL_PAGE (self));
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->terminal_top));
+}
+
+static void
+ide_terminal_page_connect_terminal (IdeTerminalPage *self,
+                                    VteTerminal     *terminal)
+{
+  GtkAdjustment *vadj;
+
+  vadj = gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (terminal));
+
+  gtk_range_set_adjustment (GTK_RANGE (self->top_scrollbar), vadj);
+
+  g_signal_connect_object (terminal,
+                           "focus-in-event",
+                           G_CALLBACK (focus_in_event_cb),
+                           self,
+                           0);
+
+  g_signal_connect_object (terminal,
+                           "window-title-changed",
+                           G_CALLBACK (window_title_changed_cb),
+                           self,
+                           0);
+
+  if (terminal_has_notification_signal ())
+    {
+      g_signal_connect_object (terminal,
+                               "notification-received",
+                               G_CALLBACK (notification_received_cb),
+                               self,
+                               0);
+    }
+}
+
+static void
+ide_terminal_page_finalize (GObject *object)
+{
+  IdeTerminalPage *self = IDE_TERMINAL_PAGE (object);
+
+  g_clear_object (&self->save_as_file_top);
+  g_clear_pointer (&self->cwd, g_free);
+  g_clear_pointer (&self->selection_buffer, g_free);
+  g_clear_object (&self->pty);
+  g_clear_object (&self->runtime);
+
+  G_OBJECT_CLASS (ide_terminal_page_parent_class)->finalize (object);
+}
+
+static void
+ide_terminal_page_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeTerminalPage *self = IDE_TERMINAL_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_MANAGE_SPAWN:
+      g_value_set_boolean (value, self->manage_spawn);
+      break;
+
+    case PROP_PTY:
+      g_value_set_object (value, self->pty);
+      break;
+
+    case PROP_RUNTIME:
+      g_value_set_object (value, self->runtime);
+      break;
+
+    case PROP_RUN_ON_HOST:
+      g_value_set_boolean (value, self->run_on_host);
+      break;
+
+    case PROP_USE_RUNNER:
+      g_value_set_boolean (value, self->use_runner);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_terminal_page_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  IdeTerminalPage *self = IDE_TERMINAL_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CWD:
+      self->cwd = g_value_dup_string (value);
+      break;
+
+    case PROP_MANAGE_SPAWN:
+      self->manage_spawn = g_value_get_boolean (value);
+      break;
+
+    case PROP_PTY:
+      self->pty = g_value_dup_object (value);
+      break;
+
+    case PROP_RUNTIME:
+      self->runtime = g_value_dup_object (value);
+      break;
+
+    case PROP_RUN_ON_HOST:
+      self->run_on_host = g_value_get_boolean (value);
+      break;
+
+    case PROP_USE_RUNNER:
+      self->use_runner = g_value_get_boolean (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_terminal_page_class_init (IdeTerminalPageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdePageClass *page_class = IDE_PAGE_CLASS (klass);
+
+  object_class->finalize = ide_terminal_page_finalize;
+  object_class->get_property = ide_terminal_page_get_property;
+  object_class->set_property = ide_terminal_page_set_property;
+
+  widget_class->realize = gbp_terminal_realize;
+  widget_class->get_preferred_width = gbp_terminal_get_preferred_width;
+  widget_class->get_preferred_height = gbp_terminal_get_preferred_height;
+  widget_class->grab_focus = gbp_terminal_grab_focus;
+
+  page_class->create_split = gbp_terminal_create_split;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-terminal/ui/ide-terminal-page.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeTerminalPage, terminal_top);
+  gtk_widget_class_bind_template_child (widget_class, IdeTerminalPage, top_scrollbar);
+  gtk_widget_class_bind_template_child (widget_class, IdeTerminalPage, terminal_overlay_top);
+
+  properties [PROP_CWD] =
+    g_param_spec_string ("cwd",
+                         "CWD",
+                         "The directory to spawn the terminal in",
+                         NULL,
+                         G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_MANAGE_SPAWN] =
+    g_param_spec_boolean ("manage-spawn",
+                          "Manage Spawn",
+                          "Manage Spawn",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PTY] =
+    g_param_spec_object ("pty",
+                         "Pty",
+                         "The pseudo terminal to use",
+                         VTE_TYPE_PTY,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RUNTIME] =
+    g_param_spec_object ("runtime",
+                         "Runtime",
+                         "The runtime to use for spawning",
+                         IDE_TYPE_RUNTIME,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RUN_ON_HOST] =
+    g_param_spec_boolean ("run-on-host",
+                          "Run on Host",
+                          "If the process should be spawned on the host",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_USE_RUNNER] =
+    g_param_spec_boolean ("use-runner",
+                          "Use Runner",
+                          "If we should use the runner interface and build target",
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_terminal_page_init (IdeTerminalPage *self)
+{
+  GtkStyleContext *style_context;
+
+  self->run_on_host = TRUE;
+  self->manage_spawn = TRUE;
+
+  self->tsearch = g_object_new (IDE_TYPE_TERMINAL_SEARCH,
+                                "visible", TRUE,
+                                NULL);
+  self->search_revealer_top = ide_terminal_search_get_revealer (self->tsearch);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  ide_page_set_icon_name (IDE_PAGE (self), "utilities-terminal-symbolic");
+  ide_page_set_can_split (IDE_PAGE (self), TRUE);
+  ide_page_set_menu_id (IDE_PAGE (self), "terminal-page-document-menu");
+
+  gtk_overlay_add_overlay (self->terminal_overlay_top, GTK_WIDGET (self->tsearch));
+
+  ide_terminal_page_connect_terminal (self, VTE_TERMINAL (self->terminal_top));
+
+  ide_terminal_search_set_terminal (self->tsearch, VTE_TERMINAL (self->terminal_top));
+
+  ide_terminal_page_actions_init (self);
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (self->terminal_top));
+  gtk_style_context_add_class (style_context, "terminal");
+  g_signal_connect_object (style_context,
+                           "changed",
+                           G_CALLBACK (style_context_changed),
+                           self,
+                           0);
+  style_context_changed (style_context, self);
+
+  gtk_widget_set_can_focus (GTK_WIDGET (self->terminal_top), TRUE);
+}
+
+void
+ide_terminal_page_set_pty (IdeTerminalPage *self,
+                           VtePty          *pty)
+{
+  g_return_if_fail (IDE_IS_TERMINAL_PAGE (self));
+  g_return_if_fail (VTE_IS_PTY (pty));
+
+  if (self->manage_spawn)
+    {
+      g_warning ("Cannot set pty when IdeTerminalPage manages tty");
+      return;
+    }
+
+  if (self->terminal_top)
+    {
+      vte_terminal_reset (VTE_TERMINAL (self->terminal_top), TRUE, TRUE);
+      vte_terminal_set_pty (VTE_TERMINAL (self->terminal_top), pty);
+    }
+}
+
+void
+ide_terminal_page_feed (IdeTerminalPage *self,
+                        const gchar     *message)
+{
+  g_return_if_fail (IDE_IS_TERMINAL_PAGE (self));
+
+  if (self->terminal_top != NULL)
+    vte_terminal_feed (VTE_TERMINAL (self->terminal_top), message, -1);
+}
diff --git a/src/libide/terminal/ide-terminal-page.h b/src/libide/terminal/ide-terminal-page.h
new file mode 100644
index 000000000..901bc5b05
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-page.h
@@ -0,0 +1,45 @@
+/* ide-terminal-page.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_TERMINAL_INSIDE) && !defined (IDE_TERMINAL_COMPILATION)
+# error "Only <libide-terminal.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <libide-gui.h>
+#include <vte/vte.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TERMINAL_PAGE (ide_terminal_page_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTerminalPage, ide_terminal_page, IDE, TERMINAL_PAGE, IdePage)
+
+IDE_AVAILABLE_IN_3_32
+void ide_terminal_page_set_pty (IdeTerminalPage *self,
+                                VtePty          *pty);
+IDE_AVAILABLE_IN_3_32
+void ide_terminal_page_feed    (IdeTerminalPage *self,
+                                const gchar     *message);
+
+G_END_DECLS
diff --git a/src/libide/terminal/ide-terminal-page.ui b/src/libide/terminal/ide-terminal-page.ui
new file mode 100644
index 000000000..708915a26
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-page.ui
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.16 -->
+  <template class="IdeTerminalPage" parent="IdePage">
+    <property name="visible">true</property>
+    <child>
+      <object class="GtkPaned" id="paned">
+        <property name="expand">true</property>
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkOverlay" id="terminal_overlay_top">
+            <property name="expand">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkBox" id="top_container">
+                <property name="orientation">horizontal</property>
+                <property name="expand">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="IdeTerminal" id="terminal_top">
+                    <property name="audible-bell">false</property>
+                    <property name="expand">true</property>
+                    <property name="visible">true</property>
+                    <property name="scrollback-lines">0xffffffff</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkScrollbar" id="top_scrollbar">
+                    <property name="orientation">vertical</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/terminal/ide-terminal-private.h b/src/libide/terminal/ide-terminal-private.h
index 4552e648f..9b53f3b25 100644
--- a/src/libide/terminal/ide-terminal-private.h
+++ b/src/libide/terminal/ide-terminal-private.h
@@ -1,6 +1,6 @@
 /* ide-terminal-private.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/libide/terminal/ide-terminal-search-private.h 
b/src/libide/terminal/ide-terminal-search-private.h
index 925cfade0..2ace9737d 100644
--- a/src/libide/terminal/ide-terminal-search-private.h
+++ b/src/libide/terminal/ide-terminal-search-private.h
@@ -14,13 +14,14 @@
  *
  * 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 <gtk/gtk.h>
-
-#include "search/ide-tagged-entry.h"
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
@@ -31,7 +32,7 @@ struct _IdeTerminalSearch
   VteTerminal         *terminal;
 
   GtkRevealer         *search_revealer;
-  
+
   IdeTaggedEntry      *search_entry;
 
   GtkButton           *search_prev_button;
diff --git a/src/libide/terminal/ide-terminal-search.c b/src/libide/terminal/ide-terminal-search.c
index 057e73dea..6eb2f4629 100644
--- a/src/libide/terminal/ide-terminal-search.c
+++ b/src/libide/terminal/ide-terminal-search.c
@@ -1,6 +1,6 @@
 /* ide-terminal-search.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-terminal-search"
@@ -23,14 +25,13 @@
 
 #include <fcntl.h>
 #include <glib/gi18n.h>
-#include <ide.h>
 #include <pcre2.h>
 #include <stdlib.h>
 #include <vte/vte.h>
 #include <unistd.h>
 
-#include "terminal/ide-terminal-search.h"
-#include "terminal/ide-terminal-search-private.h"
+#include "ide-terminal-search.h"
+#include "ide-terminal-search-private.h"
 
 G_DEFINE_TYPE (IdeTerminalSearch, ide_terminal_search, GTK_TYPE_BIN)
 
@@ -298,7 +299,7 @@ ide_terminal_search_class_init (IdeTerminalSearchClass *klass)
 
   object_class->get_property = ide_terminal_search_get_property;
 
-  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/builder/ui/ide-terminal-search.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-terminal/ui/ide-terminal-search.ui");
   gtk_widget_class_bind_template_child (widget_class, IdeTerminalSearch, search_prev_button);
   gtk_widget_class_bind_template_child (widget_class, IdeTerminalSearch, search_next_button);
   gtk_widget_class_bind_template_child (widget_class, IdeTerminalSearch, close_button);
@@ -360,7 +361,7 @@ ide_terminal_search_init (IdeTerminalSearch *self)
  * ide_terminal_search_set_terminal:
  * @self: a #IdeTerminalSearch
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_terminal_search_set_terminal (IdeTerminalSearch *self,
@@ -378,7 +379,7 @@ ide_terminal_search_set_terminal (IdeTerminalSearch *self,
  *
  * Returns: (transfer none) (nullable): a #VteRegex or %NULL.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 VteRegex *
 ide_terminal_search_get_regex (IdeTerminalSearch *self)
@@ -393,7 +394,7 @@ ide_terminal_search_get_regex (IdeTerminalSearch *self)
  * @self: a #IdeTerminalSearch
  *
  *
- * Since: 3.28
+ * Since: 3.32
  */
 gboolean
 ide_terminal_search_get_wrap_around (IdeTerminalSearch *self)
@@ -411,7 +412,7 @@ ide_terminal_search_get_wrap_around (IdeTerminalSearch *self)
  *
  * Returns: (transfer none): a #GtkRevealer
  *
- * Since: 3.28
+ * Since: 3.32
  */
 GtkRevealer *
 ide_terminal_search_get_revealer (IdeTerminalSearch *self)
diff --git a/src/libide/terminal/ide-terminal-search.h b/src/libide/terminal/ide-terminal-search.h
index 95ee46f15..1081b0d05 100644
--- a/src/libide/terminal/ide-terminal-search.h
+++ b/src/libide/terminal/ide-terminal-search.h
@@ -1,6 +1,6 @@
 /* gb-terminal-view.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,29 +14,34 @@
  *
  * 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 <vte/vte.h>
+#if !defined (IDE_TERMINAL_INSIDE) && !defined (IDE_TERMINAL_COMPILATION)
+# error "Only <libide-terminal.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <vte/vte.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_TERMINAL_SEARCH (ide_terminal_search_get_type())
 
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeTerminalSearch, ide_terminal_search, IDE, TERMINAL_SEARCH, GtkBin)
 
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 VteRegex    *ide_terminal_search_get_regex       (IdeTerminalSearch *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean     ide_terminal_search_get_wrap_around (IdeTerminalSearch *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void         ide_terminal_search_set_terminal    (IdeTerminalSearch *self,
                                                   VteTerminal       *terminal);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 GtkRevealer *ide_terminal_search_get_revealer    (IdeTerminalSearch *self);
 
 G_END_DECLS
diff --git a/src/libide/terminal/ide-terminal-surface.c b/src/libide/terminal/ide-terminal-surface.c
new file mode 100644
index 000000000..fca73db83
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-surface.c
@@ -0,0 +1,84 @@
+/* ide-terminal-surface.c
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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 "ide-terminal-surface"
+
+#include "config.h"
+
+#include "ide-terminal-page.h"
+#include "ide-terminal-surface.h"
+
+struct _IdeTerminalSurface
+{
+  IdeSurface  parent_instance;
+
+  IdeGrid    *grid;
+};
+
+G_DEFINE_TYPE (IdeTerminalSurface, ide_terminal_surface, IDE_TYPE_SURFACE)
+
+/**
+ * ide_terminal_surface_new:
+ *
+ * Create a new #IdeTerminalSurface.
+ *
+ * Returns: (transfer full): a newly created #IdeTerminalSurface
+ *
+ * Since: 3.32
+ */
+IdeTerminalSurface *
+ide_terminal_surface_new (void)
+{
+  return g_object_new (IDE_TYPE_TERMINAL_SURFACE, NULL);
+}
+
+static void
+ide_terminal_surface_add (GtkContainer *container,
+                          GtkWidget    *child)
+{
+  IdeTerminalSurface *self = (IdeTerminalSurface *)container;
+
+  g_assert (IDE_IS_TERMINAL_SURFACE (self));
+
+  if (IDE_IS_TERMINAL_PAGE (child))
+    gtk_container_add (GTK_CONTAINER (self->grid), child);
+  else
+    GTK_CONTAINER_CLASS (ide_terminal_surface_parent_class)->add (container, child);
+}
+
+static void
+ide_terminal_surface_class_init (IdeTerminalSurfaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  container_class->add = ide_terminal_surface_add;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-terminal/ui/ide-terminal-surface.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeTerminalSurface, grid);
+}
+
+static void
+ide_terminal_surface_init (IdeTerminalSurface *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_widget_set_name (GTK_WIDGET (self), "terminal");
+}
diff --git a/src/libide/terminal/ide-terminal-surface.h b/src/libide/terminal/ide-terminal-surface.h
new file mode 100644
index 000000000..4921aef4e
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-surface.h
@@ -0,0 +1,39 @@
+/* ide-terminal-surface.h
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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
+
+#if !defined (IDE_TERMINAL_INSIDE) && !defined (IDE_TERMINAL_COMPILATION)
+# error "Only <libide-terminal.h> can be included directly."
+#endif
+
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TERMINAL_SURFACE (ide_terminal_surface_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTerminalSurface, ide_terminal_surface, IDE, TERMINAL_SURFACE, IdeSurface)
+
+IDE_AVAILABLE_IN_3_32
+IdeTerminalSurface *ide_terminal_surface_new (void);
+
+G_END_DECLS
diff --git a/src/libide/terminal/ide-terminal-surface.ui b/src/libide/terminal/ide-terminal-surface.ui
new file mode 100644
index 000000000..f0110988a
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-surface.ui
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeTerminalSurface" parent="IdeSurface">
+    <child>
+      <object class="IdeGrid" id="grid">
+        <property name="visible">true</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/terminal/ide-terminal-util.c b/src/libide/terminal/ide-terminal-util.c
index eccb93ef7..8f53aad66 100644
--- a/src/libide/terminal/ide-terminal-util.c
+++ b/src/libide/terminal/ide-terminal-util.c
@@ -1,6 +1,6 @@
 /* ide-terminal-util.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-terminal-util"
@@ -21,15 +23,14 @@
 #include "config.h"
 
 #include <fcntl.h>
+#include <libide-io.h>
+#include <libide-threading.h>
 #include <stdlib.h>
 #include <unistd.h>
 #include <vte/vte.h>
 
-#include "subprocess/ide-subprocess.h"
-#include "subprocess/ide-subprocess-launcher.h"
-#include "terminal/ide-terminal-private.h"
-#include "terminal/ide-terminal-util.h"
-#include "util/ptyintercept.h"
+#include "ide-terminal-private.h"
+#include "ide-terminal-util.h"
 
 static const gchar *user_shell = "/bin/sh";
 
@@ -38,13 +39,13 @@ ide_vte_pty_create_slave (VtePty *pty)
 {
   gint master_fd;
 
-  g_return_val_if_fail (VTE_IS_PTY (pty), PTY_FD_INVALID);
+  g_return_val_if_fail (VTE_IS_PTY (pty), IDE_PTY_FD_INVALID);
 
   master_fd = vte_pty_get_fd (pty);
-  if (master_fd == PTY_FD_INVALID)
-    return PTY_FD_INVALID;
+  if (master_fd == IDE_PTY_FD_INVALID)
+    return IDE_PTY_FD_INVALID;
 
-  return pty_intercept_create_slave (master_fd, TRUE);
+  return ide_pty_intercept_create_slave (master_fd, TRUE);
 }
 
 /**
@@ -57,6 +58,8 @@ ide_vte_pty_create_slave (VtePty *pty)
  * sensible fallback.
  *
  * Returns: (not nullable): a shell such as "/bin/sh"
+ *
+ * Since: 3.32
  */
 const gchar *
 ide_get_user_shell (void)
@@ -114,7 +117,8 @@ _ide_guess_shell (void)
 
   if (!g_shell_parse_argv (command, NULL, &argv, &error))
     {
-      g_warning ("Failed to parse command into argv: %s", error->message);
+      g_warning ("Failed to parse command into argv: %s",
+                 error ? error->message : "unknown error");
       return;
     }
 
diff --git a/src/libide/terminal/ide-terminal-util.h b/src/libide/terminal/ide-terminal-util.h
index 4a9d9c11d..a07b35efc 100644
--- a/src/libide/terminal/ide-terminal-util.h
+++ b/src/libide/terminal/ide-terminal-util.h
@@ -1,6 +1,6 @@
 /* gb-terminal-util.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,19 +14,24 @@
  *
  * 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 <vte/vte.h>
+#if !defined (IDE_TERMINAL_INSIDE) && !defined (IDE_TERMINAL_COMPILATION)
+# error "Only <libide-terminal.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <vte/vte.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 int          ide_vte_pty_create_slave (VtePty *pty);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 const gchar *ide_get_user_shell       (void);
 
 G_END_DECLS
diff --git a/src/libide/terminal/ide-terminal-workspace.c b/src/libide/terminal/ide-terminal-workspace.c
new file mode 100644
index 000000000..ad1019e9b
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-workspace.c
@@ -0,0 +1,52 @@
+/* ide-terminal-workspace.c
+ *
+ * Copyright 2018-2019 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 "ide-terminal-workspace"
+
+#include "config.h"
+
+#include "ide-terminal-workspace.h"
+
+struct _IdeTerminalWorkspace
+{
+  IdeWorkspace  parent_instance;
+
+  IdeHeaderBar *header_bar;
+};
+
+G_DEFINE_TYPE (IdeTerminalWorkspace, ide_terminal_workspace, IDE_TYPE_WORKSPACE)
+
+static void
+ide_terminal_workspace_class_init (IdeTerminalWorkspaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdeWorkspaceClass *workspace_class = IDE_WORKSPACE_CLASS (klass);
+
+  ide_workspace_class_set_kind (workspace_class, "terminal");
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/libide-terminal/ui/ide-terminal-workspace.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeTerminalWorkspace, header_bar);
+}
+
+static void
+ide_terminal_workspace_init (IdeTerminalWorkspace *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/libide/terminal/ide-terminal-workspace.h b/src/libide/terminal/ide-terminal-workspace.h
new file mode 100644
index 000000000..41ac6f5a1
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-workspace.h
@@ -0,0 +1,37 @@
+/* ide-terminal-workspace.h
+ *
+ * Copyright 2018-2019 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
+
+#if !defined (IDE_TERMINAL_INSIDE) && !defined (IDE_TERMINAL_COMPILATION)
+# error "Only <libide-terminal.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TERMINAL_WORKSPACE (ide_terminal_workspace_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTerminalWorkspace, ide_terminal_workspace, IDE, TERMINAL_WORKSPACE, IdeWorkspace)
+
+G_END_DECLS
diff --git a/src/libide/terminal/ide-terminal-workspace.ui b/src/libide/terminal/ide-terminal-workspace.ui
new file mode 100644
index 000000000..fc03b508e
--- /dev/null
+++ b/src/libide/terminal/ide-terminal-workspace.ui
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeTerminalWorkspace" parent="IdeWorkspace">
+    <property name="default-width">750</property>
+    <property name="default-height">450</property>
+    <child type="titlebar">
+      <object class="IdeHeaderBar" id="header_bar">
+        <property name="show-close-button">true</property>
+        <property name="show-fullscreen-button">true</property>
+        <property name="menu-id">ide-terminal-workspace-menu</property>
+        <property name="visible">true</property>
+      </object>
+    </child>
+    <child internal-child="surfaces">
+      <object class="GtkStack" id="surfaces">
+        <property name="visible">true</property>
+        <child>
+          <object class="IdeTerminalSurface">
+            <property name="visible">true</property>
+            <child>
+              <object class="IdeTerminalPage">
+                <property name="visible">true</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="name">terminal</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/libide/terminal/ide-terminal.c b/src/libide/terminal/ide-terminal.c
index 92e0101f9..3410a2c4b 100644
--- a/src/libide/terminal/ide-terminal.c
+++ b/src/libide/terminal/ide-terminal.c
@@ -1,6 +1,6 @@
 /* ide-terminal.c
  *
- * Copyright 2016-2017 Christian Hergert <christian hergert me>
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-terminal"
@@ -22,9 +24,9 @@
 
 #include <dazzle.h>
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-gui.h>
 
-#include "terminal/ide-terminal.h"
+#include "ide-terminal.h"
 
 #define BUILDER_PCRE2_MULTILINE 0x00000400u
 
@@ -272,7 +274,7 @@ ide_terminal_copy_link_address (IdeTerminal *self)
   g_assert (IDE_IS_TERMINAL (self));
   g_assert (priv->url != NULL);
 
-  if (dzl_str_empty0 (priv->url))
+  if (ide_str_empty0 (priv->url))
     return FALSE;
 
   gtk_clipboard_set_text (gtk_widget_get_clipboard (GTK_WIDGET (self), GDK_SELECTION_CLIPBOARD),
@@ -291,7 +293,7 @@ ide_terminal_open_link (IdeTerminal *self)
   g_assert (IDE_IS_TERMINAL (self));
   g_assert (priv->url != NULL);
 
-  if (dzl_str_empty0 (priv->url))
+  if (ide_str_empty0 (priv->url))
     return FALSE;
 
   if (NULL != (app = GTK_APPLICATION (g_application_get_default ())) &&
diff --git a/src/libide/terminal/ide-terminal.h b/src/libide/terminal/ide-terminal.h
index 8c62ec397..c4687e6c2 100644
--- a/src/libide/terminal/ide-terminal.h
+++ b/src/libide/terminal/ide-terminal.h
@@ -1,6 +1,6 @@
 /* ide-terminal.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,19 +14,24 @@
  *
  * 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 <vte/vte.h>
+#if !defined (IDE_TERMINAL_INSIDE) && !defined (IDE_TERMINAL_COMPILATION)
+# error "Only <libide-terminal.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <vte/vte.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_TERMINAL (ide_terminal_get_type())
 
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeTerminal, ide_terminal, IDE, TERMINAL, VteTerminal)
 
 struct _IdeTerminalClass
@@ -41,10 +46,11 @@ struct _IdeTerminalClass
   gboolean (*open_link)           (IdeTerminal *self);
   gboolean (*copy_link_address)   (IdeTerminal *self);
 
+  /*< private >*/
   gpointer padding[16];
 };
 
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 GtkWidget *ide_terminal_new (void);
 
 G_END_DECLS
diff --git a/src/libide/terminal/libide-terminal.gresource.xml 
b/src/libide/terminal/libide-terminal.gresource.xml
new file mode 100644
index 000000000..a8623ac95
--- /dev/null
+++ b/src/libide/terminal/libide-terminal.gresource.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/libide-terminal">
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+  </gresource>
+  <gresource prefix="/org/gnome/libide-terminal/ui">
+    <file preprocess="xml-stripblanks">ide-terminal-page.ui</file>
+    <file preprocess="xml-stripblanks">ide-terminal-search.ui</file>
+    <file preprocess="xml-stripblanks">ide-terminal-surface.ui</file>
+    <file preprocess="xml-stripblanks">ide-terminal-workspace.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/terminal/libide-terminal.h b/src/libide/terminal/libide-terminal.h
new file mode 100644
index 000000000..37838c104
--- /dev/null
+++ b/src/libide/terminal/libide-terminal.h
@@ -0,0 +1,38 @@
+/* libide-terminal.h
+ *
+ * Copyright 2018-2019 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-core.h>
+#include <libide-io.h>
+#include <libide-gui.h>
+#include <libide-threading.h>
+#include <vte/vte.h>
+
+#define IDE_TERMINAL_INSIDE
+
+#include "ide-terminal.h"
+#include "ide-terminal-page.h"
+#include "ide-terminal-search.h"
+#include "ide-terminal-surface.h"
+#include "ide-terminal-util.h"
+#include "ide-terminal-workspace.h"
+
+#undef IDE_TERMINAL_INSIDE
diff --git a/src/libide/terminal/meson.build b/src/libide/terminal/meson.build
index fca092cce..4ab3a67cd 100644
--- a/src/libide/terminal/meson.build
+++ b/src/libide/terminal/meson.build
@@ -1,16 +1,96 @@
-terminal_headers = [
-  'ide-terminal.h',
+libide_terminal_header_subdir = join_paths(libide_header_subdir, 'terminal')
+libide_include_directories += include_directories('.')
+
+libide_terminal_generated_headers = []
+
+#
+# Public API Headers
+#
+
+libide_terminal_public_headers = [
+  'ide-terminal-page.h',
   'ide-terminal-search.h',
+  'ide-terminal-surface.h',
   'ide-terminal-util.h',
+  'ide-terminal-workspace.h',
+  'ide-terminal.h',
+  'libide-terminal.h',
 ]
 
-terminal_sources = [
-  'ide-terminal.c',
+install_headers(libide_terminal_public_headers, subdir: libide_terminal_header_subdir)
+
+#
+# Sources
+#
+
+libide_terminal_private_headers = [
+  'ide-terminal-page-actions.h',
+  'ide-terminal-page-private.h',
+  'ide-terminal-private.h',
+  'ide-terminal-search-private.h',
+]
+
+libide_terminal_public_sources = [
+  'ide-terminal-page.c',
   'ide-terminal-search.c',
+  'ide-terminal-surface.c',
   'ide-terminal-util.c',
+  'ide-terminal-workspace.c',
+  'ide-terminal.c',
 ]
 
-libide_public_headers += files(terminal_headers)
-libide_public_sources += files(terminal_sources)
+libide_terminal_private_sources = [
+  'ide-terminal-page-actions.c',
+]
+
+#
+# Generated Resource Files
+#
+
+libide_terminal_resources = gnome.compile_resources(
+  'ide-terminal-resources',
+  'libide-terminal.gresource.xml',
+  c_name: 'ide_terminal',
+)
+libide_terminal_generated_headers += [libide_terminal_resources[1]]
+
+
+#
+# Dependencies
+#
+
+libide_terminal_deps = [
+  libgio_dep,
+  libgtk_dep,
+  libdazzle_dep,
+  libvte_dep,
+
+  libide_core_dep,
+  libide_io_dep,
+  libide_threading_dep,
+  libide_gui_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_terminal = static_library('ide-terminal-' + libide_api_version,
+   libide_terminal_public_sources + libide_terminal_private_sources + [libide_terminal_resources[0]],
+   dependencies: libide_terminal_deps,
+         c_args: libide_args + release_args + ['-DIDE_TERMINAL_COMPILATION'],
+)
+
+libide_terminal_dep = declare_dependency(
+              sources: libide_terminal_generated_headers,
+         dependencies: libide_terminal_deps,
+           link_whole: libide_terminal,
+  include_directories: include_directories('.'),
+)
 
-install_headers(terminal_headers, subdir: join_paths(libide_header_subdir, 'terminal'))
+gnome_builder_public_sources += files(libide_terminal_public_sources)
+gnome_builder_public_headers += files(libide_terminal_public_headers)
+gnome_builder_private_sources += files(libide_terminal_private_sources)
+gnome_builder_private_headers += files(libide_terminal_private_headers)
+gnome_builder_include_subdirs += libide_terminal_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-terminal.h', '-DIDE_TERMINAL_COMPILATION']
diff --git a/src/libide/themes/libide-themes.c b/src/libide/themes/libide-themes.c
new file mode 100644
index 000000000..62512df92
--- /dev/null
+++ b/src/libide/themes/libide-themes.c
@@ -0,0 +1,32 @@
+/* libide-themes.c
+ *
+ * Copyright 2018-2019 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 "ide-themes-global"
+
+#include "config.h"
+
+#include "libide-themes.h"
+#include "ide-themes-resources.h"
+
+void
+ide_themes_init (void)
+{
+  g_resources_register (ide_themes_get_resource ());
+}
diff --git a/src/libide/themes/libide-themes.gresource.xml b/src/libide/themes/libide-themes.gresource.xml
new file mode 100644
index 000000000..2d22cd6b7
--- /dev/null
+++ b/src/libide/themes/libide-themes.gresource.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gnome/builder">
+    <file compressed="true">themes/Adwaita.css</file>
+    <file compressed="true">themes/Adwaita-dark.css</file>
+    <file compressed="true">themes/Adwaita-shared.css</file>
+
+    <file compressed="true" alias="themes/Arc.css">themes/Arc.css</file>
+    <file compressed="true" alias="themes/Arc-Dark.css">themes/Arc-Dark.css</file>
+    <file compressed="true" alias="themes/Arc-Darker.css">themes/Arc-Darker.css</file>
+    <file compressed="true" alias="themes/Arc-dark.css">themes/Arc-Dark.css</file>
+    <file compressed="true" alias="themes/Arc-Dark-dark.css">themes/Arc-Dark.css</file>
+    <file compressed="true" alias="themes/Arc-Darker-dark.css">themes/Arc-Dark.css</file>
+    <file compressed="true" alias="themes/Arc-shared.css">themes/Arc-shared.css</file>
+
+    <file compressed="true">themes/elementary.css</file>
+
+    <file compressed="true">themes/shared.css</file>
+    <file compressed="true">themes/shared/shared-buildui.css</file>
+    <file compressed="true">themes/shared/shared-completion.css</file>
+    <file compressed="true">themes/shared/shared-debugger.css</file>
+    <file compressed="true">themes/shared/shared-editor.css</file>
+    <file compressed="true">themes/shared/shared-greeter.css</file>
+    <file compressed="true">themes/shared/shared-hoverer.css</file>
+    <file compressed="true">themes/shared/shared-layout.css</file>
+    <file compressed="true">themes/shared/shared-omnibar.css</file>
+    <file compressed="true">themes/shared/shared-search.css</file>
+    <file compressed="true">themes/shared/shared-treeview.css</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/themes/libide-themes.h b/src/libide/themes/libide-themes.h
new file mode 100644
index 000000000..a16cdbd88
--- /dev/null
+++ b/src/libide/themes/libide-themes.h
@@ -0,0 +1,29 @@
+/* libide-themes.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+void ide_themes_init (void);
+
+G_END_DECLS
diff --git a/src/libide/themes/meson.build b/src/libide/themes/meson.build
new file mode 100644
index 000000000..9d6c8e247
--- /dev/null
+++ b/src/libide/themes/meson.build
@@ -0,0 +1,53 @@
+libide_themes_header_subdir = join_paths(libide_header_subdir, 'themes')
+
+#
+# Sources
+#
+
+libide_themes_sources = ['libide-themes.c']
+libide_themes_headers = ['libide-themes.h']
+
+#
+# Generated Resources
+#
+
+libide_themes_resources = gnome.compile_resources(
+  'ide-themes-resources',
+  'libide-themes.gresource.xml',
+  c_name: 'ide_themes',
+)
+
+#
+# Install Headers
+#
+
+install_headers(libide_themes_headers, subdir: libide_themes_header_subdir)
+
+#
+# Dependencies
+#
+
+libide_themes_deps = [
+  libgio_dep,
+
+  libide_core_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_themes = static_library('ide-themes-' + libide_api_version,
+   libide_themes_sources + libide_themes_resources,
+   dependencies: libide_themes_deps,
+         c_args: libide_args + release_args + ['-DIDE_THEMES_COMPILATION'],
+)
+
+libide_themes_dep = declare_dependency(
+              sources: libide_themes_resources[1],
+         dependencies: libide_themes_deps,
+           link_whole: libide_themes,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_include_subdirs += libide_themes_header_subdir
diff --git a/src/libide/themes/themes/Adwaita-dark.css b/src/libide/themes/themes/Adwaita-dark.css
new file mode 100644
index 000000000..eb67aa394
--- /dev/null
+++ b/src/libide/themes/themes/Adwaita-dark.css
@@ -0,0 +1,26 @@
+@import url("resource:///org/gnome/builder/themes/Adwaita-shared.css");
+
+
+surfaceswitcher {
+  background-color: #272c2e;
+}
+surfaceswitcher button {
+  color: #696c6b;
+}
+surfaceswitcher button:checked,
+surfaceswitcher button:checked:hover {
+  color: #eeeeec;
+}
+surfaceswitcher button:hover {
+  color: #b2b4b2;
+}
+
+
+entry.search-missing {
+  border-color: #330000;
+}
+
+/* spellchecker word count background darker with dark theme */
+.countbox {
+  background-color: #587A9E;
+}
diff --git a/src/libide/themes/themes/Adwaita-shared.css b/src/libide/themes/themes/Adwaita-shared.css
new file mode 100644
index 000000000..579aa6fe3
--- /dev/null
+++ b/src/libide/themes/themes/Adwaita-shared.css
@@ -0,0 +1,96 @@
+@import url("resource:///org/gnome/builder/themes/shared.css");
+
+ideframeheader > button:last-child {
+  margin-right: 3px;
+}
+
+entry.search-missing {
+  background-color: #cc0000;
+  color: white;
+  text-shadow: none;
+}
+
+entry.search-missing > image {
+  color: white;
+}
+
+
+/* tweak icons for treeviews */
+treeview.image { color: alpha(currentColor, 0.8); }
+treeview.image:selected { color: alpha(@theme_selected_fg_color, 0.9); }
+
+
+/* utilities stack switcher */
+ideeditorutilities > dzldockpaned > box > stackswitcher {
+  margin: 6px;
+}
+
+
+/* buildui panel */
+ideeditorsidebar notebook header {
+  background: transparent;
+}
+ideeditorsidebar notebook header tab {
+  padding: 0;
+}
+
+ideeditorproperties entry:last-child {
+  border-radius: 0;
+  border-right: none;
+  border-left: none;
+}
+
+/* Omnibar */
+popover.omnibar {
+  padding: 0;
+}
+popover.omnibar list {
+  border-top: 1px solid @borders;
+  border-radius: 0 0 5px 5px;
+}
+popover.omnibar list row:not(:last-child) {
+  border-bottom: 1px solid alpha(@borders, 0.3);
+}
+popover.omnibar row.notification .title {
+  font-weight: bold;
+}
+popover.omnibar row.notification .body {
+  opacity: 0.8;
+  font-size: 0.95em;
+}
+
+/* Notifications button (transfers, etc) */
+popover.notificationsbutton {
+  padding: 0;
+}
+popover.notificationsbutton list {
+  background: transparent;
+  border-radius: 5px;
+}
+popover.notificationsbutton list row:not(:last-child) {
+  border-bottom: 1px solid alpha(@borders, 0.3);
+}
+popover.notificationsbutton row.notification .title {
+  font-weight: bold;
+}
+popover.notificationsbutton row.notification .body {
+  opacity: 0.8;
+  font-size: 0.95em;
+}
+
+/* development styles */
+window.development-version  headerbar:last-child {
+  background: transparent -gtk-icontheme("system-run-symbolic") 80% 0/128px 128px no-repeat,
+              linear-gradient(to left,
+                              mix(@theme_fg_color, @theme_bg_color, 0.5) 0%,
+                              @theme_bg_color 25%);
+  color: alpha(@theme_fg_color, 0.2);
+}
+
+window.development-version headerbar label:not(:disabled) {
+  color: @theme_fg_color;
+}
+
+window.development-version headerbar .suggested-action label {
+  color: @theme_selected_fg_color;
+}
diff --git a/data/themes/Adwaita.css b/src/libide/themes/themes/Adwaita.css
similarity index 100%
rename from data/themes/Adwaita.css
rename to src/libide/themes/themes/Adwaita.css
diff --git a/data/themes/Arc-Dark.css b/src/libide/themes/themes/Arc-Dark.css
similarity index 100%
rename from data/themes/Arc-Dark.css
rename to src/libide/themes/themes/Arc-Dark.css
diff --git a/src/libide/themes/themes/Arc-Darker.css b/src/libide/themes/themes/Arc-Darker.css
new file mode 100644
index 000000000..6acaa6b2a
--- /dev/null
+++ b/src/libide/themes/themes/Arc-Darker.css
@@ -0,0 +1,7 @@
+@import url("resource:///org/gnome/builder/themes/Arc-shared.css");
+
+/* Darker border */
+surfaceswitcher {
+  border-top: 1px solid #2b2e39;
+  border-right: 1px solid #2b2e39;
+}
diff --git a/src/libide/themes/themes/Arc-shared.css b/src/libide/themes/themes/Arc-shared.css
new file mode 100644
index 000000000..a104cb1f8
--- /dev/null
+++ b/src/libide/themes/themes/Arc-shared.css
@@ -0,0 +1,83 @@
+@import url("resource:///org/gnome/builder/themes/shared.css");
+
+/* Darker grey accents used throughtout */
+@define-color theme_accent_color #858c98;
+@define-color theme_accent_bg_color #353945;
+/*@define-color theme_accent_unfocused_color #89929e;
+@define-color theme_accent_bg_unfocused_color #313843;*/
+@define-color theme_button_hover_bg_color #454C5C;
+@define-color theme_button_hover_border_color #262932;
+
+surfaceswitcher {
+  background-color: @theme_accent_bg_color;
+  border-top: 1px solid @borders;
+  border-right: 1px solid @borders;
+}
+
+surfaceswitcher button {
+  color: @theme_accent_color;
+  background-color: @theme_accent_bg_color;
+  border-radius: 3px;
+  box-shadow: none;
+  border: none;
+  margin: 1px;
+}
+
+surfaceswitcher button:hover {
+  border-color: @theme_button_hover_border_color;
+  background-color: @theme_button_hover_bg_color;
+}
+
+surfaceswitcher button:checked {
+  color: white;
+  background-color: @wm_button_active_bg;
+}
+
+surfaceswitcher button:checked:backdrop {
+  color: #c2c4c7;
+}
+
+
+panel stackswitcher button {
+  color: @theme_fg_color;
+  background-color: transparent;
+  border: none;
+}
+panel stackswitcher button:checked {
+  color: @theme_selected_bg_color;
+}
+/* All boxes */
+panel > box > box.horizontal > stackswitcher > button:hover {
+  border: 1px solid @borders;
+}
+/* Box above file switcher */
+panel > box.vertical:first-child > box.horizontal {
+  border: 1px solid @borders;
+}
+
+
+/* Builder pane */
+window.workspace buildsurface list {
+  border-right: 1px solid @borders;
+  background-color: @theme_base_color;
+}
+window.workspace buildsurface list row {
+  padding: 10px;
+  border-bottom: 1px solid alpha(@borders, 0.50);
+}
+window.workspace buildsurface list row:last-child {
+  border-bottom: none;
+}
+
+
+/* omnibar popover, remove popover padding */
+popover.omnibar > * > * {
+  margin: 0;
+  padding: 0;
+}
+
+
+/* utilities stack switcher */
+ideeditorutilities > dzldockpaned > box > stackswitcher {
+  margin: 6px;
+}
diff --git a/data/themes/Arc.css b/src/libide/themes/themes/Arc.css
similarity index 100%
rename from data/themes/Arc.css
rename to src/libide/themes/themes/Arc.css
diff --git a/data/themes/elementary.css b/src/libide/themes/themes/elementary.css
similarity index 100%
rename from data/themes/elementary.css
rename to src/libide/themes/themes/elementary.css
diff --git a/src/libide/themes/themes/shared.css b/src/libide/themes/themes/shared.css
new file mode 100644
index 000000000..03f05f41f
--- /dev/null
+++ b/src/libide/themes/themes/shared.css
@@ -0,0 +1,144 @@
+@import url("resource:///org/gnome/builder/themes/shared/shared-buildui.css");
+@import url("resource:///org/gnome/builder/themes/shared/shared-completion.css");
+@import url("resource:///org/gnome/builder/themes/shared/shared-debugger.css");
+@import url("resource:///org/gnome/builder/themes/shared/shared-layout.css");
+@import url("resource:///org/gnome/builder/themes/shared/shared-editor.css");
+@import url("resource:///org/gnome/builder/themes/shared/shared-greeter.css");
+@import url("resource:///org/gnome/builder/themes/shared/shared-hoverer.css");
+@import url("resource:///org/gnome/builder/themes/shared/shared-omnibar.css");
+@import url("resource:///org/gnome/builder/themes/shared/shared-search.css");
+@import url("resource:///org/gnome/builder/themes/shared/shared-treeview.css");
+
+/* work around some gtk padding issue */
+filechooser actionbar button.combo {
+  padding: 0;
+}
+
+/* Generic styles */
+.warning {
+    color: @warning_color;
+}
+
+/* headerbar shadow in fullscreen, for depth above children */
+window.fullscreen headerbar {
+  margin-bottom: 5px;
+  box-shadow: 0 0 3px 3px alpha(@wm_shadow,.3);
+}
+
+
+/* Styling in the create-project surface. */
+createprojectsurface stack > box:first-child list row {
+  padding: 10px;
+  border-bottom: 1px solid alpha(@borders, 0.2);
+}
+createprojectsurface stack > box:first-child list row:last-child {
+  border-bottom: none;
+}
+
+/*
+ * Perspectives switcher
+ *
+ * The following tweaks the left-most sidebar containing
+ * the list of surfaces.
+ */
+surfaceswitcher {
+  border-right: 1px solid alpha(@borders, 0.5);
+}
+surfaceswitcher button {
+  background: transparent;
+  border-radius: 0;
+  border: none;
+  box-shadow: none;
+  padding: 6px;
+}
+
+
+/* Workaround Adwaita adding borders we don't want */
+textview border.left {
+  background: none;
+}
+
+treeview.dim-label {
+  color: alpha(currentColor, 0.5);
+}
+
+
+button.run-arrow-button {
+  min-width: 12px;
+}
+
+
+buildsurface list.sidebar row:selected button:hover {
+  border-color: transparent;
+  box-shadow: none;
+  background: transparent;
+  color: @theme_selected_fg_color;
+  opacity: 1;
+}
+buildsurface list.sidebar row:selected button,
+buildsurface list.sidebar row:selected button:active {
+  opacity: 0.8;
+}
+buildsurface list.sidebar {
+  border-right: 1px solid alpha(@borders, 0.55);
+}
+
+
+configurationview list row {
+  padding: 10px;
+  border-bottom: 1px solid alpha(@borders, 0.4);
+}
+configurationview list row:last-child {
+  border-bottom: none;
+}
+
+configurationview list row entry {
+  background: transparent;
+  border: none;
+  padding: 0;
+  padding-left: 5px;
+  border-radius: 3px;
+  margin: -5px;
+}
+
+configurationview list row spinbutton entry {
+  margin-left: 2px;
+}
+
+configurationview list row button {
+  margin: -5px 0;
+}
+
+
+/* hrmm, we can use this to get row separators */
+configurationview treeview {
+  border-bottom: 1px solid alpha(@borders, 0.4);
+}
+
+
+popover.transfers list {
+  background-color: transparent;
+}
+popover.transfers list row {
+  border-top: 1px solid @borders;
+}
+popover.transfers list row:first-child {
+  border-top: none;
+}
+popover.transfers list row > box {
+  padding: 10px;
+}
+
+button.run-arrow-button {
+  padding: 0px;
+}
+
+
+/* Stack list history tweaks */
+dzlstacklist row {
+  padding: 0;
+  margin: 0;
+}
+dzlstacklist .stack-header {
+  opacity: 0.55;
+}
diff --git a/data/themes/shared/shared-buildui.css b/src/libide/themes/themes/shared/shared-buildui.css
similarity index 100%
rename from data/themes/shared/shared-buildui.css
rename to src/libide/themes/themes/shared/shared-buildui.css
diff --git a/data/themes/shared/shared-completion.css b/src/libide/themes/themes/shared/shared-completion.css
similarity index 100%
rename from data/themes/shared/shared-completion.css
rename to src/libide/themes/themes/shared/shared-completion.css
diff --git a/data/themes/shared/shared-debugger.css b/src/libide/themes/themes/shared/shared-debugger.css
similarity index 100%
rename from data/themes/shared/shared-debugger.css
rename to src/libide/themes/themes/shared/shared-debugger.css
diff --git a/src/libide/themes/themes/shared/shared-editor.css 
b/src/libide/themes/themes/shared/shared-editor.css
new file mode 100644
index 000000000..e1fdc4432
--- /dev/null
+++ b/src/libide/themes/themes/shared/shared-editor.css
@@ -0,0 +1,124 @@
+ideeditorsidebar > dzldockpaned:first-child stackswitcher {
+  margin: 6px;
+}
+ideeditorsidebar .handle {
+ border: 1px solid alpha(@borders, 0.3);
+}
+ideeditorsidebar label.title {
+  margin: 8px 6px 3px 6px;
+  font-weight: bold;
+  font-size: 0.8em;
+}
+ideeditorsidebar list.open-pages row {
+  padding: 1px 0;
+  color: @theme_fg_color;
+}
+ideeditorsidebar list.open-pages row:selected {
+  color: @theme_selected_fg_color;
+}
+ideeditorsidebar list.open-pages row:backdrop {
+  color: @theme_unfocused_fg_color;
+}
+ideeditorsidebar list.open-pages row box > image:first-child {
+  opacity: 0.55;
+}
+ideeditorsidebar list.open-pages row box > image:first-child {
+  margin: 6px 8px;
+  min-height: 16px;
+  min-width: 16px;
+}
+ideeditorsidebar list.open-pages row box > button:last-child {
+  background: none;
+  box-shadow: none;
+  border: none;
+  outline: none;
+  padding: 0;
+  margin: 0 9px 0 6px;
+  color: @theme_fg_color;
+  opacity: 0.55;
+}
+ideeditorsidebar list.open-pages row box > button:last-child:hover {
+  opacity: 1;
+}
+ideeditorsidebar list.open-pages row box > button:last-child image {
+  min-height: 16px;
+  min-width: 16px;
+}
+ideeditorsidebar list.open-pages row box > button:last-child:backdrop {
+  color: @theme_unfocused_fg_color;
+}
+ideeditorsidebar label.error {
+  color: @error_color;
+  font-weight: bold;
+}
+ideeditorsidebar label.error:backdrop {
+  font-weight: normal;
+}
+ideeditorproperties button {
+  padding: 2px 12px;
+}
+ideeditorproperties checkbutton {
+  margin: 8px 0 0 0;
+  outline-offset: 2px;
+  padding: 0;
+}
+ideeditorproperties checkbutton check {
+  margin: 0 8px 0 0;
+}
+ideeditorproperties treeview {
+  color: @theme_fg_color;
+}
+ideeditorproperties treeview:selected:backdrop,
+ideeditorproperties treeview:selected {
+  color: @theme_selected_fg_color;
+}
+ideeditorproperties treeview:backdrop {
+  color: @theme_unfocused_fg_color;
+}
+ideeditorproperties button.control.flat {
+  padding: 0;
+  margin: 0 8px 0 0;
+  min-width: 16px;
+  min-height: 16px;
+}
+ideeditorsidebar .flat-menu-button {
+  border: none;
+  background: transparent;
+  box-shadow: none;
+  opacity: 0.5;
+  padding: 0;
+  margin: 0;
+  outline-offset: -3px;
+}
+ideeditorsidebar .flat-menu-button:checked,
+ideeditorsidebar .flat-menu-button:hover {
+  opacity: 1;
+}
+
+/* utilities panel buttons */
+ideeditorutilities dzltab {
+  background: @theme_bg_color;
+  padding: 6px 8px;
+  margin: 0;
+  border-style: solid;
+  border-color: @borders;
+  border-width: 1px 1px 0 1px;
+}
+ideeditorutilities dzltab:checked {
+  background-color: mix(@theme_bg_color, @borders, 0.3);
+}
+ideeditorutilities dzltab:hover {
+  background-color: mix(@theme_bg_color, @borders, 0.1);
+}
+ideeditorutilities dzltab:first-child {
+  border-radius: 3px 3px 0 0;
+  border-width: 1px 1px 0 1px;
+}
+ideeditorutilities dzltab:last-child {
+  border-radius: 0 0 3px 3px;
+  border-bottom-width: 1px;
+}
+ideeditorutilities dzltabstrip {
+  margin: 5px;
+  border-style: none;
+}
diff --git a/src/libide/themes/themes/shared/shared-greeter.css 
b/src/libide/themes/themes/shared/shared-greeter.css
new file mode 100644
index 000000000..8e8c93444
--- /dev/null
+++ b/src/libide/themes/themes/shared/shared-greeter.css
@@ -0,0 +1,32 @@
+.greeter flowboxchild {
+  border-radius: 11px;
+  outline-offset: -3px;
+  -gtk-outline-radius: 9px;
+  min-width: 164px;
+  min-height: 164px;
+}
+
+.greeter flowboxchild:selected label {
+  color: @theme_selected_fg_color;
+}
+
+.greeter flowboxchild dzlpillbox {
+ background: alpha(shade(@theme_bg_color, 0.9), 0.8);
+}
+
+/*
+ * Greeter tweaks
+ *
+ * The following tweaks the greeter surface by adding
+ * separator lines to the list box.
+ */
+.greeter list row {
+  border-bottom: 1px solid alpha(@borders, 0.2);
+}
+.greeter list row:last-child {
+  border-bottom: none;
+}
+.greeter frame border {
+  border-color: alpha(@borders, 0.6);
+}
+
diff --git a/data/themes/shared/shared-hoverer.css b/src/libide/themes/themes/shared/shared-hoverer.css
similarity index 100%
rename from data/themes/shared/shared-hoverer.css
rename to src/libide/themes/themes/shared/shared-hoverer.css
diff --git a/src/libide/themes/themes/shared/shared-layout.css 
b/src/libide/themes/themes/shared/shared-layout.css
new file mode 100644
index 000000000..9681788ca
--- /dev/null
+++ b/src/libide/themes/themes/shared/shared-layout.css
@@ -0,0 +1,83 @@
+ideframeheader {
+  min-height: 26px;
+}
+ideframeheader button {
+  border: none;
+  border-radius: 0;
+  background: transparent;
+  box-shadow: none;
+  padding: 0;
+  margin: 0;
+}
+ideframeheader button:disabled {
+  color: alpha(currentColor, 0.25);
+}
+ideframeheader button:not(:disabled) image {
+  opacity: 0.55;
+}
+ideframeheader button:checked image,
+ideframeheader button:not(:disabled):hover image {
+  opacity: 1;
+}
+ideframeheader button:checked,
+ideframeheader button:hover {
+  background: shade(@theme_bg_color, 0.9);
+}
+ideframeheader button:active {
+  background: shade(@theme_bg_color, 0.85);
+}
+ideframeheader > button {
+  padding-left: 12px;
+  padding-right: 12px;
+}
+ideframeheader * button:first-child > image,
+ideframeheader * button:last-child:dir(rtl) > image {
+  padding-left: 12px;
+  padding-right: 10px;
+}
+ideframeheader * button:first-child:dir(rtl) > image,
+ideframeheader * button:last-child > image {
+  padding-right: 12px;
+  padding-left: 9px;
+}
+idegridcolumn.handle,
+idegrid.handle {
+  border: 1px solid @borders;
+}
+popover.title-popover list row {
+  padding: 1px 0;
+}
+popover.title-popover scrolledwindow {
+  min-width: 300px;
+}
+popover.title-popover list {
+  background: transparent;
+}
+popover.title-popover list row box > image:first-child {
+  opacity: 0.55;
+}
+popover.title-popover list row box > image:first-child {
+  margin: 6px 8px;
+  min-height: 16px;
+  min-width: 16px;
+}
+popover.title-popover list row button:last-child {
+  background: none;
+  box-shadow: none;
+  border: none;
+  outline: none;
+  padding: 0;
+  margin: 0 6px;
+  color: @theme_fg_color;
+  opacity: 0.55;
+}
+popover.title-popover list row button:last-child:hover {
+  opacity: 1;
+}
+popover.title-popover list row button image {
+  min-height: 16px;
+  min-width: 16px;
+}
+popover.title-popover list row button image {
+  color: @theme_unfocused_fg_color;
+}
diff --git a/src/libide/themes/themes/shared/shared-omnibar.css 
b/src/libide/themes/themes/shared/shared-omnibar.css
new file mode 100644
index 000000000..b59edf131
--- /dev/null
+++ b/src/libide/themes/themes/shared/shared-omnibar.css
@@ -0,0 +1,46 @@
+omnibar .pan button {
+  border: none;
+  padding: 0;
+  margin: 0;
+  min-height: 12px;
+  min-width: 14px;
+  background: transparent;
+  box-shadow: none;
+  text-shadow: none;
+  text-decoration: none;
+  outline-offset: -1px;
+}
+
+omnibar .pan button:disabled {
+  opacity: 0.45;
+}
+
+omnibar notification button {
+  border: none;
+  padding: 0;
+  margin: 0;
+  min-height: 24px;
+  min-width: 24px;
+  background: transparent;
+  box-shadow: none;
+  text-shadow: none;
+  text-decoration: none;
+}
+
+omnibar entry {
+  background-color: @theme_bg_color;
+  color: alpha(@theme_fg_color, 0.8);
+}
+
+omnibar:hover entry,
+omnibar:active entry {
+  background-color: mix(@theme_bg_color, @content_view_bg, 0.9);
+  color: @theme_fg_color;
+}
+
+/* Remove animation from linked buttons because it messes up the
+ * joined state transition.
+ */
+omnibar > box.linked > button {
+  transition-duration: 0;
+}
diff --git a/data/themes/shared/shared-search.css b/src/libide/themes/themes/shared/shared-search.css
similarity index 100%
rename from data/themes/shared/shared-search.css
rename to src/libide/themes/themes/shared/shared-search.css
diff --git a/data/themes/shared/shared-treeview.css b/src/libide/themes/themes/shared/shared-treeview.css
similarity index 100%
rename from data/themes/shared/shared-treeview.css
rename to src/libide/themes/themes/shared/shared-treeview.css
diff --git a/src/libide/threading/ide-environment-variable.c b/src/libide/threading/ide-environment-variable.c
new file mode 100644
index 000000000..812a31449
--- /dev/null
+++ b/src/libide/threading/ide-environment-variable.c
@@ -0,0 +1,185 @@
+/* ide-environment-variable.c
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-environment-variable"
+
+#include "config.h"
+
+#include "ide-environment-variable.h"
+
+struct _IdeEnvironmentVariable
+{
+  GObject  parent_instance;
+  gchar   *key;
+  gchar   *value;
+};
+
+G_DEFINE_TYPE (IdeEnvironmentVariable, ide_environment_variable, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_KEY,
+  PROP_VALUE,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+ide_environment_variable_finalize (GObject *object)
+{
+  IdeEnvironmentVariable *self = (IdeEnvironmentVariable *)object;
+
+  g_clear_pointer (&self->key, g_free);
+  g_clear_pointer (&self->value, g_free);
+
+  G_OBJECT_CLASS (ide_environment_variable_parent_class)->finalize (object);
+}
+
+static void
+ide_environment_variable_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  IdeEnvironmentVariable *self = IDE_ENVIRONMENT_VARIABLE(object);
+
+  switch (prop_id)
+    {
+    case PROP_KEY:
+      g_value_set_string (value, self->key);
+      break;
+
+    case PROP_VALUE:
+      g_value_set_string (value, self->value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_environment_variable_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  IdeEnvironmentVariable *self = IDE_ENVIRONMENT_VARIABLE(object);
+
+  switch (prop_id)
+    {
+    case PROP_KEY:
+      ide_environment_variable_set_key (self, g_value_get_string (value));
+      break;
+
+    case PROP_VALUE:
+      ide_environment_variable_set_value (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+ide_environment_variable_class_init (IdeEnvironmentVariableClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_environment_variable_finalize;
+  object_class->get_property = ide_environment_variable_get_property;
+  object_class->set_property = ide_environment_variable_set_property;
+
+  properties [PROP_KEY] =
+    g_param_spec_string ("key",
+                         "Key",
+                         "The key for the environment variable",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_VALUE] =
+    g_param_spec_string ("value",
+                         "Value",
+                         "The value for the environment variable",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+ide_environment_variable_init (IdeEnvironmentVariable *self)
+{
+}
+
+const gchar *
+ide_environment_variable_get_key (IdeEnvironmentVariable *self)
+{
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT_VARIABLE (self), NULL);
+
+  return self->key;
+}
+
+void
+ide_environment_variable_set_key (IdeEnvironmentVariable *self,
+                                  const gchar            *key)
+{
+  g_return_if_fail (IDE_IS_ENVIRONMENT_VARIABLE (self));
+
+  if (g_strcmp0 (key, self->key) != 0)
+    {
+      g_free (self->key);
+      self->key = g_strdup (key);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_KEY]);
+    }
+}
+
+const gchar *
+ide_environment_variable_get_value (IdeEnvironmentVariable *self)
+{
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT_VARIABLE (self), NULL);
+
+  return self->value;
+}
+
+void
+ide_environment_variable_set_value (IdeEnvironmentVariable *self,
+                                    const gchar            *value)
+{
+  g_return_if_fail (IDE_IS_ENVIRONMENT_VARIABLE (self));
+
+  if (g_strcmp0 (value, self->value) != 0)
+    {
+      g_free (self->value);
+      self->value = g_strdup (value);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_VALUE]);
+    }
+}
+
+IdeEnvironmentVariable *
+ide_environment_variable_new (const gchar *key,
+                              const gchar *value)
+{
+  return g_object_new (IDE_TYPE_ENVIRONMENT_VARIABLE,
+                       "key", key,
+                       "value", value,
+                       NULL);
+}
diff --git a/src/libide/threading/ide-environment-variable.h b/src/libide/threading/ide-environment-variable.h
new file mode 100644
index 000000000..a58d90ddd
--- /dev/null
+++ b/src/libide/threading/ide-environment-variable.h
@@ -0,0 +1,50 @@
+/* ide-environment-variable.h
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_THREADING_INSIDE) && !defined (IDE_THREADING_COMPILATION)
+# error "Only <libide-threading.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_ENVIRONMENT_VARIABLE (ide_environment_variable_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeEnvironmentVariable, ide_environment_variable, IDE, ENVIRONMENT_VARIABLE, GObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeEnvironmentVariable *ide_environment_variable_new       (const gchar            *key,
+                                                            const gchar            *value);
+IDE_AVAILABLE_IN_3_32
+const gchar            *ide_environment_variable_get_key   (IdeEnvironmentVariable *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_environment_variable_set_key   (IdeEnvironmentVariable *self,
+                                                            const gchar            *key);
+IDE_AVAILABLE_IN_3_32
+const gchar            *ide_environment_variable_get_value (IdeEnvironmentVariable *self);
+IDE_AVAILABLE_IN_3_32
+void                    ide_environment_variable_set_value (IdeEnvironmentVariable *self,
+                                                            const gchar            *value);
+
+G_END_DECLS
diff --git a/src/libide/threading/ide-environment.c b/src/libide/threading/ide-environment.c
new file mode 100644
index 000000000..41f8e6350
--- /dev/null
+++ b/src/libide/threading/ide-environment.c
@@ -0,0 +1,379 @@
+/* ide-environment.c
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-environment"
+
+#include "config.h"
+
+#include <libide-core.h>
+
+#include "ide-environment.h"
+#include "ide-environment-variable.h"
+
+struct _IdeEnvironment
+{
+  GObject    parent_instance;
+  GPtrArray *variables;
+};
+
+static void list_model_iface_init (GListModelInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (IdeEnvironment, ide_environment, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+enum {
+  CHANGED,
+  LAST_SIGNAL
+};
+
+static guint signals [LAST_SIGNAL];
+
+static void
+ide_environment_finalize (GObject *object)
+{
+  IdeEnvironment *self = (IdeEnvironment *)object;
+
+  g_clear_pointer (&self->variables, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (ide_environment_parent_class)->finalize (object);
+}
+
+static void
+ide_environment_class_init (IdeEnvironmentClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_environment_finalize;
+
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [CHANGED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+}
+
+static void
+ide_environment_items_changed (IdeEnvironment *self)
+{
+  g_assert (IDE_IS_ENVIRONMENT (self));
+
+  g_signal_emit (self, signals [CHANGED], 0);
+}
+
+static void
+ide_environment_init (IdeEnvironment *self)
+{
+  self->variables = g_ptr_array_new_with_free_func (g_object_unref);
+
+  g_signal_connect (self,
+                    "items-changed",
+                    G_CALLBACK (ide_environment_items_changed),
+                    NULL);
+}
+
+static GType
+ide_environment_get_item_type (GListModel *model)
+{
+  return IDE_TYPE_ENVIRONMENT_VARIABLE;
+}
+
+static gpointer
+ide_environment_get_item (GListModel *model,
+                          guint       position)
+{
+  IdeEnvironment *self = (IdeEnvironment *)model;
+
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT (self), NULL);
+  g_return_val_if_fail (position < self->variables->len, NULL);
+
+  return g_object_ref (g_ptr_array_index (self->variables, position));
+}
+
+static guint
+ide_environment_get_n_items (GListModel *model)
+{
+  IdeEnvironment *self = (IdeEnvironment *)model;
+
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT (self), 0);
+
+  return self->variables->len;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_n_items = ide_environment_get_n_items;
+  iface->get_item = ide_environment_get_item;
+  iface->get_item_type = ide_environment_get_item_type;
+}
+
+static void
+ide_environment_variable_notify (IdeEnvironment         *self,
+                                 GParamSpec             *pspec,
+                                 IdeEnvironmentVariable *variable)
+{
+  g_assert (IDE_IS_ENVIRONMENT (self));
+
+  g_signal_emit (self, signals [CHANGED], 0);
+}
+
+void
+ide_environment_setenv (IdeEnvironment *self,
+                        const gchar    *key,
+                        const gchar    *value)
+{
+  guint i;
+
+  g_return_if_fail (IDE_IS_ENVIRONMENT (self));
+  g_return_if_fail (key != NULL);
+
+  for (i = 0; i < self->variables->len; i++)
+    {
+      IdeEnvironmentVariable *var = g_ptr_array_index (self->variables, i);
+      const gchar *var_key = ide_environment_variable_get_key (var);
+
+      if (g_strcmp0 (key, var_key) == 0)
+        {
+          if (value == NULL)
+            {
+              g_ptr_array_remove_index (self->variables, i);
+              g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+              return;
+            }
+
+          ide_environment_variable_set_value (var, value);
+          return;
+        }
+    }
+
+  if (value != NULL)
+    {
+      IdeEnvironmentVariable *var;
+      guint position = self->variables->len;
+
+      var = g_object_new (IDE_TYPE_ENVIRONMENT_VARIABLE,
+                          "key", key,
+                          "value", value,
+                          NULL);
+      g_signal_connect_object (var,
+                               "notify",
+                               G_CALLBACK (ide_environment_variable_notify),
+                               self,
+                               G_CONNECT_SWAPPED);
+      g_ptr_array_add (self->variables, var);
+      g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+    }
+}
+
+const gchar *
+ide_environment_getenv (IdeEnvironment *self,
+                        const gchar    *key)
+{
+  guint i;
+
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  for (i = 0; i < self->variables->len; i++)
+    {
+      IdeEnvironmentVariable *var = g_ptr_array_index (self->variables, i);
+      const gchar *var_key = ide_environment_variable_get_key (var);
+
+      if (g_strcmp0 (key, var_key) == 0)
+        return ide_environment_variable_get_value (var);
+    }
+
+  return NULL;
+}
+
+/**
+ * ide_environment_get_environ:
+ * @self: An #IdeEnvironment
+ *
+ * Gets the environment as a set of key=value pairs, suitable for use
+ * in various GLib process functions.
+ *
+ * Returns: (transfer full): A newly allocated string array.
+ *
+ * Since: 3.32
+ */
+gchar **
+ide_environment_get_environ (IdeEnvironment *self)
+{
+  GPtrArray *ar;
+  guint i;
+
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT (self), NULL);
+
+  ar = g_ptr_array_new ();
+
+  for (i = 0; i < self->variables->len; i++)
+    {
+      IdeEnvironmentVariable *var = g_ptr_array_index (self->variables, i);
+      const gchar *key = ide_environment_variable_get_key (var);
+      const gchar *value = ide_environment_variable_get_value (var);
+
+      if (value == NULL)
+        value = "";
+
+      if (key != NULL)
+        g_ptr_array_add (ar, g_strdup_printf ("%s=%s", key, value));
+    }
+
+  g_ptr_array_add (ar, NULL);
+
+  return (gchar **)g_ptr_array_free (ar, FALSE);
+}
+
+IdeEnvironment *
+ide_environment_new (void)
+{
+  return g_object_new (IDE_TYPE_ENVIRONMENT, NULL);
+}
+
+void
+ide_environment_remove (IdeEnvironment         *self,
+                        IdeEnvironmentVariable *variable)
+{
+  guint i;
+
+  g_return_if_fail (IDE_IS_ENVIRONMENT (self));
+  g_return_if_fail (IDE_IS_ENVIRONMENT_VARIABLE (variable));
+
+  for (i = 0; i < self->variables->len; i++)
+    {
+      IdeEnvironmentVariable *item = g_ptr_array_index (self->variables, i);
+
+      if (item == variable)
+        {
+          g_ptr_array_remove_index (self->variables, i);
+          g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0);
+          break;
+        }
+    }
+}
+
+void
+ide_environment_append (IdeEnvironment         *self,
+                        IdeEnvironmentVariable *variable)
+{
+  guint position;
+
+  g_return_if_fail (IDE_IS_ENVIRONMENT (self));
+  g_return_if_fail (IDE_IS_ENVIRONMENT_VARIABLE (variable));
+
+  position = self->variables->len;
+
+  g_signal_connect_object (variable,
+                           "notify",
+                           G_CALLBACK (ide_environment_variable_notify),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_ptr_array_add (self->variables, g_object_ref (variable));
+  g_list_model_items_changed (G_LIST_MODEL (self), position, 0, 1);
+}
+
+/**
+ * ide_environment_copy:
+ * @self: An #IdeEnvironment
+ *
+ * Copies the contents of #IdeEnvironment into a newly allocated #IdeEnvironment.
+ *
+ * Returns: (transfer full): An #IdeEnvironment.
+ *
+ * Since: 3.32
+ */
+IdeEnvironment *
+ide_environment_copy (IdeEnvironment *self)
+{
+  g_autoptr(IdeEnvironment) copy = NULL;
+
+  g_return_val_if_fail (IDE_IS_ENVIRONMENT (self), NULL);
+
+  copy = ide_environment_new ();
+  ide_environment_copy_into (self, copy, TRUE);
+
+  return g_steal_pointer (&copy);
+}
+
+void
+ide_environment_copy_into (IdeEnvironment *self,
+                           IdeEnvironment *dest,
+                           gboolean        replace)
+{
+  g_return_if_fail (IDE_IS_ENVIRONMENT (self));
+  g_return_if_fail (IDE_IS_ENVIRONMENT (dest));
+
+  for (guint i = 0; i < self->variables->len; i++)
+    {
+      IdeEnvironmentVariable *var = g_ptr_array_index (self->variables, i);
+      const gchar *key = ide_environment_variable_get_key (var);
+      const gchar *value = ide_environment_variable_get_value (var);
+
+      if (replace || ide_environment_getenv (dest, key) == NULL)
+        ide_environment_setenv (dest, key, value);
+    }
+}
+
+/**
+ * ide_environ_parse:
+ * @pair: the KEY=VALUE pair
+ * @key: (out) (optional): a location for a @key
+ * @value: (out) (optional): a location for a @value
+ *
+ * Parses a KEY=VALUE style key-pair into @key and @value.
+ *
+ * Returns: %TRUE if @pair was successfully parsed
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_environ_parse (const gchar  *pair,
+                   gchar       **key,
+                   gchar       **value)
+{
+  const gchar *eq;
+
+  g_return_val_if_fail (pair != NULL, FALSE);
+
+  if (key != NULL)
+    *key = NULL;
+
+  if (value != NULL)
+    *value = NULL;
+
+  if ((eq = strchr (pair, '=')))
+    {
+      if (key != NULL)
+        *key = g_strndup (pair, eq - pair);
+
+      if (value != NULL)
+        *value = g_strdup (eq + 1);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
diff --git a/src/libide/threading/ide-environment.h b/src/libide/threading/ide-environment.h
new file mode 100644
index 000000000..597b80ab1
--- /dev/null
+++ b/src/libide/threading/ide-environment.h
@@ -0,0 +1,67 @@
+/* ide-environment.h
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+#if !defined (IDE_THREADING_INSIDE) && !defined (IDE_THREADING_COMPILATION)
+# error "Only <libide-threading.h> can be included directly."
+#endif
+
+#include <gio/gio.h>
+#include <libide-core.h>
+
+#include "ide-environment-variable.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_ENVIRONMENT (ide_environment_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeEnvironment, ide_environment, IDE, ENVIRONMENT, GObject)
+
+IDE_AVAILABLE_IN_3_32
+gboolean        ide_environ_parse           (const gchar            *pair,
+                                             gchar                 **key,
+                                             gchar                 **value);
+IDE_AVAILABLE_IN_3_32
+IdeEnvironment *ide_environment_new         (void);
+IDE_AVAILABLE_IN_3_32
+void            ide_environment_setenv      (IdeEnvironment         *self,
+                                             const gchar            *key,
+                                             const gchar            *value);
+IDE_AVAILABLE_IN_3_32
+const gchar    *ide_environment_getenv      (IdeEnvironment         *self,
+                                             const gchar            *key);
+IDE_AVAILABLE_IN_3_32
+gchar         **ide_environment_get_environ (IdeEnvironment         *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_environment_append      (IdeEnvironment         *self,
+                                             IdeEnvironmentVariable *variable);
+IDE_AVAILABLE_IN_3_32
+void            ide_environment_remove      (IdeEnvironment         *self,
+                                             IdeEnvironmentVariable *variable);
+IDE_AVAILABLE_IN_3_32
+IdeEnvironment *ide_environment_copy        (IdeEnvironment         *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_environment_copy_into   (IdeEnvironment         *self,
+                                             IdeEnvironment         *dest,
+                                             gboolean                replace);
+
+G_END_DECLS
diff --git a/src/libide/threading/ide-flatpak-subprocess-private.h 
b/src/libide/threading/ide-flatpak-subprocess-private.h
new file mode 100644
index 000000000..64628c69b
--- /dev/null
+++ b/src/libide/threading/ide-flatpak-subprocess-private.h
@@ -0,0 +1,50 @@
+/* ide-flatpak-subprocess-private.h
+ *
+ * Copyright 2016-2019 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 "ide-subprocess.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_FLATPAK_SUBPROCESS (ide_flatpak_subprocess_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeFlatpakSubprocess, ide_flatpak_subprocess, IDE, FLATPAK_SUBPROCESS, GObject)
+
+typedef struct
+{
+  gint source_fd;
+  gint dest_fd;
+} IdeBreakoutFdMapping;
+
+IdeSubprocess *_ide_flatpak_subprocess_new (const gchar                 *cwd,
+                                             const gchar * const         *argv,
+                                             const gchar * const         *env,
+                                             GSubprocessFlags             flags,
+                                             gboolean                     clear_flags,
+                                             gint                         stdin_fd,
+                                             gint                         stdout_fd,
+                                             gint                         stderr_fd,
+                                             const IdeBreakoutFdMapping  *fd_map,
+                                             guint                        fd_map_len,
+                                             GCancellable                *cancellable,
+                                             GError                     **error) G_GNUC_INTERNAL;
+
+G_END_DECLS
diff --git a/src/libide/threading/ide-flatpak-subprocess.c b/src/libide/threading/ide-flatpak-subprocess.c
new file mode 100644
index 000000000..981dc185c
--- /dev/null
+++ b/src/libide/threading/ide-flatpak-subprocess.c
@@ -0,0 +1,1776 @@
+/* GIO - GLib Input, Output and Streaming Library
+ *
+ * Copyright 2012, 2013, 2016 Red Hat, Inc.
+ * Copyright 2012, 2013 Canonical Limited
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published
+ * by the Free Software Foundation; either version 2 of the licence or (at
+ * your option) any later version.
+ *
+ * See the included COPYING file for more information.
+ *
+ * Authors: Colin Walters <walters verbum org>
+ *          Ryan Lortie <desrt desrt ca>
+ *          Alexander Larsson <alexl redhat com>
+ *          Christian Hergert <chergert redhat com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#define G_LOG_DOMAIN "ide-flatpak-subprocess"
+
+#include "config.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <gio/gunixinputstream.h>
+#include <gio/gunixoutputstream.h>
+#include <gio/gunixfdlist.h>
+#include <glib-unix.h>
+#include <libide-core.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "ide-flatpak-subprocess-private.h"
+#include "ide-gtask-private.h"
+
+#ifndef FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV
+# define FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV (1 << 0)
+#endif
+
+/*
+ * One very non-ideal thing about this implementation is that we use a new
+ * GDBusConnection for every instance. This is due to some difficulty in
+ * dealing with our connection being closed out from underneath us. If we
+ * can determine what was/is causing that, we should be able to move back
+ * to a shared connection (although we might want a dedicated connection
+ * for all subprocesses so that we can have exit-on-close => false).
+ */
+
+struct _IdeFlatpakSubprocess
+{
+  GObject parent_instance;
+
+  GDBusConnection *connection;
+  gulong connection_closed_handler;
+
+  GPid client_pid;
+  gint status;
+
+  GSubprocessFlags flags;
+
+  /* No reference */
+  GThread *spawn_thread;
+
+  gchar **argv;
+  gchar **env;
+  gchar *cwd;
+
+  gchar *identifier;
+
+  gint stdin_fd;
+  gint stdout_fd;
+  gint stderr_fd;
+
+  GOutputStream *stdin_pipe;
+  GInputStream *stdout_pipe;
+  GInputStream *stderr_pipe;
+
+  IdeBreakoutFdMapping *fd_mapping;
+  guint fd_mapping_len;
+
+  GMainContext *main_context;
+
+  guint sigint_id;
+  guint sigterm_id;
+  guint exited_subscription;
+
+  /* GList of GTasks for wait_async() */
+  GList *waiting;
+
+  /* Mutex/Cond pair guards client_has_exited */
+  GMutex waiter_mutex;
+  GCond waiter_cond;
+
+  guint client_has_exited : 1;
+  guint clear_env : 1;
+};
+
+/* ide_subprocess_communicate implementation below:
+ *
+ * This is a tough problem.  We have to watch 5 things at the same time:
+ *
+ *  - writing to stdin made progress
+ *  - reading from stdout made progress
+ *  - reading from stderr made progress
+ *  - process terminated
+ *  - cancellable being cancelled by caller
+ *
+ * We use a GMainContext for all of these (either as async function
+ * calls or as a GSource (in the case of the cancellable).  That way at
+ * least we don't have to worry about threading.
+ *
+ * For the sync case we use the usual trick of creating a private main
+ * context and iterating it until completion.
+ *
+ * It's very possible that the process will dump a lot of data to stdout
+ * just before it quits, so we can easily have data to read from stdout
+ * and see the process has terminated at the same time.  We want to make
+ * sure that we read all of the data from the pipes first, though, so we
+ * do IO operations at a higher priority than the wait operation (which
+ * is at G_IO_PRIORITY_DEFAULT).  Even in the case that we have to do
+ * multiple reads to get this data, the pipe() will always be polling
+ * as ready and with the async result for the read at a higher priority,
+ * the main context will not dispatch the completion for the wait().
+ *
+ * We keep our own private GCancellable.  In the event that any of the
+ * above suffers from an error condition (including the user cancelling
+ * their cancellable) we immediately dispatch the GTask with the error
+ * result and fire our cancellable to cleanup any pending operations.
+ * In the case that the error is that the user's cancellable was fired,
+ * it's vaguely wasteful to report an error because GTask will handle
+ * this automatically, so we just return FALSE.
+ *
+ * We let each pending sub-operation take a ref on the GTask of the
+ * communicate operation.  We have to be careful that we don't report
+ * the task completion more than once, though, so we keep a flag for
+ * that.
+ */
+typedef struct
+{
+  const gchar *stdin_data;
+  gsize stdin_length;
+  gsize stdin_offset;
+
+  gboolean add_nul;
+
+  GInputStream *stdin_buf;
+  GMemoryOutputStream *stdout_buf;
+  GMemoryOutputStream *stderr_buf;
+
+  GCancellable *cancellable;
+  GSource      *cancellable_source;
+
+  guint         outstanding_ops;
+  gboolean      reported_error;
+} CommunicateState;
+
+enum {
+  PROP_0,
+  PROP_ARGV,
+  PROP_CWD,
+  PROP_ENV,
+  PROP_FLAGS,
+  N_PROPS
+};
+
+static void              ide_flatpak_subprocess_sync_complete        (IdeFlatpakSubprocess  *self,
+                                                                       GAsyncResult          **result);
+static void              ide_flatpak_subprocess_sync_done            (GObject                *object,
+                                                                       GAsyncResult           *result,
+                                                                       gpointer                user_data);
+static CommunicateState *ide_flatpak_subprocess_communicate_internal (IdeFlatpakSubprocess  *subprocess,
+                                                                       gboolean                add_nul,
+                                                                       GBytes                 *stdin_buf,
+                                                                       GCancellable           *cancellable,
+                                                                       GAsyncReadyCallback     callback,
+                                                                       gpointer                user_data);
+
+static GParamSpec *properties [N_PROPS];
+
+static const gchar *
+ide_flatpak_subprocess_get_identifier (IdeSubprocess *subprocess)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  return self->identifier;
+}
+
+static GInputStream *
+ide_flatpak_subprocess_get_stdout_pipe (IdeSubprocess *subprocess)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  return self->stdout_pipe;
+}
+
+static GInputStream *
+ide_flatpak_subprocess_get_stderr_pipe (IdeSubprocess *subprocess)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  return self->stderr_pipe;
+}
+
+static GOutputStream *
+ide_flatpak_subprocess_get_stdin_pipe (IdeSubprocess *subprocess)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  return self->stdin_pipe;
+}
+
+static void
+ide_flatpak_subprocess_wait_cb (GObject      *object,
+                                 GAsyncResult *result,
+                                 gpointer      user_data)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)object;
+  gboolean *completed = user_data;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (completed != NULL);
+
+  ide_subprocess_wait_finish (IDE_SUBPROCESS (self), result, NULL);
+
+  *completed = TRUE;
+
+  if (self->main_context != NULL)
+    g_main_context_wakeup (self->main_context);
+}
+
+static gboolean
+ide_flatpak_subprocess_wait (IdeSubprocess  *subprocess,
+                              GCancellable   *cancellable,
+                              GError        **error)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  g_object_ref (self);
+
+  g_mutex_lock (&self->waiter_mutex);
+
+  if (!self->client_has_exited)
+    {
+      g_autoptr(GMainContext) free_me = NULL;
+      GMainContext *main_context;
+      gboolean completed = FALSE;
+
+      if (NULL == (main_context = g_main_context_get_thread_default ()))
+        {
+          if (IDE_IS_MAIN_THREAD ())
+            main_context = g_main_context_default ();
+          else
+            main_context = free_me = g_main_context_new ();
+        }
+
+      self->main_context = g_main_context_ref (main_context);
+      g_mutex_unlock (&self->waiter_mutex);
+
+      ide_subprocess_wait_async (IDE_SUBPROCESS (self),
+                                 cancellable,
+                                 ide_flatpak_subprocess_wait_cb,
+                                 &completed);
+
+      while (!completed)
+        g_main_context_iteration (main_context, TRUE);
+
+      goto cleanup;
+    }
+
+  g_mutex_unlock (&self->waiter_mutex);
+
+cleanup:
+  g_object_unref (self);
+
+  return self->client_has_exited;
+}
+
+static void
+ide_flatpak_subprocess_wait_async (IdeSubprocess       *subprocess,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+  g_autoptr(GTask) task = NULL;
+  g_autoptr(GMutexLocker) locker = NULL;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_flatpak_subprocess_wait_async);
+  g_task_set_priority (task, G_PRIORITY_DEFAULT_IDLE);
+
+  locker = g_mutex_locker_new (&self->waiter_mutex);
+
+  if (self->client_has_exited)
+    {
+      ide_g_task_return_boolean_from_main (task, TRUE);
+      return;
+    }
+
+  self->waiting = g_list_append (self->waiting, g_steal_pointer (&task));
+}
+
+static gboolean
+ide_flatpak_subprocess_wait_finish (IdeSubprocess  *subprocess,
+                                     GAsyncResult   *result,
+                                     GError        **error)
+{
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (subprocess));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_flatpak_subprocess_communicate_utf8_async (IdeSubprocess       *subprocess,
+                                                const char          *stdin_buf,
+                                                GCancellable        *cancellable,
+                                                GAsyncReadyCallback  callback,
+                                                gpointer             user_data)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+  g_autoptr(GBytes) stdin_bytes = NULL;
+  size_t stdin_buf_len = 0;
+
+  g_return_if_fail (IDE_IS_FLATPAK_SUBPROCESS (subprocess));
+  g_return_if_fail (stdin_buf == NULL || (self->flags & G_SUBPROCESS_FLAGS_STDIN_PIPE));
+  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+  if (stdin_buf != NULL)
+    stdin_buf_len = strlen (stdin_buf);
+  stdin_bytes = g_bytes_new (stdin_buf, stdin_buf_len);
+
+  ide_flatpak_subprocess_communicate_internal (self, TRUE, stdin_bytes, cancellable, callback, user_data);
+}
+
+static gboolean
+communicate_result_validate_utf8 (const char            *stream_name,
+                                  char                 **return_location,
+                                  GMemoryOutputStream   *buffer,
+                                  GError               **error)
+{
+  IDE_ENTRY;
+
+  if (return_location == NULL)
+    IDE_RETURN (TRUE);
+
+  if (buffer)
+    {
+      const char *end;
+      GError *local_error = NULL;
+
+      if (!g_output_stream_is_closed (G_OUTPUT_STREAM (buffer)))
+        g_output_stream_close (G_OUTPUT_STREAM (buffer), NULL, &local_error);
+
+      if (local_error != NULL)
+        {
+          g_propagate_error (error, local_error);
+          IDE_RETURN (FALSE);
+        }
+
+      *return_location = g_memory_output_stream_steal_data (buffer);
+      if (!g_utf8_validate (*return_location, -1, &end))
+        {
+          g_free (*return_location);
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                       "Invalid UTF-8 in child %s at offset %lu",
+                       stream_name,
+                       (unsigned long) (end - *return_location));
+          IDE_RETURN (FALSE);
+        }
+    }
+  else
+    *return_location = NULL;
+
+  IDE_RETURN (TRUE);
+}
+
+static gboolean
+ide_flatpak_subprocess_communicate_utf8_finish (IdeSubprocess  *subprocess,
+                                                 GAsyncResult   *result,
+                                                 char          **stdout_buf,
+                                                 char          **stderr_buf,
+                                                 GError        **error)
+{
+  gboolean ret = FALSE;
+  CommunicateState *state;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_FLATPAK_SUBPROCESS (subprocess), FALSE);
+  g_return_val_if_fail (g_task_is_valid (result, subprocess), FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  g_object_ref (result);
+
+  state = g_task_get_task_data ((GTask*)result);
+  if (!g_task_propagate_boolean ((GTask*)result, error))
+    IDE_GOTO (out);
+
+  if (!communicate_result_validate_utf8 ("stdout", stdout_buf, state->stdout_buf, error))
+    IDE_GOTO (out);
+
+  if (!communicate_result_validate_utf8 ("stderr", stderr_buf, state->stderr_buf, error))
+    IDE_GOTO (out);
+
+  ret = TRUE;
+
+ out:
+  g_object_unref (result);
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+ide_flatpak_subprocess_communicate_utf8 (IdeSubprocess  *subprocess,
+                                          const char     *stdin_buf,
+                                          GCancellable   *cancellable,
+                                          char          **stdout_buf,
+                                          char          **stderr_buf,
+                                          GError        **error)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+  g_autoptr(GAsyncResult) result = NULL;
+  g_autoptr(GBytes) stdin_bytes = NULL;
+  size_t stdin_buf_len = 0;
+  gboolean success;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_FLATPAK_SUBPROCESS (subprocess), FALSE);
+  g_return_val_if_fail (stdin_buf == NULL || (self->flags & G_SUBPROCESS_FLAGS_STDIN_PIPE), FALSE);
+  g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  if (stdin_buf != NULL)
+    stdin_buf_len = strlen (stdin_buf);
+  stdin_bytes = g_bytes_new (stdin_buf, stdin_buf_len);
+
+  ide_flatpak_subprocess_communicate_internal (self,
+                                                TRUE,
+                                                stdin_bytes,
+                                                cancellable,
+                                                ide_flatpak_subprocess_sync_done,
+                                                &result);
+  ide_flatpak_subprocess_sync_complete (self, &result);
+  success = ide_subprocess_communicate_utf8_finish (subprocess, result, stdout_buf, stderr_buf, error);
+
+  IDE_RETURN (success);
+}
+
+static gboolean
+ide_flatpak_subprocess_get_successful (IdeSubprocess *subprocess)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  return WIFEXITED (self->status) && WEXITSTATUS (self->status) == 0;
+}
+
+static gboolean
+ide_flatpak_subprocess_get_if_exited (IdeSubprocess *subprocess)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  return WIFEXITED (self->status);
+}
+
+static gint
+ide_flatpak_subprocess_get_exit_status (IdeSubprocess *subprocess)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (self->client_has_exited);
+
+  if (!WIFEXITED (self->status))
+    return 1;
+
+  return WEXITSTATUS (self->status);
+}
+
+static gboolean
+ide_flatpak_subprocess_get_if_signaled (IdeSubprocess *subprocess)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (self->client_has_exited == TRUE);
+
+  return WIFSIGNALED (self->status);
+}
+
+static gint
+ide_flatpak_subprocess_get_term_sig (IdeSubprocess *subprocess)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (self->client_has_exited == TRUE);
+
+  return WTERMSIG (self->status);
+}
+
+static gint
+ide_flatpak_subprocess_get_status (IdeSubprocess *subprocess)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (self->client_has_exited == TRUE);
+
+  return self->status;
+}
+
+static void
+ide_flatpak_subprocess_send_signal (IdeSubprocess *subprocess,
+                                     gint           signal_num)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  /* Signal delivery is not guaranteed, so we can drop this on the floor. */
+  if (self->client_has_exited || self->connection == NULL)
+    IDE_EXIT;
+
+  IDE_TRACE_MSG ("Sending signal %d to pid %u", signal_num, (guint)self->client_pid);
+
+  g_dbus_connection_call_sync (self->connection,
+                               "org.freedesktop.Flatpak",
+                               "/org/freedesktop/Flatpak/Development",
+                               "org.freedesktop.Flatpak.Development",
+                               "HostCommandSignal",
+                               g_variant_new ("(uub)", self->client_pid, signal_num, TRUE),
+                               NULL,
+                               G_DBUS_CALL_FLAGS_NONE, -1,
+                               NULL, NULL);
+
+  IDE_EXIT;
+}
+
+static void
+ide_flatpak_subprocess_force_exit (IdeSubprocess *subprocess)
+{
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (subprocess));
+
+  ide_flatpak_subprocess_send_signal (subprocess, SIGKILL);
+}
+
+static void
+ide_flatpak_subprocess_sync_complete (IdeFlatpakSubprocess  *self,
+                                       GAsyncResult          **result)
+{
+  g_autoptr(GMainContext) free_me = NULL;
+  GMainContext *main_context = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (result != NULL);
+  g_assert (*result == NULL || G_IS_ASYNC_RESULT (*result));
+
+  if (NULL == (main_context = g_main_context_get_thread_default ()))
+    {
+      if (IDE_IS_MAIN_THREAD ())
+        main_context = g_main_context_default ();
+      else
+        main_context = free_me = g_main_context_new ();
+    }
+
+  g_mutex_lock (&self->waiter_mutex);
+  self->main_context = g_main_context_ref (main_context);
+  g_mutex_unlock (&self->waiter_mutex);
+
+  while (*result == NULL)
+    g_main_context_iteration (main_context, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_flatpak_subprocess_sync_done (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)object;
+  GAsyncResult **ret = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (ret != NULL);
+  g_assert (*ret == NULL);
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  *ret = g_object_ref (result);
+
+  g_mutex_lock (&self->waiter_mutex);
+  if (self->main_context != NULL)
+    g_main_context_wakeup (self->main_context);
+  g_mutex_unlock (&self->waiter_mutex);
+
+  IDE_EXIT;
+}
+
+static void
+ide_subprocess_communicate_state_free (gpointer data)
+{
+  CommunicateState *state = data;
+
+  g_clear_object (&state->cancellable);
+  g_clear_object (&state->stdin_buf);
+  g_clear_object (&state->stdout_buf);
+  g_clear_object (&state->stderr_buf);
+
+  if (state->cancellable_source)
+    {
+      if (!g_source_is_destroyed (state->cancellable_source))
+        g_source_destroy (state->cancellable_source);
+      g_source_unref (state->cancellable_source);
+    }
+
+  g_slice_free (CommunicateState, state);
+}
+
+static gboolean
+ide_subprocess_communicate_cancelled (gpointer user_data)
+{
+  CommunicateState *state = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (state != NULL);
+  g_assert (G_IS_CANCELLABLE (state->cancellable));
+
+  g_cancellable_cancel (state->cancellable);
+
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+ide_subprocess_communicate_made_progress (GObject      *source_object,
+                                          GAsyncResult *result,
+                                          gpointer      user_data)
+{
+  CommunicateState *state;
+  IdeFlatpakSubprocess *subprocess;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GTask) task = user_data;
+  gpointer source;
+
+  IDE_ENTRY;
+
+  g_assert (source_object != NULL);
+
+  subprocess = g_task_get_source_object (task);
+  state = g_task_get_task_data (task);
+  source = source_object;
+
+  state->outstanding_ops--;
+
+  if (source == subprocess->stdin_pipe ||
+      source == state->stdout_buf ||
+      source == state->stderr_buf)
+    {
+      if (g_output_stream_splice_finish (source, result, &error) == -1)
+        IDE_GOTO (out);
+
+      if (source == state->stdout_buf || source == state->stderr_buf)
+        {
+          /* This is a memory stream, so it can't be cancelled or return
+           * an error really.
+           */
+          if (state->add_nul)
+            {
+              gsize bytes_written = 0;
+              if (!g_output_stream_write_all (source, "\0", 1, &bytes_written, NULL, &error))
+                IDE_GOTO (out);
+            }
+          if (!g_output_stream_close (source, NULL, &error))
+            IDE_GOTO (out);
+        }
+    }
+  else if (source == subprocess)
+    {
+      (void) ide_subprocess_wait_finish (IDE_SUBPROCESS (subprocess), result, &error);
+    }
+  else
+    g_assert_not_reached ();
+
+ out:
+  if (error != NULL)
+    {
+      /* Only report the first error we see.
+       *
+       * We might be seeing an error as a result of the cancellation
+       * done when the process quits.
+       */
+      if (!state->reported_error)
+        {
+          state->reported_error = TRUE;
+          g_cancellable_cancel (state->cancellable);
+          ide_g_task_return_error_from_main (task, g_steal_pointer (&error));
+        }
+    }
+  else if (state->outstanding_ops == 0)
+    {
+      ide_g_task_return_boolean_from_main (task, TRUE);
+    }
+
+  IDE_EXIT;
+}
+
+static CommunicateState *
+ide_flatpak_subprocess_communicate_internal (IdeFlatpakSubprocess *subprocess,
+                                              gboolean               add_nul,
+                                              GBytes                *stdin_buf,
+                                              GCancellable          *cancellable,
+                                              GAsyncReadyCallback    callback,
+                                              gpointer               user_data)
+{
+  CommunicateState *state;
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (subprocess));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (subprocess, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_flatpak_subprocess_communicate_internal);
+  g_task_set_priority (task, G_PRIORITY_DEFAULT_IDLE);
+
+  state = g_slice_new0 (CommunicateState);
+  g_task_set_task_data (task, state, ide_subprocess_communicate_state_free);
+
+  state->cancellable = g_cancellable_new ();
+  state->add_nul = add_nul;
+  state->outstanding_ops = 1;
+
+  if (cancellable)
+    {
+      state->cancellable_source = g_cancellable_source_new (cancellable);
+      /* No ref held here, but we unref the source from state's free function */
+      g_source_set_callback (state->cancellable_source, ide_subprocess_communicate_cancelled, state, NULL);
+      g_source_attach (state->cancellable_source, g_main_context_get_thread_default ());
+    }
+
+  /* Increment the outstanding ops count, to protect from reentrancy */
+  if (subprocess->stdin_pipe)
+    state->outstanding_ops++;
+  if (subprocess->stdout_pipe)
+    state->outstanding_ops++;
+  if (subprocess->stderr_pipe)
+    state->outstanding_ops++;
+
+  if (subprocess->stdin_pipe)
+    {
+      g_assert (stdin_buf != NULL);
+      state->stdin_buf = g_memory_input_stream_new_from_bytes (stdin_buf);
+      g_output_stream_splice_async (subprocess->stdin_pipe, (GInputStream*)state->stdin_buf,
+                                    G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE | 
G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET,
+                                    G_PRIORITY_DEFAULT, state->cancellable,
+                                    ide_subprocess_communicate_made_progress, g_object_ref (task));
+    }
+
+  if (subprocess->stdout_pipe)
+    {
+      state->stdout_buf = (GMemoryOutputStream*)g_memory_output_stream_new_resizable ();
+      g_output_stream_splice_async ((GOutputStream*)state->stdout_buf, subprocess->stdout_pipe,
+                                    G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE,
+                                    G_PRIORITY_DEFAULT, state->cancellable,
+                                    ide_subprocess_communicate_made_progress, g_object_ref (task));
+    }
+
+  if (subprocess->stderr_pipe)
+    {
+      state->stderr_buf = (GMemoryOutputStream*)g_memory_output_stream_new_resizable ();
+      g_output_stream_splice_async ((GOutputStream*)state->stderr_buf, subprocess->stderr_pipe,
+                                    G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE,
+                                    G_PRIORITY_DEFAULT, state->cancellable,
+                                    ide_subprocess_communicate_made_progress, g_object_ref (task));
+    }
+
+  ide_subprocess_wait_async (IDE_SUBPROCESS (subprocess), state->cancellable,
+                             ide_subprocess_communicate_made_progress, g_object_ref (task));
+
+  IDE_RETURN (state);
+}
+
+static void
+ide_flatpak_subprocess_communicate_async (IdeSubprocess       *subprocess,
+                                           GBytes              *stdin_buf,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_flatpak_subprocess_communicate_internal (self, FALSE, stdin_buf, cancellable, callback, user_data);
+}
+
+static gboolean
+ide_flatpak_subprocess_communicate_finish (IdeSubprocess  *subprocess,
+                                            GAsyncResult   *result,
+                                            GBytes        **stdout_buf,
+                                            GBytes        **stderr_buf,
+                                            GError        **error)
+{
+  CommunicateState *state;
+  GTask *task = (GTask *)result;
+  gboolean success;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (subprocess));
+  g_assert (G_IS_TASK (task));
+
+  g_object_ref (task);
+
+  state = g_task_get_task_data (task);
+
+  g_assert (state != NULL);
+
+  success = g_task_propagate_boolean (task, error);
+
+  if (success)
+    {
+      if (stdout_buf)
+        *stdout_buf = state->stdout_buf ?
+                      g_memory_output_stream_steal_as_bytes (state->stdout_buf) :
+                      g_bytes_new (NULL, 0);
+
+      if (stderr_buf)
+        *stderr_buf = state->stderr_buf ?
+                      g_memory_output_stream_steal_as_bytes (state->stderr_buf) :
+                      g_bytes_new (NULL, 0);
+    }
+
+  g_object_unref (task);
+
+  IDE_RETURN (success);
+}
+
+static gboolean
+ide_flatpak_subprocess_communicate (IdeSubprocess  *subprocess,
+                                     GBytes         *stdin_buf,
+                                     GCancellable   *cancellable,
+                                     GBytes        **stdout_buf,
+                                     GBytes        **stderr_buf,
+                                     GError        **error)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)subprocess;
+  g_autoptr(GAsyncResult) result = NULL;
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_flatpak_subprocess_communicate_internal (self,
+                                                FALSE,
+                                                stdin_buf,
+                                                cancellable,
+                                                ide_flatpak_subprocess_sync_done,
+                                                &result);
+  ide_flatpak_subprocess_sync_complete (self, &result);
+
+  ret = ide_flatpak_subprocess_communicate_finish (subprocess, result, stdout_buf, stderr_buf, error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+subprocess_iface_init (IdeSubprocessInterface *iface)
+{
+  iface->get_identifier = ide_flatpak_subprocess_get_identifier;
+  iface->get_stdout_pipe = ide_flatpak_subprocess_get_stdout_pipe;
+  iface->get_stderr_pipe = ide_flatpak_subprocess_get_stderr_pipe;
+  iface->get_stdin_pipe = ide_flatpak_subprocess_get_stdin_pipe;
+  iface->wait = ide_flatpak_subprocess_wait;
+  iface->wait_async = ide_flatpak_subprocess_wait_async;
+  iface->wait_finish = ide_flatpak_subprocess_wait_finish;
+  iface->get_successful = ide_flatpak_subprocess_get_successful;
+  iface->get_if_exited = ide_flatpak_subprocess_get_if_exited;
+  iface->get_exit_status = ide_flatpak_subprocess_get_exit_status;
+  iface->get_if_signaled = ide_flatpak_subprocess_get_if_signaled;
+  iface->get_term_sig = ide_flatpak_subprocess_get_term_sig;
+  iface->get_status = ide_flatpak_subprocess_get_status;
+  iface->send_signal = ide_flatpak_subprocess_send_signal;
+  iface->force_exit = ide_flatpak_subprocess_force_exit;
+  iface->communicate = ide_flatpak_subprocess_communicate;
+  iface->communicate_utf8 = ide_flatpak_subprocess_communicate_utf8;
+  iface->communicate_async = ide_flatpak_subprocess_communicate_async;
+  iface->communicate_finish = ide_flatpak_subprocess_communicate_finish;
+  iface->communicate_utf8_async = ide_flatpak_subprocess_communicate_utf8_async;
+  iface->communicate_utf8_finish = ide_flatpak_subprocess_communicate_utf8_finish;
+}
+
+static gboolean
+sigterm_handler (gpointer user_data)
+{
+  IdeFlatpakSubprocess *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  g_dbus_connection_call_sync (self->connection,
+                               "org.freedesktop.Flatpak",
+                               "/org/freedesktop/Flatpak/Development",
+                               "org.freedesktop.Flatpak.Development",
+                               "HostCommandSignal",
+                               g_variant_new ("(uub)", self->client_pid, SIGTERM, TRUE),
+                               NULL,
+                               G_DBUS_CALL_FLAGS_NONE, -1,
+                               NULL, NULL);
+
+  kill (getpid (), SIGTERM);
+
+  IDE_RETURN (G_SOURCE_CONTINUE);
+}
+
+static gboolean
+sigint_handler (gpointer user_data)
+{
+  IdeFlatpakSubprocess *self = user_data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  g_dbus_connection_call_sync (self->connection,
+                               "org.freedesktop.Flatpak",
+                               "/org/freedesktop/Flatpak/Development",
+                               "org.freedesktop.Flatpak.Development",
+                               "HostCommandSignal",
+                               g_variant_new ("(uub)", self->client_pid, SIGINT, TRUE),
+                               NULL,
+                               G_DBUS_CALL_FLAGS_NONE, -1,
+                               NULL, NULL);
+
+  kill (getpid (), SIGINT);
+
+  IDE_RETURN (G_SOURCE_CONTINUE);
+}
+
+static void
+maybe_create_input_stream (GInputStream **ret,
+                           gint          *fdptr,
+                           gboolean       needs_stream)
+{
+  g_assert (ret != NULL);
+  g_assert (*ret == NULL);
+  g_assert (fdptr != NULL);
+
+  /*
+   * Only create a stream if we aren't merging to stdio and the flags request
+   * that we need a stream.  We are also stealing the file-descriptor while
+   * doing so.
+   */
+  if (needs_stream)
+    {
+      if (*fdptr > 2)
+        *ret = g_unix_input_stream_new (*fdptr, TRUE);
+    }
+  else if (*fdptr != -1)
+    {
+      close (*fdptr);
+    }
+
+  *fdptr = -1;
+}
+
+static void
+maybe_create_output_stream (GOutputStream **ret,
+                            gint           *fdptr,
+                            gboolean        needs_stream)
+{
+  g_assert (ret != NULL);
+  g_assert (*ret == NULL);
+  g_assert (fdptr != NULL);
+
+  /*
+   * Only create a stream if we aren't merging to stdio and the flags request
+   * that we need a stream.  We are also stealing the file-descriptor while
+   * doing so.
+   */
+  if (needs_stream)
+    {
+      if (*fdptr > 2)
+        *ret = g_unix_output_stream_new (*fdptr, TRUE);
+    }
+  else if (*fdptr != -1)
+    {
+      close (*fdptr);
+    }
+
+  *fdptr = -1;
+}
+
+static void
+ide_flatpak_subprocess_complete_command_locked (IdeFlatpakSubprocess *self,
+                                                 gint                   exit_status)
+{
+  GList *waiting;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (G_IS_DBUS_CONNECTION (self->connection));
+
+  self->client_has_exited = TRUE;
+  self->status = exit_status;
+
+  /*
+   * Clear process identifiers to prevent accidental use by API consumers
+   * after the process has exited.
+   */
+  self->client_pid = 0;
+  g_clear_pointer (&self->identifier, g_free);
+
+  /* Remove our sources used for signal propagation */
+  g_clear_handle_id (&self->sigint_id, g_source_remove);
+  g_clear_handle_id (&self->sigterm_id, g_source_remove);
+
+  /* Complete async workers */
+  waiting = g_steal_pointer (&self->waiting);
+
+  for (const GList *iter = waiting; iter != NULL; iter = iter->next)
+    {
+      g_autoptr(GTask) task = iter->data;
+
+      ide_g_task_return_boolean_from_main (task, TRUE);
+    }
+
+  g_list_free (waiting);
+
+  /* Notify synchronous waiters */
+  g_cond_broadcast (&self->waiter_cond);
+
+  g_signal_handler_disconnect (self->connection, self->connection_closed_handler);
+  self->connection_closed_handler = 0;
+
+  g_clear_object (&self->connection);
+
+  if (self->main_context != NULL)
+    g_main_context_wakeup (self->main_context);
+
+  IDE_EXIT;
+}
+
+static void
+host_command_exited_cb (GDBusConnection *connection,
+                        const gchar     *sender_name,
+                        const gchar     *object_path,
+                        const gchar     *interface_name,
+                        const gchar     *signal_name,
+                        GVariant        *parameters,
+                        gpointer         user_data)
+{
+  g_autoptr(IdeFlatpakSubprocess) finalize_protect = NULL;
+  IdeFlatpakSubprocess *self = user_data;
+  g_autoptr(GMutexLocker) locker = NULL;
+  guint32 client_pid = 0;
+  guint32 exit_status = 0;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_DBUS_CONNECTION (connection));
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  finalize_protect = g_object_ref (self);
+
+  if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(uu)")))
+    IDE_EXIT;
+
+  g_variant_get (parameters, "(uu)", &client_pid, &exit_status);
+  if (client_pid != (guint32)self->client_pid)
+    IDE_EXIT;
+
+  locker = g_mutex_locker_new (&self->waiter_mutex);
+
+  IDE_TRACE_MSG ("Host process %u exited with %u",
+                 (guint)self->client_pid,
+                 (guint)exit_status);
+
+  /* We can release our dbus signal handler now */
+  if (self->exited_subscription != 0)
+    {
+      IDE_TRACE_MSG ("Unsubscribing from DBus subscription %d", self->exited_subscription);
+      g_dbus_connection_signal_unsubscribe (self->connection, self->exited_subscription);
+      self->exited_subscription = 0;
+    }
+
+  ide_flatpak_subprocess_complete_command_locked (self, exit_status);
+
+  IDE_EXIT;
+}
+
+static void
+ide_flatpak_subprocess_cancelled (IdeFlatpakSubprocess *self,
+                                   GCancellable          *cancellable)
+{
+  IDE_ENTRY;
+
+  g_assert (G_IS_CANCELLABLE (cancellable));
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  ide_subprocess_force_exit (IDE_SUBPROCESS (self));
+
+  IDE_EXIT;
+}
+
+static inline void
+maybe_close (gint *fd)
+{
+  g_assert (fd != NULL);
+  g_assert (*fd >= -1);
+
+  if (*fd > 2)
+    close (*fd);
+
+  *fd = -1;
+}
+
+static void
+ide_flatpak_subprocess_connection_closed (IdeFlatpakSubprocess *self,
+                                           gboolean               remote_peer_vanished,
+                                           const GError          *error,
+                                           GDBusConnection       *connection)
+{
+  g_autoptr(GMutexLocker) locker = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (G_IS_DBUS_CONNECTION (connection));
+
+  locker = g_mutex_locker_new (&self->waiter_mutex);
+
+  IDE_TRACE_MSG ("Synthesizing failure for client pid %u", (guint)self->client_pid);
+
+  self->exited_subscription = 0;
+  ide_flatpak_subprocess_complete_command_locked (self, -1);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_flatpak_subprocess_initable_init (GInitable     *initable,
+                                       GCancellable  *cancellable,
+                                       GError       **error)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)initable;
+  g_autoptr(GVariantBuilder) fd_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{uh}"));
+  g_autoptr(GVariantBuilder) env_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{ss}"));
+  g_autoptr(GUnixFDList) fd_list = g_unix_fd_list_new ();
+  g_autoptr(GVariant) reply = NULL;
+  g_autoptr(GVariant) params = NULL;
+  guint32 client_pid = 0;
+  gint stdout_pair[2] = { -1, -1 };
+  gint stderr_pair[2] = { -1, -1 };
+  gint stdin_pair[2] = { -1, -1 };
+  gint stdin_handle = -1;
+  gint stdout_handle = -1;
+  gint stderr_handle = -1;
+  gboolean ret = FALSE;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /*
+   * FIXME:
+   *
+   * Because we are seeing a rather difficult to track down bug where we lose
+   * the connection upon submission of the HostCommand() after a timeout period
+   * (the dbus-daemon is closing our connection) we are using a private
+   * GDBusConnection for the command.
+   *
+   * This means we need to ensure we close the connection as soon as we can
+   * so that we don't hold things open for too long. Additionally, if we do
+   * get disconnected from the daemon, we don't want to crash but instead will
+   * synthesize the completion of the command. However, this should be much
+   * more unlikely since we haven't seen this failure case.
+   *
+   * One thing we could look into for recovery is to send a SIGKILL to a new
+   * connection (of our client pid) during recovery to ensure that it dies and
+   * force our operation to fail. Callers could handle this during wait_async()
+   * wait_finish() pairs. Again, not ideal.
+   */
+  self->connection =
+    g_dbus_connection_new_for_address_sync (g_getenv ("DBUS_SESSION_BUS_ADDRESS"),
+                                            G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | 
G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
+                                            NULL,
+                                            cancellable,
+                                            error);
+
+  if (self->connection == NULL)
+    IDE_RETURN (FALSE);
+
+  g_dbus_connection_set_exit_on_close (self->connection, FALSE);
+
+
+  /*
+   * Handle STDIN for the process.
+   *
+   * Make sure we handle inherit STDIN, a new pipe (so that the application can
+   * get the stdin stream), or simply redirect to /dev/null.
+   */
+  if (self->stdin_fd != -1)
+    {
+      self->flags &= ~G_SUBPROCESS_FLAGS_STDIN_PIPE;
+      stdin_pair[0] = self->stdin_fd;
+      self->stdin_fd = -1;
+    }
+  else if (self->flags & G_SUBPROCESS_FLAGS_STDIN_INHERIT)
+    {
+      self->flags &= ~G_SUBPROCESS_FLAGS_STDIN_PIPE;
+      stdin_pair[0] = STDIN_FILENO;
+    }
+  else if (self->flags & G_SUBPROCESS_FLAGS_STDIN_PIPE)
+    {
+      if (!g_unix_open_pipe (stdin_pair, FD_CLOEXEC, error))
+        IDE_GOTO (cleanup_fds);
+    }
+  else
+    {
+      self->flags &= ~G_SUBPROCESS_FLAGS_STDIN_PIPE;
+      stdin_pair[0] = open ("/dev/null", O_CLOEXEC | O_RDWR, 0);
+      if (stdin_pair[0] == -1)
+        IDE_GOTO (cleanup_fds);
+    }
+
+  g_assert (stdin_pair[0] != -1);
+
+  stdin_handle = g_unix_fd_list_append (fd_list, stdin_pair[0], error);
+  if (stdin_handle == -1)
+    IDE_GOTO (cleanup_fds);
+  else
+    maybe_close (&stdin_pair[0]);
+
+
+  /*
+   * Setup STDOUT for the process.
+   *
+   * Make sure we redirect STDOUT to our stdout, unless a pipe was requested
+   * for the application to read. However, if silence was requested, redirect
+   * to /dev/null.
+   */
+  if (self->stdout_fd != -1)
+    {
+      self->flags &= ~G_SUBPROCESS_FLAGS_STDOUT_PIPE;
+      stdout_pair[1] = self->stdout_fd;
+      self->stdout_fd = -1;
+    }
+  else if (self->flags & G_SUBPROCESS_FLAGS_STDOUT_SILENCE)
+    {
+      self->flags &= ~G_SUBPROCESS_FLAGS_STDOUT_PIPE;
+      stdout_pair[1] = open ("/dev/null", O_CLOEXEC | O_RDWR, 0);
+      if (stdout_pair[1] == -1)
+        IDE_GOTO (cleanup_fds);
+    }
+  else if (self->flags & G_SUBPROCESS_FLAGS_STDOUT_PIPE)
+    {
+      if (!g_unix_open_pipe (stdout_pair, FD_CLOEXEC, error))
+        IDE_GOTO (cleanup_fds);
+    }
+  else
+    {
+      self->flags &= ~G_SUBPROCESS_FLAGS_STDOUT_PIPE;
+      stdout_pair[1] = STDOUT_FILENO;
+    }
+
+  g_assert (stdout_pair[1] != -1);
+
+  stdout_handle = g_unix_fd_list_append (fd_list, stdout_pair[1], error);
+  if (stdout_handle == -1)
+    IDE_GOTO (cleanup_fds);
+  else
+    maybe_close (&stdout_pair[1]);
+
+
+  /*
+   * Handle STDERR for the process.
+   *
+   * If silence is requested, we simply redirect to /dev/null. If the
+   * application requested to read from the subprocesses stderr, then we need
+   * to create a pipe. Otherwose, merge stderr into our own stderr.
+   */
+  if (self->stderr_fd != -1)
+    {
+      self->flags &= ~G_SUBPROCESS_FLAGS_STDERR_PIPE;
+      stderr_pair[1] = self->stderr_fd;
+      self->stderr_fd = -1;
+    }
+  else if (self->flags & G_SUBPROCESS_FLAGS_STDERR_SILENCE)
+    {
+      self->flags &= ~G_SUBPROCESS_FLAGS_STDERR_PIPE;
+      stderr_pair[1] = open ("/dev/null", O_CLOEXEC | O_RDWR, 0);
+      if (stderr_pair[1] == -1)
+        IDE_GOTO (cleanup_fds);
+    }
+  else if (self->flags & G_SUBPROCESS_FLAGS_STDERR_PIPE)
+    {
+      if (!g_unix_open_pipe (stderr_pair, FD_CLOEXEC, error))
+        IDE_GOTO (cleanup_fds);
+    }
+  else
+    {
+      self->flags &= ~G_SUBPROCESS_FLAGS_STDERR_PIPE;
+      stderr_pair[1] = STDERR_FILENO;
+    }
+
+  g_assert (stderr_pair[1] != -1);
+
+  stderr_handle = g_unix_fd_list_append (fd_list, stderr_pair[1], error);
+  if (stderr_handle == -1)
+    IDE_GOTO (cleanup_fds);
+  else
+    maybe_close (&stderr_pair[1]);
+
+
+  /*
+   * Build our FDs for the message.
+   */
+  g_variant_builder_add (fd_builder, "{uh}", 0, stdin_handle);
+  g_variant_builder_add (fd_builder, "{uh}", 1, stdout_handle);
+  g_variant_builder_add (fd_builder, "{uh}", 2, stderr_handle);
+
+
+  /*
+   * Now add the rest of our FDs that we might need to map in for which
+   * the subprocess launcher tried to map.
+   */
+  for (guint i = 0; i < self->fd_mapping_len; i++)
+    {
+      const IdeBreakoutFdMapping *map = &self->fd_mapping[i];
+      g_autoptr(GError) fd_error = NULL;
+      gint dest_handle;
+
+      dest_handle = g_unix_fd_list_append (fd_list, map->source_fd, &fd_error);
+
+      if (dest_handle != -1)
+        g_variant_builder_add (fd_builder, "{uh}", map->dest_fd, dest_handle);
+      else
+        g_warning ("%s", fd_error->message);
+
+      close (map->source_fd);
+    }
+
+  /*
+   * We don't want to allow these FDs to be used again.
+   */
+  self->fd_mapping_len = 0;
+  g_clear_pointer (&self->fd_mapping, g_free);
+
+
+  /*
+   * Build streams for our application to use.
+   */
+  maybe_create_output_stream (&self->stdin_pipe, &stdin_pair[1], !!(self->flags & 
G_SUBPROCESS_FLAGS_STDIN_PIPE));
+  maybe_create_input_stream (&self->stdout_pipe, &stdout_pair[0], !!(self->flags & 
G_SUBPROCESS_FLAGS_STDOUT_PIPE));
+  maybe_create_input_stream (&self->stderr_pipe, &stderr_pair[0], !!(self->flags & 
G_SUBPROCESS_FLAGS_STDERR_PIPE));
+
+
+  /*
+   * Build our environment variables message.
+   */
+  if (self->env != NULL)
+    {
+      for (guint i = 0; self->env[i]; i++)
+        {
+          const gchar *pair = self->env[i];
+          const gchar *eq = strchr (pair, '=');
+          const gchar *val = eq ? eq + 1 : "";
+          g_autofree gchar *key = eq ? g_strndup (pair, eq - pair) : g_strdup (pair);
+
+          g_variant_builder_add (env_builder, "{ss}", key, val);
+        }
+    }
+
+
+  /*
+   * Register signal handlers for SIGTERM/SIGINT so that we can terminate
+   * the host process with us (which won't be guaranteed since its outside
+   * our cgroup, nor can we use a process group leader).
+   */
+  self->sigterm_id = g_unix_signal_add (SIGTERM, sigterm_handler, self);
+  self->sigint_id = g_unix_signal_add (SIGINT, sigint_handler, self);
+
+
+  /*
+   * Make sure we've closed or stolen all of the FDs that are in play
+   * before calling the DBus service.
+   */
+  g_assert_cmpint (-1, ==, stdin_pair[0]);
+  g_assert_cmpint (-1, ==, stdin_pair[1]);
+  g_assert_cmpint (-1, ==, stdout_pair[0]);
+  g_assert_cmpint (-1, ==, stdout_pair[1]);
+  g_assert_cmpint (-1, ==, stderr_pair[0]);
+  g_assert_cmpint (-1, ==, stderr_pair[1]);
+
+
+  /*
+   * Connect to the HostCommandExited signal so that we can make progress
+   * on all tasks waiting on ide_subprocess_wait() and its async variants.
+   * We need to do this before spawning the process to avoid the race.
+   */
+  self->exited_subscription = g_dbus_connection_signal_subscribe (self->connection,
+                                                                  NULL,
+                                                                  "org.freedesktop.Flatpak.Development",
+                                                                  "HostCommandExited",
+                                                                  "/org/freedesktop/Flatpak/Development",
+                                                                  NULL,
+                                                                  G_DBUS_SIGNAL_FLAGS_NONE,
+                                                                  host_command_exited_cb,
+                                                                  self,
+                                                                  NULL);
+
+
+  /*
+   * We wait to connect to closed until here so that we don't lose our
+   * connection potentially during setup.
+   */
+  self->connection_closed_handler =
+    g_signal_connect_object (self->connection,
+                             "closed",
+                             G_CALLBACK (ide_flatpak_subprocess_connection_closed),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+
+  /*
+   * Now call the HostCommand service to execute the process within the host
+   * system. We need to ensure our fd_list is sent across for redirecting
+   * various standard streams.
+   */
+  g_assert_cmpint (g_unix_fd_list_get_length (fd_list), >=, 3);
+  params = g_variant_new ("(^ay^aay@a{uh}@a{ss}u)",
+                          self->cwd ?: g_get_home_dir (),
+                          self->argv,
+                          g_variant_builder_end (g_steal_pointer (&fd_builder)),
+                          g_variant_builder_end (g_steal_pointer (&env_builder)),
+                          self->clear_env ? FLATPAK_HOST_COMMAND_FLAGS_CLEAR_ENV : 0);
+  g_variant_take_ref (params);
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    g_autofree gchar *str = g_variant_print (params, TRUE);
+    IDE_TRACE_MSG ("Calling HostCommand with %s", str);
+  }
+#endif
+
+  reply = g_dbus_connection_call_with_unix_fd_list_sync (self->connection,
+                                                         "org.freedesktop.Flatpak",
+                                                         "/org/freedesktop/Flatpak/Development",
+                                                         "org.freedesktop.Flatpak.Development",
+                                                         "HostCommand",
+                                                         params,
+                                                         G_VARIANT_TYPE ("(u)"),
+                                                         G_DBUS_CALL_FLAGS_NONE,
+                                                         -1,
+                                                         fd_list,
+                                                         NULL,
+                                                         cancellable,
+                                                         error);
+  if (reply == NULL)
+    IDE_GOTO (cleanup_fds);
+
+  g_variant_get (reply, "(u)", &client_pid);
+
+  self->client_pid = (GPid)client_pid;
+  self->identifier = g_strdup_printf ("%u", client_pid);
+
+  IDE_TRACE_MSG ("HostCommand() spawned client_pid %u", (guint)client_pid);
+
+  if (cancellable != NULL && !g_cancellable_is_cancelled (cancellable))
+    {
+      g_signal_connect_object (cancellable,
+                               "cancelled",
+                               G_CALLBACK (ide_flatpak_subprocess_cancelled),
+                               self,
+                               G_CONNECT_SWAPPED);
+      if (g_cancellable_is_cancelled (cancellable) && !self->client_has_exited)
+        ide_flatpak_subprocess_force_exit (IDE_SUBPROCESS (self));
+    }
+
+  ret = TRUE;
+
+cleanup_fds:
+
+  /* Close lingering stdin fds */
+  maybe_close (&stdin_pair[0]);
+  maybe_close (&stdin_pair[1]);
+
+  /* Close lingering stdout fds */
+  maybe_close (&stdout_pair[0]);
+  maybe_close (&stdout_pair[1]);
+
+  /* Close lingering stderr fds */
+  maybe_close (&stderr_pair[0]);
+  maybe_close (&stderr_pair[1]);
+
+  IDE_RETURN (ret);
+}
+
+static void
+initiable_iface_init (GInitableIface *iface)
+{
+  iface->init = ide_flatpak_subprocess_initable_init;
+}
+
+G_DEFINE_TYPE_EXTENDED (IdeFlatpakSubprocess, ide_flatpak_subprocess, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initiable_iface_init)
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_SUBPROCESS, subprocess_iface_init))
+
+static void
+ide_flatpak_subprocess_dispose (GObject *object)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)object;
+
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (self));
+
+  if (self->exited_subscription != 0)
+    {
+      if (self->connection != NULL && !g_dbus_connection_is_closed (self->connection))
+        {
+          IDE_TRACE_MSG ("Unsubscribing from DBus subscription %d", self->exited_subscription);
+          g_dbus_connection_signal_unsubscribe (self->connection, self->exited_subscription);
+        }
+
+      self->exited_subscription = 0;
+    }
+
+  if (self->waiting != NULL)
+    g_warning ("improper disposal while async operations are active!");
+
+  g_clear_handle_id (&self->sigint_id, g_source_remove);
+  g_clear_handle_id (&self->sigterm_id, g_source_remove);
+
+  G_OBJECT_CLASS (ide_flatpak_subprocess_parent_class)->dispose (object);
+}
+
+static void
+ide_flatpak_subprocess_finalize (GObject *object)
+{
+  IdeFlatpakSubprocess *self = (IdeFlatpakSubprocess *)object;
+
+  IDE_ENTRY;
+
+  g_assert (self->waiting == NULL);
+  g_assert_cmpint (self->sigint_id, ==, 0);
+  g_assert_cmpint (self->sigterm_id, ==, 0);
+  g_assert_cmpint (self->exited_subscription, ==, 0);
+
+  g_clear_pointer (&self->identifier, g_free);
+  g_clear_pointer (&self->cwd, g_free);
+  g_clear_pointer (&self->argv, g_strfreev);
+  g_clear_pointer (&self->env, g_strfreev);
+  g_clear_pointer (&self->main_context, g_main_context_unref);
+
+  g_clear_object (&self->stdin_pipe);
+  g_clear_object (&self->stdout_pipe);
+  g_clear_object (&self->stderr_pipe);
+  g_clear_object (&self->connection);
+
+  g_mutex_clear (&self->waiter_mutex);
+  g_cond_clear (&self->waiter_cond);
+
+  if (self->stdin_fd != -1)
+    close (self->stdin_fd);
+
+  if (self->stdout_fd != -1)
+    close (self->stdout_fd);
+
+  if (self->stderr_fd != -1)
+    close (self->stderr_fd);
+
+  for (guint i = 0; i < self->fd_mapping_len; i++)
+    close (self->fd_mapping[i].source_fd);
+  g_clear_pointer (&self->fd_mapping, g_free);
+
+  G_OBJECT_CLASS (ide_flatpak_subprocess_parent_class)->finalize (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_flatpak_subprocess_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeFlatpakSubprocess *self = IDE_FLATPAK_SUBPROCESS (object);
+
+  switch (prop_id)
+    {
+    case PROP_CWD:
+      g_value_set_string (value, self->cwd);
+      break;
+
+    case PROP_ARGV:
+      g_value_set_boxed (value, self->argv);
+      break;
+
+    case PROP_ENV:
+      g_value_set_boxed (value, self->env);
+      break;
+
+    case PROP_FLAGS:
+      g_value_set_flags (value, self->flags);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_flatpak_subprocess_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeFlatpakSubprocess *self = IDE_FLATPAK_SUBPROCESS (object);
+
+  switch (prop_id)
+    {
+    case PROP_CWD:
+      self->cwd = g_value_dup_string (value);
+      break;
+
+    case PROP_ARGV:
+      self->argv = g_value_dup_boxed (value);
+      break;
+
+    case PROP_ENV:
+      self->env = g_value_dup_boxed (value);
+      break;
+
+    case PROP_FLAGS:
+      self->flags = g_value_get_flags (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_flatpak_subprocess_class_init (IdeFlatpakSubprocessClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_flatpak_subprocess_dispose;
+  object_class->finalize = ide_flatpak_subprocess_finalize;
+  object_class->get_property = ide_flatpak_subprocess_get_property;
+  object_class->set_property = ide_flatpak_subprocess_set_property;
+
+  properties [PROP_CWD] =
+    g_param_spec_string ("cwd",
+                         "Current Working Directory",
+                         "The working directory for spawning the process",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ARGV] =
+    g_param_spec_boxed ("argv",
+                        "Argv",
+                        "The arguments for the process, including argv0",
+                        G_TYPE_STRV,
+                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ENV] =
+    g_param_spec_boxed ("env",
+                        "Environment",
+                        "The environment variables for the process",
+                        G_TYPE_STRV,
+                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FLAGS] =
+    g_param_spec_flags ("flags",
+                        "Flags",
+                        "The subprocess flags to use when spawning",
+                        G_TYPE_SUBPROCESS_FLAGS,
+                        G_SUBPROCESS_FLAGS_NONE,
+                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_flatpak_subprocess_init (IdeFlatpakSubprocess *self)
+{
+  IDE_ENTRY;
+
+  self->stdin_fd = -1;
+  self->stdout_fd = -1;
+  self->stderr_fd = -1;
+
+  g_mutex_init (&self->waiter_mutex);
+  g_cond_init (&self->waiter_cond);
+
+  IDE_EXIT;
+}
+
+IdeSubprocess *
+_ide_flatpak_subprocess_new (const gchar                 *cwd,
+                              const gchar * const         *argv,
+                              const gchar * const         *env,
+                              GSubprocessFlags             flags,
+                              gboolean                     clear_env,
+                              gint                         stdin_fd,
+                              gint                         stdout_fd,
+                              gint                         stderr_fd,
+                              const IdeBreakoutFdMapping  *fd_mapping,
+                              guint                        fd_mapping_len,
+                              GCancellable                *cancellable,
+                              GError                     **error)
+{
+  g_autoptr(IdeFlatpakSubprocess) ret = NULL;
+
+  g_return_val_if_fail (argv != NULL, NULL);
+  g_return_val_if_fail (argv[0] != NULL, NULL);
+
+  ret = g_object_new (IDE_TYPE_FLATPAK_SUBPROCESS,
+                      "cwd", cwd,
+                      "argv", argv,
+                      "env", env,
+                      "flags", flags,
+                      NULL);
+
+  ret->clear_env = clear_env;
+  ret->stdin_fd = stdin_fd;
+  ret->stdout_fd = stdout_fd;
+  ret->stderr_fd = stderr_fd;
+
+  ret->fd_mapping = g_new0 (IdeBreakoutFdMapping, fd_mapping_len);
+  ret->fd_mapping_len = fd_mapping_len;
+  memcpy (ret->fd_mapping, fd_mapping, sizeof(IdeBreakoutFdMapping) * fd_mapping_len);
+
+  if (!g_initable_init (G_INITABLE (ret), cancellable, error))
+    return NULL;
+
+  return IDE_SUBPROCESS (g_steal_pointer (&ret));
+}
diff --git a/src/libide/threading/ide-gtask-private.h b/src/libide/threading/ide-gtask-private.h
new file mode 100644
index 000000000..006e58759
--- /dev/null
+++ b/src/libide/threading/ide-gtask-private.h
@@ -0,0 +1,37 @@
+/* ide-gtask-private.h
+ *
+ * Copyright 2018-2019 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
+
+void ide_g_task_return_boolean_from_main (GTask          *task,
+                                          gboolean        value);
+void ide_g_task_return_int_from_main     (GTask          *task,
+                                          gint            value);
+void ide_g_task_return_pointer_from_main (GTask          *task,
+                                          gpointer        value,
+                                          GDestroyNotify  notify);
+void ide_g_task_return_error_from_main   (GTask          *task,
+                                          GError         *error);
+
+G_END_DECLS
diff --git a/src/libide/threading/ide-gtask.c b/src/libide/threading/ide-gtask.c
new file mode 100644
index 000000000..4373ab0fa
--- /dev/null
+++ b/src/libide/threading/ide-gtask.c
@@ -0,0 +1,180 @@
+/* ide-gtask.c
+ *
+ * Copyright 2016-2019 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 "ide-gtask"
+
+#include "config.h"
+
+#include "ide-gtask-private.h"
+
+typedef struct
+{
+  GType type;
+  GTask *task;
+  union {
+    gboolean v_bool;
+    gint v_int;
+    GError *v_error;
+    struct {
+      gpointer pointer;
+      GDestroyNotify destroy;
+    } v_ptr;
+  } u;
+} TaskState;
+
+static gboolean
+do_return (gpointer user_data)
+{
+  TaskState *state = user_data;
+
+  switch (state->type)
+    {
+    case G_TYPE_INT:
+      g_task_return_int (state->task, state->u.v_int);
+      break;
+
+    case G_TYPE_BOOLEAN:
+      g_task_return_boolean (state->task, state->u.v_bool);
+      break;
+
+    case G_TYPE_POINTER:
+      g_task_return_pointer (state->task, state->u.v_ptr.pointer, state->u.v_ptr.destroy);
+      state->u.v_ptr.pointer = NULL;
+      state->u.v_ptr.destroy = NULL;
+      break;
+
+    default:
+      if (state->type == G_TYPE_ERROR)
+        {
+          g_task_return_error (state->task, g_steal_pointer (&state->u.v_error));
+          break;
+        }
+
+      g_assert_not_reached ();
+    }
+
+  g_clear_object (&state->task);
+  g_slice_free (TaskState, state);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+task_state_attach (TaskState *state)
+{
+  GMainContext *main_context;
+  GSource *source;
+
+  g_assert (state != NULL);
+  g_assert (G_IS_TASK (state->task));
+
+  main_context = g_task_get_context (state->task);
+
+  source = g_timeout_source_new (0);
+  g_source_set_callback (source, do_return, state, NULL);
+  g_source_set_name (source, "[ide] ide_g_task_return_from_main");
+  g_source_attach (source, main_context);
+  g_source_unref (source);
+}
+
+/**
+ * ide_g_task_return_boolean_from_main:
+ *
+ * This is just like g_task_return_boolean() except that it enforces
+ * that the current stack return to the main context before dispatching
+ * the callback.
+ *
+ * Since: 3.32
+ */
+void
+ide_g_task_return_boolean_from_main (GTask    *task,
+                                     gboolean  value)
+{
+  TaskState *state;
+
+  g_return_if_fail (G_IS_TASK (task));
+
+  state = g_slice_new0 (TaskState);
+  state->type = G_TYPE_BOOLEAN;
+  state->task = g_object_ref (task);
+  state->u.v_bool = !!value;
+
+  task_state_attach (state);
+}
+
+void
+ide_g_task_return_int_from_main (GTask *task,
+                                 gint   value)
+{
+  TaskState *state;
+
+  g_return_if_fail (G_IS_TASK (task));
+
+  state = g_slice_new0 (TaskState);
+  state->type = G_TYPE_INT;
+  state->task = g_object_ref (task);
+  state->u.v_int = value;
+
+  task_state_attach (state);
+}
+
+void
+ide_g_task_return_pointer_from_main (GTask          *task,
+                                     gpointer        value,
+                                     GDestroyNotify  notify)
+{
+  TaskState *state;
+
+  g_return_if_fail (G_IS_TASK (task));
+
+  state = g_slice_new0 (TaskState);
+  state->type = G_TYPE_POINTER;
+  state->task = g_object_ref (task);
+  state->u.v_ptr.pointer = value;
+  state->u.v_ptr.destroy = notify;
+
+  task_state_attach (state);
+}
+
+/**
+ * ide_g_task_return_error_from_main:
+ * @task: a #GTask
+ * @error: (transfer full): a #GError.
+ *
+ * Like g_task_return_error() but ensures we return to the main loop before
+ * dispatching the result.
+ *
+ * Since: 3.32
+ */
+void
+ide_g_task_return_error_from_main (GTask  *task,
+                                   GError *error)
+{
+  TaskState *state;
+
+  g_return_if_fail (G_IS_TASK (task));
+
+  state = g_slice_new0 (TaskState);
+  state->type = G_TYPE_ERROR;
+  state->task = g_object_ref (task);
+  state->u.v_error = error;
+
+  task_state_attach (state);
+}
diff --git a/src/libide/threading/ide-simple-subprocess-private.h 
b/src/libide/threading/ide-simple-subprocess-private.h
new file mode 100644
index 000000000..e1624830c
--- /dev/null
+++ b/src/libide/threading/ide-simple-subprocess-private.h
@@ -0,0 +1,39 @@
+/* ide-simple-subprocess-private.h
+ *
+ * Copyright 2016-2019 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 "ide-subprocess.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SIMPLE_SUBPROCESS (ide_simple_subprocess_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeSimpleSubprocess, ide_simple_subprocess, IDE, SIMPLE_SUBPROCESS, GObject)
+
+struct _IdeSimpleSubprocess
+{
+  GObject      parent_instance;
+  GSubprocess *subprocess;
+};
+
+IdeSubprocess *ide_simple_subprocess_new (GSubprocess *subprocess);
+
+G_END_DECLS
diff --git a/src/libide/threading/ide-simple-subprocess.c b/src/libide/threading/ide-simple-subprocess.c
new file mode 100644
index 000000000..d6d7a8f92
--- /dev/null
+++ b/src/libide/threading/ide-simple-subprocess.c
@@ -0,0 +1,435 @@
+/* ide-simple-subprocess.c
+ *
+ * Copyright 2016-2019 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 "ide-simple-subprocess"
+
+#include "config.h"
+
+#include <libide-core.h>
+
+#include "ide-simple-subprocess-private.h"
+
+static void subprocess_iface_init (IdeSubprocessInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (IdeSimpleSubprocess, ide_simple_subprocess, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_SUBPROCESS, subprocess_iface_init))
+
+static void
+ide_simple_subprocess_finalize (GObject *object)
+{
+  IdeSimpleSubprocess *self = (IdeSimpleSubprocess *)object;
+
+  IDE_ENTRY;
+
+  g_clear_object (&self->subprocess);
+
+  G_OBJECT_CLASS (ide_simple_subprocess_parent_class)->finalize (object);
+
+  IDE_EXIT;
+}
+
+static void
+ide_simple_subprocess_class_init (IdeSimpleSubprocessClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_simple_subprocess_finalize;
+}
+
+static void
+ide_simple_subprocess_init (IdeSimpleSubprocess *self)
+{
+}
+
+#define WRAP_INTERFACE_METHOD(name, ...) \
+  g_subprocess_##name(IDE_SIMPLE_SUBPROCESS(subprocess)->subprocess, ## __VA_ARGS__)
+
+static const gchar *
+ide_simple_subprocess_get_identifier (IdeSubprocess *subprocess)
+{
+  return WRAP_INTERFACE_METHOD (get_identifier);
+}
+
+static GInputStream *
+ide_simple_subprocess_get_stdout_pipe (IdeSubprocess *subprocess)
+{
+  return WRAP_INTERFACE_METHOD (get_stdout_pipe);
+}
+
+static GInputStream *
+ide_simple_subprocess_get_stderr_pipe (IdeSubprocess *subprocess)
+{
+  return WRAP_INTERFACE_METHOD (get_stderr_pipe);
+}
+
+static GOutputStream *
+ide_simple_subprocess_get_stdin_pipe (IdeSubprocess *subprocess)
+{
+  return WRAP_INTERFACE_METHOD (get_stdin_pipe);
+}
+
+static gboolean
+ide_simple_subprocess_wait (IdeSubprocess  *subprocess,
+                            GCancellable   *cancellable,
+                            GError        **error)
+{
+  return WRAP_INTERFACE_METHOD (wait, cancellable, error);
+}
+
+static void
+ide_simple_subprocess_wait_cb (GObject      *object,
+                               GAsyncResult *result,
+                               gpointer      user_data)
+{
+  GSubprocess *subprocess = (GSubprocess *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_SUBPROCESS (subprocess));
+  g_assert (G_IS_TASK (task));
+
+  g_subprocess_wait_finish (subprocess, result, &error);
+
+#ifdef IDE_ENABLE_TRACE
+  if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    {
+      if (g_subprocess_get_if_exited (subprocess))
+        IDE_TRACE_MSG ("subprocess exited with exit status: %d",
+                       g_subprocess_get_exit_status (subprocess));
+      else
+        IDE_TRACE_MSG ("subprocess exited due to signal: %d",
+                       g_subprocess_get_term_sig (subprocess));
+    }
+#endif
+
+  if (error != NULL)
+    g_task_return_error (task, g_steal_pointer (&error));
+  else
+    g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+ide_simple_subprocess_wait_async (IdeSubprocess       *subprocess,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  IdeSimpleSubprocess *self = (IdeSimpleSubprocess *)subprocess;
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SIMPLE_SUBPROCESS (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_simple_subprocess_wait_async);
+
+  g_subprocess_wait_async (self->subprocess,
+                           cancellable,
+                           ide_simple_subprocess_wait_cb,
+                           g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_simple_subprocess_wait_finish (IdeSubprocess  *subprocess,
+                                   GAsyncResult   *result,
+                                   GError        **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SIMPLE_SUBPROCESS (subprocess));
+  g_assert (G_IS_TASK (result));
+
+  ret = g_task_propagate_boolean (G_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+ide_simple_subprocess_get_successful (IdeSubprocess *subprocess)
+{
+  return WRAP_INTERFACE_METHOD (get_successful);
+}
+
+static gboolean
+ide_simple_subprocess_get_if_exited (IdeSubprocess *subprocess)
+{
+  return WRAP_INTERFACE_METHOD (get_if_exited);
+}
+
+static gint
+ide_simple_subprocess_get_exit_status (IdeSubprocess *subprocess)
+{
+  return WRAP_INTERFACE_METHOD (get_exit_status);
+}
+
+static gboolean
+ide_simple_subprocess_get_if_signaled (IdeSubprocess *subprocess)
+{
+  return WRAP_INTERFACE_METHOD (get_if_signaled);
+}
+
+static gint
+ide_simple_subprocess_get_term_sig (IdeSubprocess *subprocess)
+{
+  return WRAP_INTERFACE_METHOD (get_term_sig);
+}
+
+static gint
+ide_simple_subprocess_get_status (IdeSubprocess *subprocess)
+{
+  return WRAP_INTERFACE_METHOD (get_status);
+}
+
+static void
+ide_simple_subprocess_send_signal (IdeSubprocess *subprocess,
+                                   gint           signal_num)
+{
+  IDE_ENTRY;
+  WRAP_INTERFACE_METHOD (send_signal, signal_num);
+  IDE_EXIT;
+}
+
+static void
+ide_simple_subprocess_force_exit (IdeSubprocess *subprocess)
+{
+  IDE_ENTRY;
+  WRAP_INTERFACE_METHOD (force_exit);
+  IDE_EXIT;
+}
+
+static gboolean
+ide_simple_subprocess_communicate (IdeSubprocess  *subprocess,
+                                   GBytes         *stdin_buf,
+                                   GCancellable   *cancellable,
+                                   GBytes        **stdout_buf,
+                                   GBytes        **stderr_buf,
+                                   GError        **error)
+{
+  return WRAP_INTERFACE_METHOD (communicate, stdin_buf, cancellable, stdout_buf, stderr_buf, error);
+}
+
+static gboolean
+ide_simple_subprocess_communicate_utf8 (IdeSubprocess  *subprocess,
+                                        const gchar    *stdin_buf,
+                                        GCancellable   *cancellable,
+                                        gchar         **stdout_buf,
+                                        gchar         **stderr_buf,
+                                        GError        **error)
+{
+  return WRAP_INTERFACE_METHOD (communicate_utf8, stdin_buf, cancellable, stdout_buf, stderr_buf, error);
+}
+
+static void
+free_object_pair (gpointer data)
+{
+  gpointer *pair = data;
+
+  g_clear_object (&pair[0]);
+  g_clear_object (&pair[1]);
+  g_free (pair);
+}
+
+static void
+ide_simple_subprocess_communicate_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  GSubprocess *subprocess = (GSubprocess *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GBytes) stdout_buf = NULL;
+  g_autoptr(GBytes) stderr_buf = NULL;
+  gpointer *data;
+
+  if (!g_subprocess_communicate_finish (subprocess, result, &stdout_buf, &stderr_buf, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  data = g_new0 (gpointer, 2);
+  data[0] = g_steal_pointer (&stdout_buf);
+  data[1] = g_steal_pointer (&stderr_buf);
+
+  g_task_return_pointer (task, data, free_object_pair);
+}
+
+static void
+ide_simple_subprocess_communicate_async (IdeSubprocess       *subprocess,
+                                         GBytes              *stdin_buf,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  IdeSimpleSubprocess *self = (IdeSimpleSubprocess *)subprocess;
+  GTask *task = g_task_new (self, cancellable, callback, user_data);
+  g_subprocess_communicate_async (self->subprocess, stdin_buf, cancellable, 
ide_simple_subprocess_communicate_cb, task);
+}
+
+static gboolean
+ide_simple_subprocess_communicate_finish (IdeSubprocess  *subprocess,
+                                          GAsyncResult   *result,
+                                          GBytes        **stdout_buf,
+                                          GBytes        **stderr_buf,
+                                          GError        **error)
+{
+  gpointer *pair;
+
+  pair = g_task_propagate_pointer (G_TASK (result), error);
+
+  if (pair != NULL)
+    {
+      if (stdout_buf != NULL)
+        *stdout_buf = g_steal_pointer (&pair[0]);
+
+      if (stderr_buf != NULL)
+        *stderr_buf = g_steal_pointer (&pair[1]);
+
+      free_object_pair (pair);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_simple_subprocess_communicate_utf8_cb (GObject      *object,
+                                           GAsyncResult *result,
+                                           gpointer      user_data)
+{
+  GSubprocess *subprocess = (GSubprocess *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *stdout_buf = NULL;
+  g_autofree gchar *stderr_buf = NULL;
+  gpointer *data;
+
+  if (!g_subprocess_communicate_utf8_finish (subprocess, result, &stdout_buf, &stderr_buf, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  data = g_new0 (gpointer, 2);
+  data[0] = g_steal_pointer (&stdout_buf);
+  data[1] = g_steal_pointer (&stderr_buf);
+
+  g_task_return_pointer (task, data, free_object_pair);
+}
+
+static void
+ide_simple_subprocess_communicate_utf8_async (IdeSubprocess       *subprocess,
+                                              const gchar         *stdin_buf,
+                                              GCancellable        *cancellable,
+                                              GAsyncReadyCallback  callback,
+                                              gpointer             user_data)
+{
+  IdeSimpleSubprocess *self = (IdeSimpleSubprocess *)subprocess;
+  GTask *task = g_task_new (self, cancellable, callback, user_data);
+  g_subprocess_communicate_utf8_async (self->subprocess, stdin_buf, cancellable, 
ide_simple_subprocess_communicate_utf8_cb, task);
+}
+
+static gboolean
+ide_simple_subprocess_communicate_utf8_finish (IdeSubprocess  *subprocess,
+                                               GAsyncResult   *result,
+                                               gchar         **stdout_buf,
+                                               gchar         **stderr_buf,
+                                               GError        **error)
+{
+  gpointer *pair;
+
+  pair = g_task_propagate_pointer (G_TASK (result), error);
+
+  if (pair != NULL)
+    {
+      if (stdout_buf != NULL)
+        *stdout_buf = g_steal_pointer (&pair[0]);
+
+      if (stderr_buf != NULL)
+        *stderr_buf = g_steal_pointer (&pair[1]);
+
+      g_free (pair[0]);
+      g_free (pair[1]);
+      g_free (pair);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+subprocess_iface_init (IdeSubprocessInterface *iface)
+{
+  iface->get_identifier = ide_simple_subprocess_get_identifier;
+  iface->get_stdout_pipe = ide_simple_subprocess_get_stdout_pipe;
+  iface->get_stderr_pipe = ide_simple_subprocess_get_stderr_pipe;
+  iface->get_stdin_pipe = ide_simple_subprocess_get_stdin_pipe;
+  iface->wait = ide_simple_subprocess_wait;
+  iface->wait_async = ide_simple_subprocess_wait_async;
+  iface->wait_finish = ide_simple_subprocess_wait_finish;
+  iface->get_successful = ide_simple_subprocess_get_successful;
+  iface->get_if_exited = ide_simple_subprocess_get_if_exited;
+  iface->get_exit_status = ide_simple_subprocess_get_exit_status;
+  iface->get_if_signaled = ide_simple_subprocess_get_if_signaled;
+  iface->get_term_sig = ide_simple_subprocess_get_term_sig;
+  iface->get_status = ide_simple_subprocess_get_status;
+  iface->send_signal = ide_simple_subprocess_send_signal;
+  iface->force_exit = ide_simple_subprocess_force_exit;
+  iface->communicate = ide_simple_subprocess_communicate;
+  iface->communicate_utf8 = ide_simple_subprocess_communicate_utf8;
+  iface->communicate_async = ide_simple_subprocess_communicate_async;
+  iface->communicate_finish = ide_simple_subprocess_communicate_finish;
+  iface->communicate_utf8_async = ide_simple_subprocess_communicate_utf8_async;
+  iface->communicate_utf8_finish = ide_simple_subprocess_communicate_utf8_finish;
+}
+
+/**
+ * ide_simple_subprocess_new:
+ *
+ * Creates a new #IdeSimpleSubprocess wrapping the #GSubprocess.
+ *
+ * Returns: (transfer full): A new #IdeSubprocess
+ *
+ * Since: 3.32
+ */
+IdeSubprocess *
+ide_simple_subprocess_new (GSubprocess *subprocess)
+{
+  IdeSimpleSubprocess *ret;
+
+  g_return_val_if_fail (G_IS_SUBPROCESS (subprocess), NULL);
+
+  ret = g_object_new (IDE_TYPE_SIMPLE_SUBPROCESS, NULL);
+  ret->subprocess = g_object_ref (subprocess);
+
+  return IDE_SUBPROCESS (ret);
+}
diff --git a/src/libide/threading/ide-subprocess-launcher.c b/src/libide/threading/ide-subprocess-launcher.c
new file mode 100644
index 000000000..6e7f7aacf
--- /dev/null
+++ b/src/libide/threading/ide-subprocess-launcher.c
@@ -0,0 +1,1073 @@
+/* ide-subprocess-launcher.c
+ *
+ * Copyright 2016-2019 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 "ide-subprocess-launcher"
+
+#include "config.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <libide-core.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "ide-environment.h"
+#include "ide-environment-variable.h"
+#include "ide-flatpak-subprocess-private.h"
+#include "ide-simple-subprocess-private.h"
+#include "ide-subprocess-launcher.h"
+
+#define is_flatpak() (ide_get_process_kind() == IDE_PROCESS_KIND_FLATPAK)
+
+typedef struct
+{
+  GSubprocessFlags  flags;
+
+  GPtrArray        *argv;
+  gchar            *cwd;
+  gchar           **environ;
+  GArray           *fd_mapping;
+  gchar            *stdout_file_path;
+
+  gint              stdin_fd;
+  gint              stdout_fd;
+  gint              stderr_fd;
+
+  guint             run_on_host : 1;
+  guint             clear_env : 1;
+} IdeSubprocessLauncherPrivate;
+
+typedef struct
+{
+  gint source_fd;
+  gint dest_fd;
+} FdMapping;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeSubprocessLauncher, ide_subprocess_launcher, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_CLEAR_ENV,
+  PROP_CWD,
+  PROP_ENVIRON,
+  PROP_FLAGS,
+  PROP_RUN_ON_HOST,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+child_setup_func (gpointer data)
+{
+#ifdef G_OS_UNIX
+  /*
+   * TODO: Check on FreeBSD to see if the process group id is the same as
+   *       the owning process. If not, our kill() signal might not work
+   *       as expected.
+   */
+
+  setsid ();
+  setpgid (0, 0);
+
+  if (isatty (STDIN_FILENO))
+    {
+      if (ioctl (STDIN_FILENO, TIOCSCTTY, 0) != 0)
+        g_warning ("Failed to setup TIOCSCTTY on stdin: %s",
+                   g_strerror (errno));
+    }
+#endif
+}
+
+static void
+ide_subprocess_launcher_kill_process_group (GCancellable *cancellable,
+                                            GSubprocess  *subprocess)
+{
+#ifdef G_OS_UNIX
+  const gchar *ident;
+  pid_t pid;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_CANCELLABLE (cancellable));
+  g_assert (G_IS_SUBPROCESS (subprocess));
+
+  /*
+   * This will send SIGKILL to all processes in the process group that
+   * was created for our subprocess using setsid().
+   */
+
+  if (NULL != (ident = g_subprocess_get_identifier (subprocess)))
+    {
+      g_debug ("Killing process group %s due to cancellation", ident);
+      pid = atoi (ident);
+      kill (-pid, SIGKILL);
+    }
+
+  g_signal_handlers_disconnect_by_func (cancellable,
+                                        G_CALLBACK (ide_subprocess_launcher_kill_process_group),
+                                        subprocess);
+
+  IDE_EXIT;
+#else
+# error "Your platform is not yet supported"
+#endif
+}
+
+static void
+ide_subprocess_launcher_kill_host_process (GCancellable  *cancellable,
+                                           IdeSubprocess *subprocess)
+{
+  IDE_ENTRY;
+
+  g_assert (G_IS_CANCELLABLE (cancellable));
+  g_assert (IDE_IS_FLATPAK_SUBPROCESS (subprocess));
+
+  g_signal_handlers_disconnect_by_func (cancellable,
+                                        G_CALLBACK (ide_subprocess_launcher_kill_host_process),
+                                        subprocess);
+
+  ide_subprocess_force_exit (subprocess);
+
+  IDE_EXIT;
+}
+
+IdeSubprocessLauncher *
+ide_subprocess_launcher_new (GSubprocessFlags flags)
+{
+  return g_object_new (IDE_TYPE_SUBPROCESS_LAUNCHER,
+                       "flags", flags,
+                       NULL);
+}
+
+static gboolean
+should_use_flatpak_process (IdeSubprocessLauncher *self)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_assert (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  if (g_getenv ("IDE_USE_FLATPAK_SUBPROCESS") != NULL)
+    return TRUE;
+
+  if (!priv->run_on_host)
+    return FALSE;
+
+  return is_flatpak ();
+}
+
+static void
+ide_subprocess_launcher_spawn_host_worker (GTask        *task,
+                                           gpointer      source_object,
+                                           gpointer      task_data,
+                                           GCancellable *cancellable)
+{
+  IdeSubprocessLauncher *self = source_object;
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+  g_autoptr(IdeSubprocess) process = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GArray) fds = NULL;
+  gint stdin_fd = -1;
+  gint stdout_fd = -1;
+  gint stderr_fd = -1;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    g_autofree gchar *str = NULL;
+    g_autofree gchar *env = NULL;
+    str = g_strjoinv (" ", (gchar **)priv->argv->pdata);
+    env = priv->environ ? g_strjoinv (" ", priv->environ) : g_strdup ("");
+    IDE_TRACE_MSG ("Launching '%s' with environment %s %s parent environment",
+                   str, env, priv->clear_env ? "clearing" : "inheriting");
+  }
+#endif
+
+  fds = g_steal_pointer (&priv->fd_mapping);
+
+  if (priv->stdin_fd != -1)
+    stdin_fd = dup (priv->stdin_fd);
+
+  if (priv->stdout_fd != -1)
+    stdout_fd = dup (priv->stdout_fd);
+  else if (priv->stdout_file_path != NULL)
+    stdout_fd = open (priv->stdout_file_path, O_WRONLY);
+
+  if (priv->stderr_fd != -1)
+    stderr_fd = dup (priv->stderr_fd);
+
+  process = _ide_flatpak_subprocess_new (priv->cwd,
+                                          (const gchar * const *)priv->argv->pdata,
+                                          (const gchar * const *)priv->environ,
+                                          priv->flags,
+                                          priv->clear_env,
+                                          stdin_fd,
+                                          stdout_fd,
+                                          stderr_fd,
+                                          fds ? (gpointer)fds->data : NULL,
+                                          fds ? fds->len : 0,
+                                          cancellable,
+                                          &error);
+
+  if (process == NULL)
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (cancellable != NULL)
+    {
+      g_signal_connect_object (cancellable,
+                               "cancelled",
+                               G_CALLBACK (ide_subprocess_launcher_kill_host_process),
+                               process,
+                               0);
+    }
+
+  g_task_return_pointer (task, g_steal_pointer (&process), g_object_unref);
+
+  IDE_EXIT;
+}
+
+static void
+ide_subprocess_launcher_spawn_worker (GTask        *task,
+                                      gpointer      source_object,
+                                      gpointer      task_data,
+                                      GCancellable *cancellable)
+{
+  IdeSubprocessLauncher *self = source_object;
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+  g_autoptr(GSubprocessLauncher) launcher = NULL;
+  g_autoptr(GSubprocess) real = NULL;
+  g_autoptr(IdeSubprocess) wrapped = NULL;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  {
+    g_autofree gchar *str = NULL;
+    g_autofree gchar *env = NULL;
+
+    str = g_strjoinv (" ", (gchar **)priv->argv->pdata);
+    env = priv->environ ? g_strjoinv (" ", priv->environ) : g_strdup ("");
+
+    g_debug ("Launching '%s' from directory '%s' with environment %s %s parent environment",
+             str, priv->cwd, env, priv->clear_env ? "clearing" : "inheriting");
+  }
+
+  launcher = g_subprocess_launcher_new (priv->flags);
+  g_subprocess_launcher_set_child_setup (launcher, child_setup_func, NULL, NULL);
+  g_subprocess_launcher_set_cwd (launcher, priv->cwd);
+
+  if (priv->stdout_file_path != NULL)
+    g_subprocess_launcher_set_stdout_file_path (launcher, priv->stdout_file_path);
+
+  if (priv->stdin_fd != -1)
+    {
+      g_subprocess_launcher_take_stdin_fd (launcher, priv->stdin_fd);
+      priv->stdin_fd = -1;
+    }
+
+  if (priv->stdout_fd != -1)
+    {
+      g_subprocess_launcher_take_stdout_fd (launcher, priv->stdout_fd);
+      priv->stdout_fd = -1;
+    }
+
+  if (priv->stderr_fd != -1)
+    {
+      g_subprocess_launcher_take_stderr_fd (launcher, priv->stderr_fd);
+      priv->stderr_fd = -1;
+    }
+
+  if (priv->fd_mapping != NULL)
+    {
+      g_autoptr(GArray) ar = g_steal_pointer (&priv->fd_mapping);
+
+      for (guint i = 0; i < ar->len; i++)
+        {
+          const FdMapping *map = &g_array_index (ar, FdMapping, i);
+
+          g_subprocess_launcher_take_fd (launcher, map->source_fd, map->dest_fd);
+        }
+    }
+
+  /*
+   * GSubprocessLauncher starts by inheriting the current environment.
+   * So if clear-env is set, we need to unset those environment variables.
+   * Simply setting the environ to NULL doesn't work, because glib uses
+   * execv rather than execve in that case.
+   */
+  if (priv->clear_env)
+    {
+      gchar *envp[] = { NULL };
+      g_subprocess_launcher_set_environ (launcher, envp);
+    }
+
+  /*
+   * Now override any environment variables that were set using
+   * ide_subprocess_launcher_setenv() or ide_subprocess_launcher_set_environ().
+   */
+  if (priv->environ != NULL)
+    {
+      for (guint i = 0; priv->environ[i] != NULL; i++)
+        {
+          const gchar *pair = priv->environ[i];
+          const gchar *eq = strchr (pair, '=');
+          g_autofree gchar *key = g_strndup (pair, eq - pair);
+          const gchar *val = eq ? eq + 1 : NULL;
+
+          g_subprocess_launcher_setenv (launcher, key, val, TRUE);
+        }
+    }
+
+  real = g_subprocess_launcher_spawnv (launcher,
+                                       (const gchar * const *)priv->argv->pdata,
+                                       &error);
+
+  if (real == NULL)
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (cancellable != NULL)
+    {
+      g_signal_connect_object (cancellable,
+                               "cancelled",
+                               G_CALLBACK (ide_subprocess_launcher_kill_process_group),
+                               real,
+                               0);
+    }
+
+  wrapped = ide_simple_subprocess_new (real);
+
+  g_task_return_pointer (task, g_steal_pointer (&wrapped), g_object_unref);
+
+  IDE_EXIT;
+}
+
+static IdeSubprocess *
+ide_subprocess_launcher_real_spawn (IdeSubprocessLauncher  *self,
+                                    GCancellable           *cancellable,
+                                    GError                **error)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+  g_autoptr(GTask) task = NULL;
+  IdeSubprocess *ret;
+  GError *local_error = NULL;
+
+  g_assert (IDE_IS_SUBPROCESS_LAUNCHER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, NULL, NULL);
+  g_task_set_source_tag (task, ide_subprocess_launcher_real_spawn);
+
+  if (priv->clear_env || (is_flatpak () && priv->run_on_host))
+    {
+      /*
+       * Many things break without at least PATH, HOME, etc. being set.
+       * The GbpFlatpakSubprocessLauncher will also try to set PATH so
+       * that it can get /app/bin too. Since it chains up to us, we wont
+       * overwrite PATH in that case (which is what we want).
+       */
+      ide_subprocess_launcher_setenv (self, "PATH", SAFE_PATH, FALSE);
+      ide_subprocess_launcher_setenv (self, "HOME", g_get_home_dir (), FALSE);
+      ide_subprocess_launcher_setenv (self, "USER", g_get_user_name (), FALSE);
+      ide_subprocess_launcher_setenv (self, "LANG", g_getenv ("LANG"), FALSE);
+    }
+
+  if (should_use_flatpak_process (self))
+    ide_subprocess_launcher_spawn_host_worker (task, self, NULL, cancellable);
+  else
+    ide_subprocess_launcher_spawn_worker (task, self, NULL, cancellable);
+
+  ret = g_task_propagate_pointer (task, &local_error);
+
+  if (!ret && !local_error)
+    local_error = g_error_new (G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "An unkonwn error occurred while spawning");
+
+  if (local_error != NULL)
+    g_propagate_error (error, g_steal_pointer (&local_error));
+
+  return g_steal_pointer (&ret);
+}
+
+static void
+ide_subprocess_launcher_finalize (GObject *object)
+{
+  IdeSubprocessLauncher *self = (IdeSubprocessLauncher *)object;
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  if (priv->fd_mapping != NULL)
+    {
+      for (guint i = 0; i < priv->fd_mapping->len; i++)
+        {
+          FdMapping *map = &g_array_index (priv->fd_mapping, FdMapping, i);
+
+          if (map->source_fd != -1)
+            close (map->source_fd);
+        }
+
+      g_clear_pointer (&priv->fd_mapping, g_array_unref);
+    }
+
+  g_clear_pointer (&priv->argv, g_ptr_array_unref);
+  g_clear_pointer (&priv->cwd, g_free);
+  g_clear_pointer (&priv->environ, g_strfreev);
+  g_clear_pointer (&priv->stdout_file_path, g_free);
+
+  if (priv->stdin_fd != -1)
+    {
+      close (priv->stdin_fd);
+      priv->stdin_fd = -1;
+    }
+
+  if (priv->stdout_fd != -1)
+    {
+      close (priv->stdout_fd);
+      priv->stdout_fd = -1;
+    }
+
+  if (priv->stderr_fd != -1)
+    {
+      close (priv->stderr_fd);
+      priv->stderr_fd = -1;
+    }
+
+  G_OBJECT_CLASS (ide_subprocess_launcher_parent_class)->finalize (object);
+}
+
+static void
+ide_subprocess_launcher_get_property (GObject    *object,
+                                      guint       prop_id,
+                                      GValue     *value,
+                                      GParamSpec *pspec)
+{
+  IdeSubprocessLauncher *self = IDE_SUBPROCESS_LAUNCHER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLEAR_ENV:
+      g_value_set_boolean (value, ide_subprocess_launcher_get_clear_env (self));
+      break;
+
+    case PROP_CWD:
+      g_value_set_string (value, ide_subprocess_launcher_get_cwd (self));
+      break;
+
+    case PROP_FLAGS:
+      g_value_set_flags (value, ide_subprocess_launcher_get_flags (self));
+      break;
+
+    case PROP_ENVIRON:
+      g_value_set_boxed (value, ide_subprocess_launcher_get_environ (self));
+      break;
+
+    case PROP_RUN_ON_HOST:
+      g_value_set_boolean (value, ide_subprocess_launcher_get_run_on_host (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_subprocess_launcher_set_property (GObject      *object,
+                                      guint         prop_id,
+                                      const GValue *value,
+                                      GParamSpec   *pspec)
+{
+  IdeSubprocessLauncher *self = IDE_SUBPROCESS_LAUNCHER (object);
+
+  switch (prop_id)
+    {
+    case PROP_CLEAR_ENV:
+      ide_subprocess_launcher_set_clear_env (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_CWD:
+      ide_subprocess_launcher_set_cwd (self, g_value_get_string (value));
+      break;
+
+    case PROP_FLAGS:
+      ide_subprocess_launcher_set_flags (self, g_value_get_flags (value));
+      break;
+
+    case PROP_ENVIRON:
+      ide_subprocess_launcher_set_environ (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_RUN_ON_HOST:
+      ide_subprocess_launcher_set_run_on_host (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_subprocess_launcher_class_init (IdeSubprocessLauncherClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_subprocess_launcher_finalize;
+  object_class->get_property = ide_subprocess_launcher_get_property;
+  object_class->set_property = ide_subprocess_launcher_set_property;
+
+  klass->spawn = ide_subprocess_launcher_real_spawn;
+
+  properties [PROP_CLEAR_ENV] =
+    g_param_spec_boolean ("clean-env",
+                          "Clear Environment",
+                          "If the environment should be cleared before setting environment variables.",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CWD] =
+    g_param_spec_string ("cwd",
+                         "Current Working Directory",
+                         "Current Working Directory",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FLAGS] =
+    g_param_spec_flags ("flags",
+                        "Flags",
+                        "Flags",
+                        G_TYPE_SUBPROCESS_FLAGS,
+                        G_SUBPROCESS_FLAGS_NONE,
+                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_ENVIRON] =
+    g_param_spec_boxed ("environ",
+                        "Environ",
+                        "Environ",
+                        G_TYPE_STRV,
+                        (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_RUN_ON_HOST] =
+    g_param_spec_boolean ("run-on-host",
+                          "Run on Host",
+                          "Run on Host",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_subprocess_launcher_init (IdeSubprocessLauncher *self)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  priv->clear_env = TRUE;
+
+  priv->stdin_fd = -1;
+  priv->stdout_fd = -1;
+  priv->stderr_fd = -1;
+
+  priv->argv = g_ptr_array_new_with_free_func (g_free);
+  g_ptr_array_add (priv->argv, NULL);
+
+  priv->cwd = g_strdup (".");
+}
+
+void
+ide_subprocess_launcher_set_flags (IdeSubprocessLauncher *self,
+                                   GSubprocessFlags       flags)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  if (flags != priv->flags)
+    {
+      priv->flags = flags;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FLAGS]);
+    }
+}
+
+GSubprocessFlags
+ide_subprocess_launcher_get_flags (IdeSubprocessLauncher *self)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self), 0);
+
+  return priv->flags;
+}
+
+const gchar * const *
+ide_subprocess_launcher_get_environ (IdeSubprocessLauncher *self)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self), NULL);
+
+  return (const gchar * const *)priv->environ;
+}
+
+/**
+ * ide_subprocess_launcher_set_environ:
+ * @self: an #IdeSubprocessLauncher
+ * @environ_: (array zero-terminated=1) (element-type utf8) (nullable): the list
+ * of environment variables to set
+ *
+ * Replace the environment variables by a new list of variables.
+ *
+ * Since: 3.32
+ */
+void
+ide_subprocess_launcher_set_environ (IdeSubprocessLauncher *self,
+                                     const gchar * const   *environ_)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  if (priv->environ != (gchar **)environ_)
+    {
+      g_strfreev (priv->environ);
+      priv->environ = g_strdupv ((gchar **)environ_);
+    }
+}
+
+const gchar *
+ide_subprocess_launcher_getenv (IdeSubprocessLauncher *self,
+                                const gchar           *key)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self), NULL);
+  g_return_val_if_fail (key != NULL, NULL);
+
+  return g_environ_getenv (priv->environ, key);
+}
+
+void
+ide_subprocess_launcher_setenv (IdeSubprocessLauncher *self,
+                                const gchar           *key,
+                                const gchar           *value,
+                                gboolean               replace)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+  g_return_if_fail (key != NULL);
+
+  if (value != NULL)
+    priv->environ = g_environ_setenv (priv->environ, key, value, replace);
+  else
+    priv->environ = g_environ_unsetenv (priv->environ, key);
+}
+
+void
+ide_subprocess_launcher_push_argv (IdeSubprocessLauncher *self,
+                                   const gchar           *argv)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+  g_return_if_fail (argv != NULL);
+
+  g_ptr_array_index (priv->argv, priv->argv->len - 1) = g_strdup (argv);
+  g_ptr_array_add (priv->argv, NULL);
+}
+
+/**
+ * ide_subprocess_launcher_spawn:
+ *
+ * Synchronously spawn a process using the internal state.
+ *
+ * Returns: (transfer full): an #IdeSubprocess or %NULL upon error.
+ *
+ * Since: 3.32
+ */
+IdeSubprocess *
+ide_subprocess_launcher_spawn (IdeSubprocessLauncher  *self,
+                               GCancellable           *cancellable,
+                               GError                **error)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self), NULL);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL);
+
+  return IDE_SUBPROCESS_LAUNCHER_GET_CLASS (self)->spawn (self, cancellable, error);
+}
+
+void
+ide_subprocess_launcher_set_cwd (IdeSubprocessLauncher *self,
+                                 const gchar           *cwd)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  if (ide_str_empty0 (cwd))
+    cwd = ".";
+
+  if (!ide_str_equal0 (priv->cwd, cwd))
+    {
+      g_free (priv->cwd);
+      priv->cwd = g_strdup (cwd);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CWD]);
+    }
+}
+
+const gchar *
+ide_subprocess_launcher_get_cwd (IdeSubprocessLauncher *self)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self), NULL);
+
+  return priv->cwd;
+}
+
+void
+ide_subprocess_launcher_overlay_environment (IdeSubprocessLauncher *self,
+                                             IdeEnvironment        *environment)
+{
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+  g_return_if_fail (!environment || IDE_IS_ENVIRONMENT (environment));
+
+  if (environment != NULL)
+    {
+      guint n_items = g_list_model_get_n_items (G_LIST_MODEL (environment));
+
+      for (guint i = 0; i < n_items; i++)
+        {
+          g_autoptr(IdeEnvironmentVariable) var = NULL;
+          const gchar *key;
+          const gchar *value;
+
+          var = g_list_model_get_item (G_LIST_MODEL (environment), i);
+          key = ide_environment_variable_get_key (var);
+          value = ide_environment_variable_get_value (var);
+
+          if (!ide_str_empty0 (key))
+            ide_subprocess_launcher_setenv (self, key, value ?: "", TRUE);
+        }
+    }
+}
+
+/**
+ * ide_subprocess_launcher_push_args:
+ * @self: an #IdeSubprocessLauncher
+ * @args: (array zero-terminated=1) (element-type utf8) (nullable): the arguments
+ *
+ * This function is semantically identical to calling ide_subprocess_launcher_push_argv()
+ * for each element of @args.
+ *
+ * If @args is %NULL, this function does nothing.
+ *
+ * Since: 3.32
+ */
+void
+ide_subprocess_launcher_push_args (IdeSubprocessLauncher *self,
+                                   const gchar * const   *args)
+{
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  if (args != NULL)
+    {
+      for (guint i = 0; args [i] != NULL; i++)
+        ide_subprocess_launcher_push_argv (self, args [i]);
+    }
+}
+
+gchar *
+ide_subprocess_launcher_pop_argv (IdeSubprocessLauncher *self)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+  gchar *ret = NULL;
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self), NULL);
+
+  if (priv->argv->len > 1)
+    {
+      g_assert (g_ptr_array_index (priv->argv, priv->argv->len - 1) == NULL);
+
+      ret = g_ptr_array_index (priv->argv, priv->argv->len - 2);
+      g_ptr_array_index (priv->argv, priv->argv->len - 2) = NULL;
+      g_ptr_array_set_size (priv->argv, priv->argv->len - 1);
+    }
+
+  return ret;
+}
+
+/**
+ * ide_subprocess_launcher_get_run_on_host:
+ *
+ * Gets if the process should be executed on the host system. This might be
+ * useful for situations where running in a contained environment is not
+ * sufficient to perform the given task.
+ *
+ * Currently, only flatpak is supported for breaking out of the containment
+ * zone and requires the application was built with --allow=devel.
+ *
+ * Returns: %TRUE if the process should be executed outside the containment zone.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_subprocess_launcher_get_run_on_host (IdeSubprocessLauncher *self)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self), FALSE);
+
+  return priv->run_on_host;
+}
+
+/**
+ * ide_subprocess_launcher_set_run_on_host:
+ *
+ * Sets the #IdeSubprocessLauncher:run-on-host property. See
+ * ide_subprocess_launcher_get_run_on_host() for more information.
+ *
+ * Since: 3.32
+ */
+void
+ide_subprocess_launcher_set_run_on_host (IdeSubprocessLauncher *self,
+                                         gboolean               run_on_host)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  run_on_host = !!run_on_host;
+
+  if (priv->run_on_host != run_on_host)
+    {
+      priv->run_on_host = run_on_host;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RUN_ON_HOST]);
+    }
+}
+
+gboolean
+ide_subprocess_launcher_get_clear_env (IdeSubprocessLauncher *self)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self), FALSE);
+
+  return priv->clear_env;
+}
+
+void
+ide_subprocess_launcher_set_clear_env (IdeSubprocessLauncher *self,
+                                       gboolean               clear_env)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  clear_env = !!clear_env;
+
+  if (priv->clear_env != clear_env)
+    {
+      priv->clear_env = clear_env;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CLEAR_ENV]);
+    }
+}
+
+void
+ide_subprocess_launcher_take_stdin_fd (IdeSubprocessLauncher *self,
+                                       gint                   stdin_fd)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  if (priv->stdin_fd != stdin_fd)
+    {
+      if (priv->stdin_fd != -1)
+        close (priv->stdin_fd);
+      priv->stdin_fd = stdin_fd;
+    }
+}
+
+void
+ide_subprocess_launcher_take_stdout_fd (IdeSubprocessLauncher *self,
+                                        gint                   stdout_fd)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  if (priv->stdout_fd != stdout_fd)
+    {
+      if (priv->stdout_fd != -1)
+        close (priv->stdout_fd);
+      priv->stdout_fd = stdout_fd;
+    }
+}
+
+void
+ide_subprocess_launcher_take_stderr_fd (IdeSubprocessLauncher *self,
+                                        gint                   stderr_fd)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  if (priv->stderr_fd != stderr_fd)
+    {
+      if (priv->stderr_fd != -1)
+        close (priv->stderr_fd);
+      priv->stderr_fd = stderr_fd;
+    }
+}
+
+void
+ide_subprocess_launcher_set_argv (IdeSubprocessLauncher *self,
+                                  const gchar * const   *args)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  g_ptr_array_remove_range (priv->argv, 0, priv->argv->len);
+
+  if (args != NULL)
+    {
+      for (guint i = 0; args[i] != NULL; i++)
+        g_ptr_array_add (priv->argv, g_strdup (args[i]));
+    }
+
+  g_ptr_array_add (priv->argv, NULL);
+}
+
+const gchar * const *
+ide_subprocess_launcher_get_argv (IdeSubprocessLauncher *self)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self), NULL);
+
+  return (const gchar * const *)priv->argv->pdata;
+}
+
+void
+ide_subprocess_launcher_insert_argv (IdeSubprocessLauncher *self,
+                                     guint                  index,
+                                     const gchar           *arg)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+  g_return_if_fail (priv->argv->len > 0);
+  g_return_if_fail (index < (priv->argv->len - 1));
+  g_return_if_fail (arg != NULL);
+
+  g_ptr_array_insert (priv->argv, index, g_strdup (arg));
+}
+
+void
+ide_subprocess_launcher_replace_argv (IdeSubprocessLauncher *self,
+                                      guint                  index,
+                                      const gchar           *arg)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+  gchar *old_arg;
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+  g_return_if_fail (priv->argv->len > 0);
+  g_return_if_fail (index < (priv->argv->len - 1));
+  g_return_if_fail (arg != NULL);
+
+  /* overwriting in place */
+  old_arg = g_ptr_array_index (priv->argv, index);
+  g_ptr_array_index (priv->argv, index) = g_strdup (arg);
+  g_free (old_arg);
+}
+
+void
+ide_subprocess_launcher_take_fd (IdeSubprocessLauncher *self,
+                                 gint                   source_fd,
+                                 gint                   dest_fd)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+  FdMapping map = {
+    .source_fd = source_fd,
+    .dest_fd = dest_fd
+  };
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+  g_return_if_fail (source_fd > -1);
+  g_return_if_fail (dest_fd > -1);
+
+  if (priv->fd_mapping == NULL)
+    priv->fd_mapping = g_array_new (FALSE, FALSE, sizeof (FdMapping));
+
+  g_array_append_val (priv->fd_mapping, map);
+}
+
+void
+ide_subprocess_launcher_set_stdout_file_path (IdeSubprocessLauncher *self,
+                                              const gchar           *stdout_file_path)
+{
+  IdeSubprocessLauncherPrivate *priv = ide_subprocess_launcher_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  if (g_strcmp0 (priv->stdout_file_path, stdout_file_path) != 0)
+    {
+      g_free (priv->stdout_file_path);
+      priv->stdout_file_path = g_strdup (stdout_file_path);
+    }
+}
+
+void
+ide_subprocess_launcher_append_path (IdeSubprocessLauncher *self,
+                                     const gchar           *path)
+{
+  const gchar *old_path;
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_LAUNCHER (self));
+
+  if (path == NULL)
+    return;
+
+  old_path = ide_subprocess_launcher_getenv (self, "PATH");
+
+  if (old_path != NULL)
+    {
+      g_autofree gchar *new_path = g_strdup_printf ("%s:%s", old_path, path);
+      ide_subprocess_launcher_setenv (self, "PATH", new_path, TRUE);
+    }
+  else
+    {
+      ide_subprocess_launcher_setenv (self, "PATH", path, TRUE);
+    }
+}
diff --git a/src/libide/threading/ide-subprocess-launcher.h b/src/libide/threading/ide-subprocess-launcher.h
new file mode 100644
index 000000000..7f64e8780
--- /dev/null
+++ b/src/libide/threading/ide-subprocess-launcher.h
@@ -0,0 +1,135 @@
+/* ide-subprocess-launcher.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_THREADING_INSIDE) && !defined (IDE_THREADING_COMPILATION)
+# error "Only <libide-threading.h> can be included directly."
+#endif
+
+#include <gio/gio.h>
+#include <libide-core.h>
+
+#include "ide-subprocess.h"
+#include "ide-environment.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SUBPROCESS_LAUNCHER (ide_subprocess_launcher_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeSubprocessLauncher, ide_subprocess_launcher, IDE, SUBPROCESS_LAUNCHER, GObject)
+
+struct _IdeSubprocessLauncherClass
+{
+  GObjectClass parent_class;
+
+  IdeSubprocess *(*spawn) (IdeSubprocessLauncher  *self,
+                           GCancellable           *cancellable,
+                           GError                **error);
+
+  /*< private >*/
+  gpointer _reserved[8];
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeSubprocessLauncher *ide_subprocess_launcher_new                 (GSubprocessFlags        flags);
+IDE_AVAILABLE_IN_3_32
+const gchar           *ide_subprocess_launcher_get_cwd             (IdeSubprocessLauncher  *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_set_cwd             (IdeSubprocessLauncher  *self,
+                                                                    const gchar            *cwd);
+IDE_AVAILABLE_IN_3_32
+GSubprocessFlags       ide_subprocess_launcher_get_flags           (IdeSubprocessLauncher  *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_set_flags           (IdeSubprocessLauncher  *self,
+                                                                    GSubprocessFlags        flags);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_subprocess_launcher_get_run_on_host     (IdeSubprocessLauncher  *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_set_run_on_host     (IdeSubprocessLauncher  *self,
+                                                                    gboolean                run_on_host);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_append_path         (IdeSubprocessLauncher  *self,
+                                                                    const gchar            *append_path);
+IDE_AVAILABLE_IN_3_32
+gboolean               ide_subprocess_launcher_get_clear_env       (IdeSubprocessLauncher  *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_set_clear_env       (IdeSubprocessLauncher  *self,
+                                                                    gboolean                clear_env);
+IDE_AVAILABLE_IN_3_32
+const gchar * const   *ide_subprocess_launcher_get_environ         (IdeSubprocessLauncher  *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_set_environ         (IdeSubprocessLauncher  *self,
+                                                                    const gchar * const    *environ_);
+IDE_AVAILABLE_IN_3_32
+const gchar           *ide_subprocess_launcher_getenv              (IdeSubprocessLauncher  *self,
+                                                                    const gchar            *key);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_setenv              (IdeSubprocessLauncher  *self,
+                                                                    const gchar            *key,
+                                                                    const gchar            *value,
+                                                                    gboolean                replace);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_insert_argv         (IdeSubprocessLauncher  *self,
+                                                                    guint                   index,
+                                                                    const gchar            *arg);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_replace_argv        (IdeSubprocessLauncher  *self,
+                                                                    guint                   index,
+                                                                    const gchar            *arg);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_overlay_environment (IdeSubprocessLauncher  *self,
+                                                                    IdeEnvironment         *environment);
+IDE_AVAILABLE_IN_3_32
+const gchar * const   *ide_subprocess_launcher_get_argv            (IdeSubprocessLauncher  *self);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_push_args           (IdeSubprocessLauncher  *self,
+                                                                    const gchar * const    *args);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_push_argv           (IdeSubprocessLauncher  *self,
+                                                                    const gchar            *argv);
+IDE_AVAILABLE_IN_3_32
+gchar                 *ide_subprocess_launcher_pop_argv            (IdeSubprocessLauncher  *self) 
G_GNUC_WARN_UNUSED_RESULT;
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_set_argv            (IdeSubprocessLauncher  *self,
+                                                                    const gchar * const    *argv);
+IDE_AVAILABLE_IN_3_32
+IdeSubprocess         *ide_subprocess_launcher_spawn               (IdeSubprocessLauncher  *self,
+                                                                    GCancellable           *cancellable,
+                                                                    GError                **error);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_set_stdout_file_path(IdeSubprocessLauncher  *self,
+                                                                    const gchar            
*stdout_file_path);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_take_fd             (IdeSubprocessLauncher  *self,
+                                                                    gint                    source_fd,
+                                                                    gint                    dest_fd);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_take_stdin_fd       (IdeSubprocessLauncher  *self,
+                                                                    gint                    stdin_fd);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_take_stdout_fd      (IdeSubprocessLauncher  *self,
+                                                                    gint                    stdout_fd);
+IDE_AVAILABLE_IN_3_32
+void                   ide_subprocess_launcher_take_stderr_fd      (IdeSubprocessLauncher  *self,
+                                                                    gint                    stderr_fd);
+
+G_END_DECLS
diff --git a/src/libide/threading/ide-subprocess-supervisor.c 
b/src/libide/threading/ide-subprocess-supervisor.c
new file mode 100644
index 000000000..d6f72fce5
--- /dev/null
+++ b/src/libide/threading/ide-subprocess-supervisor.c
@@ -0,0 +1,418 @@
+/* ide-subprocess-supervisor.c
+ *
+ * Copyright 2016-2019 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 "ide-subproces-supervisor"
+
+#include "config.h"
+
+#include <libide-core.h>
+
+#include "ide-subprocess.h"
+#include "ide-subprocess-supervisor.h"
+
+/*
+ * We will rate limit supervision to once per RATE_LIMIT_THRESHOLD_SECONDS
+ * so that we don't allow ourself to flap the worker process in case it is
+ * buggy and crashing/exiting too frequently.
+ */
+#define RATE_LIMIT_THRESHOLD_SECONDS 5
+
+typedef struct
+{
+  IdeSubprocessLauncher *launcher;
+  IdeSubprocess *subprocess;
+  GTimeVal last_spawn_time;
+  guint supervising : 1;
+} IdeSubprocessSupervisorPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeSubprocessSupervisor, ide_subprocess_supervisor, G_TYPE_OBJECT)
+
+enum {
+  SPAWNED,
+  SUPERVISE,
+  UNSUPERVISE,
+  EXITED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+ide_subprocess_supervisor_reset (IdeSubprocessSupervisor *self)
+{
+  IdeSubprocessSupervisorPrivate *priv = ide_subprocess_supervisor_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+
+  if (priv->subprocess != NULL)
+    {
+      g_autoptr(IdeSubprocess) subprocess = g_steal_pointer (&priv->subprocess);
+
+      /*
+       * We steal the subprocess first before possibly forcing exit from the
+       * subprocess so that when ide_subprocess_supervisor_wait_cb() is called
+       * it will not be able to match on (priv->subprocess == subprocess).
+       */
+      ide_subprocess_force_exit (subprocess);
+    }
+}
+
+static gboolean
+ide_subprocess_supervisor_real_supervise (IdeSubprocessSupervisor *self,
+                                          IdeSubprocessLauncher   *launcher)
+{
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+  g_assert (IDE_IS_SUBPROCESS_LAUNCHER (launcher));
+
+  ide_subprocess_supervisor_reset (self);
+
+  subprocess = ide_subprocess_launcher_spawn (launcher, NULL, &error);
+
+  if (subprocess != NULL)
+    ide_subprocess_supervisor_set_subprocess (self, subprocess);
+  else
+    g_warning ("%s", error->message);
+
+  return TRUE;
+}
+
+static gboolean
+ide_subprocess_supervisor_real_unsupervise (IdeSubprocessSupervisor *self,
+                                            IdeSubprocessLauncher   *launcher)
+{
+  g_assert (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+  g_assert (IDE_IS_SUBPROCESS_LAUNCHER (launcher));
+
+  ide_subprocess_supervisor_reset (self);
+
+  return TRUE;
+}
+
+static void
+ide_subprocess_supervisor_finalize (GObject *object)
+{
+  IdeSubprocessSupervisor *self = (IdeSubprocessSupervisor *)object;
+  IdeSubprocessSupervisorPrivate *priv = ide_subprocess_supervisor_get_instance_private (self);
+
+  /*
+   * Subprocess will have completed a wait by this point (or cancelled). It is
+   * safe to call force_exit() either way as it will drop the signal delivery
+   * on the floor if the process has exited.
+   */
+  if (priv->subprocess != NULL)
+    {
+      ide_subprocess_force_exit (priv->subprocess);
+      g_clear_object (&priv->subprocess);
+    }
+
+  g_clear_object (&priv->launcher);
+
+  G_OBJECT_CLASS (ide_subprocess_supervisor_parent_class)->finalize (object);
+}
+
+static void
+ide_subprocess_supervisor_class_init (IdeSubprocessSupervisorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_subprocess_supervisor_finalize;
+
+  signals [SPAWNED] =
+    g_signal_new ("spawned",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  G_STRUCT_OFFSET (IdeSubprocessSupervisorClass, spawned),
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 1, IDE_TYPE_SUBPROCESS);
+
+  signals [SUPERVISE] =
+    g_signal_new_class_handler ("supervise",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_subprocess_supervisor_real_supervise),
+                                g_signal_accumulator_true_handled, NULL,
+                                NULL,
+                                G_TYPE_BOOLEAN, 1, IDE_TYPE_SUBPROCESS_LAUNCHER);
+
+  signals [UNSUPERVISE] =
+    g_signal_new_class_handler ("unsupervise",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                G_CALLBACK (ide_subprocess_supervisor_real_unsupervise),
+                                g_signal_accumulator_true_handled, NULL,
+                                NULL,
+                                G_TYPE_BOOLEAN, 1, IDE_TYPE_SUBPROCESS_LAUNCHER);
+
+  signals [EXITED] =
+    g_signal_new_class_handler ("exited",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST,
+                                NULL, NULL, NULL,
+                                g_cclosure_marshal_VOID__OBJECT,
+                                G_TYPE_NONE, 1, IDE_TYPE_SUBPROCESS);
+}
+
+static void
+ide_subprocess_supervisor_init (IdeSubprocessSupervisor *self)
+{
+}
+
+IdeSubprocessSupervisor *
+ide_subprocess_supervisor_new (void)
+{
+  return g_object_new (IDE_TYPE_SUBPROCESS_SUPERVISOR, NULL);
+}
+
+/**
+ * ide_subprocess_supervisor_get_launcher:
+ *
+ * Returns: (nullable) (transfer none): An #IdeSubprocessLauncher or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeSubprocessLauncher *
+ide_subprocess_supervisor_get_launcher (IdeSubprocessSupervisor *self)
+{
+  IdeSubprocessSupervisorPrivate *priv = ide_subprocess_supervisor_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_SUPERVISOR (self), NULL);
+
+  return priv->launcher;
+}
+
+void
+ide_subprocess_supervisor_set_launcher (IdeSubprocessSupervisor *self,
+                                        IdeSubprocessLauncher   *launcher)
+{
+  IdeSubprocessSupervisorPrivate *priv = ide_subprocess_supervisor_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+  g_return_if_fail (!launcher || IDE_IS_SUBPROCESS_LAUNCHER (launcher));
+
+  g_set_object (&priv->launcher, launcher);
+}
+
+void
+ide_subprocess_supervisor_start (IdeSubprocessSupervisor *self)
+{
+  IdeSubprocessSupervisorPrivate *priv = ide_subprocess_supervisor_get_instance_private (self);
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+
+  if (priv->launcher == NULL)
+    {
+      g_warning ("Cannot supervise process, no launcher has been set");
+      IDE_EXIT;
+    }
+
+  priv->supervising = TRUE;
+
+  g_signal_emit (self, signals [SUPERVISE], 0, priv->launcher, &ret);
+
+  IDE_EXIT;
+}
+
+static gboolean
+ide_subprocess_supervisor_start_in_usec_cb (gpointer data)
+{
+  g_autoptr(IdeSubprocessSupervisor) self = data;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+
+  ide_subprocess_supervisor_start (self);
+
+  IDE_RETURN (G_SOURCE_REMOVE);
+}
+
+static void
+ide_subprocess_supervisor_start_in_usec (IdeSubprocessSupervisor *self,
+                                         gint64                   usec)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+
+  /* Wait to re-start the supervisor until our RATE_LIMIT_THRESHOLD_SECONDS
+   * have elapsed since our last spawn time. The amount of time required
+   * will be given to us in the @usec parameter.
+   */
+  g_timeout_add (MAX (250, usec / 1000L),
+                 ide_subprocess_supervisor_start_in_usec_cb,
+                 g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+void
+ide_subprocess_supervisor_stop (IdeSubprocessSupervisor *self)
+{
+  IdeSubprocessSupervisorPrivate *priv = ide_subprocess_supervisor_get_instance_private (self);
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+
+  if (priv->launcher == NULL)
+    {
+      g_warning ("Cannot unsupervise process, no launcher has been set");
+      IDE_EXIT;
+    }
+
+  priv->supervising = FALSE;
+
+  g_signal_emit (self, signals [UNSUPERVISE], 0, priv->launcher, &ret);
+
+  IDE_EXIT;
+}
+
+/**
+ * ide_subprocess_supervisor_get_subprocess:
+ * @self: An #IdeSubprocessSupervisor
+ *
+ * Gets the current #IdeSubprocess that is being supervised. This might be
+ * %NULL if the ide_subprocess_supervisor_start() has not yet been
+ * called or if there was a failure to spawn the process.
+ *
+ * Returns: (nullable) (transfer none): An #IdeSubprocess or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeSubprocess *
+ide_subprocess_supervisor_get_subprocess (IdeSubprocessSupervisor *self)
+{
+  IdeSubprocessSupervisorPrivate *priv = ide_subprocess_supervisor_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS_SUPERVISOR (self), NULL);
+
+  return priv->subprocess;
+}
+
+static gboolean
+ide_subprocess_supervisor_needs_rate_limit (IdeSubprocessSupervisor *self,
+                                            gint64                  *required_sleep)
+{
+  IdeSubprocessSupervisorPrivate *priv = ide_subprocess_supervisor_get_instance_private (self);
+  GTimeVal now;
+  gint64 now_usec;
+  gint64 last_usec;
+  gint64 span;
+
+  g_assert (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+  g_assert (required_sleep != NULL);
+
+  g_get_current_time (&now);
+
+  now_usec = (now.tv_sec * G_USEC_PER_SEC) + now.tv_usec;
+  last_usec = (priv->last_spawn_time.tv_sec * G_USEC_PER_SEC) + priv->last_spawn_time.tv_usec;
+  span = now_usec - last_usec;
+
+  if (span < (RATE_LIMIT_THRESHOLD_SECONDS * G_USEC_PER_SEC))
+    {
+      *required_sleep = (RATE_LIMIT_THRESHOLD_SECONDS * G_USEC_PER_SEC) - span;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+ide_subprocess_supervisor_wait_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  g_autoptr(IdeSubprocessSupervisor) self = user_data;
+  IdeSubprocessSupervisorPrivate *priv = ide_subprocess_supervisor_get_instance_private (self);
+  g_autoptr(GError) error = NULL;
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+  g_return_if_fail (IDE_IS_SUBPROCESS (subprocess));
+
+  if (!ide_subprocess_wait_finish (subprocess, result, &error))
+    g_warning ("%s", error->message);
+
+  g_signal_emit (self, signals [EXITED], 0, subprocess);
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    if (ide_subprocess_get_if_exited (subprocess))
+      IDE_TRACE_MSG ("process exited with code: %u",
+                     ide_subprocess_get_exit_status (subprocess));
+    else
+      IDE_TRACE_MSG ("process terminated due to signal: %u",
+                     ide_subprocess_get_term_sig (subprocess));
+  }
+#endif
+
+  /*
+   * If we end up here in response to ide_subprocess_supervisor_reset() force
+   * exiting the process, we won't successfully match
+   * (priv->subprocess==subprocess) and therefore will not restart the process
+   * immediately (allowing the caller of ide_subprocess_supervisor_reset() to
+   * complete the operation.
+   */
+
+  if (priv->subprocess == subprocess)
+    {
+      g_clear_object (&priv->subprocess);
+
+      if (priv->supervising)
+        {
+          gint64 sleep_usec;
+
+          if (ide_subprocess_supervisor_needs_rate_limit (self, &sleep_usec))
+            ide_subprocess_supervisor_start_in_usec (self, sleep_usec);
+          else
+            ide_subprocess_supervisor_start (self);
+        }
+    }
+}
+
+void
+ide_subprocess_supervisor_set_subprocess (IdeSubprocessSupervisor *self,
+                                          IdeSubprocess           *subprocess)
+{
+  IdeSubprocessSupervisorPrivate *priv = ide_subprocess_supervisor_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_SUBPROCESS_SUPERVISOR (self));
+  g_return_if_fail (!subprocess || IDE_IS_SUBPROCESS (subprocess));
+
+  if (g_set_object (&priv->subprocess, subprocess))
+    {
+      if (subprocess != NULL)
+        {
+          g_get_current_time (&priv->last_spawn_time);
+          ide_subprocess_wait_async (priv->subprocess,
+                                     NULL,
+                                     ide_subprocess_supervisor_wait_cb,
+                                     g_object_ref (self));
+          g_signal_emit (self, signals [SPAWNED], 0, subprocess);
+        }
+    }
+}
diff --git a/src/libide/threading/ide-subprocess-supervisor.h 
b/src/libide/threading/ide-subprocess-supervisor.h
new file mode 100644
index 000000000..7d69349c0
--- /dev/null
+++ b/src/libide/threading/ide-subprocess-supervisor.h
@@ -0,0 +1,74 @@
+/* ide-subprocess-supervisor.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_THREADING_INSIDE) && !defined (IDE_THREADING_COMPILATION)
+# error "Only <libide-threading.h> can be included directly."
+#endif
+
+#include <libide-core.h>
+
+#include "ide-subprocess.h"
+#include "ide-subprocess-launcher.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SUBPROCESS_SUPERVISOR (ide_subprocess_supervisor_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeSubprocessSupervisor, ide_subprocess_supervisor, IDE, SUBPROCESS_SUPERVISOR, 
GObject)
+
+struct _IdeSubprocessSupervisorClass
+{
+  GObjectClass parent_class;
+
+  void (*spawned) (IdeSubprocessSupervisor *self,
+                   IdeSubprocess           *subprocess);
+
+  /*< private >*/
+  gpointer _reserved1;
+  gpointer _reserved2;
+  gpointer _reserved3;
+  gpointer _reserved4;
+  gpointer _reserved5;
+  gpointer _reserved6;
+  gpointer _reserved7;
+  gpointer _reserved8;
+};
+
+IDE_AVAILABLE_IN_3_32
+IdeSubprocessSupervisor *ide_subprocess_supervisor_new            (void);
+IDE_AVAILABLE_IN_3_32
+IdeSubprocessLauncher   *ide_subprocess_supervisor_get_launcher   (IdeSubprocessSupervisor *self);
+IDE_AVAILABLE_IN_3_32
+void                     ide_subprocess_supervisor_set_launcher   (IdeSubprocessSupervisor *self,
+                                                                   IdeSubprocessLauncher   *launcher);
+IDE_AVAILABLE_IN_3_32
+void                     ide_subprocess_supervisor_start          (IdeSubprocessSupervisor *self);
+IDE_AVAILABLE_IN_3_32
+void                     ide_subprocess_supervisor_stop           (IdeSubprocessSupervisor *self);
+IDE_AVAILABLE_IN_3_32
+IdeSubprocess           *ide_subprocess_supervisor_get_subprocess (IdeSubprocessSupervisor *self);
+IDE_AVAILABLE_IN_3_32
+void                     ide_subprocess_supervisor_set_subprocess (IdeSubprocessSupervisor *self,
+                                                                   IdeSubprocess           *subprocess);
+
+G_END_DECLS
diff --git a/src/libide/threading/ide-subprocess.c b/src/libide/threading/ide-subprocess.c
new file mode 100644
index 000000000..c2af9c13f
--- /dev/null
+++ b/src/libide/threading/ide-subprocess.c
@@ -0,0 +1,441 @@
+/* ide-subprocess.c
+ *
+ * Copyright 2016-2019 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 "ide-subprocess"
+
+#include "config.h"
+
+#include <string.h>
+#include <libide-core.h>
+
+#include "ide-subprocess.h"
+
+G_DEFINE_INTERFACE (IdeSubprocess, ide_subprocess, G_TYPE_OBJECT)
+
+static void
+ide_subprocess_default_init (IdeSubprocessInterface *iface)
+{
+}
+
+#define WRAP_INTERFACE_METHOD(self, name, default_return, ...) \
+  ((IDE_SUBPROCESS_GET_IFACE(self)->name != NULL) ? \
+    IDE_SUBPROCESS_GET_IFACE(self)->name (self, ##__VA_ARGS__) : \
+    default_return)
+
+const gchar *
+ide_subprocess_get_identifier (IdeSubprocess *self)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), NULL);
+
+  return WRAP_INTERFACE_METHOD (self, get_identifier, NULL);
+}
+
+/**
+ * ide_subprocess_get_stdout_pipe:
+ *
+ * Returns: (transfer none): a #GInputStream or %NULL.
+ *
+ * Since: 3.32
+ */
+GInputStream *
+ide_subprocess_get_stdout_pipe (IdeSubprocess *self)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), NULL);
+
+  return WRAP_INTERFACE_METHOD (self, get_stdout_pipe, NULL);
+}
+
+/**
+ * ide_subprocess_get_stderr_pipe:
+ *
+ * Returns: (transfer none): a #GInputStream or %NULL.
+ *
+ * Since: 3.32
+ */
+GInputStream *
+ide_subprocess_get_stderr_pipe (IdeSubprocess *self)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), NULL);
+
+  return WRAP_INTERFACE_METHOD (self, get_stderr_pipe, NULL);
+}
+
+/**
+ * ide_subprocess_get_stdin_pipe:
+ *
+ * Returns: (transfer none): a #GOutputStream or %NULL.
+ *
+ * Since: 3.32
+ */
+GOutputStream *
+ide_subprocess_get_stdin_pipe (IdeSubprocess *self)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), NULL);
+
+  return WRAP_INTERFACE_METHOD (self, get_stdin_pipe, NULL);
+}
+
+gboolean
+ide_subprocess_wait (IdeSubprocess  *self,
+                     GCancellable   *cancellable,
+                     GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  return WRAP_INTERFACE_METHOD (self, wait, FALSE, cancellable, error);
+}
+
+gboolean
+ide_subprocess_wait_check (IdeSubprocess  *self,
+                           GCancellable   *cancellable,
+                           GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  return ide_subprocess_wait (self, cancellable, error) &&
+         ide_subprocess_check_exit_status (self, error);
+}
+
+void
+ide_subprocess_wait_async (IdeSubprocess       *self,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_SUBPROCESS (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  WRAP_INTERFACE_METHOD (self, wait_async, NULL, cancellable, callback, user_data);
+}
+
+gboolean
+ide_subprocess_wait_finish (IdeSubprocess  *self,
+                            GAsyncResult   *result,
+                            GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+
+  return WRAP_INTERFACE_METHOD (self, wait_finish, FALSE, result, error);
+}
+
+static void
+ide_subprocess_wait_check_cb (GObject      *object,
+                              GAsyncResult *result,
+                              gpointer      user_data)
+{
+  IdeSubprocess *self = (IdeSubprocess *)object;
+  g_autoptr(GTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SUBPROCESS (self));
+  g_assert (G_IS_TASK (task));
+
+  if (!ide_subprocess_wait_finish (self, result, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  if (ide_subprocess_get_if_signaled (self))
+    {
+      gint term_sig = ide_subprocess_get_term_sig (self);
+
+      g_task_return_new_error (task,
+                               G_SPAWN_ERROR,
+                               G_SPAWN_ERROR_FAILED,
+                               "Child process killed by signal %d",
+                               term_sig);
+      IDE_EXIT;
+    }
+
+  if (!ide_subprocess_check_exit_status (self, &error))
+    {
+      g_task_return_error (task, g_steal_pointer (&error));
+      IDE_EXIT;
+    }
+
+  g_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+void
+ide_subprocess_wait_check_async (IdeSubprocess       *self,
+                                 GCancellable        *cancellable,
+                                 GAsyncReadyCallback  callback,
+                                 gpointer             user_data)
+{
+  g_autoptr(GTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_SUBPROCESS (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_source_tag (task, ide_subprocess_wait_check_async);
+
+  ide_subprocess_wait_async (self,
+                             cancellable,
+                             ide_subprocess_wait_check_cb,
+                             g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+gboolean
+ide_subprocess_wait_check_finish (IdeSubprocess  *self,
+                                  GAsyncResult   *result,
+                                  GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+  g_return_val_if_fail (G_IS_TASK (result), FALSE);
+  g_return_val_if_fail (g_task_is_valid (G_TASK (result), self), FALSE);
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+gboolean
+ide_subprocess_get_successful (IdeSubprocess *self)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+
+  return WRAP_INTERFACE_METHOD (self, get_successful, FALSE);
+}
+
+gboolean
+ide_subprocess_get_if_exited (IdeSubprocess *self)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+
+  return WRAP_INTERFACE_METHOD (self, get_if_exited, FALSE);
+}
+
+gint
+ide_subprocess_get_exit_status (IdeSubprocess *self)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), 0);
+
+  return WRAP_INTERFACE_METHOD (self, get_exit_status, 0);
+}
+
+gboolean
+ide_subprocess_get_if_signaled (IdeSubprocess *self)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+
+  return WRAP_INTERFACE_METHOD (self, get_if_signaled, FALSE);
+}
+
+gint
+ide_subprocess_get_term_sig (IdeSubprocess *self)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), 0);
+
+  return WRAP_INTERFACE_METHOD (self, get_term_sig, 0);
+}
+
+gint
+ide_subprocess_get_status (IdeSubprocess *self)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), 0);
+
+  return WRAP_INTERFACE_METHOD (self, get_status, 0);
+}
+
+void
+ide_subprocess_send_signal (IdeSubprocess *self,
+                            gint           signal_num)
+{
+  g_return_if_fail (IDE_IS_SUBPROCESS (self));
+
+  WRAP_INTERFACE_METHOD (self, send_signal, NULL, signal_num);
+}
+
+void
+ide_subprocess_force_exit (IdeSubprocess *self)
+{
+  g_return_if_fail (IDE_IS_SUBPROCESS (self));
+
+  WRAP_INTERFACE_METHOD (self, force_exit, NULL);
+}
+
+gboolean
+ide_subprocess_communicate (IdeSubprocess  *self,
+                            GBytes         *stdin_buf,
+                            GCancellable   *cancellable,
+                            GBytes        **stdout_buf,
+                            GBytes        **stderr_buf,
+                            GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  return WRAP_INTERFACE_METHOD (self, communicate, FALSE, stdin_buf, cancellable, stdout_buf, stderr_buf, 
error);
+}
+
+/**
+ * ide_subprocess_communicate_utf8:
+ * @self: an #IdeSubprocess
+ * @stdin_buf: (nullable): input to deliver to the subprocesses stdin stream
+ * @cancellable: (nullable): an optional #GCancellable
+ * @stdout_buf: (out) (nullable): an optional location for the stdout contents
+ * @stderr_buf: (out) (nullable): an optional location for the stderr contents
+ *
+ * This process acts identical to g_subprocess_communicate_utf8().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_subprocess_communicate_utf8 (IdeSubprocess  *self,
+                                 const gchar    *stdin_buf,
+                                 GCancellable   *cancellable,
+                                 gchar         **stdout_buf,
+                                 gchar         **stderr_buf,
+                                 GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), FALSE);
+
+  return WRAP_INTERFACE_METHOD (self, communicate_utf8, FALSE, stdin_buf, cancellable, stdout_buf, 
stderr_buf, error);
+}
+
+/**
+ * ide_subprocess_communicate_async:
+ * @self: An #IdeSubprocess
+ * @stdin_buf: (nullable): a #GBytes to send to stdin or %NULL
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: A callback to complete the request
+ * @user_data: user data for @callback
+ *
+ * Asynchronously communicates with the the child process.
+ *
+ * There is no need to call ide_subprocess_wait() on the process if using
+ * this asynchronous operation as it will internally wait for the child
+ * to exit or be signaled.
+ *
+ * Ensure you've set the proper flags to ensure that you can write to stdin
+ * or read from stderr/stdout as necessary.
+ *
+ * Since: 3.32
+ */
+void
+ide_subprocess_communicate_async (IdeSubprocess       *self,
+                                  GBytes              *stdin_buf,
+                                  GCancellable        *cancellable,
+                                  GAsyncReadyCallback  callback,
+                                  gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_SUBPROCESS (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  WRAP_INTERFACE_METHOD (self, communicate_async, NULL, stdin_buf, cancellable, callback, user_data);
+}
+
+/**
+ * ide_subprocess_communicate_finish:
+ * @self: An #IdeSubprocess
+ * @result: a #GAsyncResult
+ * @stdout_buf: (out) (optional): A location for a #Bytes.
+ * @stderr_buf: (out) (optional): A location for a #Bytes.
+ * @error: a location for a #GError
+ *
+ * Finishes a request to ide_subprocess_communicate_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_subprocess_communicate_finish (IdeSubprocess  *self,
+                                   GAsyncResult   *result,
+                                   GBytes        **stdout_buf,
+                                   GBytes        **stderr_buf,
+                                   GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return WRAP_INTERFACE_METHOD (self, communicate_finish, FALSE, result, stdout_buf, stderr_buf, error);
+}
+
+/**
+ * ide_subprocess_communicate_utf8_async:
+ * @stdin_buf: (nullable): The data to send to stdin or %NULL
+ *
+ *
+ * Since: 3.32
+ */
+void
+ide_subprocess_communicate_utf8_async (IdeSubprocess       *self,
+                                       const gchar         *stdin_buf,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_SUBPROCESS (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  WRAP_INTERFACE_METHOD (self, communicate_utf8_async, NULL, stdin_buf, cancellable, callback, user_data);
+}
+
+/**
+ * ide_subprocess_communicate_utf8_finish:
+ * @self: An #IdeSubprocess
+ * @result: a #GAsyncResult
+ * @stdout_buf: (out) (optional): A location for the UTF-8 formatted output string or %NULL
+ * @stderr_buf: (out) (optional): A location for the UTF-8 formatted output string or %NULL
+ * @error: A location for a #GError, or %NULL
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_subprocess_communicate_utf8_finish (IdeSubprocess  *self,
+                                        GAsyncResult   *result,
+                                        gchar         **stdout_buf,
+                                        gchar         **stderr_buf,
+                                        GError        **error)
+{
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return WRAP_INTERFACE_METHOD (self, communicate_utf8_finish, FALSE, result, stdout_buf, stderr_buf, error);
+}
+
+gboolean
+ide_subprocess_check_exit_status (IdeSubprocess  *self,
+                                  GError        **error)
+{
+  gint exit_status;
+
+  g_return_val_if_fail (IDE_IS_SUBPROCESS (self), FALSE);
+
+  exit_status = ide_subprocess_get_exit_status (self);
+
+  return g_spawn_check_exit_status (exit_status, error);
+}
diff --git a/src/libide/threading/ide-subprocess.h b/src/libide/threading/ide-subprocess.h
new file mode 100644
index 000000000..583592a88
--- /dev/null
+++ b/src/libide/threading/ide-subprocess.h
@@ -0,0 +1,191 @@
+/* ide-subprocess.h
+ *
+ * Copyright 2016-2019 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
+
+#if !defined (IDE_THREADING_INSIDE) && !defined (IDE_THREADING_COMPILATION)
+# error "Only <libide-threading.h> can be included directly."
+#endif
+
+#include <gio/gio.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_SUBPROCESS (ide_subprocess_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeSubprocess, ide_subprocess, IDE, SUBPROCESS, GObject)
+
+struct _IdeSubprocessInterface
+{
+  GTypeInterface parent_interface;
+
+  const gchar   *(*get_identifier)          (IdeSubprocess        *self);
+  GInputStream  *(*get_stdout_pipe)         (IdeSubprocess        *self);
+  GInputStream  *(*get_stderr_pipe)         (IdeSubprocess        *self);
+  GOutputStream *(*get_stdin_pipe)          (IdeSubprocess        *self);
+  gboolean       (*wait)                    (IdeSubprocess        *self,
+                                             GCancellable         *cancellable,
+                                             GError              **error);
+  void           (*wait_async)              (IdeSubprocess        *self,
+                                             GCancellable         *cancellable,
+                                             GAsyncReadyCallback   callback,
+                                             gpointer              user_data);
+  gboolean       (*wait_finish)             (IdeSubprocess        *self,
+                                             GAsyncResult         *result,
+                                             GError              **error);
+  gboolean       (*get_successful)          (IdeSubprocess        *self);
+  gboolean       (*get_if_exited)           (IdeSubprocess        *self);
+  gint           (*get_exit_status)         (IdeSubprocess        *self);
+  gboolean       (*get_if_signaled)         (IdeSubprocess        *self);
+  gint           (*get_term_sig)            (IdeSubprocess        *self);
+  gint           (*get_status)              (IdeSubprocess        *self);
+  void           (*send_signal)             (IdeSubprocess        *self,
+                                             gint                  signal_num);
+  void           (*force_exit)              (IdeSubprocess        *self);
+  gboolean       (*communicate)             (IdeSubprocess        *self,
+                                             GBytes               *stdin_buf,
+                                             GCancellable         *cancellable,
+                                             GBytes              **stdout_buf,
+                                             GBytes              **stderr_buf,
+                                             GError              **error);
+  gboolean       (*communicate_utf8)        (IdeSubprocess        *self,
+                                             const gchar          *stdin_buf,
+                                             GCancellable         *cancellable,
+                                             gchar               **stdout_buf,
+                                             gchar               **stderr_buf,
+                                             GError              **error);
+  void           (*communicate_async)       (IdeSubprocess        *self,
+                                             GBytes               *stdin_buf,
+                                             GCancellable         *cancellable,
+                                             GAsyncReadyCallback   callback,
+                                             gpointer              user_data);
+  gboolean       (*communicate_finish)      (IdeSubprocess        *self,
+                                             GAsyncResult         *result,
+                                             GBytes              **stdout_buf,
+                                             GBytes              **stderr_buf,
+                                             GError              **error);
+  void           (*communicate_utf8_async)  (IdeSubprocess        *self,
+                                             const gchar          *stdin_buf,
+                                             GCancellable         *cancellable,
+                                             GAsyncReadyCallback   callback,
+                                             gpointer              user_data);
+  gboolean       (*communicate_utf8_finish) (IdeSubprocess        *self,
+                                             GAsyncResult         *result,
+                                             gchar               **stdout_buf,
+                                             gchar               **stderr_buf,
+                                             GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_subprocess_get_identifier          (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+GInputStream  *ide_subprocess_get_stdout_pipe         (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+GInputStream  *ide_subprocess_get_stderr_pipe         (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+GOutputStream *ide_subprocess_get_stdin_pipe          (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_wait                    (IdeSubprocess        *self,
+                                                       GCancellable         *cancellable,
+                                                       GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_wait_check              (IdeSubprocess        *self,
+                                                       GCancellable         *cancellable,
+                                                       GError              **error);
+IDE_AVAILABLE_IN_3_32
+void           ide_subprocess_wait_async              (IdeSubprocess        *self,
+                                                       GCancellable         *cancellable,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_wait_finish             (IdeSubprocess        *self,
+                                                       GAsyncResult         *result,
+                                                       GError              **error);
+IDE_AVAILABLE_IN_3_32
+void           ide_subprocess_wait_check_async        (IdeSubprocess        *self,
+                                                       GCancellable         *cancellable,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_wait_check_finish       (IdeSubprocess        *self,
+                                                       GAsyncResult         *result,
+                                                       GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_check_exit_status       (IdeSubprocess        *self,
+                                                       GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_get_successful          (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_get_if_exited           (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+gint           ide_subprocess_get_exit_status         (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_get_if_signaled         (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+gint           ide_subprocess_get_term_sig            (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+gint           ide_subprocess_get_status              (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_subprocess_send_signal             (IdeSubprocess        *self,
+                                                       gint                  signal_num);
+IDE_AVAILABLE_IN_3_32
+void           ide_subprocess_force_exit              (IdeSubprocess        *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_communicate             (IdeSubprocess        *self,
+                                                       GBytes               *stdin_buf,
+                                                       GCancellable         *cancellable,
+                                                       GBytes              **stdout_buf,
+                                                       GBytes              **stderr_buf,
+                                                       GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_communicate_utf8        (IdeSubprocess        *self,
+                                                       const gchar          *stdin_buf,
+                                                       GCancellable         *cancellable,
+                                                       gchar               **stdout_buf,
+                                                       gchar               **stderr_buf,
+                                                       GError              **error);
+IDE_AVAILABLE_IN_3_32
+void           ide_subprocess_communicate_async       (IdeSubprocess        *self,
+                                                       GBytes               *stdin_buf,
+                                                       GCancellable         *cancellable,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_communicate_finish      (IdeSubprocess        *self,
+                                                       GAsyncResult         *result,
+                                                       GBytes              **stdout_buf,
+                                                       GBytes              **stderr_buf,
+                                                       GError              **error);
+IDE_AVAILABLE_IN_3_32
+void           ide_subprocess_communicate_utf8_async  (IdeSubprocess        *self,
+                                                       const gchar          *stdin_buf,
+                                                       GCancellable         *cancellable,
+                                                       GAsyncReadyCallback   callback,
+                                                       gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_subprocess_communicate_utf8_finish (IdeSubprocess        *self,
+                                                       GAsyncResult         *result,
+                                                       gchar               **stdout_buf,
+                                                       gchar               **stderr_buf,
+                                                       GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/threading/ide-task.c b/src/libide/threading/ide-task.c
index 12f6ba346..e1af5e84a 100644
--- a/src/libide/threading/ide-task.c
+++ b/src/libide/threading/ide-task.c
@@ -15,17 +15,26 @@
  * You should have received a copy of the GNU Lesser General Public
  * License along with this library; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "ide-task"
 
 #include "config.h"
 
-#include <dazzle.h>
+#include <libide-core.h>
+
+#include "ide-task.h"
+#include "ide-thread-pool.h"
+#include "ide-thread-private.h"
+
+/* From GDK_PRIORITY_REDRAW */
+#define PRIORITY_REDRAW (G_PRIORITY_HIGH_IDLE + 20)
 
-#include "threading/ide-task.h"
-#include "threading/ide-thread-pool.h"
-#include "threading/ide-thread-private.h"
+#if 0
+# define ENABLE_TIME_CHART
+#endif
 
 /**
  * SECTION:ide-task
@@ -59,7 +68,7 @@
  * provides a simplified API over ide_task_return_pointer() which also allows
  * copying the result to chained tasks.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 
 typedef struct
@@ -237,6 +246,11 @@ typedef struct
    */
   gpointer source_tag;
 
+#ifdef ENABLE_TIME_CHART
+  /* The time the task was created */
+  gint64 begin_time;
+#endif
+
   /*
    * Our priority for scheduling tasks in the particular workqueue.
    */
@@ -320,8 +334,6 @@ static void     ide_task_release        (IdeTask           *self,
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeTaskData, ide_task_data_free);
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeTaskResult, ide_task_result_free);
 
-DZL_DEFINE_COUNTER (instances, "Tasks", "Instances", "Number of active tasks")
-
 G_DEFINE_TYPE_WITH_CODE (IdeTask, ide_task, G_TYPE_OBJECT,
                          G_ADD_PRIVATE (IdeTask)
                          G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_RESULT, async_result_init_iface))
@@ -623,8 +635,6 @@ ide_task_finalize (GObject *object)
   g_mutex_clear (&priv->mutex);
 
   G_OBJECT_CLASS (ide_task_parent_class)->finalize (object);
-
-  DZL_COUNTER_DEC (instances);
 }
 
 static void
@@ -676,14 +686,12 @@ ide_task_init (IdeTask *self)
 {
   IdeTaskPrivate *priv = ide_task_get_instance_private (self);
 
-  DZL_COUNTER_INC (instances);
-
   g_mutex_init (&priv->mutex);
 
   priv->check_cancellable = TRUE;
   priv->release_on_propagate = TRUE;
   priv->priority = G_PRIORITY_DEFAULT;
-  priv->complete_priority = GDK_PRIORITY_REDRAW + 1;
+  priv->complete_priority = PRIORITY_REDRAW + 1;
   priv->main_context = g_main_context_ref_thread_default ();
   priv->global_link.data = self;
 
@@ -710,7 +718,7 @@ ide_task_init (IdeTask *self)
  *
  * Returns: (transfer none) (nullable) (type GObject.Object): a #GObject or %NULL
  *
- * Since: 3.30
+ * Since: 3.32
  */
 gpointer
 ide_task_get_source_object (IdeTask *self)
@@ -743,7 +751,7 @@ ide_task_get_source_object (IdeTask *self)
  *
  * Returns: (transfer full): an #IdeTask
  *
- * Since: 3.30
+ * Since: 3.32
  */
 IdeTask *
 (ide_task_new) (gpointer             source_object,
@@ -764,20 +772,23 @@ IdeTask *
   priv->cancellable = cancellable ? g_object_ref (cancellable) : NULL;
   priv->callback = callback;
   priv->user_data = user_data;
+#ifdef ENABLE_TIME_CHART
+  priv->begin_time = g_get_monotonic_time ();
+#endif
 
   return g_steal_pointer (&self);
 }
 
 /**
  * ide_task_is_valid:
- * @self: (nullable) (type Ide.Task): a #IdeTask
+ * @self: (nullable) (type IdeTask): a #IdeTask
  * @source_object: (nullable): a #GObject or %NULL
  *
  * Checks if @source_object matches the object the task was created with.
  *
  * Returns: %TRUE is source_object matches
  *
- * Since: 3.30
+ * Since: 3.32
  */
 gboolean
 ide_task_is_valid (gpointer self,
@@ -800,7 +811,7 @@ ide_task_is_valid (gpointer self,
  *
  * Returns: %TRUE if the task has completed
  *
- * Since: 3.30
+ * Since: 3.32
  */
 gboolean
 ide_task_get_completed (IdeTask *self)
@@ -881,7 +892,7 @@ ide_task_set_complete_priority (IdeTask *self,
  *
  * Returns: (transfer none) (nullable): a #GCancellable or %NULL
  *
- * Since: 3.30
+ * Since: 3.32
  */
 GCancellable *
 ide_task_get_cancellable (IdeTask *self)
@@ -979,6 +990,12 @@ ide_task_return_cb (gpointer user_data)
   self = g_steal_pointer (&result->task);
   priv = ide_task_get_instance_private (self);
 
+#ifdef ENABLE_TIME_CHART
+  g_message ("TASK-END: %s: duration=%lf",
+             priv->name,
+             (g_get_monotonic_time () - priv->begin_time) / (gdouble)G_USEC_PER_SEC);
+#endif
+
   g_mutex_lock (&priv->mutex);
 
   g_assert (priv->return_source != 0);
@@ -1140,7 +1157,7 @@ ide_task_return (IdeTask       *self,
  * Other tasks depending on the result will be notified after returning
  * to the #GMainContext of the task.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_return_int (IdeTask *self,
@@ -1167,7 +1184,7 @@ ide_task_return_int (IdeTask *self,
  * Other tasks depending on the result will be notified after returning
  * to the #GMainContext of the task.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_return_boolean (IdeTask  *self,
@@ -1194,7 +1211,7 @@ ide_task_return_boolean (IdeTask  *self,
  * know the boxed #GType so that the result may be propagated to chained
  * tasks.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_return_boxed (IdeTask  *self,
@@ -1225,7 +1242,7 @@ ide_task_return_boxed (IdeTask  *self,
  * Takes ownership of @instance to allow saving a reference increment and
  * decrement by the caller.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_return_object (IdeTask  *self,
@@ -1259,7 +1276,7 @@ ide_task_return_object (IdeTask  *self,
  * If you need task chaining with pointers, see ide_task_return_boxed()
  * or ide_task_return_object().
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_return_pointer (IdeTask        *self,
@@ -1285,7 +1302,7 @@ ide_task_return_pointer (IdeTask        *self,
  *
  * Sets @error as the result of the #IdeTask
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_return_error (IdeTask *self,
@@ -1311,7 +1328,7 @@ ide_task_return_error (IdeTask *self,
  *
  * Creates a new #GError and sets it as the result for the task.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_return_new_error (IdeTask     *self,
@@ -1341,7 +1358,7 @@ ide_task_return_new_error (IdeTask     *self,
  *
  * Returns: %TRUE if the task was cancelled and error returned.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 gboolean
 ide_task_return_error_if_cancelled (IdeTask *self)
@@ -1377,7 +1394,7 @@ ide_task_return_error_if_cancelled (IdeTask *self)
  * Generally, you want to leave this as %TRUE to ensure thread-safety on the
  * dependent objects and task data.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_set_release_on_propagate (IdeTask  *self,
@@ -1402,7 +1419,7 @@ ide_task_set_release_on_propagate (IdeTask  *self,
  * Sets the source tag for the task. Generally this is a function pointer
  * of the function that created the task.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_set_source_tag (IdeTask  *self,
@@ -1427,7 +1444,7 @@ ide_task_set_source_tag (IdeTask  *self,
  * before propagating a result. If cancelled, an error will be returned
  * instead of the result.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_set_check_cancellable (IdeTask  *self,
@@ -1455,7 +1472,7 @@ ide_task_set_check_cancellable (IdeTask  *self,
  * ide_task_return_boolean(), ide_task_return_int(), or
  * ide_task_return_pointer().
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_run_in_thread (IdeTask           *self,
@@ -1633,7 +1650,7 @@ ide_task_propagate_int (IdeTask  *self,
  * Returns: (transfer full) (type GObject.Object): a #GObject or %NULL
  *   and @error may be set.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 gpointer
 ide_task_propagate_object (IdeTask  *self,
@@ -1690,7 +1707,7 @@ ide_task_propagate_pointer (IdeTask  *self,
  * have called ide_task_set_release_on_propagate() with @self and set
  * release_on_propagate to %FALSE, or @self has not yet completed.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_chain (IdeTask *self,
@@ -1779,7 +1796,7 @@ ide_task_set_kind (IdeTask     *self,
  *
  * Returns: (transfer none): previously registered task data or %NULL
  *
- * Since: 3.30
+ * Since: 3.32
  */
 gpointer
 ide_task_get_task_data (IdeTask *self)
@@ -1898,6 +1915,8 @@ ide_task_cancellable_cancelled_cb (GCancellable  *cancellable,
  *
  * Gets the return_on_cancel value, which means the task will return
  * immediately when the #GCancellable is cancelled.
+ *
+ * Since: 3.32
  */
 gboolean
 ide_task_get_return_on_cancel (IdeTask *self)
@@ -1928,7 +1947,7 @@ ide_task_get_return_on_cancel (IdeTask *self)
  * will outlive the threaded worker so that task state can be freed in a delayed
  * fashion.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_set_return_on_cancel (IdeTask  *self,
@@ -2011,7 +2030,7 @@ ide_task_report_new_error (gpointer              source_object,
  *
  * Returns: (nullable): a string or %NULL
  *
- * Since: 3.30
+ * Since: 3.32
  */
 const gchar *
 ide_task_get_name (IdeTask *self)
@@ -2044,7 +2063,7 @@ ide_task_get_name (IdeTask *self)
  * If using #IdeTask from C, a default name is set using the source
  * file name and line number.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_task_set_name (IdeTask *self,
@@ -2059,6 +2078,10 @@ ide_task_set_name (IdeTask *self,
   g_mutex_lock (&priv->mutex);
   priv->name = name;
   g_mutex_unlock (&priv->mutex);
+
+#ifdef ENABLE_TIME_CHART
+  g_message ("TASK-BEGIN: %s", name);
+#endif
 }
 
 /**
@@ -2069,7 +2092,7 @@ ide_task_set_name (IdeTask *self,
  *
  * Returns: %TRUE if an error has occurred
  *
- * Since: 3.30
+ * Since: 3.32
  */
 gboolean
 ide_task_had_error (IdeTask *self)
@@ -2130,7 +2153,7 @@ async_result_init_iface (GAsyncResultIface *iface)
 }
 
 void
-ide_dump_tasks (void)
+_ide_dump_tasks (void)
 {
   guint i = 0;
 
diff --git a/src/libide/threading/ide-task.h b/src/libide/threading/ide-task.h
index cef8a3f3f..2938a91f3 100644
--- a/src/libide/threading/ide-task.h
+++ b/src/libide/threading/ide-task.h
@@ -15,19 +15,23 @@
  * You should have received a copy of the GNU Lesser General Public
  * License along with this library; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #pragma once
 
-#include <gio/gio.h>
+#if !defined (IDE_THREADING_INSIDE) && !defined (IDE_THREADING_COMPILATION)
+# error "Only <libide-threading.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <gio/gio.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_TASK (ide_task_get_type())
 
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeTask, ide_task, IDE, TASK, GObject)
 
 typedef void (*IdeTaskThreadFunc) (IdeTask      *task,
@@ -51,114 +55,114 @@ struct _IdeTaskClass
   gpointer _reserved[16];
 };
 
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 IdeTask      *ide_task_new                       (gpointer              source_object,
                                                   GCancellable         *cancellable,
                                                   GAsyncReadyCallback   callback,
                                                   gpointer              user_data);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_chain                     (IdeTask              *self,
                                                   IdeTask              *other_task);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 GCancellable *ide_task_get_cancellable           (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gboolean      ide_task_get_completed             (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 IdeTaskKind   ide_task_get_kind                  (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 const gchar  *ide_task_get_name                  (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gint          ide_task_get_priority              (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gint          ide_task_get_complete_priority     (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gpointer      ide_task_get_source_object         (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gpointer      ide_task_get_source_tag            (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gpointer      ide_task_get_task_data             (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gboolean      ide_task_had_error                 (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gboolean      ide_task_is_valid                  (gpointer              self,
                                                   gpointer              source_object);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gboolean      ide_task_propagate_boolean         (IdeTask              *self,
                                                   GError              **error);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gpointer      ide_task_propagate_boxed           (IdeTask              *self,
                                                   GError              **error);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gssize        ide_task_propagate_int             (IdeTask              *self,
                                                   GError              **error);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gpointer      ide_task_propagate_object          (IdeTask              *self,
                                                   GError              **error);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gpointer      ide_task_propagate_pointer         (IdeTask              *self,
                                                   GError              **error);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_return_boolean            (IdeTask              *self,
                                                   gboolean              result);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_return_boxed              (IdeTask              *self,
                                                   GType                 result_type,
                                                   gpointer              result);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_return_error              (IdeTask              *self,
                                                   GError               *error);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gboolean      ide_task_return_error_if_cancelled (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_return_int                (IdeTask              *self,
                                                   gssize                result);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 gboolean      ide_task_get_return_on_cancel      (IdeTask              *self);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_return_new_error          (IdeTask              *self,
                                                   GQuark                error_domain,
                                                   gint                  error_code,
                                                   const gchar          *format,
                                                   ...) G_GNUC_PRINTF (4, 5);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_return_object             (IdeTask              *self,
                                                   gpointer              instance);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_return_pointer            (IdeTask              *self,
                                                   gpointer              data,
                                                   GDestroyNotify        destroy);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_run_in_thread             (IdeTask              *self,
                                                   IdeTaskThreadFunc     thread_func);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_set_check_cancellable     (IdeTask              *self,
                                                   gboolean              check_cancellable);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_set_kind                  (IdeTask              *self,
                                                   IdeTaskKind           kind);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_set_name                  (IdeTask              *self,
                                                   const gchar          *name);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_set_priority              (IdeTask              *self,
                                                   gint                  priority);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_set_complete_priority     (IdeTask              *self,
                                                   gint                  complete_priority);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_set_release_on_propagate  (IdeTask              *self,
                                                   gboolean              release_on_propagate);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_set_return_on_cancel      (IdeTask              *self,
                                                   gboolean              return_on_cancel);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_set_source_tag            (IdeTask              *self,
                                                   gpointer              source_tag);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_set_task_data             (IdeTask              *self,
                                                   gpointer              task_data,
                                                   GDestroyNotify        task_data_destroy);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void          ide_task_report_new_error          (gpointer              source_object,
                                                   GAsyncReadyCallback   callback,
                                                   gpointer              callback_data,
@@ -167,8 +171,6 @@ void          ide_task_report_new_error          (gpointer              source_o
                                                   gint                  code,
                                                   const gchar          *format,
                                                   ...) G_GNUC_PRINTF (7, 8);
-IDE_AVAILABLE_IN_3_30
-void          ide_dump_tasks                     (void);
 
 #ifdef __GNUC__
 # define ide_task_new(self, cancellable, callback, user_data)                      \
diff --git a/src/libide/threading/ide-thread-pool.c b/src/libide/threading/ide-thread-pool.c
index 7ea2436ba..d21579b9d 100644
--- a/src/libide/threading/ide-thread-pool.c
+++ b/src/libide/threading/ide-thread-pool.c
@@ -1,6 +1,6 @@
 /* ide-thread-pool.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,18 +14,18 @@
  *
  * 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 "ide-thread-pool"
 
 #include "config.h"
 
-#include <dazzle.h>
-
-#include "ide-debug.h"
+#include <libide-core.h>
 
-#include "threading/ide-thread-pool.h"
-#include "threading/ide-thread-private.h"
+#include "ide-thread-pool.h"
+#include "ide-thread-private.h"
 
 typedef struct
 {
@@ -52,9 +52,6 @@ struct _IdeThreadPool
   gboolean           exclusive;
 };
 
-DZL_DEFINE_COUNTER (TotalTasks, "ThreadPool", "Total Tasks", "Total number of tasks processed.")
-DZL_DEFINE_COUNTER (QueuedTasks, "ThreadPool", "Queued Tasks", "Current number of pending tasks.")
-
 static IdeThreadPool thread_pools[] = {
   { NULL, IDE_THREAD_POOL_DEFAULT, 10, 1, FALSE },
   { NULL, IDE_THREAD_POOL_COMPILER, 2, 2, FALSE },
@@ -86,6 +83,8 @@ ide_thread_pool_get_pool (IdeThreadPoolKind kind)
  *
  * This pushes a task to be executed on a worker thread based on the task kind as denoted by
  * @kind. Some tasks will be placed on special work queues or throttled based on priority.
+ *
+ * Since: 3.32
  */
 void
 ide_thread_pool_push_task (IdeThreadPoolKind  kind,
@@ -101,8 +100,6 @@ ide_thread_pool_push_task (IdeThreadPoolKind  kind,
   g_return_if_fail (G_IS_TASK (task));
   g_return_if_fail (func != NULL);
 
-  DZL_COUNTER_INC (TotalTasks);
-
   pool = ide_thread_pool_get_pool (kind);
 
   if (pool != NULL)
@@ -115,8 +112,6 @@ ide_thread_pool_push_task (IdeThreadPoolKind  kind,
       work_item->task.task = g_object_ref (task);
       work_item->task.func = func;
 
-      DZL_COUNTER_INC (QueuedTasks);
-
       g_thread_pool_push (pool, work_item, NULL);
     }
   else
@@ -134,6 +129,8 @@ ide_thread_pool_push_task (IdeThreadPoolKind  kind,
  * @func_data: user data for @func.
  *
  * Runs the callback on the thread pool thread.
+ *
+ * Since: 3.32
  */
 void
 ide_thread_pool_push (IdeThreadPoolKind kind,
@@ -151,6 +148,8 @@ ide_thread_pool_push (IdeThreadPoolKind kind,
  * @func_data: user data for @func.
  *
  * Runs the callback on the thread pool thread.
+ *
+ * Since: 3.32
  */
 void
 ide_thread_pool_push_with_priority (IdeThreadPoolKind kind,
@@ -166,8 +165,6 @@ ide_thread_pool_push_with_priority (IdeThreadPoolKind kind,
   g_return_if_fail (kind < IDE_THREAD_POOL_LAST);
   g_return_if_fail (func != NULL);
 
-  DZL_COUNTER_INC (TotalTasks);
-
   pool = ide_thread_pool_get_pool (kind);
 
   if (pool != NULL)
@@ -180,8 +177,6 @@ ide_thread_pool_push_with_priority (IdeThreadPoolKind kind,
       work_item->func.callback = func;
       work_item->func.data = func_data;
 
-      DZL_COUNTER_INC (QueuedTasks);
-
       g_thread_pool_push (pool, work_item, NULL);
     }
   else
@@ -200,8 +195,6 @@ ide_thread_pool_worker (gpointer data,
 
   g_assert (work_item != NULL);
 
-  DZL_COUNTER_DEC (QueuedTasks);
-
   if (work_item->type == TYPE_TASK)
     {
       gpointer source_object = g_task_get_source_object (work_item->task.task);
diff --git a/src/libide/threading/ide-thread-pool.h b/src/libide/threading/ide-thread-pool.h
index c2eb93e6e..2bc9bb99e 100644
--- a/src/libide/threading/ide-thread-pool.h
+++ b/src/libide/threading/ide-thread-pool.h
@@ -1,6 +1,6 @@
 /* ide-thread-pool.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,13 +14,18 @@
  *
  * 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>
+#if !defined (IDE_THREADING_INSIDE) && !defined (IDE_THREADING_COMPILATION)
+# error "Only <libide-threading.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <gio/gio.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
@@ -39,19 +44,21 @@ typedef enum
  * IdeThreadFunc:
  * @user_data: (closure) (transfer full): The closure for the callback.
  *
+ *
+ * Since: 3.32
  */
 typedef void (*IdeThreadFunc) (gpointer user_data);
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void     ide_thread_pool_push               (IdeThreadPoolKind  kind,
                                              IdeThreadFunc      func,
                                              gpointer           func_data);
-IDE_AVAILABLE_IN_3_30
+IDE_AVAILABLE_IN_3_32
 void     ide_thread_pool_push_with_priority (IdeThreadPoolKind  kind,
                                              gint               priority,
                                              IdeThreadFunc      func,
                                              gpointer           func_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void     ide_thread_pool_push_task          (IdeThreadPoolKind  kind,
                                              GTask             *task,
                                              GTaskThreadFunc    func);
diff --git a/src/libide/threading/ide-thread-private.h b/src/libide/threading/ide-thread-private.h
index a149a432c..defe1fc7d 100644
--- a/src/libide/threading/ide-thread-private.h
+++ b/src/libide/threading/ide-thread-private.h
@@ -1,6 +1,6 @@
 /* ide-thread-private.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
@@ -23,5 +25,6 @@
 G_BEGIN_DECLS
 
 void _ide_thread_pool_init (gboolean is_worker);
+void _ide_dump_tasks       (void);
 
 G_END_DECLS
diff --git a/src/libide/threading/libide-threading.h b/src/libide/threading/libide-threading.h
new file mode 100644
index 000000000..b321f06ab
--- /dev/null
+++ b/src/libide/threading/libide-threading.h
@@ -0,0 +1,35 @@
+/* ide-threading.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+#define IDE_THREADING_INSIDE
+
+#include "ide-environment.h"
+#include "ide-environment-variable.h"
+#include "ide-subprocess-launcher.h"
+#include "ide-subprocess-supervisor.h"
+#include "ide-subprocess.h"
+#include "ide-task.h"
+#include "ide-thread-pool.h"
+
+#undef IDE_THREADING_INSIDE
diff --git a/src/libide/threading/meson.build b/src/libide/threading/meson.build
index 201e188e0..d38ddfb64 100644
--- a/src/libide/threading/meson.build
+++ b/src/libide/threading/meson.build
@@ -1,20 +1,78 @@
-threading_headers = [
-  'ide-thread-pool.h',
+libide_threading_header_subdir = join_paths(libide_header_subdir, 'threading')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_threading_public_headers = [
+  'ide-environment.h',
+  'ide-environment-variable.h',
+  'ide-subprocess.h',
+  'ide-subprocess-launcher.h',
+  'ide-subprocess-supervisor.h',
   'ide-task.h',
+  'ide-thread-pool.h',
+  'libide-threading.h',
 ]
 
-threading_sources = [
-  'ide-thread-pool.c',
+install_headers(libide_threading_public_headers, subdir: libide_threading_header_subdir)
+
+#
+# Sources
+#
+
+libide_threading_private_headers = [
+  'ide-thread-private.h',
+  'ide-flatpak-subprocess-private.h',
+  'ide-gtask-private.h',
+  'ide-simple-subprocess-private.h',
+]
+
+libide_threading_private_sources = [
+  'ide-flatpak-subprocess.c',
+  'ide-simple-subprocess.c',
+]
+
+libide_threading_public_sources = [
+  'ide-environment-variable.c',
+  'ide-environment.c',
+  'ide-gtask.c',
+  'ide-subprocess-launcher.c',
+  'ide-subprocess-supervisor.c',
+  'ide-subprocess.c',
   'ide-task.c',
+  'ide-thread-pool.c',
 ]
 
-threading_enums = [
-  'ide-thread-pool.h',
-  'ide-task.h',
+libide_threading_sources = libide_threading_public_sources + libide_threading_private_sources
+
+#
+# Library Definitions
+#
+
+libide_threading_deps = [
+  libgio_dep,
+  libgiounix_dep,
+
+  libide_core_dep,
 ]
 
-libide_public_headers += files(threading_headers)
-libide_public_sources += files(threading_sources)
-libide_enum_headers += files(threading_enums)
+libide_threading = static_library('ide-threading-' + libide_api_version, libide_threading_sources,
+   dependencies: libide_threading_deps,
+         c_args: libide_args + release_args + ['-DIDE_THREADING_COMPILATION'],
+)
+
+libide_threading_dep = declare_dependency(
+              sources: libide_threading_private_headers,
+         dependencies: libide_threading_deps,
+           link_whole: libide_threading,
+  include_directories: include_directories('.'),
+)
 
-install_headers(threading_headers, subdir: join_paths(libide_header_subdir, 'threading'))
+gnome_builder_public_sources += files(libide_threading_public_sources)
+gnome_builder_public_headers += files(libide_threading_public_headers)
+gnome_builder_private_sources += files(libide_threading_private_sources)
+gnome_builder_private_headers += files(libide_threading_private_headers)
+gnome_builder_include_subdirs += libide_threading_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-threading.h', '-DIDE_THREADING_COMPILATION']
diff --git a/src/libide/tree/ide-tree-addin.c b/src/libide/tree/ide-tree-addin.c
new file mode 100644
index 000000000..973ce7ffa
--- /dev/null
+++ b/src/libide/tree/ide-tree-addin.c
@@ -0,0 +1,366 @@
+/* ide-tree-addin.c
+ *
+ * Copyright 2018-2019 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 "ide-tree-addin"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-tree-addin.h"
+
+G_DEFINE_INTERFACE (IdeTreeAddin, ide_tree_addin, G_TYPE_OBJECT)
+
+static void
+ide_tree_addin_real_build_children_async (IdeTreeAddin        *self,
+                                          IdeTreeNode         *node,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_tree_addin_real_build_children_async);
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->build_children)
+    IDE_TREE_ADDIN_GET_IFACE (self)->build_children (self, node);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_tree_addin_real_build_children_finish (IdeTreeAddin  *self,
+                                           GAsyncResult  *result,
+                                           GError       **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_tree_addin_real_node_dropped_async (IdeTreeAddin        *self,
+                                        IdeTreeNode         *drag_node,
+                                        IdeTreeNode         *drop_node,
+                                        GtkSelectionData    *selection,
+                                        GdkDragAction        actions,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  g_task_report_new_error (self, callback, user_data,
+                           ide_tree_addin_real_node_dropped_async,
+                           G_IO_ERROR,
+                           G_IO_ERROR_NOT_SUPPORTED,
+                           "Addin does not support dropping nodes");
+}
+
+static gboolean
+ide_tree_addin_real_node_dropped_finish (IdeTreeAddin  *self,
+                                         GAsyncResult  *result,
+                                         GError       **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (self));
+  g_assert (G_IS_TASK (result));
+
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+ide_tree_addin_default_init (IdeTreeAddinInterface *iface)
+{
+  iface->build_children_async = ide_tree_addin_real_build_children_async;
+  iface->build_children_finish = ide_tree_addin_real_build_children_finish;
+  iface->node_dropped_async = ide_tree_addin_real_node_dropped_async;
+  iface->node_dropped_finish = ide_tree_addin_real_node_dropped_finish;
+}
+
+/**
+ * ide_tree_addin_build_children_async:
+ * @self: a #IdeTreeAddin
+ * @node: a #IdeTreeNode
+ * @cancellable: (nullable): a #GCancellable or %NULL
+ * @callback: a #GAsyncReadyCallback or %NULL
+ * @user_data: user data for @callback
+ *
+ * This function is called when building the children of a node. This
+ * happens when expanding an node that might have children, or building the
+ * root node.
+ *
+ * You may want to use ide_tree_node_holds() to determine if the node
+ * contains an item that you are interested in.
+ *
+ * This function will call the synchronous form of
+ * IdeTreeAddin.build_children() if no asynchronous form is available.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_addin_build_children_async (IdeTreeAddin        *self,
+                                     IdeTreeNode         *node,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_TREE_ADDIN_GET_IFACE (self)->build_children_async (self, node, cancellable, callback, user_data);
+}
+
+/**
+ * ide_tree_addin_build_children_finish:
+ * @self: a #IdeTreeAddin
+ * @result: result given to callback in ide_tree_addin_build_children_async()
+ * @error: a location for a #GError, or %NULL
+ *
+ * Completes an asynchronous request to ide_tree_addin_build_children_async().
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_addin_build_children_finish (IdeTreeAddin  *self,
+                                      GAsyncResult  *result,
+                                      GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_TREE_ADDIN_GET_IFACE (self)->build_children_finish (self, result, error);
+}
+
+/**
+ * ide_tree_addin_build_node:
+ * @self: a #IdeTreeAddin
+ * @node: a #IdeTreeNode
+ *
+ * This function is called when preparing a node for display in the tree.
+ *
+ * Addins should adjust any state on the node that makes sense based on the
+ * addin.
+ *
+ * You may want to use ide_tree_node_holds() to determine if the node
+ * contains an item that you are interested in.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_addin_build_node (IdeTreeAddin *self,
+                           IdeTreeNode  *node)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->build_node)
+    IDE_TREE_ADDIN_GET_IFACE (self)->build_node (self, node);
+}
+
+/**
+ * ide_tree_addin_activated:
+ * @self: an #IdeTreeAddin
+ * @tree: an #IdeTree
+ * @node: an #IdeTreeNode
+ *
+ * This function is called when a node has been activated in the tree
+ * and allows for the addin to perform any necessary operations in response
+ * to that.
+ *
+ * If the addin performs an action based on the activation request, then it
+ * should return %TRUE from this function so that no further addins may
+ * respond to the action.
+ *
+ * Returns: %TRUE if the activation was handled, otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_addin_node_activated (IdeTreeAddin *self,
+                               IdeTree      *tree,
+                               IdeTreeNode  *node)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE (tree), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (node), FALSE);
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->node_activated)
+    return IDE_TREE_ADDIN_GET_IFACE (self)->node_activated (self, tree, node);
+
+  return FALSE;
+}
+
+void
+ide_tree_addin_load (IdeTreeAddin *self,
+                     IdeTree      *tree,
+                     IdeTreeModel *model)
+{
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE (tree));
+  g_return_if_fail (IDE_IS_TREE_MODEL (model));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->load)
+    IDE_TREE_ADDIN_GET_IFACE (self)->load (self, tree, model);
+}
+
+void
+ide_tree_addin_unload (IdeTreeAddin *self,
+                       IdeTree      *tree,
+                       IdeTreeModel *model)
+{
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE (tree));
+  g_return_if_fail (IDE_IS_TREE_MODEL (model));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->unload)
+    IDE_TREE_ADDIN_GET_IFACE (self)->unload (self, tree, model);
+}
+
+void
+ide_tree_addin_selection_changed (IdeTreeAddin *self,
+                                  IdeTreeNode  *selection)
+{
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (!selection || IDE_IS_TREE_NODE (selection));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->selection_changed)
+    IDE_TREE_ADDIN_GET_IFACE (self)->selection_changed (self, selection);
+}
+
+void
+ide_tree_addin_node_expanded (IdeTreeAddin *self,
+                              IdeTreeNode  *node)
+{
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->node_expanded)
+    IDE_TREE_ADDIN_GET_IFACE (self)->node_expanded (self, node);
+}
+
+void
+ide_tree_addin_node_collapsed (IdeTreeAddin *self,
+                               IdeTreeNode  *node)
+{
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->node_collapsed)
+    IDE_TREE_ADDIN_GET_IFACE (self)->node_collapsed (self, node);
+}
+
+gboolean
+ide_tree_addin_node_draggable (IdeTreeAddin *self,
+                               IdeTreeNode  *node)
+{
+  g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (node), FALSE);
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->node_draggable)
+    return IDE_TREE_ADDIN_GET_IFACE (self)->node_draggable (self, node);
+
+  return FALSE;
+}
+
+gboolean
+ide_tree_addin_node_droppable (IdeTreeAddin     *self,
+                               IdeTreeNode      *drag_node,
+                               IdeTreeNode      *drop_node,
+                               GtkSelectionData *selection)
+{
+  g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+  g_return_val_if_fail (!drag_node || IDE_IS_TREE_NODE (drag_node), FALSE);
+  g_return_val_if_fail (!drop_node || IDE_IS_TREE_NODE (drop_node), FALSE);
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->node_droppable)
+    return IDE_TREE_ADDIN_GET_IFACE (self)->node_droppable (self, drag_node, drop_node, selection);
+
+  return FALSE;
+}
+
+void
+ide_tree_addin_node_dropped_async (IdeTreeAddin        *self,
+                                   IdeTreeNode         *drag_node,
+                                   IdeTreeNode         *drop_node,
+                                   GtkSelectionData    *selection,
+                                   GdkDragAction        actions,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (!drag_node || IDE_IS_TREE_NODE (drag_node));
+  g_return_if_fail (!drop_node || IDE_IS_TREE_NODE (drop_node));
+  g_return_if_fail (selection != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_TREE_ADDIN_GET_IFACE (self)->node_dropped_async (self,
+                                                       drag_node,
+                                                       drop_node,
+                                                       selection,
+                                                       actions,
+                                                       cancellable,
+                                                       callback,
+                                                       user_data);
+}
+
+gboolean
+ide_tree_addin_node_dropped_finish (IdeTreeAddin  *self,
+                                    GAsyncResult  *result,
+                                    GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_ADDIN (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_TREE_ADDIN_GET_IFACE (self)->node_dropped_finish (self, result, error);
+}
+
+void
+ide_tree_addin_cell_data_func (IdeTreeAddin    *self,
+                               IdeTreeNode     *node,
+                               GtkCellRenderer *cell)
+{
+  g_return_if_fail (IDE_IS_TREE_ADDIN (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+  g_return_if_fail (GTK_IS_CELL_RENDERER (cell));
+
+  if (IDE_TREE_ADDIN_GET_IFACE (self)->cell_data_func)
+    IDE_TREE_ADDIN_GET_IFACE (self)->cell_data_func (self, node, cell);
+}
diff --git a/src/libide/tree/ide-tree-addin.h b/src/libide/tree/ide-tree-addin.h
new file mode 100644
index 000000000..92275620d
--- /dev/null
+++ b/src/libide/tree/ide-tree-addin.h
@@ -0,0 +1,149 @@
+/* ide-tree-addin.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+#include "ide-tree.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE_ADDIN (ide_tree_addin_get_type ())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeTreeAddin, ide_tree_addin, IDE, TREE_ADDIN, GObject)
+
+struct _IdeTreeAddinInterface
+{
+  GTypeInterface parent;
+
+  void     (*load)                  (IdeTreeAddin         *self,
+                                     IdeTree              *tree,
+                                     IdeTreeModel         *model);
+  void     (*unload)                (IdeTreeAddin         *self,
+                                     IdeTree              *tree,
+                                     IdeTreeModel         *model);
+  void     (*build_node)            (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node);
+  void     (*build_children)        (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node);
+  void     (*build_children_async)  (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node,
+                                     GCancellable         *cancellable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              user_data);
+  gboolean (*build_children_finish) (IdeTreeAddin         *self,
+                                     GAsyncResult         *result,
+                                     GError              **error);
+  void     (*cell_data_func)        (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node,
+                                     GtkCellRenderer      *cell);
+  gboolean (*node_activated)        (IdeTreeAddin         *self,
+                                     IdeTree              *tree,
+                                     IdeTreeNode          *node);
+  void     (*selection_changed)     (IdeTreeAddin         *self,
+                                     IdeTreeNode          *selection);
+  void     (*node_expanded)         (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node);
+  void     (*node_collapsed)        (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node);
+  gboolean (*node_draggable)        (IdeTreeAddin         *self,
+                                     IdeTreeNode          *node);
+  gboolean (*node_droppable)        (IdeTreeAddin         *self,
+                                     IdeTreeNode          *drag_node,
+                                     IdeTreeNode          *drop_node,
+                                     GtkSelectionData     *selection);
+  void     (*node_dropped_async)    (IdeTreeAddin         *self,
+                                     IdeTreeNode          *drag_node,
+                                     IdeTreeNode          *drop_node,
+                                     GtkSelectionData     *selection,
+                                     GdkDragAction         actions,
+                                     GCancellable         *cancellable,
+                                     GAsyncReadyCallback   callback,
+                                     gpointer              user_data);
+  gboolean (*node_dropped_finish)   (IdeTreeAddin         *self,
+                                     GAsyncResult         *result,
+                                     GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_load                  (IdeTreeAddin         *self,
+                                               IdeTree              *tree,
+                                               IdeTreeModel         *model);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_unload                (IdeTreeAddin         *self,
+                                               IdeTree              *tree,
+                                               IdeTreeModel         *model);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_build_node            (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_build_children_async  (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_build_children_finish (IdeTreeAddin         *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_activated        (IdeTreeAddin         *self,
+                                               IdeTree              *tree,
+                                               IdeTreeNode          *node);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_selection_changed     (IdeTreeAddin         *self,
+                                               IdeTreeNode          *selection);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_node_expanded         (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_node_collapsed        (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_draggable        (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_droppable        (IdeTreeAddin         *self,
+                                               IdeTreeNode          *drag_node,
+                                               IdeTreeNode          *drop_node,
+                                               GtkSelectionData     *selection);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_node_dropped_async    (IdeTreeAddin         *self,
+                                               IdeTreeNode          *drag_node,
+                                               IdeTreeNode          *drop_node,
+                                               GtkSelectionData     *selection,
+                                               GdkDragAction         actions,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_tree_addin_node_dropped_finish   (IdeTreeAddin         *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+IDE_AVAILABLE_IN_3_32
+void     ide_tree_addin_cell_data_func        (IdeTreeAddin         *self,
+                                               IdeTreeNode          *node,
+                                               GtkCellRenderer      *cell);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree-model.c b/src/libide/tree/ide-tree-model.c
new file mode 100644
index 000000000..aa797d645
--- /dev/null
+++ b/src/libide/tree/ide-tree-model.c
@@ -0,0 +1,1626 @@
+/* ide-tree-model.c
+ *
+ * Copyright 2018-2019 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 "ide-tree-model"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+#include <string.h>
+
+#include "ide-tree-addin.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+#include "ide-tree-private.h"
+#include "ide-tree.h"
+
+struct _IdeTreeModel
+{
+  IdeObject               parent_instance;
+  IdeExtensionSetAdapter *addins;
+  gchar                  *kind;
+  IdeTreeNode            *root;
+  IdeTree                *tree;
+};
+
+typedef struct
+{
+  IdeTreeNode      *drag_node;
+  IdeTreeNode      *drop_node;
+  GtkSelectionData *selection;
+  GdkDragAction     actions;
+  gint              n_active;
+} DragDataReceived;
+
+static void tree_model_iface_init       (GtkTreeModelIface      *iface);
+static void tree_drag_dest_iface_init   (GtkTreeDragDestIface   *iface);
+static void tree_drag_source_iface_init (GtkTreeDragSourceIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (IdeTreeModel, ide_tree_model, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, tree_model_iface_init)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_DRAG_DEST, tree_drag_dest_iface_init)
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_DRAG_SOURCE, tree_drag_source_iface_init))
+
+enum {
+  PROP_0,
+  PROP_KIND,
+  PROP_ROOT,
+  PROP_TREE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+drag_data_received_free (DragDataReceived *data)
+{
+  g_assert (data != NULL);
+  g_assert (!data->drag_node || IDE_IS_TREE_NODE (data->drag_node));
+  g_assert (!data->drop_node || IDE_IS_TREE_NODE (data->drop_node));
+  g_assert (data->n_active == 0);
+
+  g_clear_object (&data->drag_node);
+  g_clear_object (&data->drop_node);
+  g_clear_pointer (&data->selection, gtk_selection_data_free);
+  g_slice_free (DragDataReceived, data);
+}
+
+static IdeTreeNode *
+create_root (void)
+{
+  return g_object_new (IDE_TYPE_TREE_NODE,
+                       "children-possible", TRUE,
+                       NULL);
+}
+
+static void
+ide_tree_model_build_node_cb (IdeExtensionSetAdapter *set,
+                              PeasPluginInfo         *plugin_info,
+                              PeasExtension          *exten,
+                              gpointer                user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+  IdeTreeNode *node = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  ide_tree_addin_build_node (addin, node);
+}
+
+void
+_ide_tree_model_build_node (IdeTreeModel *self,
+                            IdeTreeNode  *node)
+{
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_build_node_cb,
+                                     node);
+}
+
+static IdeTreeNodeVisit
+ide_tree_model_addin_added_traverse_cb (IdeTreeNode *node,
+                                        gpointer     user_data)
+{
+  IdeTreeAddin *addin = user_data;
+
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  if (!ide_tree_node_is_empty (node))
+    {
+      ide_tree_addin_build_node (addin, node);
+
+      if (ide_tree_node_get_children_possible (node))
+        _ide_tree_node_set_needs_build_children (node, TRUE);
+    }
+
+  return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+static void
+ide_tree_model_addin_added_cb (IdeExtensionSetAdapter *adapter,
+                               PeasPluginInfo         *plugin_info,
+                               PeasExtension          *exten,
+                               gpointer                user_data)
+{
+  IdeTreeModel *self = user_data;
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (IDE_IS_TREE (self->tree));
+
+  ide_tree_addin_load (addin, self->tree, self);
+
+  ide_tree_node_traverse (self->root,
+                          G_PRE_ORDER,
+                          G_TRAVERSE_ALL,
+                          -1,
+                          ide_tree_model_addin_added_traverse_cb,
+                          addin);
+}
+
+static void
+ide_tree_model_addin_removed_cb (IdeExtensionSetAdapter *adapter,
+                                 PeasPluginInfo         *plugin_info,
+                                 PeasExtension          *exten,
+                                 gpointer                user_data)
+{
+  IdeTreeModel *self = user_data;
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (adapter));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE_MODEL (self));
+
+  ide_tree_addin_unload (addin, self->tree, self);
+}
+
+static void
+ide_tree_model_parent_set (IdeObject *object,
+                           IdeObject *parent)
+{
+  IdeTreeModel *self = (IdeTreeModel *)object;
+  g_autoptr(IdeContext) context = NULL;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (self->addins != NULL || parent == NULL ||
+      !(context = ide_object_ref_context (IDE_OBJECT (self))))
+    return;
+
+  g_assert (IDE_IS_TREE (self->tree));
+
+  self->addins = ide_extension_set_adapter_new (IDE_OBJECT (self),
+                                                peas_engine_get_default (),
+                                                IDE_TYPE_TREE_ADDIN,
+                                                "Tree-Kind",
+                                                self->kind);
+
+  g_signal_connect_object (self->addins,
+                           "extension-added",
+                           G_CALLBACK (ide_tree_model_addin_added_cb),
+                           self,
+                           0);
+
+  g_signal_connect_object (self->addins,
+                           "extension-removed",
+                           G_CALLBACK (ide_tree_model_addin_removed_cb),
+                           self,
+                           0);
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_addin_added_cb,
+                                     self);
+}
+
+static void
+ide_tree_model_dispose (GObject *object)
+{
+  IdeTreeModel *self = (IdeTreeModel *)object;
+
+  /* Clear the model back-pointer for root so that it cannot emit anu
+   * further signals on our tree model.
+   */
+  if (self->root != NULL)
+    _ide_tree_node_set_model (self->root, NULL);
+
+  g_clear_object (&self->tree);
+  ide_clear_and_destroy_object (&self->addins);
+  g_clear_object (&self->root);
+  g_clear_pointer (&self->kind, g_free);
+
+  G_OBJECT_CLASS (ide_tree_model_parent_class)->dispose (object);
+}
+
+static void
+ide_tree_model_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  IdeTreeModel *self = IDE_TREE_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_KIND:
+      g_value_set_string (value, ide_tree_model_get_kind (self));
+      break;
+
+    case PROP_ROOT:
+      g_value_set_object (value, ide_tree_model_get_root (self));
+      break;
+
+    case PROP_TREE:
+      g_value_set_object (value, self->tree);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_tree_model_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  IdeTreeModel *self = IDE_TREE_MODEL (object);
+
+  switch (prop_id)
+    {
+    case PROP_KIND:
+      ide_tree_model_set_kind (self, g_value_get_string (value));
+      break;
+
+    case PROP_ROOT:
+      ide_tree_model_set_root (self, g_value_get_object (value));
+      break;
+
+    case PROP_TREE:
+      self->tree = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_tree_model_class_init (IdeTreeModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_tree_model_dispose;
+  object_class->get_property = ide_tree_model_get_property;
+  object_class->set_property = ide_tree_model_set_property;
+
+  i_object_class->parent_set = ide_tree_model_parent_set;
+
+  properties [PROP_TREE] =
+    g_param_spec_object ("tree",
+                         "Tree",
+                         "The tree the model belongs to",
+                         IDE_TYPE_TREE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeModel:root:
+   *
+   * The "root" property contains the root #IdeTreeNode that is used to build
+   * the tree. It should contain an object for the #IdeTreeNode:item property
+   * so that #IdeTreeAddin's may use it to build the node and any children.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ROOT] =
+    g_param_spec_object ("root",
+                         "Root",
+                         "The root IdeTreeNode",
+                         IDE_TYPE_TREE_NODE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeModel:kind:
+   *
+   * The "kind" property is used to determine what #IdeTreeAddin plugins to
+   * load. Only plugins which match the "kind" will be loaded to extend the
+   * tree contents.
+   *
+   * For example, to extend the project-tree, plugins should set
+   * "X-Tree-Kind=project" in their .plugin manifest.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_KIND] =
+    g_param_spec_string ("kind",
+                         "Kind",
+                         "The kind of tree model that is being generated",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_tree_model_init (IdeTreeModel *self)
+{
+  self->root = create_root ();
+}
+
+IdeTreeModel *
+_ide_tree_model_new (IdeTree *tree)
+{
+  return g_object_new (IDE_TYPE_TREE_MODEL,
+                       "tree", tree,
+                       NULL);
+}
+
+void
+_ide_tree_model_release_addins (IdeTreeModel *self)
+{
+  g_assert (IDE_IS_TREE_MODEL (self));
+
+  ide_clear_and_destroy_object (&self->addins);
+}
+
+static GtkTreeModelFlags
+ide_tree_model_get_flags (GtkTreeModel *model)
+{
+  return 0;
+}
+
+static gint
+ide_tree_model_get_n_columns (GtkTreeModel *model)
+{
+  return 1;
+}
+
+static GType
+ide_tree_model_get_column_type (GtkTreeModel *model,
+                                gint          index_)
+{
+  return IDE_TYPE_TREE_NODE;
+}
+
+static GtkTreePath *
+ide_tree_model_get_path (GtkTreeModel *tree_model,
+                         GtkTreeIter  *iter)
+{
+  g_autoptr(GArray) indexes = NULL;
+  IdeTreeModel *self = (IdeTreeModel *)tree_model;
+  IdeTreeNode *node;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (iter != NULL);
+  g_assert (IDE_IS_TREE_NODE (iter->user_data));
+
+  node = iter->user_data;
+
+  if (ide_tree_node_is_root (node))
+    return NULL;
+
+  indexes = g_array_new (FALSE, FALSE, sizeof (gint));
+
+  do
+    {
+      gint position;
+
+      position = ide_tree_node_get_index (node);
+      g_array_prepend_val (indexes, position);
+    }
+  while ((node = ide_tree_node_get_parent (node)) &&
+         !ide_tree_node_is_root (node));
+
+  return gtk_tree_path_new_from_indicesv (&g_array_index (indexes, gint, 0), indexes->len);
+}
+
+static gboolean
+ide_tree_model_get_iter (GtkTreeModel *model,
+                         GtkTreeIter  *iter,
+                         GtkTreePath  *path)
+{
+  IdeTreeModel *self = (IdeTreeModel *)model;
+  IdeTreeNode *node;
+  gint *indices;
+  gint depth = 0;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (iter != NULL);
+  g_assert (path != NULL);
+
+  memset (iter, 0, sizeof *iter);
+
+  if (self->root == NULL)
+    return FALSE;
+
+  indices = gtk_tree_path_get_indices_with_depth (path, &depth);
+
+  node = self->root;
+
+  for (gint i = 0; i < depth; i++)
+    {
+      if (!(node = ide_tree_node_get_nth_child (node, indices[i])))
+        return FALSE;
+    }
+
+  if (ide_tree_node_is_root (node))
+    return FALSE;
+
+  iter->user_data = node;
+  return TRUE;
+}
+
+static void
+ide_tree_model_get_value (GtkTreeModel *model,
+                          GtkTreeIter  *iter,
+                          gint          column,
+                          GValue       *value)
+{
+  g_value_init (value, IDE_TYPE_TREE_NODE);
+  g_value_set_object (value, iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_next (GtkTreeModel *model,
+                          GtkTreeIter  *iter)
+{
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (iter->user_data)
+    iter->user_data = ide_tree_node_get_next (iter->user_data);
+
+  return IDE_IS_TREE_NODE (iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_previous (GtkTreeModel *model,
+                              GtkTreeIter  *iter)
+{
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (iter->user_data)
+    iter->user_data = ide_tree_node_get_previous (iter->user_data);
+
+  return IDE_IS_TREE_NODE (iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_nth_child (GtkTreeModel *model,
+                               GtkTreeIter  *iter,
+                               GtkTreeIter  *parent,
+                               gint          n)
+{
+  IdeTreeModel *self = (IdeTreeModel  *)model;
+  IdeTreeNode *pnode;
+
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+
+  if (self->root == NULL)
+    return FALSE;
+
+  g_assert (parent == NULL || IDE_IS_TREE_NODE (parent->user_data));
+
+  n = CLAMP (n, 0, G_MAXINT);
+
+  memset (iter, 0, sizeof *iter);
+
+  if (parent == NULL)
+    pnode = self->root;
+  else
+    pnode = parent->user_data;
+  g_assert (IDE_IS_TREE_NODE (pnode));
+
+  iter->user_data = ide_tree_node_get_nth_child (pnode, n);
+  g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+  return IDE_IS_TREE_NODE (iter->user_data);
+}
+
+static gboolean
+ide_tree_model_iter_children (GtkTreeModel *model,
+                              GtkTreeIter  *iter,
+                              GtkTreeIter  *parent)
+{
+  return ide_tree_model_iter_nth_child (model, iter, parent, 0);
+}
+
+static gboolean
+ide_tree_model_iter_has_child (GtkTreeModel *model,
+                               GtkTreeIter  *iter)
+{
+  gboolean ret;
+
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (IDE_IS_TREE_NODE (iter->user_data));
+
+  ret = ide_tree_node_has_child (iter->user_data);
+
+  IDE_TRACE_MSG ("%s has child -> %s",
+                 ide_tree_node_get_display_name (iter->user_data),
+                 ret ? "yes" : "no");
+
+  return ret;
+}
+
+static gint
+ide_tree_model_iter_n_children (GtkTreeModel *model,
+                                GtkTreeIter  *iter)
+{
+  IdeTreeModel *self = (IdeTreeModel *)model;
+  gint ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (self != NULL);
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (IDE_IS_TREE_NODE (self->root));
+  g_assert (iter == NULL || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (iter == NULL)
+    ret = ide_tree_node_get_n_children (self->root);
+  else if (iter->user_data)
+    ret = ide_tree_node_get_n_children (iter->user_data);
+  else
+    ret = 0;
+
+  IDE_RETURN (ret);
+}
+
+static gboolean
+ide_tree_model_iter_parent (GtkTreeModel *model,
+                            GtkTreeIter  *iter,
+                            GtkTreeIter  *child)
+{
+  IdeTreeModel *self = (IdeTreeModel *)model;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (iter != NULL);
+  g_assert (child != NULL);
+  g_assert (IDE_IS_TREE_NODE (child->user_data));
+
+  memset (iter, 0, sizeof *iter);
+
+  iter->user_data = ide_tree_node_get_parent (child->user_data);
+
+  return !ide_tree_node_is_root (iter->user_data);
+}
+
+static void
+ide_tree_model_row_inserted (GtkTreeModel *model,
+                             GtkTreePath  *path,
+                             GtkTreeIter  *iter)
+{
+  IdeTreeModel *self = (IdeTreeModel *)model;
+  IdeTreeNode *node;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (path != NULL);
+  g_assert (iter != NULL);
+
+  node = iter->user_data;
+
+  g_assert (IDE_IS_TREE_NODE (node));
+
+#if 0
+  g_print ("Building %s (child of %s)\n",
+           ide_tree_node_get_display_name (node),
+           ide_tree_node_get_display_name (ide_tree_node_get_parent (node)));
+#endif
+
+  /*
+   * If this node holds an IdeObject which is not rooted on our object
+   * tree, add it to the object tree beneath us so that it can get destroy
+   * propagation and access to the IdeContext.
+   */
+  if (ide_tree_node_holds (node, IDE_TYPE_OBJECT))
+    {
+      IdeObject *object = ide_tree_node_get_item (node);
+
+      if (!ide_object_get_parent (object))
+        ide_object_append (IDE_OBJECT (self), object);
+    }
+
+  _ide_tree_model_build_node (self, node);
+}
+
+static void
+ide_tree_model_ref_node (GtkTreeModel *model,
+                         GtkTreeIter  *iter)
+{
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (iter->user_data)
+    g_object_ref (iter->user_data);
+}
+
+static void
+ide_tree_model_unref_node (GtkTreeModel *model,
+                           GtkTreeIter  *iter)
+{
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (!iter->user_data || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (iter->user_data)
+    g_object_unref (iter->user_data);
+}
+
+static void
+tree_model_iface_init (GtkTreeModelIface *iface)
+{
+  iface->get_flags = ide_tree_model_get_flags;
+  iface->get_n_columns = ide_tree_model_get_n_columns;
+  iface->get_column_type = ide_tree_model_get_column_type;
+  iface->get_iter = ide_tree_model_get_iter;
+  iface->get_path = ide_tree_model_get_path;
+  iface->get_value = ide_tree_model_get_value;
+  iface->iter_next = ide_tree_model_iter_next;
+  iface->iter_previous = ide_tree_model_iter_previous;
+  iface->iter_children = ide_tree_model_iter_children;
+  iface->iter_has_child = ide_tree_model_iter_has_child;
+  iface->iter_n_children = ide_tree_model_iter_n_children;
+  iface->iter_nth_child = ide_tree_model_iter_nth_child;
+  iface->iter_parent = ide_tree_model_iter_parent;
+  iface->row_inserted = ide_tree_model_row_inserted;
+  iface->ref_node = ide_tree_model_ref_node;
+  iface->unref_node = ide_tree_model_unref_node;
+}
+
+/**
+ * ide_tree_model_get_path_for_node:
+ * @self: an #IdeTreeModel
+ * @node: an #IdeTreeNode
+ *
+ * Gets the #GtkTreePath pointing at @node.
+ *
+ * Returns: (transfer full) (nullable): a new #GtkTreePath
+ *
+ * Since: 3.32
+ */
+GtkTreePath *
+ide_tree_model_get_path_for_node (IdeTreeModel *self,
+                                  IdeTreeNode  *node)
+{
+  GtkTreeIter iter;
+
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (node), NULL);
+
+  if (ide_tree_model_get_iter_for_node (self, &iter, node))
+    return gtk_tree_model_get_path (GTK_TREE_MODEL (self), &iter);
+
+  return NULL;
+}
+
+/**
+ * ide_tree_model_get_iter_for_node:
+ * @self: an #IdeTreeModel
+ * @iter: (out): a #GtkTreeIter
+ * @node: an #IdeTreeNode
+ *
+ * Gets a #GtkTreeIter that points at @node.
+ *
+ * Returns: %TRUE if @iter was set; otherwise %FALSE
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_model_get_iter_for_node (IdeTreeModel *self,
+                                  GtkTreeIter  *iter,
+                                  IdeTreeNode  *node)
+{
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), FALSE);
+
+  if (_ide_tree_model_contains_node (self, node))
+    {
+      memset (iter, 0, sizeof *iter);
+      iter->user_data = node;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_tree_model_get_root:
+ * @self: a #IdeTreeModel
+ *
+ * Gets the root #IdeTreeNode. This node is never visualized in the tree, but
+ * is used to build the immediate children which are displayed in the tree.
+ *
+ * Returns: (transfer none) (not nullable): an #IdeTreeNode
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_model_get_root (IdeTreeModel *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+
+  return self->root;
+}
+
+static IdeTreeNodeVisit
+ide_tree_model_remove_all_cb (IdeTreeNode *node,
+                              gpointer     user_data)
+{
+  IdeTreeModel *self = user_data;
+
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (IDE_IS_TREE_MODEL (self));
+
+  if (node != self->root)
+    {
+      GtkTreePath *tree_path;
+
+      tree_path = ide_tree_model_get_path_for_node (self, node);
+      gtk_tree_model_row_deleted (GTK_TREE_MODEL (self), tree_path);
+      gtk_tree_path_free (tree_path);
+    }
+
+  return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+static void
+ide_tree_model_remove_all (IdeTreeModel *self)
+{
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+
+  ide_tree_node_traverse (self->root,
+                          G_POST_ORDER,
+                          G_TRAVERSE_ALL,
+                          -1,
+                          ide_tree_model_remove_all_cb,
+                          self);
+}
+
+void
+ide_tree_model_set_root (IdeTreeModel *self,
+                         IdeTreeNode  *root)
+{
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (!root || IDE_IS_TREE_NODE (root));
+
+  if (root != self->root)
+    {
+      ide_tree_model_remove_all (self);
+      g_clear_object (&self->root);
+
+      if (root != NULL)
+        self->root = g_object_ref (root);
+      else
+        self->root = create_root ();
+
+      _ide_tree_node_set_model (self->root, self);
+
+      /* Root always requires building children */
+      if (!ide_tree_node_get_children_possible (self->root))
+        ide_tree_node_set_children_possible (self->root, TRUE);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ROOT]);
+    }
+}
+
+/**
+ * ide_tree_model_get_kind:
+ * @self: a #IdeTreeModel
+ *
+ * Gets the kind of model that is being generated. See #IdeTreeModel:kind
+ * for more information.
+ *
+ * Returns: (nullable): a string containing the kind, or %NULL
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_tree_model_get_kind (IdeTreeModel *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+
+  return self->kind;
+}
+
+/**
+ * ide_tree_model_set_kind:
+ * @self: a #IdeTreeModel
+ * @kind: a string describing the kind of model
+ *
+ * Sets the kind of model that is being created. This determines what plugins
+ * are used to generate the tree contents.
+ *
+ * This should be set before adding the #IdeTreeModel to an #IdeObject to
+ * ensure the tree builds the proper contents.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_model_set_kind (IdeTreeModel *self,
+                         const gchar  *kind)
+{
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+
+  if (!ide_str_equal0 (kind, self->kind))
+    {
+      g_free (self->kind);
+      self->kind = g_strdup (kind);
+
+      if (self->addins != NULL)
+        ide_extension_set_adapter_set_value (self->addins, kind);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_KIND]);
+    }
+}
+
+typedef struct
+{
+  IdeTreeNode *node;
+  IdeTree     *tree;
+  gboolean     handled;
+} RowActivated;
+
+static void
+ide_tree_model_row_activated_cb (IdeExtensionSetAdapter *set,
+                                 PeasPluginInfo         *plugin_info,
+                                 PeasExtension          *exten,
+                                 gpointer                user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+  RowActivated *state = user_data;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (state != NULL);
+
+  if (state->handled)
+    return;
+
+  state->handled = ide_tree_addin_node_activated (addin, state->tree, state->node);
+}
+
+gboolean
+_ide_tree_model_row_activated (IdeTreeModel *self,
+                               IdeTree      *tree,
+                               GtkTreePath  *path)
+{
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (path != NULL);
+
+  if (gtk_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+    {
+      RowActivated state = {
+        .node = iter.user_data,
+        .tree = tree,
+        .handled = FALSE,
+      };
+
+      ide_extension_set_adapter_foreach (self->addins,
+                                         ide_tree_model_row_activated_cb,
+                                         &state);
+
+      return state.handled;
+    }
+
+  return FALSE;
+}
+
+/**
+ * ide_tree_model_get_node:
+ * @self: a #IdeTreeModel
+ * @iter: a #GtkTreeIter
+ *
+ * Gets the #IdeTreeNode found at @iter.
+ *
+ * Returns: (transfer none) (nullable): an #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_model_get_node (IdeTreeModel *self,
+                         GtkTreeIter  *iter)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+  g_return_val_if_fail (iter != NULL, NULL);
+
+  if (IDE_IS_TREE_NODE (iter->user_data))
+    return iter->user_data;
+
+  return NULL;
+}
+
+gboolean
+_ide_tree_model_contains_node (IdeTreeModel *self,
+                               IdeTreeNode  *node)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), FALSE);
+  g_return_val_if_fail (!node || IDE_IS_TREE_NODE (node), FALSE);
+
+  if (node == NULL)
+    return FALSE;
+
+  return self->root == ide_tree_node_get_root (node);
+}
+
+static void
+inc_active (IdeTask *task)
+{
+  gint n_active = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "N_ACTIVE"));
+  n_active++;
+  g_object_set_data (G_OBJECT (task), "N_ACTIVE", GINT_TO_POINTER (n_active));
+}
+
+static gboolean
+dec_active_and_test (IdeTask *task)
+{
+  gint n_active = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (task), "N_ACTIVE"));
+  n_active--;
+  g_object_set_data (G_OBJECT (task), "N_ACTIVE", GINT_TO_POINTER (n_active));
+  return n_active == 0;
+}
+
+static void
+ide_tree_model_addin_build_children_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  ide_tree_addin_build_children_finish (addin, result, &error);
+
+  if (dec_active_and_test (task))
+    {
+#if 0
+      {
+        IdeTreeNode *node = ide_task_get_task_data (task);
+        _ide_tree_node_dump (ide_tree_node_get_root (node));
+      }
+#endif
+
+      ide_task_return_boolean (task, TRUE);
+    }
+}
+
+static void
+ide_tree_model_expand_foreach_cb (IdeExtensionSetAdapter *set,
+                                  PeasPluginInfo         *plugin_info,
+                                  PeasExtension          *exten,
+                                  gpointer                user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+  IdeTreeNode *node;
+  IdeTask *task = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TASK (task));
+
+  node = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  inc_active (task);
+
+  ide_tree_addin_build_children_async (addin,
+                                       node,
+                                       ide_task_get_cancellable (task),
+                                       ide_tree_model_addin_build_children_cb,
+                                       g_object_ref (task));
+
+  _ide_tree_node_set_needs_build_children (node, FALSE);
+}
+
+static void
+ide_tree_model_expand_completed (IdeTreeNode *node,
+                                 GParamSpec  *pspec,
+                                 IdeTask     *task)
+{
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (pspec != NULL);
+  g_assert (IDE_IS_TASK (task));
+
+  _ide_tree_node_set_loading (node, FALSE);
+}
+
+void
+ide_tree_model_expand_async (IdeTreeModel        *self,
+                             IdeTreeNode         *node,
+                             GCancellable        *cancellable,
+                             GAsyncReadyCallback  callback,
+                             gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_tree_model_expand_async);
+  ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (ide_tree_model_expand_completed),
+                           node,
+                           G_CONNECT_SWAPPED);
+
+  /* If no building is necessary, then just skip any work here */
+  if (!_ide_tree_node_get_needs_build_children (node) ||
+      ide_extension_set_adapter_get_n_extensions (self->addins) == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  _ide_tree_node_set_loading (node, TRUE);
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_expand_foreach_cb,
+                                     task);
+}
+
+gboolean
+ide_tree_model_expand_finish (IdeTreeModel  *self,
+                              GAsyncResult  *result,
+                              GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static IdeTreeNodeVisit
+ide_tree_model_invalidate_traverse_cb (IdeTreeNode *node,
+                                       gpointer     user_data)
+{
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  if (!ide_tree_node_is_root (node))
+    ide_tree_node_remove (ide_tree_node_get_parent (node), node);
+
+  return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+/**
+ * ide_tree_model_invalidate:
+ * @self: a #IdeTreeModel
+ * @node: (nullable): an #IdeTreeNode or %NULL
+ *
+ * Invalidates @model starting from @node so that those items
+ * are rebuilt using the configured tree addins.
+ *
+ * If @node is %NULL, the root of the tree is invalidated.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_model_invalidate (IdeTreeModel *self,
+                           IdeTreeNode  *node)
+{
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (!node || IDE_IS_TREE_NODE (node));
+
+  if (node == NULL)
+    node = self->root;
+
+  ide_tree_node_traverse (node,
+                          G_POST_ORDER,
+                          G_TRAVERSE_ALL,
+                          -1,
+                          ide_tree_model_invalidate_traverse_cb,
+                          NULL);
+
+  _ide_tree_node_set_needs_build_children (node, TRUE);
+  ide_tree_model_expand_async (self, node, NULL, NULL, NULL);
+}
+
+static void
+ide_tree_model_propagate_selection_changed_cb (IdeExtensionSetAdapter *set,
+                                               PeasPluginInfo         *plugin_info,
+                                               PeasExtension          *exten,
+                                               gpointer                user_data)
+{
+  IdeTreeNode *node = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (!node || IDE_IS_TREE_NODE (node));
+
+  ide_tree_addin_selection_changed (IDE_TREE_ADDIN (exten), node);
+}
+
+void
+_ide_tree_model_selection_changed (IdeTreeModel *self,
+                                   GtkTreeIter  *iter)
+{
+  IdeTreeNode *node = NULL;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (!iter || IDE_IS_TREE_NODE (iter->user_data));
+
+  if (self->addins == NULL)
+    return;
+
+  if (iter != NULL)
+    node = ide_tree_model_get_node (self, iter);
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_propagate_selection_changed_cb,
+                                     node);
+}
+
+static void
+ide_tree_model_propagate_node_expanded_cb (IdeExtensionSetAdapter *set,
+                                           PeasPluginInfo         *plugin_info,
+                                           PeasExtension          *exten,
+                                           gpointer                user_data)
+{
+  IdeTreeNode *node = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (!node || IDE_IS_TREE_NODE (node));
+
+  ide_tree_addin_node_expanded (IDE_TREE_ADDIN (exten), node);
+}
+
+void
+_ide_tree_model_row_expanded (IdeTreeModel *self,
+                              IdeTree      *tree,
+                              GtkTreePath  *path)
+{
+  GtkTreeIter iter;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (IDE_IS_TREE (tree));
+  g_return_if_fail (path != NULL);
+
+  if (self->addins == NULL)
+    return;
+
+  if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+    {
+      IdeTreeNode *node = ide_tree_model_get_node (self, &iter);
+
+      ide_extension_set_adapter_foreach (self->addins,
+                                         ide_tree_model_propagate_node_expanded_cb,
+                                         node);
+    }
+}
+
+static void
+ide_tree_model_propagate_node_collapsed_cb (IdeExtensionSetAdapter *set,
+                                            PeasPluginInfo         *plugin_info,
+                                            PeasExtension          *exten,
+                                            gpointer                user_data)
+{
+  IdeTreeNode *node = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (!node || IDE_IS_TREE_NODE (node));
+
+  ide_tree_addin_node_collapsed (IDE_TREE_ADDIN (exten), node);
+}
+
+void
+_ide_tree_model_row_collapsed (IdeTreeModel *self,
+                               IdeTree      *tree,
+                               GtkTreePath  *path)
+{
+  GtkTreeIter iter;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (IDE_IS_TREE (tree));
+  g_return_if_fail (path != NULL);
+
+  if (self->addins == NULL)
+    return;
+
+  if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+    {
+      IdeTreeNode *node = ide_tree_model_get_node (self, &iter);
+
+      ide_extension_set_adapter_foreach (self->addins,
+                                         ide_tree_model_propagate_node_collapsed_cb,
+                                         node);
+    }
+}
+
+/**
+ * ide_tree_model_get_tree:
+ * @self: a #IdeTreeModel
+ *
+ * Returns: (transfer none): an #IdeTree
+ *
+ * Since: 3.32
+ */
+IdeTree *
+ide_tree_model_get_tree (IdeTreeModel *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_MODEL (self), NULL);
+
+  return self->tree;
+}
+
+static void
+ide_tree_model_cell_data_func_cb (IdeExtensionSetAdapter *set,
+                                  PeasPluginInfo         *plugin_info,
+                                  PeasExtension          *exten,
+                                  gpointer                user_data)
+{
+  struct {
+    IdeTreeNode     *node;
+    GtkCellRenderer *cell;
+  } *state = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (state != NULL);
+  g_assert (IDE_IS_TREE_NODE (state->node));
+  g_assert (GTK_IS_CELL_RENDERER (state->cell));
+
+  ide_tree_addin_cell_data_func (IDE_TREE_ADDIN (exten), state->node, state->cell);
+}
+
+void
+_ide_tree_model_cell_data_func (IdeTreeModel    *self,
+                                GtkTreeIter     *iter,
+                                GtkCellRenderer *cell)
+{
+  struct {
+    IdeTreeNode     *node;
+    GtkCellRenderer *cell;
+  } state;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_MODEL (self));
+  g_return_if_fail (iter != NULL);
+  g_return_if_fail (GTK_IS_CELL_RENDERER (cell));
+
+  state.node = iter->user_data;
+  state.cell = cell;
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_cell_data_func_cb,
+                                     &state);
+}
+
+static void
+ide_tree_model_row_draggable_cb (IdeExtensionSetAdapter *set,
+                                 PeasPluginInfo         *plugin_info,
+                                 PeasExtension          *exten,
+                                 gpointer                user_data)
+{
+  struct {
+    IdeTreeNode *node;
+    gboolean     draggable;
+  } *state = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (state != NULL);
+  g_assert (IDE_IS_TREE_NODE (state->node));
+
+  state->draggable |= ide_tree_addin_node_draggable (IDE_TREE_ADDIN (exten), state->node);
+}
+
+static gboolean
+ide_tree_model_row_draggable (GtkTreeDragSource *source,
+                              GtkTreePath       *path)
+{
+  IdeTreeModel *self = (IdeTreeModel *)source;
+  GtkTreeIter iter;
+  struct {
+    IdeTreeNode *node;
+    gboolean     draggable;
+  } state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (self));
+
+  if (!ide_tree_model_get_iter (GTK_TREE_MODEL (source), &iter, path))
+    return FALSE;
+
+  if (!IDE_IS_TREE_NODE (iter.user_data))
+    return FALSE;
+
+  state.node = iter.user_data;
+  state.draggable = FALSE;
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_row_draggable_cb,
+                                     &state);
+
+  return state.draggable;
+}
+
+static gboolean
+ide_tree_model_drag_data_get (GtkTreeDragSource *source,
+                              GtkTreePath       *path,
+                              GtkSelectionData  *selection)
+{
+  IdeTreeModel *self = (IdeTreeModel *)source;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (path != NULL);
+  g_assert (selection != NULL);
+
+  return gtk_tree_set_row_drag_data (selection, GTK_TREE_MODEL (self), path);
+}
+
+static gboolean
+ide_tree_model_drag_data_delete (GtkTreeDragSource *source,
+                                 GtkTreePath       *path)
+{
+  IdeTreeModel *self = (IdeTreeModel *)source;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (path != NULL);
+
+  return FALSE;
+}
+
+static void
+tree_drag_source_iface_init (GtkTreeDragSourceIface *iface)
+{
+  iface->row_draggable = ide_tree_model_row_draggable;
+  iface->drag_data_get = ide_tree_model_drag_data_get;
+  iface->drag_data_delete = ide_tree_model_drag_data_delete;
+}
+
+static void
+ide_tree_model_drag_data_received_addin_cb (GObject      *object,
+                                            GAsyncResult *result,
+                                            gpointer      user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  DragDataReceived *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_tree_addin_node_dropped_finish (addin, result, &error))
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+        g_warning ("%s: %s", G_OBJECT_TYPE_NAME (addin), error->message);
+    }
+
+  state = ide_task_get_task_data (task);
+  g_assert (state != NULL);
+  g_assert (!state->drag_node || IDE_IS_TREE_NODE (state->drag_node));
+  g_assert (!state->drop_node || IDE_IS_TREE_NODE (state->drop_node));
+  g_assert (state->n_active > 0);
+
+  state->n_active--;
+
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_tree_model_drag_data_received_cb (IdeExtensionSetAdapter *set,
+                                      PeasPluginInfo         *plugin_info,
+                                      PeasExtension          *exten,
+                                      gpointer                user_data)
+{
+  IdeTreeAddin *addin = (IdeTreeAddin *)exten;
+  IdeTask *task = user_data;
+  DragDataReceived *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+  g_assert (state != NULL);
+  g_assert (!state->drag_node || IDE_IS_TREE_NODE (state->drag_node));
+  g_assert (!state->drop_node || IDE_IS_TREE_NODE (state->drop_node));
+
+  state->n_active++;
+
+  ide_tree_addin_node_dropped_async (addin,
+                                     state->drag_node,
+                                     state->drop_node,
+                                     state->selection,
+                                     state->actions,
+                                     NULL,
+                                     ide_tree_model_drag_data_received_addin_cb,
+                                     g_object_ref (task));
+}
+
+static gboolean
+ide_tree_model_drag_data_received (GtkTreeDragDest  *dest,
+                                   GtkTreePath      *path,
+                                   GtkSelectionData *selection)
+{
+  IdeTreeModel *self = (IdeTreeModel *)dest;
+  g_autoptr(GtkTreePath) source_path = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  GtkTreeModel *source_model = NULL;
+  DragDataReceived *state;
+  IdeTreeNode *drag_node = NULL;
+  IdeTreeNode *drop_node = NULL;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (path != NULL);
+  g_assert (selection != NULL);
+
+  if (gtk_tree_get_row_drag_data (selection, &source_model, &source_path))
+    {
+      if (IDE_IS_TREE_MODEL (source_model))
+        {
+          if (ide_tree_model_get_iter (source_model, &iter, source_path))
+            drag_node = IDE_TREE_NODE (iter.user_data);
+        }
+    }
+
+  drop_node = _ide_tree_get_drop_node (self->tree);
+
+  state = g_slice_new0 (DragDataReceived);
+  g_set_object (&state->drag_node, drag_node);
+  g_set_object (&state->drop_node, drop_node);
+  state->selection = gtk_selection_data_copy (selection);
+  state->actions = _ide_tree_get_drop_actions (self->tree);
+
+
+  task = ide_task_new (self, NULL, NULL, NULL);
+  ide_task_set_source_tag (task, ide_tree_model_drag_data_received);
+  ide_task_set_task_data (task, state, drag_data_received_free);
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_drag_data_received_cb,
+                                     task);
+
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+
+  return TRUE;
+}
+
+static void
+ide_tree_model_row_drop_possible_cb (IdeExtensionSetAdapter *set,
+                                     PeasPluginInfo         *plugin_info,
+                                     PeasExtension          *exten,
+                                     gpointer                user_data)
+{
+  struct {
+    IdeTreeNode      *drag_node;
+    IdeTreeNode      *drop_node;
+    GtkSelectionData *selection;
+    gboolean          drop_possible;
+  } *state = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_TREE_ADDIN (exten));
+  g_assert (state != NULL);
+  g_assert (state->selection != NULL);
+
+  state->drop_possible |= ide_tree_addin_node_droppable (IDE_TREE_ADDIN (exten),
+                                                         state->drag_node,
+                                                         state->drop_node,
+                                                         state->selection);
+}
+
+static gboolean
+ide_tree_model_row_drop_possible (GtkTreeDragDest  *dest,
+                                  GtkTreePath      *path,
+                                  GtkSelectionData *selection)
+{
+  IdeTreeModel *self = (IdeTreeModel *)dest;
+  g_autoptr(GtkTreePath) source_path = NULL;
+  GtkTreeModel *source_model = NULL;
+  IdeTreeNode *drag_node = NULL;
+  IdeTreeNode *drop_node = NULL;
+  GtkTreeIter iter = {0};
+  struct {
+    IdeTreeNode      *drag_node;
+    IdeTreeNode      *drop_node;
+    GtkSelectionData *selection;
+    gboolean          drop_possible;
+  } state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (self));
+  g_assert (path != NULL);
+  g_assert (selection != NULL);
+
+  if (gtk_tree_get_row_drag_data (selection, &source_model, &source_path))
+    {
+      if (IDE_IS_TREE_MODEL (source_model))
+        {
+          if (ide_tree_model_get_iter (source_model, &iter, source_path))
+            drag_node = IDE_TREE_NODE (iter.user_data);
+        }
+    }
+
+  if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, path))
+    {
+      drop_node = IDE_TREE_NODE (iter.user_data);
+    }
+  else
+    {
+      g_autoptr(GtkTreePath) copy = gtk_tree_path_copy (path);
+
+      gtk_tree_path_up (copy);
+
+      if (ide_tree_model_get_iter (GTK_TREE_MODEL (self), &iter, copy))
+        drop_node = IDE_TREE_NODE (iter.user_data);
+    }
+
+  state.drag_node = drag_node;
+  state.drop_node = drop_node;
+  state.selection = selection;
+  state.drop_possible = FALSE;
+
+  ide_extension_set_adapter_foreach (self->addins,
+                                     ide_tree_model_row_drop_possible_cb,
+                                     &state);
+
+  return state.drop_possible;
+}
+
+static void
+tree_drag_dest_iface_init (GtkTreeDragDestIface *iface)
+{
+  iface->drag_data_received = ide_tree_model_drag_data_received;
+  iface->row_drop_possible = ide_tree_model_row_drop_possible;
+}
diff --git a/src/libide/tree/ide-tree-model.h b/src/libide/tree/ide-tree-model.h
new file mode 100644
index 000000000..5590b8943
--- /dev/null
+++ b/src/libide/tree/ide-tree-model.h
@@ -0,0 +1,72 @@
+/* ide-tree-model.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-tree.h"
+#include "ide-tree-node.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE_MODEL (ide_tree_model_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTreeModel, ide_tree_model, IDE, TREE_MODEL, IdeObject)
+
+IDE_AVAILABLE_IN_3_32
+IdeTree      *ide_tree_model_get_tree          (IdeTreeModel *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode  *ide_tree_model_get_root          (IdeTreeModel *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_tree_model_set_root          (IdeTreeModel *self,
+                                                IdeTreeNode  *root);
+IDE_AVAILABLE_IN_3_32
+const gchar  *ide_tree_model_get_kind          (IdeTreeModel *self);
+IDE_AVAILABLE_IN_3_32
+void          ide_tree_model_set_kind          (IdeTreeModel *self,
+                                                const gchar  *kind);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode  *ide_tree_model_get_node          (IdeTreeModel *self,
+                                                GtkTreeIter  *iter);
+IDE_AVAILABLE_IN_3_32
+GtkTreePath  *ide_tree_model_get_path_for_node (IdeTreeModel *self,
+                                                IdeTreeNode  *node);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_tree_model_get_iter_for_node (IdeTreeModel *self,
+                                                GtkTreeIter  *iter,
+                                                IdeTreeNode  *node);
+IDE_AVAILABLE_IN_3_32
+void          ide_tree_model_invalidate        (IdeTreeModel *self,
+                                                IdeTreeNode  *node);
+IDE_AVAILABLE_IN_3_32
+void          ide_tree_model_expand_async      (IdeTreeModel         *self,
+                                                IdeTreeNode          *node,
+                                                GCancellable         *cancellable,
+                                                GAsyncReadyCallback   callback,
+                                                gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean      ide_tree_model_expand_finish     (IdeTreeModel         *self,
+                                                GAsyncResult         *result,
+                                                GError              **error);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree-node.c b/src/libide/tree/ide-tree-node.c
new file mode 100644
index 000000000..aeda3c25b
--- /dev/null
+++ b/src/libide/tree/ide-tree-node.c
@@ -0,0 +1,1863 @@
+/* ide-tree-node.c
+ *
+ * Copyright 2018-2019 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 "ide-tree-node"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+#include "ide-tree-private.h"
+
+/**
+ * SECTION:ide-tree-node
+ * @title: IdeTreeNode
+ * @short_description: a node within the tree
+ *
+ * The #IdeTreeNode class is used to represent an item that should
+ * be displayed in the tree of the Ide application. The
+ * #IdeTreeAddin plugins create and maintain these nodes during the
+ * lifetime of the program.
+ *
+ * Plugins that want to add items to the tree should implement the
+ * #IdeTreeAddin interface and register it during plugin
+ * initialization.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeTreeNode
+{
+  GObject parent_instance;
+
+  /* A pointer to the model, which is only set on the root node. */
+  IdeTreeModel *model;
+
+  /*
+   * The following are fields containing the values for various properties
+   * on the tree node. Usually, icon, display_name, and item will be set
+   * on all nodes.
+   */
+  GIcon   *icon;
+  GIcon   *expanded_icon;
+  gchar   *display_name;
+  GObject *item;
+  gchar   *tag;
+  GList   *emblems;
+
+  /*
+   * The following items are used to maintain a tree structure of
+   * nodes for which we can use O(1) operations. The link is inserted
+   * into the parents children queue. The parent pointer is unowned,
+   * and set by the parent (cleared upon removal).
+   *
+   * This also allows maintaining the tree structure with zero additional
+   * allocations beyond the nodes themselves.
+   */
+  IdeTreeNode *parent;
+  GQueue       children;
+  GList        link;
+
+  /* Foreground and Background colors */
+  GdkRGBA      background;
+  GdkRGBA      foreground;
+
+  /* When did we start loading? This is used to avoid drawing "Loading..."
+   * when the tree loads really quickly. Otherwise, we risk looking janky
+   * when the loads are quite fast.
+   */
+  gint64 started_loading_at;
+
+  /* If we're currently loading */
+  guint is_loading : 1;
+
+  /* If the node is a header (bold, etc) */
+  guint is_header : 1;
+
+  /* If this is a synthesized empty node */
+  guint is_empty : 1;
+
+  /* If the node maybe has children */
+  guint children_possible : 1;
+
+  /* If this node needs to have the children built */
+  guint needs_build_children : 1;
+
+  /* If true, we remove all children on collapse */
+  guint reset_on_collapse : 1;
+
+  /* If true, we use ide_clear_and_destroy_object() */
+  guint destroy_item : 1;
+
+  /* If colors are set */
+  guint background_set : 1;
+  guint foreground_set : 1;
+};
+
+G_DEFINE_TYPE (IdeTreeNode, ide_tree_node, G_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_CHILDREN_POSSIBLE,
+  PROP_DESTROY_ITEM,
+  PROP_DISPLAY_NAME,
+  PROP_EXPANDED_ICON,
+  PROP_EXPANDED_ICON_NAME,
+  PROP_ICON,
+  PROP_ICON_NAME,
+  PROP_IS_HEADER,
+  PROP_ITEM,
+  PROP_RESET_ON_COLLAPSE,
+  PROP_TAG,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static IdeTreeModel *
+ide_tree_node_get_model (IdeTreeNode *self)
+{
+  return ide_tree_node_get_root (self)->model;
+}
+
+/**
+ * ide_tree_node_new:
+ *
+ * Create a new #IdeTreeNode.
+ *
+ * Returns: (transfer full): a newly created #IdeTreeNode
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_new (void)
+{
+  return g_object_new (IDE_TYPE_TREE_NODE, NULL);
+}
+
+static void
+ide_tree_node_emit_changed (IdeTreeNode *self)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+  IdeTreeModel *model;
+  GtkTreeIter iter = { .user_data = self };
+
+  g_assert (IDE_IS_TREE_NODE (self));
+
+  if (!(model = ide_tree_node_get_model (self)))
+    return;
+
+  if ((path = ide_tree_model_get_path_for_node (model, self)))
+    gtk_tree_model_row_changed (GTK_TREE_MODEL (model), path, &iter);
+}
+
+static void
+ide_tree_node_remove_with_dispose (IdeTreeNode *self,
+                                   IdeTreeNode *child)
+{
+  g_object_ref (child);
+  ide_tree_node_remove (self, child);
+  g_object_run_dispose (G_OBJECT (child));
+  g_object_unref (child);
+}
+
+static void
+ide_tree_node_dispose (GObject *object)
+{
+  IdeTreeNode *self = (IdeTreeNode *)object;
+
+  while (self->children.length > 0)
+    ide_tree_node_remove_with_dispose (self, g_queue_peek_nth (&self->children, 0));
+
+  if (self->destroy_item && IDE_IS_OBJECT (self->item))
+    ide_clear_and_destroy_object (&self->item);
+  else
+    g_clear_object (&self->item);
+
+  g_list_free_full (self->emblems, g_object_unref);
+  self->emblems = NULL;
+
+  g_clear_object (&self->icon);
+  g_clear_object (&self->expanded_icon);
+  g_clear_pointer (&self->display_name, g_free);
+  g_clear_pointer (&self->tag, g_free);
+
+  G_OBJECT_CLASS (ide_tree_node_parent_class)->dispose (object);
+}
+
+static void
+ide_tree_node_finalize (GObject *object)
+{
+  IdeTreeNode *self = (IdeTreeNode *)object;
+
+  g_clear_weak_pointer (&self->model);
+
+  g_assert (self->children.head == NULL);
+  g_assert (self->children.tail == NULL);
+  g_assert (self->children.length == 0);
+
+  if (self->destroy_item && IDE_IS_OBJECT (self->item))
+    ide_clear_and_destroy_object (&self->item);
+  else
+    g_clear_object (&self->item);
+
+  g_clear_object (&self->icon);
+  g_clear_object (&self->expanded_icon);
+  g_clear_pointer (&self->display_name, g_free);
+  g_clear_pointer (&self->tag, g_free);
+
+  G_OBJECT_CLASS (ide_tree_node_parent_class)->finalize (object);
+}
+
+static void
+ide_tree_node_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  IdeTreeNode *self = IDE_TREE_NODE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CHILDREN_POSSIBLE:
+      g_value_set_boolean (value, ide_tree_node_get_children_possible (self));
+      break;
+
+    case PROP_DESTROY_ITEM:
+      g_value_set_boolean (value, self->destroy_item);
+      break;
+
+    case PROP_DISPLAY_NAME:
+      g_value_set_string (value, ide_tree_node_get_display_name (self));
+      break;
+
+    case PROP_ICON:
+      g_value_set_object (value, ide_tree_node_get_icon (self));
+      break;
+
+    case PROP_IS_HEADER:
+      g_value_set_boolean (value, ide_tree_node_get_is_header (self));
+      break;
+
+    case PROP_ITEM:
+      g_value_set_object (value, ide_tree_node_get_item (self));
+      break;
+
+    case PROP_RESET_ON_COLLAPSE:
+      g_value_set_boolean (value, ide_tree_node_get_reset_on_collapse (self));
+      break;
+
+    case PROP_TAG:
+      g_value_set_string (value, ide_tree_node_get_tag (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_tree_node_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  IdeTreeNode *self = IDE_TREE_NODE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CHILDREN_POSSIBLE:
+      ide_tree_node_set_children_possible (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_DESTROY_ITEM:
+      self->destroy_item = g_value_get_boolean (value);
+      break;
+
+    case PROP_DISPLAY_NAME:
+      ide_tree_node_set_display_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_EXPANDED_ICON:
+      ide_tree_node_set_expanded_icon (self, g_value_get_object (value));
+      break;
+
+    case PROP_EXPANDED_ICON_NAME:
+      ide_tree_node_set_expanded_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_ICON:
+      ide_tree_node_set_icon (self, g_value_get_object (value));
+      break;
+
+    case PROP_ICON_NAME:
+      ide_tree_node_set_icon_name (self, g_value_get_string (value));
+      break;
+
+    case PROP_IS_HEADER:
+      ide_tree_node_set_is_header (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_ITEM:
+      ide_tree_node_set_item (self, g_value_get_object (value));
+      break;
+
+    case PROP_RESET_ON_COLLAPSE:
+      ide_tree_node_set_reset_on_collapse (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_TAG:
+      ide_tree_node_set_tag (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_tree_node_class_init (IdeTreeNodeClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_tree_node_dispose;
+  object_class->finalize = ide_tree_node_finalize;
+  object_class->get_property = ide_tree_node_get_property;
+  object_class->set_property = ide_tree_node_set_property;
+
+  /**
+   * IdeTreeNode:children-possible:
+   *
+   * The "children-possible" property denotes if the node may have children
+   * even if it doesn't have children yet. This is useful for delayed loading
+   * of children nodes.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_CHILDREN_POSSIBLE] =
+    g_param_spec_boolean ("children-possible",
+                          "Children Possible",
+                          "If children are possible for the node",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeNode:destroy-item:
+   *
+   * If %TRUE and #IdeTreeNode:item is an #IdeObject, it will be destroyed
+   * when the node is destroyed.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_DESTROY_ITEM] =
+    g_param_spec_boolean ("destroy-item",
+                          "Destroy Item",
+                          "If the item should be destroyed with the node.",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeNode:display-name:
+   *
+   * The "display-name" property is the name for the node as it should be
+   * displayed in the tree.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_DISPLAY_NAME] =
+    g_param_spec_string ("display-name",
+                         "Display Name",
+                         "Display name for the node in the tree",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeNode:expanded-icon:
+   *
+   * The "expanded-icon" property is the icon that should be displayed to the
+   * user in the tree for this node.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_EXPANDED_ICON] =
+    g_param_spec_object ("expanded-icon",
+                         "Expanded Icon",
+                         "The expanded icon to display in the tree",
+                         G_TYPE_ICON,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeNode:expanded-icon-name:
+   *
+   * The "expanded-icon-name" is a convenience property to set the
+   * #IdeTreeNode:expanded-icon property using an icon-name.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_EXPANDED_ICON_NAME] =
+    g_param_spec_string ("expanded-icon-name",
+                         "Expanded Icon Name",
+                         "The expanded icon-name for the GIcon",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeNode:icon:
+   *
+   * The "icon" property is the icon that should be displayed to the
+   * user in the tree for this node.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ICON] =
+    g_param_spec_object ("icon",
+                         "Icon",
+                         "The icon to display in the tree",
+                         G_TYPE_ICON,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeNode:icon-name:
+   *
+   * The "icon-name" is a convenience property to set the #IdeTreeNode:icon
+   * property using an icon-name.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ICON_NAME] =
+    g_param_spec_string ("icon-name",
+                         "Icon Name",
+                         "The icon-name for the GIcon",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeNode:is-header:
+   *
+   * The "is-header" property denotes the node should be styled as a group
+   * header.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_IS_HEADER] =
+    g_param_spec_boolean ("is-header",
+                          "Is Header",
+                          "If the node is a header",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeNode:item:
+   *
+   * The "item" property is an optional #GObject that can be used to
+   * store information about the node, which is sometimes useful when
+   * creating #IdeTreeAddin plugins.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_ITEM] =
+    g_param_spec_object ("item",
+                         "Item",
+                         "Item",
+                         G_TYPE_OBJECT,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeNode:reset-on-collapse:
+   *
+   * The "reset-on-collapse" denotes that children should be removed when
+   * the node is collapsed.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_RESET_ON_COLLAPSE] =
+    g_param_spec_boolean ("reset-on-collapse",
+                          "Reset on Collapse",
+                          "If the children are removed when the node is collapsed",
+                          TRUE,
+                          (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeTreeNode:tag:
+   *
+   * The "tag" property can be used to denote the type of node when you do not have an
+   * object to assign to #IdeTreeNode:item.
+   *
+   * See ide_tree_node_is_tag() to match a tag when building.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_TAG] =
+    g_param_spec_string ("tag",
+                         "Tag",
+                         "The tag for the node if any",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_tree_node_init (IdeTreeNode *self)
+{
+  self->reset_on_collapse = TRUE;
+  self->link.data = self;
+}
+
+/**
+ * ide_tree_node_get_display_name:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the #IdeTreeNode:display-name property.
+ *
+ * Returns: (nullable): a string containing the display name
+ *
+ * Since: 3.32
+ */
+const gchar *
+ide_tree_node_get_display_name (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self->display_name;
+}
+
+/**
+ * ide_tree_node_set_display_name:
+ *
+ * Sets the #IdeTreeNode:display-name property, which is the text to
+ * use when displaying the item in the tree.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_display_name (IdeTreeNode *self,
+                                const gchar *display_name)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (g_strcmp0 (display_name, self->display_name) != 0)
+    {
+      g_free (self->display_name);
+      self->display_name = g_strdup (display_name);
+      ide_tree_node_emit_changed (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DISPLAY_NAME]);
+    }
+}
+
+/**
+ * ide_tree_node_get_icon:
+ * @self: a #IdeTree
+ *
+ * Gets the icon associated with the tree node.
+ *
+ * Returns: (transfer none) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_tree_node_get_icon (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self->icon;
+}
+
+/**
+ * ide_tree_node_set_icon:
+ * @self: a @IdeTreeNode
+ * @icon: (nullable): a #GIcon or %NULL
+ *
+ * Sets the icon for the tree node.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_icon (IdeTreeNode *self,
+                        GIcon       *icon)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (g_set_object (&self->icon, icon))
+    {
+      ide_tree_node_emit_changed (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ICON]);
+    }
+}
+
+/**
+ * ide_tree_node_get_expanded_icon:
+ * @self: a #IdeTree
+ *
+ * Gets the expanded icon associated with the tree node.
+ *
+ * Returns: (transfer none) (nullable): a #GIcon or %NULL
+ *
+ * Since: 3.32
+ */
+GIcon *
+ide_tree_node_get_expanded_icon (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self->expanded_icon ? self->expanded_icon : self->icon;
+}
+
+/**
+ * ide_tree_node_set_expanded_icon:
+ * @self: a @IdeTreeNode
+ * @expanded_icon: (nullable): a #GIcon or %NULL
+ *
+ * Sets the expanded icon for the tree node.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_expanded_icon (IdeTreeNode *self,
+                                 GIcon       *expanded_icon)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (g_set_object (&self->expanded_icon, expanded_icon))
+    {
+      ide_tree_node_emit_changed (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EXPANDED_ICON]);
+    }
+}
+
+/**
+ * ide_tree_node_get_item:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the item that has been associated with the node.
+ *
+ * Returns: (transfer none) (type GObject.Object) (nullable): a #GObject
+ *   if the item has been previously set.
+ *
+ * Since: 3.32
+ */
+gpointer
+ide_tree_node_get_item (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+  g_return_val_if_fail (!self->item || G_IS_OBJECT (self->item), NULL);
+
+  return self->item;
+}
+
+void
+ide_tree_node_set_item (IdeTreeNode *self,
+                        gpointer     item)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (!item || G_IS_OBJECT (item));
+
+  if (g_set_object (&self->item, item))
+    {
+      ide_tree_node_emit_changed (self);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ITEM]);
+    }
+}
+
+static IdeTreeNodeVisit
+ide_tree_node_row_inserted_traverse_cb (IdeTreeNode *node,
+                                        gpointer     user_data)
+{
+  IdeTreeModel *model = user_data;
+  g_autoptr(GtkTreePath) path = NULL;
+  GtkTreeIter iter = { .user_data = node };
+
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  /* Ignore the root node, nothing to do with that */
+  if (ide_tree_node_is_root (node))
+    return IDE_TREE_NODE_VISIT_CHILDREN;
+
+  /* It would be faster to create our paths as we traverse the tree,
+   * but that complicates the traversal. Generally this path should get
+   * hit very little (as usually it's only a single "child node").
+   */
+  if ((path = gtk_tree_model_get_path (GTK_TREE_MODEL (model), &iter)))
+    {
+      gtk_tree_model_row_inserted (GTK_TREE_MODEL (model), path, &iter);
+
+      if (ide_tree_node_is_first (node))
+        {
+          IdeTreeNode *parent = ide_tree_node_get_parent (node);
+
+          if (!ide_tree_node_is_root (parent))
+            {
+              iter.user_data = parent;
+              gtk_tree_path_up (path);
+              gtk_tree_model_row_has_child_toggled (GTK_TREE_MODEL (model), path, &iter);
+            }
+        }
+    }
+
+  return IDE_TREE_NODE_VISIT_CHILDREN;
+}
+
+static void
+ide_tree_node_row_inserted (IdeTreeNode *self,
+                            IdeTreeNode *child)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+  IdeTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_NODE (self));
+  g_assert (IDE_IS_TREE_NODE (child));
+
+  if (!(model = ide_tree_node_get_model (self)) ||
+      !ide_tree_model_get_iter_for_node (model, &iter, child) ||
+      !(path = ide_tree_model_get_path_for_node (model, child)))
+    return;
+
+  ide_tree_node_traverse (child,
+                          G_PRE_ORDER,
+                          G_TRAVERSE_ALL,
+                          -1,
+                          ide_tree_node_row_inserted_traverse_cb,
+                          model);
+}
+
+void
+_ide_tree_node_set_model (IdeTreeNode  *self,
+                          IdeTreeModel *model)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (!model || IDE_IS_TREE_MODEL (model));
+
+  if (g_set_weak_pointer (&self->model, model))
+    {
+      if (self->model != NULL)
+        ide_tree_node_row_inserted (self, self);
+    }
+}
+
+/**
+ * ide_tree_node_prepend:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Prepends @child as a child of @self at the 0 index.
+ *
+ * This operation is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_prepend (IdeTreeNode *self,
+                       IdeTreeNode *child)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (child));
+  g_return_if_fail (child->parent == NULL);
+
+  child->parent = self;
+  g_object_ref (child);
+  g_queue_push_head_link (&self->children, &child->link);
+
+  ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * ide_tree_node_append:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Appends @child as a child of @self at the last position.
+ *
+ * This operation is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_append (IdeTreeNode *self,
+                      IdeTreeNode *child)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (child));
+  g_return_if_fail (child->parent == NULL);
+
+  child->parent = self;
+  g_object_ref (child);
+  g_queue_push_tail_link (&self->children, &child->link);
+
+  ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * ide_tree_node_insert_before:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Inserts @child directly before @self by adding it to the parent of @self.
+ *
+ * This operation is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_insert_before (IdeTreeNode *self,
+                             IdeTreeNode *child)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (child));
+  g_return_if_fail (self->parent != NULL);
+  g_return_if_fail (child->parent == NULL);
+
+  child->parent = self->parent;
+  g_object_ref (child);
+  _g_queue_insert_before_link (&self->parent->children, &self->link, &child->link);
+
+  ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * ide_tree_node_insert_after:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Inserts @child directly after @self by adding it to the parent of @self.
+ *
+ * This operation is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_insert_after (IdeTreeNode *self,
+                            IdeTreeNode *child)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (child));
+  g_return_if_fail (self->parent != NULL);
+  g_return_if_fail (child->parent == NULL);
+
+  child->parent = self->parent;
+  g_object_ref (child);
+  _g_queue_insert_after_link (&self->parent->children, &self->link, &child->link);
+
+  ide_tree_node_row_inserted (self, child);
+}
+
+/**
+ * ide_tree_node_remove:
+ * @self: a #IdeTreeNode
+ * @child: a #IdeTreeNode
+ *
+ * Removes the child node @child from @self. @self must be the parent of @child.
+ *
+ * This function is O(1).
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_remove (IdeTreeNode *self,
+                      IdeTreeNode *child)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+  IdeTreeModel *model;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (child));
+  g_return_if_fail (child->parent == self);
+
+  if ((model = ide_tree_node_get_model (self)))
+    path = ide_tree_model_get_path_for_node (model, child);
+
+  child->parent = NULL;
+  g_queue_unlink (&self->children, &child->link);
+
+  if (path != NULL)
+    gtk_tree_model_row_deleted (GTK_TREE_MODEL (model), path);
+
+  g_object_unref (child);
+}
+
+/**
+ * ide_tree_node_get_parent:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the parent node of @self.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_parent (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self->parent;
+}
+
+/**
+ * ide_tree_node_get_root:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the root #IdeTreeNode by following the #IdeTreeNode:parent
+ * properties of each node.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_root (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  while (self->parent != NULL)
+    self = self->parent;
+
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self;
+}
+
+/**
+ * ide_tree_node_holds:
+ * @self: a #IdeTreeNode
+ * @type: a #GType
+ *
+ * Checks to see if the #IdeTreeNode:item property matches @type
+ * or is a subclass of @type.
+ *
+ * Returns: %TRUE if @self holds a @type item
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_holds (IdeTreeNode *self,
+                     GType        type)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return G_TYPE_CHECK_INSTANCE_TYPE (self->item, type);
+}
+
+/**
+ * ide_tree_node_get_index:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the position of the @self.
+ *
+ * Returns: the offset of @self with it's siblings.
+ *
+ * Since: 3.32
+ */
+guint
+ide_tree_node_get_index (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), 0);
+
+  if (self->parent != NULL)
+    return g_list_position (self->parent->children.head, &self->link);
+
+  return 0;
+}
+
+/**
+ * ide_tree_node_get_nth_child:
+ * @self: a #IdeTreeNode
+ * @index_: the index of the child
+ *
+ * Gets the @nth child of the tree node or %NULL if it does not exist.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_nth_child (IdeTreeNode *self,
+                             guint        index_)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return g_queue_peek_nth (&self->children, index_);
+}
+
+/**
+ * ide_tree_node_get_next:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the next sibling after @self.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_next (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  if (self->link.next)
+    return self->link.next->data;
+
+  return NULL;
+}
+
+/**
+ * ide_tree_node_get_previous:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the previous sibling before @self.
+ *
+ * Returns: (transfer none) (nullable): a #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_node_get_previous (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  if (self->link.prev)
+    return self->link.prev->data;
+
+  return NULL;
+}
+
+/**
+ * ide_tree_node_get_children_possible:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if the node can have children, and if so, returns %TRUE.
+ * It may not actually have children yet.
+ *
+ * Returns: %TRUE if the children may have children
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_get_children_possible (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->children_possible;
+}
+
+/**
+ * ide_tree_node_set_children_possible:
+ * @self: a #IdeTreeNode
+ * @children_possible: if children are possible
+ *
+ * Sets if the children are possible for the node.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_children_possible (IdeTreeNode *self,
+                                     gboolean     children_possible)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  children_possible = !!children_possible;
+
+  if (children_possible != self->children_possible)
+    {
+      self->children_possible = children_possible;
+      self->needs_build_children = children_possible;
+
+      if (self->children_possible && self->children.length == 0)
+        {
+          g_autoptr(IdeTreeNode) child = NULL;
+
+          child = g_object_new (IDE_TYPE_TREE_NODE,
+                                "display-name", _("(Empty)"),
+                                NULL);
+          child->is_empty = TRUE;
+          ide_tree_node_append (self, child);
+
+          g_assert (ide_tree_node_has_child (self) == children_possible);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_CHILDREN_POSSIBLE]);
+    }
+}
+
+/**
+ * ide_tree_node_has_child:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if @self has any children.
+ *
+ * Returns: %TRUE if @self has one or more children.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_has_child (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->children.length > 0;
+}
+
+/**
+ * ide_tree_node_get_n_children:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the number of children that @self contains.
+ *
+ * Returns: the number of children
+ *
+ * Since: 3.32
+ */
+guint
+ide_tree_node_get_n_children (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), 0);
+
+  return self->children.length;
+}
+
+/**
+ * ide_tree_node_get_is_header:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the #IdeTreeNode:is-header property.
+ *
+ * If this is %TRUE, then the node will be rendered with alternate
+ * styling for group headers.
+ *
+ * Returns: %TRUE if @self is a header.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_get_is_header (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->is_header;
+}
+
+/**
+ * ide_tree_node_set_is_header:
+ * @self: a #IdeTreeNode
+ *
+ * Sets the #IdeTreeNode:is-header property.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_is_header (IdeTreeNode *self,
+                             gboolean     is_header)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  is_header = !!is_header;
+
+  if (self->is_header != is_header)
+    {
+      self->is_header = is_header;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_HEADER]);
+    }
+}
+
+typedef struct
+{
+  GTraverseType       type;
+  GTraverseFlags      flags;
+  gint                depth;
+  IdeTreeTraverseFunc callback;
+  gpointer            user_data;
+} IdeTreeTraversal;
+
+static inline gboolean
+can_callback_node (IdeTreeNode    *node,
+                   GTraverseFlags  flags)
+{
+  return ((flags & G_TRAVERSE_LEAVES) && node->children.length == 0) ||
+         ((flags & G_TRAVERSE_NON_LEAVES) && node->children.length > 0);
+}
+
+static gboolean
+do_traversal (IdeTreeNode      *node,
+              IdeTreeTraversal *traversal)
+{
+  const GList *iter;
+  IdeTreeNodeVisit ret = IDE_TREE_NODE_VISIT_BREAK;
+
+  if (traversal->depth < 0)
+    return IDE_TREE_NODE_VISIT_CONTINUE;
+
+  traversal->depth--;
+
+  if (traversal->type == G_PRE_ORDER && can_callback_node (node, traversal->flags))
+    {
+      ret = traversal->callback (node, traversal->user_data);
+
+      if (!ide_tree_node_is_root (node) &&
+          (ret == IDE_TREE_NODE_VISIT_CONTINUE || ret == IDE_TREE_NODE_VISIT_BREAK))
+        goto finish;
+    }
+
+  iter = node->children.head;
+
+  while (iter != NULL)
+    {
+      IdeTreeNode *child = iter->data;
+
+      iter = iter->next;
+
+      ret = do_traversal (child, traversal);
+
+      if (ret == IDE_TREE_NODE_VISIT_BREAK)
+        goto finish;
+    }
+
+  if (traversal->type == G_POST_ORDER && can_callback_node (node, traversal->flags))
+    ret = traversal->callback (node, traversal->user_data);
+
+finish:
+  traversal->depth++;
+
+  return ret;
+}
+
+/**
+ * ide_tree_node_traverse:
+ * @self: a #IdeTreeNode
+ * @traverse_type: the type of traversal, pre and post supported
+ * @traverse_flags: the flags for what nodes to match
+ * @max_depth: the max depth for the traversal or -1 for all
+ * @traverse_func: (scope call): the callback for each matching node
+ * @user_data: user data for @traverse_func
+ *
+ * Calls @traverse_func for each node that matches the requested
+ * type, flags, and depth.
+ *
+ * Traversal is stopped if @traverse_func returns %TRUE.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_traverse (IdeTreeNode         *self,
+                        GTraverseType        traverse_type,
+                        GTraverseFlags       traverse_flags,
+                        gint                 max_depth,
+                        IdeTreeTraverseFunc  traverse_func,
+                        gpointer             user_data)
+{
+  IdeTreeTraversal traverse;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (traverse_type == G_PRE_ORDER ||
+                    traverse_type == G_POST_ORDER);
+  g_return_if_fail (traverse_func != NULL);
+
+  traverse.type = traverse_type;
+  traverse.flags = traverse_flags;
+  traverse.depth = max_depth < 0 ? G_MAXINT : max_depth;
+  traverse.callback = traverse_func;
+  traverse.user_data = user_data;
+
+  do_traversal (self, &traverse);
+}
+
+/**
+ * ide_tree_node_is_empty:
+ * @self: a #IdeTreeNode
+ *
+ * This function checks if @self is a synthesized "empty" node.
+ *
+ * Empty nodes are added to #IdeTreeNode that may have children in the
+ * future, but are currently empty. It allows the tree to display the
+ * "(Empty)" contents and show a proper expander arrow.
+ *
+ * Returns: %TRUE if @self is a synthesized empty node.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_is_empty (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->is_empty;
+}
+
+gboolean
+_ide_tree_node_get_needs_build_children (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->needs_build_children;
+}
+
+void
+_ide_tree_node_set_needs_build_children (IdeTreeNode *self,
+                                         gboolean     needs_build_children)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  self->needs_build_children = !!needs_build_children;
+}
+
+/**
+ * ide_tree_node_set_icon_name:
+ * @self: a #IdeTreeNode
+ * @icon_name: (nullable): the name of the icon, or %NULL
+ *
+ * Sets the #IdeTreeNode:icon property using an icon-name.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_icon_name (IdeTreeNode *self,
+                             const gchar *icon_name)
+{
+  g_autoptr(GIcon) icon = NULL;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (icon_name != NULL)
+    icon = g_themed_icon_new (icon_name);
+  ide_tree_node_set_icon (self, icon);
+}
+
+/**
+ * ide_tree_node_set_expanded_icon_name:
+ * @self: a #IdeTreeNode
+ * @expanded_icon_name: (nullable): the name of the icon, or %NULL
+ *
+ * Sets the #IdeTreeNode:icon property using an icon-name.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_expanded_icon_name (IdeTreeNode *self,
+                                      const gchar *expanded_icon_name)
+{
+  g_autoptr(GIcon) icon = NULL;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (expanded_icon_name != NULL)
+    icon = g_themed_icon_new (expanded_icon_name);
+  ide_tree_node_set_expanded_icon (self, icon);
+}
+
+/**
+ * ide_tree_node_is_root:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if @self is the root node, meaning it has no parent.
+ *
+ * Returns: %TRUE if @self has no parent.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_is_root (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->parent == NULL;
+}
+
+/**
+ * ide_tree_node_is_first:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if @self is the first sibling.
+ *
+ * Returns: %TRUE if @self is the first sibling
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_is_first (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->link.prev == NULL;
+}
+
+/**
+ * ide_tree_node_is_last:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if @self is the last sibling.
+ *
+ * Returns: %TRUE if @self is the last sibling
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_is_last (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->link.next == NULL;
+}
+
+static void
+ide_tree_node_dump_internal (IdeTreeNode *self,
+                             gint         depth)
+{
+  g_autofree gchar *space = g_strnfill (depth * 2, ' ');
+
+  g_print ("%s%s\n", space, ide_tree_node_get_display_name (self));
+
+  g_assert (self->children.length == 0 || self->children.head);
+  g_assert (self->children.length == 0 || self->children.tail);
+  g_assert (self->children.length > 0 || !self->children.head);
+  g_assert (self->children.length > 0 || !self->children.tail);
+
+  for (const GList *iter = self->children.head; iter; iter = iter->next)
+    ide_tree_node_dump_internal (iter->data, depth + 1);
+}
+
+void
+_ide_tree_node_dump (IdeTreeNode *self)
+{
+  ide_tree_node_dump_internal (self, 0);
+}
+
+gboolean
+_ide_tree_node_get_loading (IdeTreeNode *self,
+                            gint64      *started_loading_at)
+{
+  g_assert (IDE_IS_TREE_NODE (self));
+  g_assert (started_loading_at != NULL);
+
+  *started_loading_at = self->started_loading_at;
+
+  return self->is_loading;
+}
+
+void
+_ide_tree_node_set_loading (IdeTreeNode *self,
+                            gboolean     loading)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  self->is_loading = !!loading;
+
+  if (self->is_loading)
+    self->started_loading_at = g_get_monotonic_time ();
+
+  for (const GList *iter = self->children.head; iter; iter = iter->next)
+    {
+      IdeTreeNode *child = iter->data;
+
+      if (child->is_empty)
+        {
+          if (loading)
+            ide_tree_node_set_display_name (child, _("Loading…"));
+          else
+            ide_tree_node_set_display_name (child, _("(Empty)"));
+
+          if (self->children.length > 1)
+            ide_tree_node_remove (self, child);
+
+          break;
+        }
+    }
+}
+
+void
+_ide_tree_node_remove_all (IdeTreeNode *self)
+{
+  const GList *iter;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  iter = self->children.head;
+
+  while (iter != NULL)
+    {
+      IdeTreeNode *child = iter->data;
+      iter = iter->next;
+      ide_tree_node_remove (self, child);
+    }
+
+  if (ide_tree_node_get_children_possible (self))
+    {
+      g_autoptr(IdeTreeNode) child = g_object_new (IDE_TYPE_TREE_NODE,
+                                                   "display-name", _("(Empty)"),
+                                                   NULL);
+      child->is_empty = TRUE;
+      ide_tree_node_append (self, child);
+      _ide_tree_node_set_needs_build_children (self, TRUE);
+    }
+}
+
+/**
+ * ide_tree_node_get_reset_on_collapse:
+ * @self: a #IdeTreeNode
+ *
+ * Checks if the node should have all children removed when collapsed.
+ *
+ * Returns: %TRUE if children are removed on collapse
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_tree_node_get_reset_on_collapse (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return self->reset_on_collapse;
+}
+
+/**
+ * ide_tree_node_set_reset_on_collapse:
+ * @self: a #IdeTreeNode
+ * @reset_on_collapse: if the children should be removed on collapse
+ *
+ * If %TRUE, then children will be removed when the row is collapsed.
+ *
+ * Since: 3.32
+ */
+void
+ide_tree_node_set_reset_on_collapse (IdeTreeNode *self,
+                                     gboolean     reset_on_collapse)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  reset_on_collapse = !!reset_on_collapse;
+
+  if (reset_on_collapse != self->reset_on_collapse)
+    {
+      self->reset_on_collapse = reset_on_collapse;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_RESET_ON_COLLAPSE]);
+    }
+}
+
+/**
+ * ide_tree_node_get_path:
+ * @self: a #IdeTreeNode
+ *
+ * Gets the path for the tree node.
+ *
+ * Returns: (transfer full) (nullable): a path or %NULL
+ *
+ * Since: 3.32
+ */
+GtkTreePath *
+ide_tree_node_get_path (IdeTreeNode *self)
+{
+  IdeTreeModel *model;
+
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  if ((model = ide_tree_node_get_model (self)))
+    return ide_tree_model_get_path_for_node (model, self);
+
+  return NULL;
+}
+
+static void
+ide_tree_node_get_area (IdeTreeNode  *node,
+                        IdeTree      *tree,
+                        GdkRectangle *area)
+{
+  GtkTreeViewColumn *column;
+  g_autoptr(GtkTreePath) path = NULL;
+
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (area != NULL);
+
+  path = ide_tree_node_get_path (node);
+  column = gtk_tree_view_get_column (GTK_TREE_VIEW (tree), 0);
+  gtk_tree_view_get_cell_area (GTK_TREE_VIEW (tree), path, column, area);
+}
+
+typedef struct
+{
+  IdeTreeNode *self;
+  IdeTree     *tree;
+  GtkPopover  *popover;
+} PopupRequest;
+
+static gboolean
+ide_tree_node_show_popover_timeout_cb (gpointer data)
+{
+  PopupRequest *popreq = data;
+  GdkRectangle rect;
+  GtkAllocation alloc;
+
+  g_assert (popreq);
+  g_assert (IDE_IS_TREE_NODE (popreq->self));
+  g_assert (GTK_IS_POPOVER (popreq->popover));
+
+  ide_tree_node_get_area (popreq->self, popreq->tree, &rect);
+  gtk_widget_get_allocation (GTK_WIDGET (popreq->tree), &alloc);
+
+  if ((rect.x + rect.width) > (alloc.x + alloc.width))
+    rect.width = (alloc.x + alloc.width) - rect.x;
+
+  /* FIXME: Wouldn't this be better placed in a theme? */
+  switch (gtk_popover_get_position (popreq->popover))
+    {
+    case GTK_POS_BOTTOM:
+    case GTK_POS_TOP:
+      rect.y += 3;
+      rect.height -= 6;
+      break;
+    case GTK_POS_RIGHT:
+    case GTK_POS_LEFT:
+      rect.x += 3;
+      rect.width -= 6;
+      break;
+
+    default:
+      break;
+    }
+
+  gtk_popover_set_relative_to (popreq->popover, GTK_WIDGET (popreq->tree));
+  gtk_popover_set_pointing_to (popreq->popover, &rect);
+  gtk_popover_popup (popreq->popover);
+
+  g_clear_object (&popreq->self);
+  g_clear_object (&popreq->popover);
+  g_slice_free (PopupRequest, popreq);
+
+  return G_SOURCE_REMOVE;
+}
+
+void
+_ide_tree_node_show_popover (IdeTreeNode *self,
+                             IdeTree     *tree,
+                             GtkPopover  *popover)
+{
+  GdkRectangle cell_area;
+  GdkRectangle visible_rect;
+  PopupRequest *popreq;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+  g_return_if_fail (IDE_IS_TREE (tree));
+  g_return_if_fail (GTK_IS_POPOVER (popover));
+
+  gtk_tree_view_get_visible_rect (GTK_TREE_VIEW (tree), &visible_rect);
+  ide_tree_node_get_area (self, tree, &cell_area);
+  gtk_tree_view_convert_bin_window_to_tree_coords (GTK_TREE_VIEW (tree),
+                                                   cell_area.x,
+                                                   cell_area.y,
+                                                   &cell_area.x,
+                                                   &cell_area.y);
+
+  popreq = g_slice_new0 (PopupRequest);
+  popreq->self = g_object_ref (self);
+  popreq->tree = g_object_ref (tree);
+  popreq->popover = g_object_ref (popover);
+
+  /*
+   * If the node is not on screen, we need to animate until we get there.
+   */
+  if ((cell_area.y < visible_rect.y) ||
+      ((cell_area.y + cell_area.height) >
+       (visible_rect.y + visible_rect.height)))
+    {
+      GtkTreePath *path;
+
+      path = ide_tree_node_get_path (self);
+      gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW (tree), path, NULL, FALSE, 0, 0);
+      g_clear_pointer (&path, gtk_tree_path_free);
+
+      /*
+       * FIXME: Time period comes from gtk animation duration.
+       *        Not curently available in pubic API.
+       *        We need to be greater than the max timeout it
+       *        could take to move, since we must have it
+       *        on screen by then.
+       *
+       *        One alternative might be to check the result
+       *        and if we are still not on screen, then just
+       *        pin it to a row-height from the top or bottom.
+       */
+      g_timeout_add (300,
+                     ide_tree_node_show_popover_timeout_cb,
+                     popreq);
+
+      return;
+    }
+
+  ide_tree_node_show_popover_timeout_cb (g_steal_pointer (&popreq));
+}
+
+const gchar *
+ide_tree_node_get_tag (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self->tag;
+}
+
+void
+ide_tree_node_set_tag (IdeTreeNode *self,
+                       const gchar *tag)
+{
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (!ide_str_equal0 (self->tag, tag))
+    {
+      g_free (self->tag);
+      self->tag = g_strdup (tag);
+    }
+}
+
+gboolean
+ide_tree_node_is_tag (IdeTreeNode *self,
+                      const gchar *tag)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  return tag && ide_str_equal0 (self->tag, tag);
+}
+
+void
+ide_tree_node_add_emblem (IdeTreeNode *self,
+                          GEmblem     *emblem)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  self->emblems = g_list_append (self->emblems, g_object_ref (emblem));
+}
+
+GIcon *
+_ide_tree_node_apply_emblems (IdeTreeNode *self,
+                              GIcon       *base)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  if (self->emblems != NULL)
+    {
+      g_autoptr(GIcon) emblemed = g_emblemed_icon_new (base, NULL);
+
+      for (const GList *iter = self->emblems; iter; iter = iter->next)
+        g_emblemed_icon_add_emblem (G_EMBLEMED_ICON (emblemed), iter->data);
+
+      return G_ICON (g_steal_pointer (&emblemed));
+    }
+
+  return g_object_ref (base);
+}
+
+const GdkRGBA *
+ide_tree_node_get_foreground_rgba (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self->foreground_set ? &self->foreground : NULL;
+}
+
+void
+ide_tree_node_set_foreground_rgba (IdeTreeNode   *self,
+                                   const GdkRGBA *foreground_rgba)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  self->foreground_set = !!foreground_rgba;
+
+  if (foreground_rgba)
+    self->foreground = *foreground_rgba;
+
+  ide_tree_node_emit_changed (self);
+}
+
+const GdkRGBA *
+ide_tree_node_get_background_rgba (IdeTreeNode *self)
+{
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), NULL);
+
+  return self->background_set ? &self->background : NULL;
+}
+
+void
+ide_tree_node_set_background_rgba (IdeTreeNode   *self,
+                                   const GdkRGBA *background_rgba)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  self->background_set = !!background_rgba;
+
+  if (background_rgba)
+    self->background = *background_rgba;
+
+  ide_tree_node_emit_changed (self);
+}
+
+void
+_ide_tree_node_apply_colors (IdeTreeNode     *self,
+                             GtkCellRenderer *cell)
+{
+  PangoAttrList *attrs = NULL;
+
+  g_return_if_fail (IDE_IS_TREE_NODE (self));
+
+  if (self->foreground_set)
+    {
+      if (!attrs)
+        attrs = pango_attr_list_new ();
+      pango_attr_list_insert (attrs,
+                              pango_attr_foreground_new (self->foreground.red * 65535,
+                                                         self->foreground.green * 65535,
+                                                         self->foreground.blue * 65535));
+    }
+
+  if (self->background_set)
+    {
+      if (!attrs)
+        attrs = pango_attr_list_new ();
+      pango_attr_list_insert (attrs,
+                              pango_attr_background_new (self->background.red * 65535,
+                                                         self->background.green * 65535,
+                                                         self->background.blue * 65535));
+    }
+
+  g_object_set (cell, "attributes", attrs, NULL);
+  g_clear_pointer (&attrs, pango_attr_list_unref);
+}
+
+gboolean
+ide_tree_node_is_selected (IdeTreeNode *self)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+  GtkTreeSelection *selection;
+  IdeTreeModel *model;
+  IdeTree *tree;
+
+  g_return_val_if_fail (IDE_IS_TREE_NODE (self), FALSE);
+
+  if ((path = ide_tree_node_get_path (self)) &&
+      (model = ide_tree_node_get_model (self)) &&
+      (tree = ide_tree_model_get_tree (model)) &&
+      (selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree))))
+    return gtk_tree_selection_path_is_selected (selection, path);
+
+  return FALSE;
+}
diff --git a/src/libide/tree/ide-tree-node.h b/src/libide/tree/ide-tree-node.h
new file mode 100644
index 000000000..2a0339dd2
--- /dev/null
+++ b/src/libide/tree/ide-tree-node.h
@@ -0,0 +1,172 @@
+/* ide-tree-node.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE_NODE (ide_tree_node_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeTreeNode, ide_tree_node, IDE, TREE_NODE, GObject)
+
+typedef enum
+{
+  IDE_TREE_NODE_VISIT_BREAK    = 0,
+  IDE_TREE_NODE_VISIT_CONTINUE = 0x1,
+  IDE_TREE_NODE_VISIT_CHILDREN = 0x3,
+} IdeTreeNodeVisit;
+
+/**
+ * IdeTreeTraverseFunc:
+ * @node: an #IdeTreeNode
+ * @user_data: closure data provided to ide_tree_node_traverse()
+ *
+ * This function prototype is used to traverse a tree of #IdeTreeNode.
+ *
+ * Returns: #IdeTreeNodeVisit, %IDE_TREE_NODE_VISIT_BREAK to stop traversal.
+ *
+ * Since: 3.32
+ */
+typedef IdeTreeNodeVisit (*IdeTreeTraverseFunc) (IdeTreeNode *node,
+                                                 gpointer     user_data);
+
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_new                    (void);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_tree_node_get_tag                (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_tag                (IdeTreeNode         *self,
+                                                     const gchar         *tag);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_tag                 (IdeTreeNode         *self,
+                                                     const gchar         *tag);
+IDE_AVAILABLE_IN_3_32
+GtkTreePath   *ide_tree_node_get_path               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+const gchar   *ide_tree_node_get_display_name       (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_display_name       (IdeTreeNode         *self,
+                                                     const gchar         *display_name);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_get_is_header          (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_is_header          (IdeTreeNode         *self,
+                                                     gboolean             header);
+IDE_AVAILABLE_IN_3_32
+GIcon         *ide_tree_node_get_icon               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_icon               (IdeTreeNode         *self,
+                                                     GIcon               *icon);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_icon_name          (IdeTreeNode         *self,
+                                                     const gchar         *icon_name);
+IDE_AVAILABLE_IN_3_32
+GIcon         *ide_tree_node_get_expanded_icon      (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_expanded_icon      (IdeTreeNode         *self,
+                                                     GIcon               *expanded_icon);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_expanded_icon_name (IdeTreeNode         *self,
+                                                     const gchar         *expanded_icon_name);
+IDE_AVAILABLE_IN_3_32
+gpointer       ide_tree_node_get_item               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_item               (IdeTreeNode         *self,
+                                                     gpointer             item);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_get_children_possible  (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_children_possible  (IdeTreeNode         *self,
+                                                     gboolean             children_possible);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_empty               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_has_child              (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+guint          ide_tree_node_get_n_children         (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_get_next               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_get_previous           (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+guint          ide_tree_node_get_index              (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_get_nth_child          (IdeTreeNode         *self,
+                                                     guint                index_);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_prepend                (IdeTreeNode         *self,
+                                                     IdeTreeNode         *child);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_append                 (IdeTreeNode         *self,
+                                                     IdeTreeNode         *child);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_insert_before          (IdeTreeNode         *self,
+                                                     IdeTreeNode         *child);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_insert_after           (IdeTreeNode         *self,
+                                                     IdeTreeNode         *child);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_remove                 (IdeTreeNode         *self,
+                                                     IdeTreeNode         *child);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_get_parent             (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_root                (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_first               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_last                (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode   *ide_tree_node_get_root               (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_holds                  (IdeTreeNode         *self,
+                                                     GType                type);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_traverse               (IdeTreeNode         *self,
+                                                     GTraverseType        traverse_type,
+                                                     GTraverseFlags       traverse_flags,
+                                                     gint                 max_depth,
+                                                     IdeTreeTraverseFunc  traverse_func,
+                                                     gpointer             user_data);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_add_emblem             (IdeTreeNode         *self,
+                                                     GEmblem             *emblem);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_get_reset_on_collapse  (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_reset_on_collapse  (IdeTreeNode         *self,
+                                                     gboolean             reset_on_collapse);
+IDE_AVAILABLE_IN_3_32
+const GdkRGBA *ide_tree_node_get_background_rgba    (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_background_rgba    (IdeTreeNode         *self,
+                                                     const GdkRGBA       *background_rgba);
+IDE_AVAILABLE_IN_3_32
+const GdkRGBA *ide_tree_node_get_foreground_rgba    (IdeTreeNode         *self);
+IDE_AVAILABLE_IN_3_32
+void           ide_tree_node_set_foreground_rgba    (IdeTreeNode         *self,
+                                                     const GdkRGBA       *foreground_rgba);
+IDE_AVAILABLE_IN_3_32
+gboolean       ide_tree_node_is_selected            (IdeTreeNode         *self);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree-private.h b/src/libide/tree/ide-tree-private.h
new file mode 100644
index 000000000..77968a522
--- /dev/null
+++ b/src/libide/tree/ide-tree-private.h
@@ -0,0 +1,70 @@
+/* ide-tree-private.h
+ *
+ * Copyright 2018-2019 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 "ide-tree.h"
+#include "ide-tree-node.h"
+#include "ide-tree-model.h"
+
+G_BEGIN_DECLS
+
+GdkDragAction _ide_tree_get_drop_actions              (IdeTree         *tree);
+IdeTreeModel *_ide_tree_model_new                     (IdeTree         *tree);
+IdeTreeNode  *_ide_tree_get_drop_node                 (IdeTree         *tree);
+void          _ide_tree_model_release_addins          (IdeTreeModel    *self);
+void          _ide_tree_model_selection_changed       (IdeTreeModel    *model,
+                                                       GtkTreeIter     *selection);
+void          _ide_tree_model_build_node              (IdeTreeModel    *self,
+                                                       IdeTreeNode     *node);
+gboolean      _ide_tree_model_row_activated           (IdeTreeModel    *self,
+                                                       IdeTree         *tree,
+                                                       GtkTreePath     *path);
+void          _ide_tree_model_row_expanded            (IdeTreeModel    *self,
+                                                       IdeTree         *tree,
+                                                       GtkTreePath     *path);
+void          _ide_tree_model_row_collapsed           (IdeTreeModel    *self,
+                                                       IdeTree         *tree,
+                                                       GtkTreePath     *path);
+void          _ide_tree_model_cell_data_func          (IdeTreeModel    *self,
+                                                       GtkTreeIter     *iter,
+                                                       GtkCellRenderer *cell);
+gboolean      _ide_tree_model_contains_node           (IdeTreeModel    *self,
+                                                       IdeTreeNode     *node);
+gboolean      _ide_tree_node_get_loading              (IdeTreeNode     *self,
+                                                       gint64          *loading_started_at);
+void          _ide_tree_node_set_loading              (IdeTreeNode     *self,
+                                                       gboolean         loading);
+void          _ide_tree_node_dump                     (IdeTreeNode     *self);
+void          _ide_tree_node_remove_all               (IdeTreeNode     *self);
+void          _ide_tree_node_set_model                (IdeTreeNode     *self,
+                                                       IdeTreeModel    *model);
+gboolean      _ide_tree_node_get_needs_build_children (IdeTreeNode     *self);
+void          _ide_tree_node_set_needs_build_children (IdeTreeNode     *self,
+                                                       gboolean         needs_build_children);
+void          _ide_tree_node_show_popover             (IdeTreeNode     *node,
+                                                       IdeTree         *tree,
+                                                       GtkPopover      *popover);
+GIcon        *_ide_tree_node_apply_emblems            (IdeTreeNode     *self,
+                                                       GIcon           *base);
+void          _ide_tree_node_apply_colors             (IdeTreeNode     *self,
+                                                       GtkCellRenderer *cell);
+
+G_END_DECLS
diff --git a/src/libide/tree/ide-tree.c b/src/libide/tree/ide-tree.c
new file mode 100644
index 000000000..24d557da1
--- /dev/null
+++ b/src/libide/tree/ide-tree.c
@@ -0,0 +1,764 @@
+/* ide-tree.c
+ *
+ * Copyright 2018-2019 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 "ide-tree"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-core.h>
+#include <libide-threading.h>
+
+#include "ide-tree.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+#include "ide-tree-private.h"
+
+typedef struct
+{
+  /* This #GCancellable will be automatically cancelled when the widget is
+   * destroyed. That is usefulf or async operations that you want to be
+   * cleaned up as the workspace is destroyed or the widget in question
+   * removed from the widget tree.
+   */
+  GCancellable *cancellable;
+
+  /* To keep rendering of common styles fast, we share these PangoAttrList
+   * so that we need not re-create them many times.
+   */
+  PangoAttrList *dim_label_attributes;
+  PangoAttrList *header_attributes;
+
+  /* The context menu to use for popups */
+  GMenu *context_menu;
+
+  /* Our context menu popover */
+  GtkPopover *popover;
+
+  /* Stashed drop information to propagate on drop */
+  GdkDragAction drop_action;
+  GtkTreePath *drop_path;
+  GtkTreeViewDropPosition drop_pos;
+} IdeTreePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (IdeTree, ide_tree, GTK_TYPE_TREE_VIEW)
+
+static IdeTreeModel *
+ide_tree_get_model (IdeTree *self)
+{
+  GtkTreeModel *model;
+
+  g_assert (IDE_IS_TREE (self));
+
+  if (!(model = gtk_tree_view_get_model (GTK_TREE_VIEW (self))) ||
+      !IDE_IS_TREE_MODEL (model))
+    return NULL;
+
+  return IDE_TREE_MODEL (model);
+}
+
+static void
+ide_tree_selection_changed_cb (IdeTree          *self,
+                               GtkTreeSelection *selection)
+{
+  IdeTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (GTK_IS_TREE_SELECTION (selection));
+
+  if (!(model = ide_tree_get_model (self)))
+    return;
+
+  if (gtk_tree_selection_get_selected (selection, NULL, &iter))
+    _ide_tree_model_selection_changed (model, &iter);
+  else
+    _ide_tree_model_selection_changed (model, NULL);
+}
+
+static void
+ide_tree_unselect (IdeTree *self)
+{
+  g_assert (IDE_IS_TREE (self));
+
+  gtk_tree_selection_unselect_all (gtk_tree_view_get_selection (GTK_TREE_VIEW (self)));
+}
+
+static void
+ide_tree_select (IdeTree     *self,
+                 IdeTreeNode *node)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+  GtkTreeSelection *selection;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  ide_tree_unselect (self);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self));
+  path = ide_tree_node_get_path (node);
+  gtk_tree_selection_select_path (selection, path);
+}
+
+static void
+text_cell_func (GtkCellLayout   *layout,
+                GtkCellRenderer *cell,
+                GtkTreeModel    *model,
+                GtkTreeIter     *iter,
+                gpointer         user_data)
+{
+  IdeTree *self = user_data;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  const gchar *display_name = NULL;
+  IdeTreeNode *node;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  g_object_set (cell,
+                "attributes", NULL,
+                "foreground-set", FALSE,
+                NULL);
+
+  if (!(node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
+    return;
+
+  _ide_tree_model_cell_data_func (IDE_TREE_MODEL (model), iter, cell);
+
+  /* If we're loading the node, avoid showing the "Loading..." text for 250
+   * milliseconds, so that we don't flash the user with information they'll
+   * never be able to read.
+   */
+  if (ide_tree_node_is_empty (node))
+    {
+      IdeTreeNode *parent = ide_tree_node_get_parent (node);
+      gint64 started_loading_at;
+
+      if (_ide_tree_node_get_loading (parent, &started_loading_at))
+        {
+          gint64 now = g_get_monotonic_time ();
+
+          if ((now - started_loading_at) < (G_USEC_PER_SEC / 4L))
+            goto set_props;
+        }
+    }
+
+  /* Only apply styling if the node isn't selected */
+  if (!ide_tree_node_is_selected (node))
+    {
+      if (ide_tree_node_get_is_header (node))
+        g_object_set (cell, "attributes", priv->header_attributes, NULL);
+      else if (ide_tree_node_is_empty (node))
+        g_object_set (cell, "attributes", priv->dim_label_attributes, NULL);
+    }
+
+  display_name = ide_tree_node_get_display_name (node);
+
+set_props:
+  g_object_set (cell,
+                "text", display_name,
+                NULL);
+}
+
+static void
+pixbuf_cell_func (GtkCellLayout   *layout,
+                  GtkCellRenderer *cell,
+                  GtkTreeModel    *model,
+                  GtkTreeIter     *iter,
+                  gpointer         user_data)
+{
+  IdeTree *self = user_data;
+  g_autoptr(GtkTreePath) path = NULL;
+  g_autoptr(GIcon) emblems = NULL;
+  IdeTreeNode *node;
+  GIcon *icon;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  if (!(node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
+    return;
+
+  path = gtk_tree_model_get_path (model, iter);
+
+  if (gtk_tree_view_row_expanded (GTK_TREE_VIEW (self), path))
+    icon = ide_tree_node_get_expanded_icon (node);
+  else
+    icon = ide_tree_node_get_icon (node);
+
+  if (icon != NULL)
+    emblems = _ide_tree_node_apply_emblems (node, icon);
+
+  g_object_set (cell, "gicon", emblems, NULL);
+}
+
+static void
+ide_tree_expand_cb (GObject      *object,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+  IdeTreeModel *model = (IdeTreeModel *)object;
+  g_autoptr(GtkTreePath) path = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  IdeTreeNode *node;
+  IdeTree *self;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  node = ide_task_get_task_data (task);
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  if ((path = ide_tree_model_get_path_for_node (model, node)))
+    {
+      if (ide_tree_model_expand_finish (model, result, NULL))
+        {
+          /* If node was detached during our async operation, we'll get NULL
+           * back for the GtkTreePath (in which case, we'll just ignore).
+           */
+          gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
+        }
+
+      _ide_tree_model_row_expanded (model, self, path);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_tree_row_activated (GtkTreeView       *tree_view,
+                        GtkTreePath       *path,
+                        GtkTreeViewColumn *column)
+{
+  IdeTree *self = (IdeTree *)tree_view;
+  IdeTreeModel *model;
+  IdeTreeNode *node;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (path != NULL);
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+
+  /* Get our model, and the node in question. Ignore everything if this
+   * is a synthesized "Empty" node.
+   */
+  if (!(model = ide_tree_get_model (self)) ||
+      !gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path) ||
+      !(node = ide_tree_model_get_node (model, &iter)) ||
+      ide_tree_node_is_empty (node))
+    return;
+
+  if (!_ide_tree_model_row_activated (model, self, path))
+    {
+      if (gtk_tree_view_row_expanded (tree_view, path))
+        gtk_tree_view_collapse_row (tree_view, path);
+      else
+        gtk_tree_view_expand_row (tree_view, path, FALSE);
+    }
+}
+
+static void
+ide_tree_row_expanded (GtkTreeView *tree_view,
+                       GtkTreeIter *iter,
+                       GtkTreePath *path)
+{
+  IdeTree *self = (IdeTree *)tree_view;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  g_autoptr(IdeTask) task = NULL;
+  IdeTreeModel *model;
+  IdeTreeNode *node;
+
+  g_assert (IDE_IS_TREE (tree_view));
+  g_assert (iter != NULL);
+  g_assert (IDE_IS_TREE_NODE (iter->user_data));
+  g_assert (path != NULL);
+
+  if (!(model = ide_tree_get_model (self)) ||
+      !(node = ide_tree_model_get_node (model, iter)) ||
+      !ide_tree_node_get_children_possible (node))
+    return;
+
+  task = ide_task_new (self, priv->cancellable, NULL, NULL);
+  ide_task_set_source_tag (task, ide_tree_row_expanded);
+  ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+
+  /* We want to expand the row if we can, but we need to ensure the
+   * children have been built first (it might only have a fake "empty"
+   * node currently). So we request that the model expand the row and
+   * then expand to the path on the callback. The model will do nothing
+   * more than complete the async request if there is nothing to build.
+   */
+  ide_tree_model_expand_async (IDE_TREE_MODEL (model),
+                               node,
+                               priv->cancellable,
+                               ide_tree_expand_cb,
+                               g_steal_pointer (&task));
+}
+
+static void
+ide_tree_row_collapsed (GtkTreeView *tree_view,
+                        GtkTreeIter *iter,
+                        GtkTreePath *path)
+{
+  IdeTree *self = (IdeTree *)tree_view;
+  IdeTreeModel *model;
+  IdeTreeNode *node;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (iter != NULL);
+  g_assert (path != NULL);
+
+  if (!(model = ide_tree_get_model (self)) ||
+      !(node = ide_tree_model_get_node (IDE_TREE_MODEL (model), iter)))
+    return;
+
+  /*
+   * If we are collapsing a row that requests to have its children removed
+   * and the dummy node re-inserted, go ahead and do so now.
+   */
+  if (ide_tree_node_get_reset_on_collapse (node))
+    _ide_tree_node_remove_all (node);
+
+  _ide_tree_model_row_collapsed (model, self, path);
+}
+
+static void
+ide_tree_popup (IdeTree        *self,
+                IdeTreeNode    *node,
+                GdkEventButton *event,
+                gint            target_x,
+                gint            target_y)
+{
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  const GdkRectangle area = { target_x, target_y, 0, 0 };
+  GtkTextDirection dir;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  if (priv->context_menu == NULL)
+    return;
+
+  dir = gtk_widget_get_direction (GTK_WIDGET (self));
+
+  if (priv->popover == NULL)
+    {
+      priv->popover = GTK_POPOVER (gtk_popover_new_from_model (GTK_WIDGET (self),
+                                                               G_MENU_MODEL (priv->context_menu)));
+      g_signal_connect (priv->popover,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        &priv->popover);
+    }
+
+  gtk_popover_set_pointing_to (priv->popover, &area);
+  gtk_popover_set_position (priv->popover, dir == GTK_TEXT_DIR_LTR ? GTK_POS_RIGHT : GTK_POS_LEFT);
+
+  ide_tree_show_popover_at_node (self, node, priv->popover);
+}
+
+static gboolean
+ide_tree_button_press_event (GtkWidget      *widget,
+                             GdkEventButton *event)
+{
+  IdeTree *self = (IdeTree *)widget;
+  IdeTreeModel *model;
+
+  g_assert (IDE_IS_TREE (self));
+  g_assert (event != NULL);
+
+  if ((model = ide_tree_get_model (self)) &&
+      (event->type == GDK_BUTTON_PRESS) &&
+      (event->button == GDK_BUTTON_SECONDARY))
+    {
+      g_autoptr(GtkTreePath) path = NULL;
+      gint cell_y;
+
+      if (!gtk_widget_has_focus (GTK_WIDGET (self)))
+        gtk_widget_grab_focus (GTK_WIDGET (self));
+
+      gtk_tree_view_get_path_at_pos (GTK_TREE_VIEW (self),
+                                     event->x,
+                                     event->y,
+                                     &path,
+                                     NULL,
+                                     NULL,
+                                     &cell_y);
+
+      if (path == NULL)
+        {
+          ide_tree_unselect (self);
+        }
+      else
+        {
+          GtkAllocation alloc;
+          GtkTreeIter iter;
+
+          gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+          if (gtk_tree_model_get_iter (GTK_TREE_MODEL (model), &iter, path))
+            {
+              IdeTreeNode *node;
+
+              node = ide_tree_model_get_node (IDE_TREE_MODEL (model), &iter);
+              ide_tree_select (self, node);
+              ide_tree_popup (self, node, event, alloc.x + alloc.width, event->y - cell_y);
+            }
+        }
+
+      return GDK_EVENT_STOP;
+    }
+
+  return GTK_WIDGET_CLASS (ide_tree_parent_class)->button_press_event (widget, event);
+}
+
+static gboolean
+ide_tree_drag_motion (GtkWidget      *widget,
+                      GdkDragContext *context,
+                      gint            x,
+                      gint            y,
+                      guint           time_)
+{
+  IdeTree *self = (IdeTree *)widget;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  gboolean ret;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE (self));
+  g_assert (context != NULL);
+
+  ret = GTK_WIDGET_CLASS (ide_tree_parent_class)->drag_motion (widget, context, x, y, time_);
+
+  /*
+   * Cache the current drop position so we can use it
+   * later to determine how to drop on a given node.
+   */
+  g_clear_pointer (&priv->drop_path, gtk_tree_path_free);
+  gtk_tree_view_get_drag_dest_row (GTK_TREE_VIEW (self), &priv->drop_path, &priv->drop_pos);
+
+  /* Save the drag action for builders dispatch */
+  priv->drop_action = gdk_drag_context_get_selected_action (context);
+
+  return ret;
+}
+
+static void
+ide_tree_destroy (GtkWidget *widget)
+{
+  IdeTree *self = (IdeTree *)widget;
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  IdeTreeModel *model;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  if ((model = ide_tree_get_model (self)))
+    _ide_tree_model_release_addins (model);
+
+  if (priv->popover != NULL)
+    gtk_widget_destroy (GTK_WIDGET (priv->popover));
+
+  gtk_tree_view_set_model (GTK_TREE_VIEW (self), NULL);
+
+  g_cancellable_cancel (priv->cancellable);
+  g_clear_object (&priv->cancellable);
+
+  g_clear_object (&priv->context_menu);
+
+  g_clear_pointer (&priv->dim_label_attributes, pango_attr_list_unref);
+  g_clear_pointer (&priv->header_attributes, pango_attr_list_unref);
+  g_clear_pointer (&priv->drop_path, gtk_tree_path_free);
+
+  GTK_WIDGET_CLASS (ide_tree_parent_class)->destroy (widget);
+}
+
+static void
+ide_tree_class_init (IdeTreeClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkTreeViewClass *tree_view_class = GTK_TREE_VIEW_CLASS (klass);
+
+  widget_class->destroy = ide_tree_destroy;
+  widget_class->button_press_event = ide_tree_button_press_event;
+  widget_class->drag_motion = ide_tree_drag_motion;
+
+  tree_view_class->row_activated = ide_tree_row_activated;
+  tree_view_class->row_expanded = ide_tree_row_expanded;
+  tree_view_class->row_collapsed = ide_tree_row_collapsed;
+}
+
+static void
+ide_tree_init (IdeTree *self)
+{
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  GtkCellRenderer *cell;
+  GtkTreeViewColumn *column;
+
+  priv->cancellable = g_cancellable_new ();
+
+  g_signal_connect_object (gtk_tree_view_get_selection (GTK_TREE_VIEW (self)),
+                           "changed",
+                           G_CALLBACK (ide_tree_selection_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (self), FALSE);
+  gtk_tree_view_set_activate_on_single_click (GTK_TREE_VIEW (self), TRUE);
+
+  column = gtk_tree_view_column_new ();
+  cell = g_object_new (GTK_TYPE_CELL_RENDERER_PIXBUF,
+                       "xpad", 6,
+                       NULL);
+  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, FALSE);
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, pixbuf_cell_func, self, NULL);
+
+  cell = gtk_cell_renderer_text_new ();
+  gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, text_cell_func, self, NULL);
+
+  gtk_tree_view_append_column (GTK_TREE_VIEW (self), column);
+
+  priv->dim_label_attributes = pango_attr_list_new ();
+  pango_attr_list_insert (priv->dim_label_attributes,
+                          pango_attr_foreground_alpha_new (65535 * 0.55));
+
+  priv->header_attributes = pango_attr_list_new ();
+  pango_attr_list_insert (priv->header_attributes,
+                          pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+}
+
+GtkWidget *
+ide_tree_new (void)
+{
+  return g_object_new (IDE_TYPE_TREE, NULL);
+}
+
+void
+ide_tree_set_context_menu (IdeTree *self,
+                           GMenu   *menu)
+{
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+
+  g_return_if_fail (IDE_IS_TREE (self));
+  g_return_if_fail (!menu || G_IS_MENU (menu));
+
+  if (g_set_object (&priv->context_menu, menu))
+    {
+      if (priv->popover != NULL)
+        gtk_widget_destroy (GTK_WIDGET (priv->popover));
+    }
+
+  g_return_if_fail (priv->popover == NULL);
+}
+
+void
+ide_tree_show_popover_at_node (IdeTree     *self,
+                               IdeTreeNode *node,
+                               GtkPopover  *popover)
+{
+  g_return_if_fail (IDE_IS_TREE (self));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+  g_return_if_fail (GTK_IS_POPOVER (popover));
+
+  _ide_tree_node_show_popover (node, self, popover);
+}
+
+/**
+ * ide_tree_get_selected_node:
+ * @self: a #IdeTree
+ *
+ * Gets the currently selected node, or %NULL
+ *
+ * Returns: (transfer none) (nullable): an #IdeTreeNode or %NULL
+ *
+ * Since: 3.32
+ */
+IdeTreeNode *
+ide_tree_get_selected_node (IdeTree *self)
+{
+  GtkTreeSelection *selection;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_return_val_if_fail (IDE_IS_TREE (self), NULL);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self));
+
+  if (gtk_tree_selection_get_selected (selection, &model, &iter) && IDE_IS_TREE_MODEL (model))
+    return ide_tree_model_get_node (IDE_TREE_MODEL (model), &iter);
+
+  return NULL;
+}
+
+void
+ide_tree_select_node (IdeTree     *self,
+                      IdeTreeNode *node)
+{
+  g_return_if_fail (IDE_IS_TREE (self));
+  g_return_if_fail (!node || IDE_IS_TREE_NODE (node));
+
+  if (node == NULL)
+    ide_tree_unselect (self);
+  else
+    ide_tree_select (self, node);
+}
+
+static void
+ide_tree_expand_node_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  IdeTreeModel *model = (IdeTreeModel *)object;
+  g_autoptr(IdeTask) task = user_data;
+
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (ide_tree_model_expand_finish (model, result, NULL))
+    {
+      g_autoptr(GtkTreePath) path = NULL;
+      IdeTreeNode *node;
+      IdeTree *self;
+
+      self = ide_task_get_source_object (task);
+      node = ide_task_get_task_data (task);
+
+      g_assert (IDE_IS_TREE (self));
+      g_assert (IDE_IS_TREE_NODE (node));
+
+      if ((path = ide_tree_node_get_path (node)))
+        gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+void
+ide_tree_expand_node (IdeTree     *self,
+                      IdeTreeNode *node)
+{
+  g_autoptr(IdeTask) task = NULL;
+  IdeTreeModel *model;
+
+  g_return_if_fail (IDE_IS_TREE (self));
+
+  if (!(model = ide_tree_get_model (self)))
+    return;
+
+  task = ide_task_new (self, NULL, NULL, NULL);
+  ide_task_set_source_tag (task, ide_tree_expand_node);
+  ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+
+  ide_tree_model_expand_async (model,
+                               node,
+                               NULL,
+                               ide_tree_expand_node_cb,
+                               g_steal_pointer (&task));
+}
+
+gboolean
+ide_tree_node_expanded (IdeTree     *self,
+                        IdeTreeNode *node)
+{
+  g_autoptr(GtkTreePath) path = NULL;
+
+  g_return_val_if_fail (IDE_IS_TREE (self), FALSE);
+  g_return_val_if_fail (!node || IDE_IS_TREE_NODE (node), FALSE);
+
+  if (node == NULL)
+    return FALSE;
+
+  if (!(path = ide_tree_node_get_path (node)))
+    return FALSE;
+
+  return gtk_tree_view_row_expanded (GTK_TREE_VIEW (self), path);
+}
+
+void
+ide_tree_collapse_node (IdeTree     *self,
+                        IdeTreeNode *node)
+{
+  IdeTreeModel *model;
+  g_autoptr(GtkTreePath) path = NULL;
+
+  g_return_if_fail (IDE_IS_TREE (self));
+
+  if (!(model = ide_tree_get_model (self)))
+    return;
+
+  if ((path = ide_tree_node_get_path (node)))
+    gtk_tree_view_collapse_row (GTK_TREE_VIEW (self), path);
+}
+
+GdkDragAction
+_ide_tree_get_drop_actions (IdeTree *self)
+{
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+
+  g_return_val_if_fail (IDE_IS_TREE (self), 0);
+
+  return priv->drop_action;
+}
+
+IdeTreeNode *
+_ide_tree_get_drop_node (IdeTree *self)
+{
+  IdeTreePrivate *priv = ide_tree_get_instance_private (self);
+  g_autoptr(GtkTreePath) copy = NULL;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_return_val_if_fail (IDE_IS_TREE (self), NULL);
+
+  if (priv->drop_path == NULL)
+    return NULL;
+
+  copy = gtk_tree_path_copy (priv->drop_path);
+
+  if (priv->drop_pos == GTK_TREE_VIEW_DROP_BEFORE ||
+      priv->drop_pos == GTK_TREE_VIEW_DROP_AFTER)
+    {
+      if (gtk_tree_path_get_depth (copy) > 1)
+        gtk_tree_path_up (copy);
+    }
+
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (self));
+
+  if (gtk_tree_model_get_iter (model, &iter, copy))
+    {
+      IdeTreeNode *node = iter.user_data;
+
+      if (IDE_IS_TREE_NODE (node))
+        {
+          if (ide_tree_node_is_empty (node))
+            node = ide_tree_node_get_parent (node);
+        }
+
+      return node;
+    }
+
+  return NULL;
+}
diff --git a/src/libide/tree/ide-tree.h b/src/libide/tree/ide-tree.h
new file mode 100644
index 000000000..0b4c16b6e
--- /dev/null
+++ b/src/libide/tree/ide-tree.h
@@ -0,0 +1,67 @@
+/* ide-tree.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+#include <libide-core.h>
+
+#include "ide-tree-node.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_TREE (ide_tree_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_DERIVABLE_TYPE (IdeTree, ide_tree, IDE, TREE, GtkTreeView)
+
+struct _IdeTreeClass
+{
+  GtkTreeViewClass parent_type;
+
+  /*< private >*/
+  gpointer _reserved[16];
+};
+
+IDE_AVAILABLE_IN_3_32
+GtkWidget   *ide_tree_new                  (void);
+IDE_AVAILABLE_IN_3_32
+void         ide_tree_set_context_menu     (IdeTree     *self,
+                                            GMenu       *menu);
+IDE_AVAILABLE_IN_3_32
+void         ide_tree_show_popover_at_node (IdeTree     *self,
+                                            IdeTreeNode *node,
+                                            GtkPopover  *popover);
+IDE_AVAILABLE_IN_3_32
+IdeTreeNode *ide_tree_get_selected_node    (IdeTree     *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_tree_select_node          (IdeTree     *self,
+                                            IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void         ide_tree_expand_node          (IdeTree     *self,
+                                            IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+void         ide_tree_collapse_node        (IdeTree     *self,
+                                            IdeTreeNode *node);
+IDE_AVAILABLE_IN_3_32
+gboolean     ide_tree_node_expanded        (IdeTree     *self,
+                                            IdeTreeNode *node);
+
+G_END_DECLS
diff --git a/src/libide/tree/libide-tree.h b/src/libide/tree/libide-tree.h
new file mode 100644
index 000000000..515828d11
--- /dev/null
+++ b/src/libide/tree/libide-tree.h
@@ -0,0 +1,36 @@
+/* libide-tree.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TREE_INSIDE
+
+#include "ide-tree.h"
+#include "ide-tree-addin.h"
+#include "ide-tree-model.h"
+#include "ide-tree-node.h"
+
+#undef IDE_TREE_INSIDE
+
+G_END_DECLS
diff --git a/src/libide/tree/meson.build b/src/libide/tree/meson.build
new file mode 100644
index 000000000..8e47f196d
--- /dev/null
+++ b/src/libide/tree/meson.build
@@ -0,0 +1,62 @@
+libide_tree_header_subdir = join_paths(libide_header_subdir, 'tree')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_tree_public_headers = [
+  'ide-tree.h',
+  'ide-tree-addin.h',
+  'ide-tree-model.h',
+  'ide-tree-node.h',
+  'libide-tree.h',
+]
+
+install_headers(libide_tree_public_headers, subdir: libide_tree_header_subdir)
+
+#
+# Sources
+#
+
+libide_tree_public_sources = [
+  'ide-tree.c',
+  'ide-tree-addin.c',
+  'ide-tree-model.c',
+  'ide-tree-node.c',
+]
+
+libide_tree_sources = libide_tree_public_sources
+
+#
+# Dependencies
+#
+
+libide_tree_deps = [
+  libgtk_dep,
+  libpeas_dep,
+
+  libide_core_dep,
+  libide_plugins_dep,
+  libide_threading_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_tree = static_library('ide-tree-' + libide_api_version, libide_tree_sources,
+   dependencies: libide_tree_deps,
+         c_args: libide_args + release_args + ['-DIDE_TREE_COMPILATION'],
+)
+
+libide_tree_dep = declare_dependency(
+         dependencies: libide_tree_deps,
+           link_whole: libide_tree,
+  include_directories: include_directories('.'),
+)
+
+gnome_builder_public_sources += files(libide_tree_public_sources)
+gnome_builder_public_headers += files(libide_tree_public_headers)
+gnome_builder_include_subdirs += libide_tree_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-tree.h', '-DIDE_TREE_COMPILATION']
diff --git a/src/libide/vcs/ide-directory-vcs.c b/src/libide/vcs/ide-directory-vcs.c
new file mode 100644
index 000000000..6311168d8
--- /dev/null
+++ b/src/libide/vcs/ide-directory-vcs.c
@@ -0,0 +1,180 @@
+/* ide-directory-vcs.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-directory-vcs"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+
+#include "ide-context.h"
+
+#include "ide-directory-vcs.h"
+
+struct _IdeDirectoryVcs
+{
+  IdeObject  parent_instances;
+  GFile     *workdir;
+};
+
+#define LOAD_MAX_FILES 5000
+
+static void vcs_iface_init (IdeVcsInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (IdeDirectoryVcs, ide_directory_vcs, IDE_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_VCS, vcs_iface_init))
+
+enum {
+  PROP_0,
+  N_PROPS,
+
+  /* Override Properties */
+  PROP_BRANCH_NAME,
+  PROP_WORKDIR,
+};
+
+static gchar *
+ide_directory_vcs_get_branch_name (IdeVcs *vcs)
+{
+  return g_strdup (_("unversioned"));
+}
+
+static GFile *
+ide_directory_vcs_get_workdir (IdeVcs *vcs)
+{
+  IdeDirectoryVcs *self = (IdeDirectoryVcs *)vcs;
+
+  g_return_val_if_fail (IDE_IS_DIRECTORY_VCS (vcs), NULL);
+
+  /* Note: This function is expected to be thread-safe for
+   *       those holding a reference to @vcs. So
+   *       @workdir cannot be changed after creation
+   *       and must be valid for the lifetime of @vcs.
+   */
+
+  return self->workdir;
+}
+
+static gboolean
+ide_directory_vcs_is_ignored (IdeVcs  *vcs,
+                              GFile   *file,
+                              GError **error)
+{
+  g_autofree gchar *reversed = NULL;
+
+  g_assert (IDE_IS_VCS (vcs));
+  g_assert (G_IS_FILE (file));
+
+  reversed = g_strreverse (g_file_get_basename (file));
+
+  /* check suffixes, in reverse */
+  if ((reversed [0] == '~') ||
+      (strncmp (reversed, "al.", 3) == 0) ||        /* .la */
+      (strncmp (reversed, "ol.", 3) == 0) ||        /* .lo */
+      (strncmp (reversed, "o.", 2) == 0) ||         /* .o */
+      (strncmp (reversed, "pws.", 4) == 0) ||       /* .swp */
+      (strncmp (reversed, "sped.", 5) == 0) ||      /* .deps */
+      (strncmp (reversed, "sbil.", 5) == 0) ||      /* .libs */
+      (strncmp (reversed, "cyp.", 4) == 0) ||       /* .pyc */
+      (strncmp (reversed, "oyp.", 4) == 0) ||       /* .pyo */
+      (strncmp (reversed, "omg.", 4) == 0) ||       /* .gmo */
+      (strncmp (reversed, "tig.", 4) == 0) ||       /* .git */
+      (strncmp (reversed, "rzb.", 4) == 0) ||       /* .bzr */
+      (strncmp (reversed, "nvs.", 4) == 0) ||       /* .svn */
+      (strncmp (reversed, "pmatsrid.", 9) == 0) ||  /* .dirstamp */
+      (strncmp (reversed, "hcg.", 4) == 0))         /* .gch */
+    return TRUE;
+
+  return FALSE;
+}
+
+static void
+ide_directory_vcs_dispose (GObject *object)
+{
+  IdeDirectoryVcs *self = (IdeDirectoryVcs *)object;
+
+  g_clear_object (&self->workdir);
+
+  G_OBJECT_CLASS (ide_directory_vcs_parent_class)->dispose (object);
+}
+
+static void
+ide_directory_vcs_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  IdeDirectoryVcs *self = IDE_DIRECTORY_VCS (object);
+
+  switch (prop_id)
+    {
+    case PROP_BRANCH_NAME:
+      g_value_take_string (value, ide_directory_vcs_get_branch_name (IDE_VCS (self)));
+      break;
+
+    case PROP_WORKDIR:
+      g_value_set_object (value, ide_directory_vcs_get_workdir (IDE_VCS (self)));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_directory_vcs_class_init (IdeDirectoryVcsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = ide_directory_vcs_dispose;
+  object_class->get_property = ide_directory_vcs_get_property;
+
+  g_object_class_override_property (object_class, PROP_BRANCH_NAME, "branch-name");
+  g_object_class_override_property (object_class, PROP_WORKDIR, "workdir");
+}
+
+static void
+ide_directory_vcs_init (IdeDirectoryVcs *self)
+{
+}
+
+static gint
+ide_directory_vcs_get_priority (IdeVcs *vcs)
+{
+  return G_MAXINT;
+}
+
+static void
+vcs_iface_init (IdeVcsInterface *iface)
+{
+  iface->get_workdir = ide_directory_vcs_get_workdir;
+  iface->is_ignored = ide_directory_vcs_is_ignored;
+  iface->get_priority = ide_directory_vcs_get_priority;
+  iface->get_branch_name = ide_directory_vcs_get_branch_name;
+}
+
+IdeDirectoryVcs *
+ide_directory_vcs_new (GFile *workdir)
+{
+  IdeDirectoryVcs *self = g_object_new (IDE_TYPE_DIRECTORY_VCS, NULL);
+  self->workdir = g_file_dup (workdir);
+  return self;
+}
diff --git a/src/libide/vcs/ide-directory-vcs.h b/src/libide/vcs/ide-directory-vcs.h
new file mode 100644
index 000000000..3f1b9f575
--- /dev/null
+++ b/src/libide/vcs/ide-directory-vcs.h
@@ -0,0 +1,36 @@
+/* ide-directory-vcs.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-core.h>
+
+#include "ide-vcs.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DIRECTORY_VCS (ide_directory_vcs_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_FINAL_TYPE (IdeDirectoryVcs, ide_directory_vcs, IDE, DIRECTORY_VCS, IdeObject)
+
+IdeDirectoryVcs *ide_directory_vcs_new (GFile *workdir);
+
+G_END_DECLS
diff --git a/src/libide/vcs/ide-vcs-cloner.c b/src/libide/vcs/ide-vcs-cloner.c
new file mode 100644
index 000000000..b1c798e89
--- /dev/null
+++ b/src/libide/vcs/ide-vcs-cloner.c
@@ -0,0 +1,148 @@
+/* ide-vcs-cloner.c
+ *
+ * Copyright 2018-2019 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 "ide-vcs-cloner"
+
+#include "config.h"
+
+#include <libide-threading.h>
+
+#include "ide-vcs-cloner.h"
+
+G_DEFINE_INTERFACE (IdeVcsCloner, ide_vcs_cloner, G_TYPE_OBJECT)
+
+static void
+ide_vcs_cloner_default_init (IdeVcsClonerInterface *iface)
+{
+}
+
+/**
+ * ide_vcs_cloner_validate_uri:
+ * @self: a #IdeVcsCloner
+ * @uri: a string containing the URI to validate
+ * @errmsg: (out) (optional): a location for an error message
+ *
+ * Checks to see if @uri is valid, and if not, sets @errmsg to a string
+ * describing how the URI is invalid.
+ *
+ * Returns: %TRUE if @uri is valid, otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_vcs_cloner_validate_uri (IdeVcsCloner  *self,
+                             const gchar   *uri,
+                             gchar        **errmsg)
+{
+  g_return_val_if_fail (IDE_IS_VCS_CLONER (self), FALSE);
+
+  if (errmsg != NULL)
+    *errmsg = NULL;
+
+  if (IDE_VCS_CLONER_GET_IFACE (self)->validate_uri)
+    return IDE_VCS_CLONER_GET_IFACE (self)->validate_uri (self, uri, errmsg);
+
+  return FALSE;
+}
+
+/**
+ * ide_vcs_cloner_clone_async:
+ * @self: an #IdeVcsCloner
+ * @uri: a string containing the URI
+ * @destination: a string containing the destination path
+ * @options: a #GVariantDict containing any user supplied options
+ * @cancellable: (nullable): a #GCancellable
+ * @progress: (out) (optional): a location for an #IdeNotification, or %NULL
+ * @callback: a #GAsyncReadyCallback to execute upon completion
+ * @user_data: closure data for @callback
+ *
+ * Since: 3.32
+ */
+void
+ide_vcs_cloner_clone_async (IdeVcsCloner         *self,
+                            const gchar          *uri,
+                            const gchar          *destination,
+                            GVariantDict         *options,
+                            GCancellable         *cancellable,
+                            IdeNotification     **progress,
+                            GAsyncReadyCallback   callback,
+                            gpointer              user_data)
+{
+  g_return_if_fail (IDE_IS_VCS_CLONER (self));
+  g_return_if_fail (uri != NULL);
+  g_return_if_fail (destination != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (progress != NULL)
+    *progress = NULL;
+
+  IDE_VCS_CLONER_GET_IFACE (self)->clone_async (self,
+                                                uri,
+                                                destination,
+                                                options,
+                                                cancellable,
+                                                progress,
+                                                callback,
+                                                user_data);
+}
+
+/**
+ * ide_vcs_cloner_clone_finish:
+ * @self: an #IdeVcsCloner
+ * @result: a #GAsyncResult provided to callback
+ * @error: a location for a #GError, or %NULL
+ *
+ * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
+ */
+gboolean
+ide_vcs_cloner_clone_finish (IdeVcsCloner  *self,
+                             GAsyncResult  *result,
+                             GError       **error)
+{
+  g_return_val_if_fail (IDE_IS_VCS_CLONER (self), FALSE);
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), FALSE);
+
+  return IDE_VCS_CLONER_GET_IFACE (self)->clone_finish (self, result, error);
+}
+
+/**
+ * ide_vcs_cloner_get_title:
+ * @self: a #IdeVcsCloner
+ *
+ * Gets the for the cloner, such as "Git". This may be used to present
+ * a selector to the user based on the backend clone engine. Other suitable
+ * titles might be "Subversion" or "CVS".
+ *
+ * Returns: (transfer full): a string containing the title
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_vcs_cloner_get_title (IdeVcsCloner *self)
+{
+  g_return_val_if_fail (IDE_IS_VCS_CLONER (self), NULL);
+
+  if (IDE_VCS_CLONER_GET_IFACE (self)->get_title)
+    return IDE_VCS_CLONER_GET_IFACE (self)->get_title (self);
+
+  return NULL;
+}
diff --git a/src/libide/vcs/ide-vcs-cloner.h b/src/libide/vcs/ide-vcs-cloner.h
new file mode 100644
index 000000000..69e464c9f
--- /dev/null
+++ b/src/libide/vcs/ide-vcs-cloner.h
@@ -0,0 +1,73 @@
+/* ide-vcs-cloner.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_VCS_CLONER (ide_vcs_cloner_get_type())
+
+IDE_AVAILABLE_IN_3_32
+G_DECLARE_INTERFACE (IdeVcsCloner, ide_vcs_cloner, IDE, VCS_CLONER, GObject)
+
+struct _IdeVcsClonerInterface
+{
+  GTypeInterface parent_iface;
+
+  gchar    *(*get_title)    (IdeVcsCloner         *self);
+  gboolean  (*validate_uri) (IdeVcsCloner         *self,
+                             const gchar          *uri,
+                             gchar               **errmsg);
+  void      (*clone_async)  (IdeVcsCloner         *self,
+                             const gchar          *uri,
+                             const gchar          *destination,
+                             GVariantDict         *options,
+                             GCancellable         *cancellable,
+                             IdeNotification     **progress,
+                             GAsyncReadyCallback   callback,
+                             gpointer              user_data);
+  gboolean  (*clone_finish) (IdeVcsCloner         *self,
+                             GAsyncResult         *result,
+                             GError              **error);
+};
+
+IDE_AVAILABLE_IN_3_32
+gchar   *ide_vcs_cloner_get_title    (IdeVcsCloner         *self);
+IDE_AVAILABLE_IN_3_32
+void     ide_vcs_cloner_clone_async  (IdeVcsCloner         *self,
+                                      const gchar          *uri,
+                                      const gchar          *destination,
+                                      GVariantDict         *options,
+                                      GCancellable         *cancellable,
+                                      IdeNotification     **progress,
+                                      GAsyncReadyCallback   callback,
+                                      gpointer              user_data);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_vcs_cloner_clone_finish (IdeVcsCloner         *self,
+                                      GAsyncResult         *result,
+                                      GError              **error);
+IDE_AVAILABLE_IN_3_32
+gboolean ide_vcs_cloner_validate_uri (IdeVcsCloner         *self,
+                                      const gchar          *uri,
+                                      gchar               **errmsg);
+
+G_END_DECLS
diff --git a/src/libide/vcs/ide-vcs-config.c b/src/libide/vcs/ide-vcs-config.c
index 2c0085b3a..8b6c4780a 100644
--- a/src/libide/vcs/ide-vcs-config.c
+++ b/src/libide/vcs/ide-vcs-config.c
@@ -14,13 +14,16 @@
  *
  * 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 "ide-vcs-config"
 
 #include "config.h"
 
-#include "vcs/ide-vcs-config.h"
+#include "ide-vcs-config.h"
+#include "ide-vcs-enums.h"
 
 G_DEFINE_INTERFACE (IdeVcsConfig, ide_vcs_config, G_TYPE_OBJECT)
 
diff --git a/src/libide/vcs/ide-vcs-config.h b/src/libide/vcs/ide-vcs-config.h
index e882112a2..457639791 100644
--- a/src/libide/vcs/ide-vcs-config.h
+++ b/src/libide/vcs/ide-vcs-config.h
@@ -14,19 +14,23 @@
  *
  * 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>
+#if !defined (IDE_VCS_INSIDE) && !defined (IDE_VCS_COMPILATION)
+# error "Only <libide-vcs.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_VCS_CONFIG (ide_vcs_config_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_INTERFACE (IdeVcsConfig, ide_vcs_config, IDE, VCS_CONFIG, GObject)
 
 typedef enum
@@ -47,11 +51,11 @@ struct _IdeVcsConfigInterface
                       const GValue    *value);
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void ide_vcs_config_get_config (IdeVcsConfig     *self,
                                 IdeVcsConfigType  type,
                                 GValue           *value);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void ide_vcs_config_set_config (IdeVcsConfig     *self,
                                 IdeVcsConfigType  type,
                                 const GValue     *value);
diff --git a/src/libide/vcs/ide-vcs-file-info.c b/src/libide/vcs/ide-vcs-file-info.c
index 5be771527..7a2164eeb 100644
--- a/src/libide/vcs/ide-vcs-file-info.c
+++ b/src/libide/vcs/ide-vcs-file-info.c
@@ -1,6 +1,6 @@
 /* ide-vcs-file-info.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,15 +14,16 @@
  *
  * 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 "ide-vcs-file-info"
 
 #include "config.h"
 
-#include "ide-enums.h"
-
-#include "vcs/ide-vcs-file-info.h"
+#include "ide-vcs-enums.h"
+#include "ide-vcs-file-info.h"
 
 typedef struct
 {
@@ -49,7 +50,7 @@ static GParamSpec *properties [N_PROPS];
  *
  * Returns: (transfer none): a #GFile
  *
- * Since: 3.28
+ * Since: 3.32
  */
 GFile *
 ide_vcs_file_info_get_file (IdeVcsFileInfo *self)
@@ -178,7 +179,7 @@ ide_vcs_file_info_class_init (IdeVcsFileInfoClass *klass)
                        IDE_TYPE_VCS_FILE_STATUS,
                        IDE_VCS_FILE_STATUS_UNCHANGED,
                        (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
-  
+
   g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
diff --git a/src/libide/vcs/ide-vcs-file-info.h b/src/libide/vcs/ide-vcs-file-info.h
index 94306f586..0e0600324 100644
--- a/src/libide/vcs/ide-vcs-file-info.h
+++ b/src/libide/vcs/ide-vcs-file-info.h
@@ -1,6 +1,6 @@
 /* ide-vcs-file-info.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,19 +14,23 @@
  *
  * 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>
+#if !defined (IDE_VCS_INSIDE) && !defined (IDE_VCS_COMPILATION)
+# error "Only <libide-vcs.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_VCS_FILE_INFO (ide_vcs_file_info_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_DERIVABLE_TYPE (IdeVcsFileInfo, ide_vcs_file_info, IDE, VCS_FILE_INFO, GObject)
 
 typedef enum
@@ -48,13 +52,13 @@ struct _IdeVcsFileInfoClass
   gpointer _reserved[16];
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeVcsFileInfo   *ide_vcs_file_info_new        (GFile            *file);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 GFile            *ide_vcs_file_info_get_file   (IdeVcsFileInfo   *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeVcsFileStatus  ide_vcs_file_info_get_status (IdeVcsFileInfo   *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void              ide_vcs_file_info_set_status (IdeVcsFileInfo   *self,
                                                 IdeVcsFileStatus  status);
 
diff --git a/src/libide/vcs/ide-vcs-initializer.c b/src/libide/vcs/ide-vcs-initializer.c
index 75a39b0aa..9742b845f 100644
--- a/src/libide/vcs/ide-vcs-initializer.c
+++ b/src/libide/vcs/ide-vcs-initializer.c
@@ -1,6 +1,6 @@
 /* ide-vcs-initializer.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-vcs-initializer"
 
 #include "config.h"
 
-#include "vcs/ide-vcs-initializer.h"
+#include "ide-vcs-initializer.h"
 
 G_DEFINE_INTERFACE (IdeVcsInitializer, ide_vcs_initializer, G_TYPE_OBJECT)
 
diff --git a/src/libide/vcs/ide-vcs-initializer.h b/src/libide/vcs/ide-vcs-initializer.h
index 674cd1e1c..44193b97e 100644
--- a/src/libide/vcs/ide-vcs-initializer.h
+++ b/src/libide/vcs/ide-vcs-initializer.h
@@ -1,6 +1,6 @@
 /* ide-vcs-initializer.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,19 +14,23 @@
  *
  * 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>
+#if !defined (IDE_VCS_INSIDE) && !defined (IDE_VCS_COMPILATION)
+# error "Only <libide-vcs.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_VCS_INITIALIZER (ide_vcs_initializer_get_type ())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_INTERFACE (IdeVcsInitializer, ide_vcs_initializer, IDE, VCS_INITIALIZER, GObject)
 
 struct _IdeVcsInitializerInterface
@@ -44,15 +48,15 @@ struct _IdeVcsInitializerInterface
                                  GError              **error);
 };
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gchar   *ide_vcs_initializer_get_title         (IdeVcsInitializer    *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void     ide_vcs_initializer_initialize_async  (IdeVcsInitializer    *self,
                                                 GFile                *file,
                                                 GCancellable         *cancellable,
                                                 GAsyncReadyCallback   callback,
                                                 gpointer              user_data);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gboolean ide_vcs_initializer_initialize_finish (IdeVcsInitializer    *self,
                                                 GAsyncResult         *result,
                                                 GError              **error);
diff --git a/src/libide/vcs/ide-vcs-monitor.c b/src/libide/vcs/ide-vcs-monitor.c
index 7602aa39d..5bb44902b 100644
--- a/src/libide/vcs/ide-vcs-monitor.c
+++ b/src/libide/vcs/ide-vcs-monitor.c
@@ -1,6 +1,6 @@
 /* ide-vcs-monitor.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-vcs-monitor"
@@ -21,20 +23,21 @@
 #include "config.h"
 
 #include <dazzle.h>
+#include <libide-core.h>
 
-#include "ide-context.h"
-#include "ide-debug.h"
-
-#include "vcs/ide-vcs.h"
-#include "vcs/ide-vcs-file-info.h"
-#include "vcs/ide-vcs-monitor.h"
+#include "ide-vcs.h"
+#include "ide-vcs-file-info.h"
+#include "ide-vcs-monitor.h"
 
 struct _IdeVcsMonitor
 {
   IdeObject                parent_instance;
 
   GFile                   *root;
+  IdeVcs                  *vcs;
+  DzlSignalGroup          *vcs_signals;
   DzlRecursiveFileMonitor *monitor;
+  DzlSignalGroup          *monitor_signals;
   GHashTable              *status_by_file;
 
   guint                    cache_source;
@@ -53,6 +56,7 @@ enum {
 enum {
   PROP_0,
   PROP_ROOT,
+  PROP_VCS,
   N_PROPS
 };
 
@@ -60,13 +64,14 @@ static GParamSpec *properties [N_PROPS];
 static guint signals [N_SIGNALS];
 
 static void
-ide_vcs_monitor_add_parents (GHashTable       *hash,
-                             GFile            *file,
-                             GFile            *toplevel,
-                             IdeVcsFileStatus  status)
+ide_vcs_monitor_add_parents_locked (GHashTable       *hash,
+                                    GFile            *file,
+                                    GFile            *toplevel,
+                                    IdeVcsFileStatus  status)
 {
   GFile *parent;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (hash != NULL);
   g_assert (G_IS_FILE (file));
   g_assert (G_IS_FILE (toplevel));
@@ -108,74 +113,77 @@ ide_vcs_monitor_list_status_cb (GObject      *object,
   IdeVcs *vcs = (IdeVcs *)object;
   g_autoptr(IdeVcsMonitor) self = user_data;
   g_autoptr(GListModel) model = NULL;
-  g_autoptr(GHashTable) status_by_file = NULL;
-  GFile *workdir;
-  guint n_items;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_VCS (vcs));
   g_assert (IDE_IS_VCS_MONITOR (self));
 
+  ide_object_lock (IDE_OBJECT (self));
+
   self->busy = FALSE;
 
-  model = ide_vcs_list_status_finish (vcs, result, NULL);
-  if (model == NULL)
-    return;
+  if ((model = ide_vcs_list_status_finish (vcs, result, NULL)))
+    {
+      g_autoptr(GHashTable) status_by_file = NULL;
+      guint n_items;
 
-  n_items = g_list_model_get_n_items (model);
-  workdir = ide_vcs_get_working_directory (vcs);
-  status_by_file = g_hash_table_new_full (g_file_hash,
-                                          (GEqualFunc) g_file_equal,
-                                          g_object_unref,
-                                          g_object_unref);
+      n_items = g_list_model_get_n_items (model);
+      status_by_file = g_hash_table_new_full (g_file_hash,
+                                              (GEqualFunc)g_file_equal,
+                                              g_object_unref,
+                                              g_object_unref);
 
-  for (guint i = 0; i < n_items; i++)
-    {
-      g_autoptr(IdeVcsFileInfo) info = NULL;
-      IdeVcsFileStatus status;
-      GFile *file;
+      for (guint i = 0; i < n_items; i++)
+        {
+          g_autoptr(IdeVcsFileInfo) info = NULL;
+          IdeVcsFileStatus status;
+          GFile *file;
 
-      info = g_list_model_get_item (model, i);
-      file = ide_vcs_file_info_get_file (info);
-      status = ide_vcs_file_info_get_status (info);
+          info = g_list_model_get_item (model, i);
+          file = ide_vcs_file_info_get_file (info);
+          status = ide_vcs_file_info_get_status (info);
 
-      g_hash_table_insert (status_by_file,
-                           g_file_dup (file),
-                           g_steal_pointer (&info));
+          g_hash_table_insert (status_by_file,
+                               g_file_dup (file),
+                               g_steal_pointer (&info));
 
-      ide_vcs_monitor_add_parents (status_by_file, file, workdir, status);
-    }
+          ide_vcs_monitor_add_parents_locked (status_by_file, file, self->root, status);
+        }
 
-  g_clear_pointer (&self->status_by_file, g_hash_table_unref);
-  self->status_by_file = g_steal_pointer (&status_by_file);
+      g_clear_pointer (&self->status_by_file, g_hash_table_unref);
+      self->status_by_file = g_steal_pointer (&status_by_file);
+
+      g_signal_emit (self, signals [RELOADED], 0);
+    }
 
-  g_signal_emit (self, signals[RELOADED], 0);
+  ide_object_unlock (IDE_OBJECT (self));
 }
 
 static gboolean
 ide_vcs_monitor_cache_cb (gpointer data)
 {
   IdeVcsMonitor *self = data;
-  IdeContext *context;
-  IdeVcs *vcs;
-  GFile *workdir;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_VCS_MONITOR (self));
 
-  self->cache_source = 0;
+  ide_object_lock (IDE_OBJECT (self));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  self->cache_source = 0;
 
-  self->busy = TRUE;
+  if (self->vcs != NULL)
+    {
+      self->busy = TRUE;
+      ide_vcs_list_status_async (self->vcs,
+                                 self->root,
+                                 TRUE,
+                                 G_PRIORITY_LOW,
+                                 NULL,
+                                 ide_vcs_monitor_list_status_cb,
+                                 g_object_ref (self));
+    }
 
-  ide_vcs_list_status_async (vcs,
-                             workdir,
-                             TRUE,
-                             G_PRIORITY_LOW,
-                             NULL,
-                             ide_vcs_monitor_list_status_cb,
-                             g_object_ref (self));
+  ide_object_unlock (IDE_OBJECT (self));
 
   return G_SOURCE_REMOVE;
 }
@@ -183,13 +191,16 @@ ide_vcs_monitor_cache_cb (gpointer data)
 static void
 ide_vcs_monitor_queue_reload (IdeVcsMonitor *self)
 {
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_VCS_MONITOR (self));
 
+  ide_object_lock (IDE_OBJECT (self));
   if (self->cache_source == 0 && !self->busy)
     self->cache_source = g_idle_add_full (G_PRIORITY_LOW,
                                           ide_vcs_monitor_cache_cb,
                                           g_object_ref (self),
                                           g_object_unref);
+  ide_object_unlock (IDE_OBJECT (self));
 }
 
 static void
@@ -201,6 +212,7 @@ ide_vcs_monitor_changed_cb (IdeVcsMonitor           *self,
 {
   IDE_ENTRY;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_VCS_MONITOR (self));
   g_assert (G_IS_FILE (file));
   g_assert (!other_file || G_IS_FILE (other_file));
@@ -219,12 +231,15 @@ ide_vcs_monitor_vcs_changed_cb (IdeVcsMonitor *self,
 {
   IDE_ENTRY;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_VCS_MONITOR (self));
   g_assert (IDE_IS_VCS (vcs));
 
   /* Everything is invalidated by new VCS index, reload now */
+  ide_object_lock (IDE_OBJECT (self));
   g_clear_pointer (&self->status_by_file, g_hash_table_unref);
   ide_vcs_monitor_queue_reload (self);
+  ide_object_unlock (IDE_OBJECT (self));
 
   IDE_EXIT;
 }
@@ -234,15 +249,15 @@ ide_vcs_monitor_ignore_func (GFile    *file,
                              gpointer  data)
 {
   IdeVcsMonitor *self = data;
-  IdeContext *context;
-  IdeVcs *vcs;
+  gboolean ret;
 
   g_assert (IDE_IS_VCS_MONITOR (self));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
+  ide_object_lock (IDE_OBJECT (self));
+  ret = ide_vcs_is_ignored (self->vcs, file, NULL);
+  ide_object_unlock (IDE_OBJECT (self));
 
-  return ide_vcs_is_ignored (vcs, file, NULL);
+  return ret;
 }
 
 static void
@@ -254,6 +269,7 @@ ide_vcs_monitor_start_cb (GObject      *object,
   g_autoptr(IdeVcsMonitor) self = user_data;
   g_autoptr(GError) error = NULL;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (DZL_IS_RECURSIVE_FILE_MONITOR (monitor));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_VCS_MONITOR (self));
@@ -265,47 +281,41 @@ ide_vcs_monitor_start_cb (GObject      *object,
 }
 
 static void
-ide_vcs_monitor_constructed (GObject *object)
+ide_vcs_monitor_maybe_reload_locked (IdeVcsMonitor *self)
 {
-  IdeVcsMonitor *self = (IdeVcsMonitor *)object;
-  IdeContext *context;
-  IdeVcs *vcs;
-
-  G_OBJECT_CLASS (ide_vcs_monitor_parent_class)->constructed (object);
-
-  context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-
-  g_signal_connect_object (vcs,
-                           "changed",
-                           G_CALLBACK (ide_vcs_monitor_vcs_changed_cb),
-                           self,
-                           G_CONNECT_SWAPPED);
-
-  self->monitor = dzl_recursive_file_monitor_new (self->root);
+  g_assert (IDE_IS_VCS_MONITOR (self));
 
-  dzl_recursive_file_monitor_set_ignore_func (self->monitor,
-                                              ide_vcs_monitor_ignore_func,
-                                              self, NULL);
+  g_clear_pointer (&self->status_by_file, g_hash_table_unref);
+  g_clear_handle_id (&self->cache_source, g_source_remove);
 
-  g_signal_connect_object (self->monitor,
-                           "changed",
-                           G_CALLBACK (ide_vcs_monitor_changed_cb),
-                           self,
-                           G_CONNECT_SWAPPED);
+  if (self->monitor)
+    {
+      dzl_signal_group_set_target (self->monitor_signals, NULL);
+      dzl_recursive_file_monitor_set_ignore_func (self->monitor, NULL, NULL, NULL);
+      dzl_recursive_file_monitor_cancel (self->monitor);
+      g_clear_object (&self->monitor);
+    }
 
-  dzl_recursive_file_monitor_start_async (self->monitor,
-                                          NULL,
-                                          ide_vcs_monitor_start_cb,
-                                          g_object_ref (self));
+  if (G_IS_FILE (self->root) && IDE_IS_VCS (self->vcs))
+    {
+      self->monitor = dzl_recursive_file_monitor_new (self->root);
+      dzl_recursive_file_monitor_set_ignore_func (self->monitor,
+                                                  ide_vcs_monitor_ignore_func,
+                                                  self, NULL);
+      dzl_signal_group_set_target (self->monitor_signals, self->monitor);
+      dzl_recursive_file_monitor_start_async (self->monitor,
+                                              NULL,
+                                              ide_vcs_monitor_start_cb,
+                                              g_object_ref (self));
+    }
 }
 
 static void
-ide_vcs_monitor_dispose (GObject *object)
+ide_vcs_monitor_destroy (IdeObject *object)
 {
   IdeVcsMonitor *self = (IdeVcsMonitor *)object;
 
-  dzl_clear_source (&self->cache_source);
+  g_clear_handle_id (&self->cache_source, g_source_remove);
   g_clear_pointer (&self->status_by_file, g_hash_table_unref);
 
   if (self->monitor != NULL)
@@ -315,9 +325,25 @@ ide_vcs_monitor_dispose (GObject *object)
       g_clear_object (&self->monitor);
     }
 
+  dzl_signal_group_set_target (self->monitor_signals, NULL);
+  dzl_signal_group_set_target (self->vcs_signals, NULL);
+
+  g_clear_object (&self->vcs);
+
+  IDE_OBJECT_CLASS (ide_vcs_monitor_parent_class)->destroy (object);
+}
+
+static void
+ide_vcs_monitor_finalize (GObject *object)
+{
+  IdeVcsMonitor *self = (IdeVcsMonitor *)object;
+
+  g_clear_pointer (&self->status_by_file, g_hash_table_unref);
   g_clear_object (&self->root);
+  g_clear_object (&self->monitor_signals);
+  g_clear_object (&self->vcs_signals);
 
-  G_OBJECT_CLASS (ide_vcs_monitor_parent_class)->dispose (object);
+  G_OBJECT_CLASS (ide_vcs_monitor_parent_class)->finalize (object);
 }
 
 static void
@@ -331,7 +357,11 @@ ide_vcs_monitor_get_property (GObject    *object,
   switch (prop_id)
     {
     case PROP_ROOT:
-      g_value_set_object (value, self->root);
+      g_value_take_object (value, ide_vcs_monitor_ref_root (self));
+      break;
+
+    case PROP_VCS:
+      g_value_take_object (value, ide_vcs_monitor_ref_vcs (self));
       break;
 
     default:
@@ -350,7 +380,11 @@ ide_vcs_monitor_set_property (GObject      *object,
   switch (prop_id)
     {
     case PROP_ROOT:
-      self->root = g_value_dup_object (value);
+      ide_vcs_monitor_set_root (self, g_value_get_object (value));
+      break;
+
+    case PROP_VCS:
+      ide_vcs_monitor_set_vcs (self, g_value_get_object (value));
       break;
 
     default:
@@ -362,21 +396,59 @@ static void
 ide_vcs_monitor_class_init (IdeVcsMonitorClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
 
-  object_class->constructed = ide_vcs_monitor_constructed;
-  object_class->dispose = ide_vcs_monitor_dispose;
+  object_class->finalize = ide_vcs_monitor_finalize;
   object_class->get_property = ide_vcs_monitor_get_property;
   object_class->set_property = ide_vcs_monitor_set_property;
 
+  i_object_class->destroy = ide_vcs_monitor_destroy;
+
+  /**
+   * IdeVcsMonitor:root:
+   *
+   * The "root" property is the root of the file-system to begin
+   * monitoring for changes.
+   *
+   * Since: 3.32
+   */
   properties [PROP_ROOT] =
     g_param_spec_object ("root",
                          "Root",
                          "The root of the directory tree",
                          G_TYPE_FILE,
-                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
-  
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * IdeVcsMonitor:vcs:
+   *
+   * The "vcs" property is the version control system to be queried for
+   * additional status information when a file has been discovered to
+   * have been changed.
+   *
+   * Since: 3.32
+   */
+  properties [PROP_VCS] =
+    g_param_spec_object ("vcs",
+                         "VCS",
+                         "The version control system in use",
+                         IDE_TYPE_VCS,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
+  /**
+   * IdeVcsMonitor::changed:
+   * @self: an #IdeVcsMonitor
+   * @file: a #GFile
+   * @other_file: (nullable): a #GFile or %NULL
+   * @event: a #GFileMonitorEvent
+   *
+   * The "changed" signal is emitted when a file has been discovered to
+   * have been changed on disk.
+   *
+   * Since: 3.32
+   */
   signals [CHANGED] =
     g_signal_new ("changed",
                   G_TYPE_FROM_CLASS (klass),
@@ -389,6 +461,14 @@ ide_vcs_monitor_class_init (IdeVcsMonitorClass *klass)
                   G_TYPE_FILE | G_SIGNAL_TYPE_STATIC_SCOPE,
                   G_TYPE_FILE_MONITOR_EVENT);
 
+  /**
+   * IdeVcsMonitor::reloaded:
+   * @self: an #IdeVcsMonitor
+   *
+   * The "reloaded" signal is emitted when the monitor has been reloaded.
+   *
+   * Since: 3.32
+   */
   signals [RELOADED] =
     g_signal_new ("reloaded",
                   G_TYPE_FROM_CLASS (klass),
@@ -399,10 +479,25 @@ ide_vcs_monitor_class_init (IdeVcsMonitorClass *klass)
 static void
 ide_vcs_monitor_init (IdeVcsMonitor *self)
 {
+  self->monitor_signals = dzl_signal_group_new (DZL_TYPE_RECURSIVE_FILE_MONITOR);
+
+  dzl_signal_group_connect_object (self->monitor_signals,
+                                   "changed",
+                                   G_CALLBACK (ide_vcs_monitor_changed_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  self->vcs_signals = dzl_signal_group_new (IDE_TYPE_VCS);
+
+  dzl_signal_group_connect_object (self->vcs_signals,
+                                   "changed",
+                                   G_CALLBACK (ide_vcs_monitor_vcs_changed_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
 }
 
 /**
- * ide_vcs_monitor_get_info:
+ * ide_vcs_monitor_ref_info:
  * @self: a #IdeVcsMonitor
  * @file: a #GFile
  *
@@ -411,25 +506,112 @@ ide_vcs_monitor_init (IdeVcsMonitor *self)
  * If the file information has not been loaded, %NULL is returned. You
  * can wait for #IdeVcsMonitor::reloaded and query again if you expect
  * the info to be there.
- * 
+ *
  * Returns: (transfer full) (nullable): an #IdeVcsFileInfo or %NULL
  *
- * Since: 3.28
+ * Since: 3.32
  */
 IdeVcsFileInfo *
-ide_vcs_monitor_get_info (IdeVcsMonitor *self,
+ide_vcs_monitor_ref_info (IdeVcsMonitor *self,
                           GFile         *file)
 {
-  IdeVcsFileInfo *info;
+  IdeVcsFileInfo *info = NULL;
+
+  g_return_val_if_fail (IDE_IS_VCS_MONITOR (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (self->status_by_file != NULL)
+    {
+      if ((info = g_hash_table_lookup (self->status_by_file, file)))
+        g_object_ref (info);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&info);
+}
+
+/**
+ * ide_vcs_monitor_ref_vcs:
+ * @self: a #IdeVcsMonitor
+ *
+ * Increments the reference count of the #IdeVcs monitored using the
+ * #IdeVcsMonitor and returns it.
+ *
+ * Returns: (transfer full) (nullable): an #IdeVcs or %NULL
+ *
+ * Since: 3.32
+ */
+IdeVcs *
+ide_vcs_monitor_ref_vcs (IdeVcsMonitor *self)
+{
+  IdeVcs *ret;
+
+  g_return_val_if_fail (IDE_IS_VCS_MONITOR (self), NULL);
+
+  ide_object_lock (IDE_OBJECT (self));
+  ret = self->vcs ? g_object_ref (self->vcs) : NULL;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return g_steal_pointer (&ret);
+}
+
+/**
+ * ide_vcs_monitor_ref_root:
+ * @self: a #IdeVcsMonitor
+ *
+ * Gets the #IdeVcsMonitor:root property and increments the reference
+ * count of the #GFile by one.
+ *
+ * Returns: (transfer full) (nullable): a #GFile or %NULL
+ *
+ * Since: 3.32
+ */
+GFile *
+ide_vcs_monitor_ref_root (IdeVcsMonitor *self)
+{
+  GFile *ret;
 
   g_return_val_if_fail (IDE_IS_VCS_MONITOR (self), NULL);
 
-  if (self->status_by_file == NULL)
-    return NULL;
+  ide_object_lock (IDE_OBJECT (self));
+  ret = self->root ? g_object_ref (self->root) : NULL;
+  ide_vcs_monitor_maybe_reload_locked (self);
+  ide_object_unlock (IDE_OBJECT (self));
 
-  info = g_hash_table_lookup (self->status_by_file, file);
-  if (info == NULL)
-    return NULL;
+  return g_steal_pointer (&ret);
+}
+
+void
+ide_vcs_monitor_set_root (IdeVcsMonitor *self,
+                          GFile         *root)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_VCS_MONITOR (self));
+  g_return_if_fail (G_IS_FILE (root));
 
-  return g_object_ref (info);
+  ide_object_lock (IDE_OBJECT (self));
+  if (g_set_object (&self->root, root))
+    {
+      ide_object_notify_by_pspec (self, properties [PROP_ROOT]);
+      ide_vcs_monitor_maybe_reload_locked (self);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
+}
+
+void
+ide_vcs_monitor_set_vcs (IdeVcsMonitor *self,
+                         IdeVcs        *vcs)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (IDE_IS_VCS_MONITOR (self));
+  g_return_if_fail (!vcs || IDE_IS_VCS (vcs));
+
+  ide_object_lock (IDE_OBJECT (self));
+  if (g_set_object (&self->vcs, vcs))
+    {
+      dzl_signal_group_set_target (self->vcs_signals, vcs);
+      ide_object_notify_by_pspec (self, properties [PROP_VCS]);
+      ide_vcs_monitor_maybe_reload_locked (self);
+    }
+  ide_object_unlock (IDE_OBJECT (self));
 }
diff --git a/src/libide/vcs/ide-vcs-monitor.h b/src/libide/vcs/ide-vcs-monitor.h
index da19f1bbd..d7835ee8c 100644
--- a/src/libide/vcs/ide-vcs-monitor.h
+++ b/src/libide/vcs/ide-vcs-monitor.h
@@ -1,6 +1,6 @@
 /* ide-vcs-monitor.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,24 +14,40 @@
  *
  * 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 "ide-object.h"
-#include "ide-version-macros.h"
+#if !defined (IDE_VCS_INSIDE) && !defined (IDE_VCS_COMPILATION)
+# error "Only <libide-vcs.h> can be included directly."
+#endif
+
+#include <libide-core.h>
 
-#include "vcs/ide-vcs-file-info.h"
+#include "ide-vcs.h"
+#include "ide-vcs-file-info.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_VCS_MONITOR (ide_vcs_monitor_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_FINAL_TYPE (IdeVcsMonitor, ide_vcs_monitor, IDE, VCS_MONITOR, IdeObject)
 
-IDE_AVAILABLE_IN_ALL
-IdeVcsFileInfo *ide_vcs_monitor_get_info (IdeVcsMonitor *self,
+IDE_AVAILABLE_IN_3_32
+IdeVcsFileInfo *ide_vcs_monitor_ref_info (IdeVcsMonitor *self,
+                                          GFile         *file);
+IDE_AVAILABLE_IN_3_32
+GFile          *ide_vcs_monitor_ref_root (IdeVcsMonitor *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_vcs_monitor_set_root (IdeVcsMonitor *self,
                                           GFile         *file);
+IDE_AVAILABLE_IN_3_32
+IdeVcs         *ide_vcs_monitor_ref_vcs  (IdeVcsMonitor *self);
+IDE_AVAILABLE_IN_3_32
+void            ide_vcs_monitor_set_vcs  (IdeVcsMonitor *self,
+                                          IdeVcs        *vcs);
 
 G_END_DECLS
diff --git a/src/libide/vcs/ide-vcs-uri.c b/src/libide/vcs/ide-vcs-uri.c
index 272421afa..db44f23d0 100644
--- a/src/libide/vcs/ide-vcs-uri.c
+++ b/src/libide/vcs/ide-vcs-uri.c
@@ -1,6 +1,6 @@
 /* ide-vcs-uri.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,17 +14,18 @@
  *
  * 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 "ide-vcs-uri"
 
 #include "config.h"
 
-#include <dazzle.h>
 #include <stdlib.h>
 #include <string.h>
 
-#include "vcs/ide-vcs-uri.h"
+#include "ide-vcs-uri.h"
 
 G_DEFINE_BOXED_TYPE (IdeVcsUri, ide_vcs_uri, ide_vcs_uri_ref, ide_vcs_uri_unref)
 
@@ -154,7 +155,7 @@ ide_vcs_uri_parse (IdeVcsUri   *self,
           g_free (tmp);
         }
 
-      if (!dzl_str_empty0 (portstr) && g_ascii_isdigit (portstr [1]))
+      if (!ide_str_empty0 (portstr) && g_ascii_isdigit (portstr [1]))
         port = CLAMP (atoi (&portstr [1]), 1, G_MAXINT16);
 
       ide_vcs_uri_set_scheme (self, scheme);
@@ -218,7 +219,7 @@ ide_vcs_uri_new (const gchar *uri)
 {
   IdeVcsUri *self;
 
-  self = g_new0 (IdeVcsUri, 1);
+  self = g_slice_new0 (IdeVcsUri);
   self->ref_count = 1;
 
   if (ide_vcs_uri_parse (self, uri) && ide_vcs_uri_validate (self))
@@ -227,7 +228,7 @@ ide_vcs_uri_new (const gchar *uri)
       return self;
     }
 
-  g_free (self);
+  ide_vcs_uri_unref (self);
 
   return NULL;
 }
@@ -240,7 +241,7 @@ ide_vcs_uri_finalize (IdeVcsUri *self)
   g_free (self->user);
   g_free (self->host);
   g_free (self->path);
-  g_free (self);
+  g_slice_free (IdeVcsUri, self);
 }
 
 IdeVcsUri *
@@ -310,7 +311,7 @@ ide_vcs_uri_set_scheme (IdeVcsUri   *self,
 {
   g_return_if_fail (self);
 
-  if (dzl_str_empty0 (scheme))
+  if (ide_str_empty0 (scheme))
     scheme = NULL;
 
   if (scheme != self->scheme)
@@ -334,7 +335,7 @@ ide_vcs_uri_set_user (IdeVcsUri   *self,
 {
   g_return_if_fail (self);
 
-  if (dzl_str_empty0 (user))
+  if (ide_str_empty0 (user))
     user = NULL;
 
   if (user != self->user)
@@ -358,7 +359,7 @@ ide_vcs_uri_set_host (IdeVcsUri   *self,
 {
   g_return_if_fail (self);
 
-  if (dzl_str_empty0 (host))
+  if (ide_str_empty0 (host))
     host = NULL;
 
   if (host != self->host)
@@ -388,7 +389,7 @@ ide_vcs_uri_set_path (IdeVcsUri   *self,
 {
   g_return_if_fail (self);
 
-  if (dzl_str_empty0 (path))
+  if (ide_str_empty0 (path))
     path = NULL;
 
   if (path != self->path)
@@ -458,3 +459,43 @@ ide_vcs_uri_is_valid (const gchar *uri_string)
 
   return ret;
 }
+
+/**
+ * ide_vcs_uri_get_clone_name:
+ * @self: an #ideVcsUri
+ *
+ * Determines a suggested name for the checkout directory. Some special
+ * handling of suffixes such as ".git" are performed to improve the the
+ * quality of results.
+ *
+ * Returns: (transfer full) (nullable): a string containing the suggested
+ *   clone directory name, or %NULL.
+ *
+ * Since: 3.32
+ */
+gchar *
+ide_vcs_uri_get_clone_name (const IdeVcsUri *self)
+{
+  g_autofree gchar *name = NULL;
+  const gchar *path;
+
+  g_return_val_if_fail (self != NULL, NULL);
+
+  if (!(path = ide_vcs_uri_get_path (self)))
+    return NULL;
+
+  if (ide_str_empty0 (path))
+    return NULL;
+
+  if (!(name = g_path_get_basename (path)))
+    return NULL;
+
+  /* Trim trailing ".git" */
+  if (g_str_has_suffix (name, ".git"))
+    *(strrchr (name, '.')) = '\0';
+
+  if (!g_str_equal (name, "/") && !g_str_equal (name, "~"))
+    return g_steal_pointer (&name);
+
+  return NULL;
+}
diff --git a/src/libide/vcs/ide-vcs-uri.h b/src/libide/vcs/ide-vcs-uri.h
index 2cd98ca3e..119275dbc 100644
--- a/src/libide/vcs/ide-vcs-uri.h
+++ b/src/libide/vcs/ide-vcs-uri.h
@@ -1,6 +1,6 @@
 /* ide-vcs-uri.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,13 +14,17 @@
  *
  * 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>
+#if !defined (IDE_VCS_INSIDE) && !defined (IDE_VCS_COMPILATION)
+# error "Only <libide-vcs.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
@@ -28,43 +32,45 @@ G_BEGIN_DECLS
 
 typedef struct _IdeVcsUri IdeVcsUri;
 
-IDE_AVAILABLE_IN_ALL
-GType        ide_vcs_uri_get_type   (void);
-IDE_AVAILABLE_IN_ALL
-IdeVcsUri   *ide_vcs_uri_new        (const gchar     *uri);
-IDE_AVAILABLE_IN_ALL
-IdeVcsUri   *ide_vcs_uri_ref        (IdeVcsUri       *self);
-IDE_AVAILABLE_IN_ALL
-void         ide_vcs_uri_unref      (IdeVcsUri       *self);
-IDE_AVAILABLE_IN_ALL
-const gchar *ide_vcs_uri_get_scheme (const IdeVcsUri *self);
-IDE_AVAILABLE_IN_ALL
-const gchar *ide_vcs_uri_get_user   (const IdeVcsUri *self);
-IDE_AVAILABLE_IN_ALL
-const gchar *ide_vcs_uri_get_host   (const IdeVcsUri *self);
-IDE_AVAILABLE_IN_ALL
-guint        ide_vcs_uri_get_port   (const IdeVcsUri *self);
-IDE_AVAILABLE_IN_ALL
-const gchar *ide_vcs_uri_get_path   (const IdeVcsUri *self);
-IDE_AVAILABLE_IN_ALL
-void         ide_vcs_uri_set_scheme (IdeVcsUri       *self,
-                                     const gchar     *scheme);
-IDE_AVAILABLE_IN_ALL
-void         ide_vcs_uri_set_user   (IdeVcsUri       *self,
-                                     const gchar     *user);
-IDE_AVAILABLE_IN_ALL
-void         ide_vcs_uri_set_host   (IdeVcsUri       *self,
-                                     const gchar     *host);
-IDE_AVAILABLE_IN_ALL
-void         ide_vcs_uri_set_port   (IdeVcsUri       *self,
-                                     guint            port);
-IDE_AVAILABLE_IN_ALL
-void         ide_vcs_uri_set_path   (IdeVcsUri       *self,
-                                     const gchar     *path);
-IDE_AVAILABLE_IN_ALL
-gchar       *ide_vcs_uri_to_string  (const IdeVcsUri *self);
-IDE_AVAILABLE_IN_ALL
-gboolean     ide_vcs_uri_is_valid   (const gchar     *uri_string);
+IDE_AVAILABLE_IN_3_32
+GType        ide_vcs_uri_get_type       (void);
+IDE_AVAILABLE_IN_3_32
+IdeVcsUri   *ide_vcs_uri_new            (const gchar     *uri);
+IDE_AVAILABLE_IN_3_32
+IdeVcsUri   *ide_vcs_uri_ref            (IdeVcsUri       *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_vcs_uri_unref          (IdeVcsUri       *self);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_vcs_uri_get_scheme     (const IdeVcsUri *self);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_vcs_uri_get_user       (const IdeVcsUri *self);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_vcs_uri_get_host       (const IdeVcsUri *self);
+IDE_AVAILABLE_IN_3_32
+guint        ide_vcs_uri_get_port       (const IdeVcsUri *self);
+IDE_AVAILABLE_IN_3_32
+const gchar *ide_vcs_uri_get_path       (const IdeVcsUri *self);
+IDE_AVAILABLE_IN_3_32
+void         ide_vcs_uri_set_scheme     (IdeVcsUri       *self,
+                                         const gchar     *scheme);
+IDE_AVAILABLE_IN_3_32
+void         ide_vcs_uri_set_user       (IdeVcsUri       *self,
+                                         const gchar     *user);
+IDE_AVAILABLE_IN_3_32
+void         ide_vcs_uri_set_host       (IdeVcsUri       *self,
+                                         const gchar     *host);
+IDE_AVAILABLE_IN_3_32
+void         ide_vcs_uri_set_port       (IdeVcsUri       *self,
+                                         guint            port);
+IDE_AVAILABLE_IN_3_32
+void         ide_vcs_uri_set_path       (IdeVcsUri       *self,
+                                         const gchar     *path);
+IDE_AVAILABLE_IN_3_32
+gchar       *ide_vcs_uri_to_string      (const IdeVcsUri *self);
+IDE_AVAILABLE_IN_3_32
+gboolean     ide_vcs_uri_is_valid       (const gchar     *uri_string);
+IDE_AVAILABLE_IN_3_32
+gchar       *ide_vcs_uri_get_clone_name (const IdeVcsUri *self);
 
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (IdeVcsUri, ide_vcs_uri_unref)
 
diff --git a/src/libide/vcs/ide-vcs.c b/src/libide/vcs/ide-vcs.c
index 6bb2dc2cd..f4c178efe 100644
--- a/src/libide/vcs/ide-vcs.c
+++ b/src/libide/vcs/ide-vcs.c
@@ -1,6 +1,6 @@
 /* ide-vcs.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,19 +14,20 @@
  *
  * 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 "ide-vcs"
 
 #include "config.h"
 
+#include <libide-io.h>
 #include <string.h>
 
-#include "ide-context.h"
-
-#include "buffers/ide-buffer.h"
-#include "buffers/ide-buffer-change-monitor.h"
-#include "vcs/ide-vcs.h"
+#include "ide-directory-vcs.h"
+#include "ide-vcs.h"
+#include "ide-vcs-enums.h"
 
 G_DEFINE_INTERFACE (IdeVcs, ide_vcs, IDE_TYPE_OBJECT)
 
@@ -36,19 +37,6 @@ enum {
 };
 
 static guint signals [N_SIGNALS];
-static GPtrArray *ignored;
-
-G_LOCK_DEFINE_STATIC (ignored);
-
-void
-ide_vcs_register_ignored (const gchar *pattern)
-{
-  G_LOCK (ignored);
-  if (ignored == NULL)
-    ignored = g_ptr_array_new ();
-  g_ptr_array_add (ignored, g_pattern_spec_new (pattern));
-  G_UNLOCK (ignored);
-}
 
 static void
 ide_vcs_real_list_status_async (IdeVcs              *self,
@@ -91,7 +79,7 @@ ide_vcs_default_init (IdeVcsInterface *iface)
                                                             (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS)));
 
   g_object_interface_install_property (iface,
-                                       g_param_spec_object ("working-directory",
+                                       g_param_spec_object ("workdir",
                                                             "Working Directory",
                                                             "The working directory for the VCS",
                                                             G_TYPE_FILE,
@@ -103,6 +91,8 @@ ide_vcs_default_init (IdeVcsInterface *iface)
    * The "changed" signal should be emitted when the VCS has detected a change
    * to the underlying VCS storage. This can be used by consumers to reload
    * their respective data structures.
+   *
+   * Since: 3.32
    */
   signals [CHANGED] =
     g_signal_new ("changed",
@@ -115,14 +105,6 @@ ide_vcs_default_init (IdeVcsInterface *iface)
   g_signal_set_va_marshaller (signals [CHANGED],
                               G_TYPE_FROM_INTERFACE (iface),
                               g_cclosure_marshal_VOID__VOIDv);
-
-
-  /* Ignore Gio temporary files */
-  ide_vcs_register_ignored (".goutputstream-*");
-
-  /* Ignore minified JS */
-  ide_vcs_register_ignored ("*.min.js");
-  ide_vcs_register_ignored ("*.min.js.*");
 }
 
 /**
@@ -145,61 +127,29 @@ ide_vcs_default_init (IdeVcsInterface *iface)
  *   #IdeVcs implementations are required to ensure this function
  *   is thread-safe.
  *
- * Since: 3.18
+ * Since: 3.32
  */
 gboolean
 ide_vcs_is_ignored (IdeVcs  *self,
                     GFile   *file,
                     GError **error)
 {
-  g_autofree gchar *name = NULL;
-  g_autofree gchar *reversed = NULL;
-  gboolean ret = FALSE;
-  gsize len;
-
   g_return_val_if_fail (!self || IDE_IS_VCS (self), FALSE);
   g_return_val_if_fail (!file || G_IS_FILE (file), FALSE);
 
   if (file == NULL)
     return TRUE;
 
-  name = g_file_get_basename (file);
-  if (name == NULL || *name == 0)
+  if (ide_g_file_is_ignored (file))
     return TRUE;
 
-  len = strlen (name);
-
-  /* Ignore builtin backup files by GIO */
-  if (name[len - 1] == '~')
-    return TRUE;
-
-  reversed = g_utf8_strreverse (name, len);
-
-  G_LOCK (ignored);
-
-  if G_LIKELY (ignored != NULL)
-    {
-      for (guint i = 0; i < ignored->len; i++)
-        {
-          GPatternSpec *pattern_spec = g_ptr_array_index (ignored, i);
-
-          if (g_pattern_match (pattern_spec, len, name, reversed))
-            {
-              ret = TRUE;
-              break;
-            }
-        }
-    }
-
-  G_UNLOCK (ignored);
-
   if (self != NULL)
     {
-      if (!ret && IDE_VCS_GET_IFACE (self)->is_ignored)
-        ret = IDE_VCS_GET_IFACE (self)->is_ignored (self, file, error);
+      if (IDE_VCS_GET_IFACE (self)->is_ignored)
+        return IDE_VCS_GET_IFACE (self)->is_ignored (self, file, error);
     }
 
-  return ret;
+  return FALSE;
 }
 
 /**
@@ -224,16 +174,13 @@ ide_vcs_is_ignored (IdeVcs  *self,
  *   #IdeVcs implementations are required to ensure this function
  *   is thread-safe.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 gboolean
 ide_vcs_path_is_ignored (IdeVcs       *self,
                          const gchar  *path,
                          GError      **error)
 {
-  g_autofree gchar *name = NULL;
-  g_autofree gchar *reversed = NULL;
-  gsize len;
   gboolean ret = FALSE;
 
   g_return_val_if_fail (!self || IDE_IS_VCS (self), FALSE);
@@ -241,36 +188,9 @@ ide_vcs_path_is_ignored (IdeVcs       *self,
   if (path == NULL)
     return TRUE;
 
-  name = g_path_get_basename (path);
-  if (name == NULL || *name == 0)
-    return TRUE;
-
-  len = strlen (name);
-
-  /* Ignore builtin backup files by GIO */
-  if (name[len - 1] == '~')
+  if (ide_path_is_ignored (path))
     return TRUE;
 
-  reversed = g_utf8_strreverse (name, len);
-
-  G_LOCK (ignored);
-
-  if G_LIKELY (ignored != NULL)
-    {
-      for (guint i = 0; i < ignored->len; i++)
-        {
-          GPatternSpec *pattern_spec = g_ptr_array_index (ignored, i);
-
-          if (g_pattern_match (pattern_spec, len, name, reversed))
-            {
-              ret = TRUE;
-              break;
-            }
-        }
-    }
-
-  G_UNLOCK (ignored);
-
   if (self != NULL)
     {
       if (!ret && IDE_VCS_GET_IFACE (self)->is_ignored)
@@ -280,7 +200,7 @@ ide_vcs_path_is_ignored (IdeVcs       *self,
           if (g_path_is_absolute (path))
             file = g_file_new_for_path (path);
           else
-            file = g_file_get_child (ide_vcs_get_working_directory (self), path);
+            file = g_file_get_child (ide_vcs_get_workdir (self), path);
 
           ret = IDE_VCS_GET_IFACE (self)->is_ignored (self, file, error);
         }
@@ -303,7 +223,7 @@ ide_vcs_get_priority (IdeVcs *self)
 }
 
 /**
- * ide_vcs_get_working_directory:
+ * ide_vcs_get_workdir:
  * @self: An #IdeVcs.
  *
  * Retrieves the working directory for the context. This is the root of where
@@ -313,7 +233,7 @@ ide_vcs_get_priority (IdeVcs *self)
  *
  * Returns: (transfer none): a #GFile.
  *
- * Since: 3.18
+ * Since: 3.32
  *
  * Thread safety: this function is safe to call from threads. The working
  *   directory should only be set at creating and therefore safe to call
@@ -321,90 +241,16 @@ ide_vcs_get_priority (IdeVcs *self)
  *   implementing #IdeVcs are required to ensure this invariant holds true.
  */
 GFile *
-ide_vcs_get_working_directory (IdeVcs *self)
+ide_vcs_get_workdir (IdeVcs *self)
 {
   g_return_val_if_fail (IDE_IS_VCS (self), NULL);
 
-  if (IDE_VCS_GET_IFACE (self)->get_working_directory)
-    return IDE_VCS_GET_IFACE (self)->get_working_directory (self);
+  if (IDE_VCS_GET_IFACE (self)->get_workdir)
+    return IDE_VCS_GET_IFACE (self)->get_workdir (self);
 
   return NULL;
 }
 
-/**
- * ide_vcs_get_buffer_change_monitor:
- *
- * Gets an #IdeBufferChangeMonitor for the buffer provided. If the #IdeVcs implementation does not
- * support change monitoring, or cannot for the current file, then %NULL is returned.
- *
- * Returns: (transfer full) (nullable): An #IdeBufferChangeMonitor or %NULL.
- */
-IdeBufferChangeMonitor *
-ide_vcs_get_buffer_change_monitor (IdeVcs    *self,
-                                   IdeBuffer *buffer)
-{
-  IdeBufferChangeMonitor *ret = NULL;
-
-  g_return_val_if_fail (IDE_IS_VCS (self), NULL);
-  g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
-
-  if (IDE_VCS_GET_IFACE (self)->get_buffer_change_monitor)
-    ret = IDE_VCS_GET_IFACE (self)->get_buffer_change_monitor (self, buffer);
-
-  g_return_val_if_fail (!ret || IDE_IS_BUFFER_CHANGE_MONITOR (ret), NULL);
-
-  return ret;
-}
-
-static gint
-sort_by_priority (gconstpointer a,
-                  gconstpointer b,
-                  gpointer      user_data)
-{
-  IdeVcs *vcs_a = *(IdeVcs **)a;
-  IdeVcs *vcs_b = *(IdeVcs **)b;
-
-  return ide_vcs_get_priority (vcs_a) - ide_vcs_get_priority (vcs_b);
-}
-
-void
-ide_vcs_new_async (IdeContext           *context,
-                   int                   io_priority,
-                   GCancellable         *cancellable,
-                   GAsyncReadyCallback   callback,
-                   gpointer              user_data)
-{
-  ide_object_new_for_extension_async (IDE_TYPE_VCS,
-                                      sort_by_priority,
-                                      NULL,
-                                      io_priority,
-                                      cancellable,
-                                      callback,
-                                      user_data,
-                                      "context", context,
-                                      NULL);
-}
-
-/**
- * ide_vcs_new_finish:
- *
- * Completes a call to ide_vcs_new_async().
- *
- * Returns: (transfer full): An #IdeVcs.
- */
-IdeVcs *
-ide_vcs_new_finish (GAsyncResult  *result,
-                    GError       **error)
-{
-  IdeObject *ret;
-
-  g_return_val_if_fail (G_IS_ASYNC_RESULT (result), NULL);
-
-  ret = ide_object_new_finish (result, error);
-
-  return IDE_VCS (ret);
-}
-
 void
 ide_vcs_emit_changed (IdeVcs *self)
 {
@@ -420,6 +266,8 @@ ide_vcs_emit_changed (IdeVcs *self)
  * support access to configuration, then %NULL is returned.
  *
  * Returns: (transfer full) (nullable): An #IdeVcsConfig or %NULL.
+ *
+ * Since: 3.32
  */
 IdeVcsConfig *
 ide_vcs_get_config (IdeVcs *self)
@@ -442,16 +290,25 @@ ide_vcs_get_config (IdeVcs *self)
  * Retrieves the name of the branch in the current working directory.
  *
  * Returns: (transfer full): A string containing the branch name.
+ *
+ * Since: 3.32
  */
 gchar *
 ide_vcs_get_branch_name (IdeVcs *self)
 {
+  gchar *ret = NULL;
+
   g_return_val_if_fail (IDE_IS_VCS (self), NULL);
 
+  ide_object_lock (IDE_OBJECT (self));
   if (IDE_VCS_GET_IFACE (self)->get_branch_name)
-    return IDE_VCS_GET_IFACE (self)->get_branch_name (self);
+    ret = IDE_VCS_GET_IFACE (self)->get_branch_name (self);
+  ide_object_unlock (IDE_OBJECT (self));
 
-  return g_strdup ("primary");
+  if (ret == NULL)
+    ret = g_strdup ("primary");
+
+  return ret;
 }
 
 /**
@@ -474,7 +331,7 @@ ide_vcs_get_branch_name (IdeVcs *self)
  * The function specified by @callback should call ide_vcs_list_status_finish()
  * to retrieve the result of this asynchronous operation.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 void
 ide_vcs_list_status_async (IdeVcs              *self,
@@ -490,7 +347,7 @@ ide_vcs_list_status_async (IdeVcs              *self,
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   if (directory_or_file == NULL)
-    directory_or_file = ide_vcs_get_working_directory (self);
+    directory_or_file = ide_vcs_get_workdir (self);
 
   IDE_VCS_GET_IFACE (self)->list_status_async (self,
                                                directory_or_file,
@@ -516,7 +373,7 @@ ide_vcs_list_status_async (IdeVcs              *self,
  *   A #GListModel containing an #IdeVcsFileInfo for each of the files scanned
  *   by the #IdeVcs. Upon failure, %NULL is returned and @error is set.
  *
- * Since: 3.28
+ * Since: 3.32
  */
 GListModel *
 ide_vcs_list_status_finish (IdeVcs        *self,
@@ -528,3 +385,58 @@ ide_vcs_list_status_finish (IdeVcs        *self,
 
   return IDE_VCS_GET_IFACE (self)->list_status_finish (self, result, error);
 }
+
+/**
+ * ide_vcs_from_context:
+ * @context: an #IdeContext
+ *
+ * Gets the #IdeVcs for the context.
+ *
+ * Returns: (transfer none): an #IdeVcs
+ *
+ * Since: 3.32
+ */
+IdeVcs *
+ide_vcs_from_context (IdeContext *context)
+{
+  IdeVcs *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  /* Release full reference, into borrowed ref */
+  ret = ide_vcs_ref_from_context (context);
+  g_object_unref (ret);
+
+  return ret;
+}
+
+/**
+ * ide_vcs_ref_from_context:
+ * @context: an #IdeContext
+ *
+ * A thread-safe version of ide_vcs_from_context().
+ *
+ * Returns: (transfer full): an #IdeVcs
+ *
+ * Since: 3.32
+ */
+IdeVcs *
+ide_vcs_ref_from_context (IdeContext *context)
+{
+  IdeVcs *ret;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  ide_object_lock (IDE_OBJECT (context));
+  ret = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_VCS);
+  if (ret == NULL)
+    {
+      g_autoptr(GFile) workdir = ide_context_ref_workdir (context);
+      ret = (IdeVcs *)ide_directory_vcs_new (workdir);
+      ide_object_prepend (IDE_OBJECT (context), IDE_OBJECT (ret));
+    }
+  ide_object_unlock (IDE_OBJECT (context));
+
+  return g_steal_pointer (&ret);
+}
diff --git a/src/libide/vcs/ide-vcs.h b/src/libide/vcs/ide-vcs.h
index e40a3e4fe..aa5824a0a 100644
--- a/src/libide/vcs/ide-vcs.h
+++ b/src/libide/vcs/ide-vcs.h
@@ -1,6 +1,6 @@
 /* ide-vcs.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,31 +14,32 @@
  *
  * 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>
+#if !defined (IDE_VCS_INSIDE) && !defined (IDE_VCS_COMPILATION)
+# error "Only <libide-vcs.h> can be included directly."
+#endif
 
-#include "ide-version-macros.h"
+#include <libide-core.h>
 
-#include "ide-object.h"
-#include "vcs/ide-vcs-config.h"
+#include "ide-vcs-config.h"
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_VCS (ide_vcs_get_type())
 
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 G_DECLARE_INTERFACE (IdeVcs, ide_vcs, IDE, VCS, IdeObject)
 
 struct _IdeVcsInterface
 {
   GTypeInterface            parent_interface;
 
-  GFile                  *(*get_working_directory)     (IdeVcs               *self);
-  IdeBufferChangeMonitor *(*get_buffer_change_monitor) (IdeVcs               *self,
-                                                        IdeBuffer            *buffer);
+  GFile                  *(*get_workdir)               (IdeVcs               *self);
   gboolean                (*is_ignored)                (IdeVcs               *self,
                                                         GFile                *file,
                                                         GError              **error);
@@ -58,39 +59,29 @@ struct _IdeVcsInterface
                                                         GError              **error);
 };
 
-IDE_AVAILABLE_IN_ALL
-void                    ide_vcs_register_ignored          (const gchar          *pattern);
-IDE_AVAILABLE_IN_ALL
-IdeBufferChangeMonitor *ide_vcs_get_buffer_change_monitor (IdeVcs               *self,
-                                                           IdeBuffer            *buffer);
-IDE_AVAILABLE_IN_ALL
-GFile                  *ide_vcs_get_working_directory     (IdeVcs               *self);
-IDE_AVAILABLE_IN_ALL
-void                    ide_vcs_new_async                 (IdeContext           *context,
-                                                           int                   io_priority,
-                                                           GCancellable         *cancellable,
-                                                           GAsyncReadyCallback   callback,
-                                                           gpointer              user_data);
-IDE_AVAILABLE_IN_ALL
-IdeVcs                 *ide_vcs_new_finish                (GAsyncResult         *result,
-                                                           GError              **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
+IdeVcs                 *ide_vcs_from_context              (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+IdeVcs                 *ide_vcs_ref_from_context          (IdeContext           *context);
+IDE_AVAILABLE_IN_3_32
+GFile                  *ide_vcs_get_workdir               (IdeVcs               *self);
+IDE_AVAILABLE_IN_3_32
 gboolean                ide_vcs_is_ignored                (IdeVcs               *self,
                                                            GFile                *file,
                                                            GError              **error);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 gboolean                ide_vcs_path_is_ignored           (IdeVcs               *self,
                                                            const gchar          *path,
                                                            GError              **error);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gint                    ide_vcs_get_priority              (IdeVcs               *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 void                    ide_vcs_emit_changed              (IdeVcs               *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 IdeVcsConfig           *ide_vcs_get_config                (IdeVcs               *self);
-IDE_AVAILABLE_IN_ALL
+IDE_AVAILABLE_IN_3_32
 gchar                  *ide_vcs_get_branch_name           (IdeVcs               *self);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 void                    ide_vcs_list_status_async         (IdeVcs               *self,
                                                            GFile                *directory_or_file,
                                                            gboolean              include_descendants,
@@ -98,7 +89,7 @@ void                    ide_vcs_list_status_async         (IdeVcs
                                                            GCancellable         *cancellable,
                                                            GAsyncReadyCallback   callback,
                                                            gpointer              user_data);
-IDE_AVAILABLE_IN_3_28
+IDE_AVAILABLE_IN_3_32
 GListModel             *ide_vcs_list_status_finish        (IdeVcs               *self,
                                                            GAsyncResult         *result,
                                                            GError              **error);
diff --git a/src/libide/vcs/libide-vcs.h b/src/libide/vcs/libide-vcs.h
new file mode 100644
index 000000000..bc6352468
--- /dev/null
+++ b/src/libide/vcs/libide-vcs.h
@@ -0,0 +1,38 @@
+/* ide-vcs.h
+ *
+ * Copyright 2014-2019 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-core.h>
+#include <libide-io.h>
+
+#define IDE_VCS_INSIDE
+
+#include "ide-directory-vcs.h"
+#include "ide-vcs-cloner.h"
+#include "ide-vcs-config.h"
+#include "ide-vcs-enums.h"
+#include "ide-vcs-initializer.h"
+#include "ide-vcs-uri.h"
+#include "ide-vcs-file-info.h"
+#include "ide-vcs.h"
+#include "ide-vcs-monitor.h"
+
+#undef IDE_VCS_INSIDE
diff --git a/src/libide/vcs/meson.build b/src/libide/vcs/meson.build
index 551dbaa30..14beb6904 100644
--- a/src/libide/vcs/meson.build
+++ b/src/libide/vcs/meson.build
@@ -1,14 +1,38 @@
-vcs_headers = [
+libide_vcs_header_dir = join_paths(libide_header_dir, 'vcs')
+libide_vcs_header_subdir = join_paths(libide_header_subdir, 'vcs')
+libide_include_directories += include_directories('.')
+
+#
+# Public API Headers
+#
+
+libide_vcs_public_headers = [
+  'ide-directory-vcs.h',
   'ide-vcs-config.h',
+  'ide-vcs-cloner.h',
   'ide-vcs-file-info.h',
   'ide-vcs-initializer.h',
   'ide-vcs-monitor.h',
   'ide-vcs-uri.h',
   'ide-vcs.h',
+  'libide-vcs.h',
+]
+
+libide_vcs_enum_headers = [
+  'ide-vcs-config.h',
+  'ide-vcs-file-info.h',
 ]
 
-vcs_sources = [
+install_headers(libide_vcs_public_headers, subdir: libide_vcs_header_subdir)
+
+#
+# Sources
+#
+
+libide_vcs_public_sources = [
+  'ide-directory-vcs.c',
   'ide-vcs-config.c',
+  'ide-vcs-cloner.c',
   'ide-vcs-file-info.c',
   'ide-vcs-initializer.c',
   'ide-vcs-monitor.c',
@@ -16,13 +40,54 @@ vcs_sources = [
   'ide-vcs.c',
 ]
 
-vcs_enums = [
-  'ide-vcs-config.h',
-  'ide-vcs-file-info.h',
+#
+# Enum generation
+#
+
+libide_vcs_enums = gnome.mkenums_simple('ide-vcs-enums',
+     body_prefix: '#include "config.h"',
+   header_prefix: '#include <libide-core.h>',
+       decorator: '_IDE_EXTERN',
+         sources: libide_vcs_enum_headers,
+  install_header: true,
+     install_dir: libide_vcs_header_dir,
+)
+libide_vcs_generated_sources = [libide_vcs_enums[0]]
+libide_vcs_generated_headers = [libide_vcs_enums[1]]
+
+#
+# Dependencies
+#
+
+libide_vcs_deps = [
+  libgio_dep,
+  libgtk_dep,
+
+  libide_core_dep,
+  libide_io_dep,
+  libide_threading_dep,
 ]
 
-libide_public_headers += files(vcs_headers)
-libide_public_sources += files(vcs_sources)
-libide_enum_headers += files(vcs_enums)
+#
+# Library Definitions
+#
+
+libide_vcs = static_library('ide-vcs-' + libide_api_version,
+   libide_vcs_public_sources + libide_vcs_generated_sources + libide_vcs_generated_headers,
+   dependencies: libide_vcs_deps,
+         c_args: libide_args + release_args + ['-DIDE_VCS_COMPILATION'],
+)
+
+libide_vcs_dep = declare_dependency(
+         dependencies: libide_vcs_deps,
+           link_whole: libide_vcs,
+  include_directories: include_directories('.'),
+              sources: libide_vcs_generated_headers,
+)
 
-install_headers(vcs_headers, subdir: join_paths(libide_header_subdir, 'vcs'))
+gnome_builder_public_sources += files(libide_vcs_public_sources)
+gnome_builder_public_headers += files(libide_vcs_public_headers)
+gnome_builder_generated_headers += libide_vcs_generated_headers
+gnome_builder_generated_sources += libide_vcs_generated_sources
+gnome_builder_include_subdirs += libide_vcs_header_subdir
+gnome_builder_gir_extra_args += ['--c-include=libide-vcs.h', '-DIDE_VCS_COMPILATION']
diff --git a/src/libide/webkit/ide-webkit-plugin.c b/src/libide/webkit/ide-webkit-plugin.c
new file mode 100644
index 000000000..3892af24c
--- /dev/null
+++ b/src/libide/webkit/ide-webkit-plugin.c
@@ -0,0 +1,36 @@
+/* ide-webkit-plugin.c
+ *
+ * Copyright 2016-2019 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 "ide-webkit-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <webkit2/webkit2.h>
+#include <girepository.h>
+
+_IDE_EXTERN void _ide_webkit_register_types (PeasObjectModule *module);
+
+void
+_ide_webkit_register_types (PeasObjectModule *module)
+{
+  g_type_ensure (WEBKIT_TYPE_WEB_VIEW);
+  g_irepository_require (NULL, "WebKit2", "4.0", 0, NULL);
+}
diff --git a/src/libide/webkit/libide-webkit.gresource.xml b/src/libide/webkit/libide-webkit.gresource.xml
new file mode 100644
index 000000000..f1621dbaa
--- /dev/null
+++ b/src/libide/webkit/libide-webkit.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/webkit">
+    <file>webkit.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/libide/webkit/meson.build b/src/libide/webkit/meson.build
new file mode 100644
index 000000000..d92786c26
--- /dev/null
+++ b/src/libide/webkit/meson.build
@@ -0,0 +1,45 @@
+
+#
+# Sources
+#
+
+libide_webkit_sources = [
+  'ide-webkit-plugin.c',
+]
+
+#
+# Generated Resource Files
+#
+
+libide_webkit_resources = gnome.compile_resources(
+  'ide-webkit-resources',
+  'libide-webkit.gresource.xml',
+  c_name: 'ide_webkit',
+)
+libide_webkit_generated_headers = [libide_webkit_resources[1]]
+libide_webkit_sources += libide_webkit_resources[0]
+
+#
+# Dependencies
+#
+
+libide_webkit_deps = [
+  libwebkit_dep,
+  libpeas_dep,
+]
+
+#
+# Library Definitions
+#
+
+libide_webkit = static_library('ide-webkit-' + libide_api_version, libide_webkit_sources,
+   dependencies: libide_webkit_deps,
+         c_args: libide_args + release_args + ['-DIDE_WEBKIT_COMPILATION'],
+)
+
+libide_webkit_dep = declare_dependency(
+         dependencies: libide_webkit_deps,
+           link_whole: libide_webkit,
+  include_directories: include_directories('.'),
+              sources: libide_webkit_generated_headers,
+)
diff --git a/src/libide/webkit/webkit.plugin b/src/libide/webkit/webkit.plugin
index 67b6a03ad..ea68c01be 100644
--- a/src/libide/webkit/webkit.plugin
+++ b/src/libide/webkit/webkit.plugin
@@ -6,4 +6,4 @@ Authors=Christian Hergert <christian hergert me>
 Copyright=Copyright © 2016 Christian Hergert
 Builtin=true
 Hidden=true
-Embedded=ide_webkit_register_types
+Embedded=_ide_webkit_register_types
diff --git a/src/main.c b/src/main.c
index 989720326..ade1bf4c4 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,6 +1,6 @@
 /* main.c
  *
- * Copyright © 2014 Christian Hergert <christian hergert me>
+ * Copyright 2018-2019 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
@@ -14,21 +14,29 @@
  *
  * 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 "builder"
+#define G_LOG_DOMAIN "main"
+
+#include "config.h"
 
-#include <ide.h>
+#include <girepository.h>
 #include <glib/gi18n.h>
-#include <gtksourceview/gtksource.h>
-#include <stdlib.h>
+#include <libide-core.h>
+#include <libide-code.h>
+#include <libide-editor.h>
+#include <libide-greeter.h>
+#include <libide-gui.h>
+#include <libide-threading.h>
 
-#include "plugins/gnome-builder-plugins.h"
+#include "ide-application-private.h"
+#include "ide-thread-private.h"
+#include "ide-terminal-private.h"
 
 #include "bug-buddy.h"
 
-static IdeApplicationMode early_mode;
-
 static gboolean
 verbose_cb (const gchar  *option_name,
             const gchar  *value,
@@ -40,35 +48,44 @@ verbose_cb (const gchar  *option_name,
 }
 
 static void
-early_params_check (gint    *argc,
-                    gchar ***argv)
+early_params_check (gint       *argc,
+                    gchar    ***argv,
+                    gboolean   *standalone,
+                    gchar     **type,
+                    gchar     **plugin,
+                    gchar     **dbus_address)
 {
-  g_autofree gchar *type = NULL;
   g_autoptr(GOptionContext) context = NULL;
+  g_autoptr(GOptionGroup) gir_group = NULL;
   GOptionEntry entries[] = {
+    { "standalone", 's', 0, G_OPTION_ARG_NONE, standalone, N_("Run a new instance of Builder") },
     { "verbose", 'v', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, verbose_cb },
-    { "type", 0, 0, G_OPTION_ARG_STRING, &type },
+    { "plugin", 0, 0, G_OPTION_ARG_STRING, plugin },
+    { "type", 0, 0, G_OPTION_ARG_STRING, type },
+    { "dbus-address", 0, 0, G_OPTION_ARG_STRING, dbus_address },
     { NULL }
   };
 
+  gir_group = g_irepository_get_option_group ();
+
   context = g_option_context_new (NULL);
   g_option_context_set_ignore_unknown_options (context, TRUE);
   g_option_context_set_help_enabled (context, FALSE);
   g_option_context_add_main_entries (context, entries, NULL);
+  g_option_context_add_group (context, gir_group);
   g_option_context_parse (context, argc, argv, NULL);
-
-  if (g_strcmp0 (type, "worker") == 0)
-    early_mode = IDE_APPLICATION_MODE_WORKER;
-  else if (g_strcmp0 (type, "cli") == 0)
-    early_mode = IDE_APPLICATION_MODE_TOOL;
 }
 
-int
-main (int   argc,
-      char *argv[])
+gint
+main (gint   argc,
+      gchar *argv[])
 {
+  g_autofree gchar *plugin = NULL;
+  g_autofree gchar *type = NULL;
+  g_autofree gchar *dbus_address = NULL;
   IdeApplication *app;
   const gchar *desktop;
+  gboolean standalone = FALSE;
   int ret;
 
   /* Setup our gdb fork()/exec() helper */
@@ -77,15 +94,16 @@ main (int   argc,
   /* Always ignore SIGPIPE */
   signal (SIGPIPE, SIG_IGN);
 
-  /*
-   * We require a desktop session that provides a properly working
-   * DBus environment. Bail if for some reason that is not the case.
-   */
-  if (g_getenv ("DBUS_SESSION_BUS_ADDRESS") == NULL)
-    {
-      g_printerr (_("GNOME Builder requires a desktop session with D-Bus. Please set 
DBUS_SESSION_BUS_ADDRESS."));
-      return EXIT_FAILURE;
-    }
+  /* Setup various application name/id defaults. */
+  g_set_prgname (ide_get_program_name ());
+  g_set_application_name (_("Builder"));
+
+#if 0
+  /* TODO: allow support for parallel nightly install */
+#ifdef DEVELOPMENT_BUILD
+  ide_set_application_id ("org.gnome.Builder-Devel");
+#endif
+#endif
 
   /* Early init of logging so that we get messages in a consistent
    * format. If we deferred this to GApplication, we'd get them in
@@ -93,8 +111,8 @@ main (int   argc,
    */
   ide_log_init (TRUE, NULL);
 
-  /* Extract options like -vvvv and --type=worker only */
-  early_params_check (&argc, &argv);
+  /* Extract options like -vvvv */
+  early_params_check (&argc, &argv, &standalone, &type, &plugin, &dbus_address);
 
   /* Log what desktop is being used to simplify tracking down
    * quirks in the future.
@@ -109,25 +127,22 @@ main (int   argc,
              gtk_get_minor_version (),
              gtk_get_micro_version ());
 
-  /* Setup the application instance */
-  app = ide_application_new (early_mode);
+  /* Initialize thread pools */
+  _ide_thread_pool_init (FALSE);
 
-  /* Ensure that our static plugins init routine is called.
-   * This is necessary to ensure that -Wl,--as-needed does not
-   * drop our link to this shared library.
-   */
-  gnome_builder_plugins_init ();
+  /* Guess the user shell early */
+  _ide_guess_shell ();
 
-  /* Block until the application exits */
+  app = _ide_application_new (standalone, type, plugin, dbus_address);
+  g_application_add_option_group (G_APPLICATION (app), g_irepository_get_option_group ());
   ret = g_application_run (G_APPLICATION (app), argc, argv);
-
   /* Force disposal of the application (to help catch cleanup
    * issues at shutdown) and then (hopefully) finalize the app.
    */
   g_object_run_dispose (G_OBJECT (app));
   g_clear_object (&app);
 
-  /* Cleanup logging and flush anything that still needs it */
+  /* Flush any outstanding logs */
   ide_log_shutdown ();
 
   return ret;
diff --git a/src/meson.build b/src/meson.build
index 6be920151..2f2f758b7 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,26 +1,52 @@
-exe_link_args = [ '-pie' ]
+# These are updated by subdir() meson.build files so that we
+# can generate gir files and what not appropriate for the
+# final binary (which statically links their .a libraries)
+gnome_builder_include_subdirs = []
+gnome_builder_public_sources = []
+gnome_builder_public_headers = []
+gnome_builder_private_sources = []
+gnome_builder_private_headers = []
+gnome_builder_generated_sources = []
+gnome_builder_generated_headers = []
+gnome_builder_gir_extra_args = ['--pkg-export=gnome-builder-1.0']
+# To allow all resources to be initialized with static constructors
+# inside the final executable, we delay compiling them until the
+# final binary (otherwise they are silenty dropped when linking).
+
+exe_link_args = [ '-pie', '-export-dynamic' ]
 exe_c_args = [ '-fPIE' ]
 
-subdir('libeditorconfig')
 subdir('gstyle')
 subdir('libide')
-subdir('plugins')
 subdir('tests')
+subdir('plugins')
 
-gnome_builder_sources = [
-  'main.c',
-  'bug-buddy.c',
-  'bug-buddy.h',
-]
+gnome_builder_deps = [
+  libgio_dep,
+  libgiounix_dep,
+  libdazzle_dep,
+  libgtk_dep,
 
-executable('gnome-builder', gnome_builder_sources,
-           gui_app: true,
-           install: true,
-            c_args: exe_c_args + release_args,
-         link_args: exe_link_args,
-     install_rpath: pkglibdir_abs,
-      dependencies: gnome_builder_plugins_deps + [libide_dep, gnome_builder_plugins_dep],
-)
+  libide_code_dep,
+  libide_core_dep,
+  libide_debugger_dep,
+  libide_editor_dep,
+  libide_foundry_dep,
+  libide_greeter_dep,
+  libide_gui_dep,
+  libide_io_dep,
+  libide_lsp_dep,
+  libide_plugins_dep,
+  libide_projects_dep,
+  libide_search_dep,
+  libide_sourceview_dep,
+  libide_terminal_dep,
+  libide_themes_dep,
+  libide_threading_dep,
+  libide_vcs_dep,
+  libide_webkit_dep,
+  libide_tree_dep,
+]
 
 if get_option('fusermount_wrapper')
 
@@ -31,7 +57,70 @@ if get_option('fusermount_wrapper')
               c_args: exe_c_args + release_args,
            link_args: exe_link_args,
        install_rpath: pkglibdir_abs,
-        dependencies: [libide_deps, libide_dep],
+        dependencies: [libide_core_deps, libide_io_dep, libide_threading_dep],
   )
 
 endif
+
+gnome_builder = executable('gnome-builder', 'main.c', 'bug-buddy.c',
+           gui_app: true,
+           install: true,
+            c_args: libide_args + exe_c_args + release_args,
+         link_args: exe_link_args,
+        link_whole: plugins,
+     install_rpath: pkglibdir_abs,
+      dependencies: gnome_builder_deps,
+)
+
+# We use requires: instead of libraries: so that our link args of
+# things like -Wl,--require-defined= do not leak into the .pc file.
+pkgconfig.generate(
+      subdirs: gnome_builder_include_subdirs,
+      version: meson.project_version(),
+         name: 'gnome-builder-@0@.@1@'.format(MAJOR_VERSION, MINOR_VERSION),
+     filebase: 'gnome-builder-@0@.@1@'.format(MAJOR_VERSION, MINOR_VERSION),
+  description: 'Contains the plugin container for Builder.',
+  install_dir: join_paths(pkglibdir, 'pkgconfig'),
+     requires: [ 'gio-2.0', 'gio-unix-2.0', 'gtk+-3.0', 'libdazzle-1.0' ],
+)
+
+libide_gir = gnome.generate_gir(gnome_builder,
+              sources: gnome_builder_generated_headers +
+                       gnome_builder_generated_sources +
+                       gnome_builder_public_headers +
+                       gnome_builder_public_sources,
+            nsversion: libide_api_version,
+            namespace: 'Ide',
+        symbol_prefix: 'ide',
+    identifier_prefix: 'Ide',
+             includes: [ 'Gio-2.0', 'Gtk-3.0', 'Dazzle-1.0', 'Peas-1.0', 'Vte-2.91', 'GtkSource-4', 
'Template-1.0' ],
+              install: true,
+      install_dir_gir: pkggirdir,
+  install_dir_typelib: pkgtypelibdir,
+           extra_args: gnome_builder_gir_extra_args,
+)
+
+configure_file(
+          input: 'libide.deps',
+         output: 'libide-' + libide_api_version + '.deps',
+           copy: true,
+        install: true,
+    install_dir: pkgvapidir,
+)
+
+libide_vapi = gnome.generate_vapi('libide-' + libide_api_version,
+      sources: libide_gir[0],
+      install: true,
+  install_dir: pkgvapidir,
+     packages: [ 'gio-2.0',
+                 'gtk+-3.0',
+                 'gtksourceview-4',
+                 'json-glib-1.0',
+                 'libdazzle-1.0',
+                 'libpeas-1.0',
+                 'template-glib-1.0',
+                 'vte-2.91' ],
+)
+
+# Must be after vapi generation
+subdir('plugins/vala-pack')
diff --git a/src/plugins/auto-save/auto-save-plugin.c b/src/plugins/auto-save/auto-save-plugin.c
new file mode 100644
index 000000000..459524251
--- /dev/null
+++ b/src/plugins/auto-save/auto-save-plugin.c
@@ -0,0 +1,36 @@
+/* auto-save-plugin.c
+ *
+ * Copyright 2018-2019 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 "auto-save-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-code.h>
+
+#include "gbp-auto-save-buffer-addin.h"
+
+_IDE_EXTERN void
+_gbp_auto_save_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUFFER_ADDIN,
+                                              GBP_TYPE_AUTO_SAVE_BUFFER_ADDIN);
+}
diff --git a/src/plugins/auto-save/auto-save.gresource.xml b/src/plugins/auto-save/auto-save.gresource.xml
new file mode 100644
index 000000000..cfc9b54a0
--- /dev/null
+++ b/src/plugins/auto-save/auto-save.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/auto-save">
+    <file>auto-save.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/auto-save/auto-save.plugin b/src/plugins/auto-save/auto-save.plugin
new file mode 100644
index 000000000..be353fd26
--- /dev/null
+++ b/src/plugins/auto-save/auto-save.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2018 Christian Hergert
+Depends=editor;
+Description=Auto-save support for source code files
+Embedded=_gbp_auto_save_register_types
+Hidden=true
+Module=auto-save
+Name=Auto-Save
diff --git a/src/plugins/auto-save/gbp-auto-save-buffer-addin.c 
b/src/plugins/auto-save/gbp-auto-save-buffer-addin.c
new file mode 100644
index 000000000..404d2f2a0
--- /dev/null
+++ b/src/plugins/auto-save/gbp-auto-save-buffer-addin.c
@@ -0,0 +1,237 @@
+/* gbp-auto-save-buffer-addin.c
+ *
+ * Copyright 2018-2019 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-auto-save-buffer-addin"
+
+#include "config.h"
+
+#include <libide-code.h>
+
+#include "gbp-auto-save-buffer-addin.h"
+
+struct _GbpAutoSaveBufferAddin
+{
+  GObject    parent_instance;
+  IdeBuffer *buffer;
+  GSettings *settings;
+  guint      source_id;
+  guint      auto_save_timeout;
+  guint      auto_save : 1;
+};
+
+static gboolean
+gbp_auto_save_buffer_addin_source_cb (gpointer user_data)
+{
+  GbpAutoSaveBufferAddin *self = user_data;
+
+  g_assert (GBP_IS_AUTO_SAVE_BUFFER_ADDIN (self));
+
+  self->source_id = 0;
+
+  if (gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (self->buffer)))
+    ide_buffer_save_file_async (self->buffer, NULL, NULL, NULL, NULL, NULL);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_auto_save_buffer_addin_create_source (GbpAutoSaveBufferAddin *self)
+{
+  g_assert (GBP_IS_AUTO_SAVE_BUFFER_ADDIN (self));
+
+  if (!self->auto_save)
+    return;
+
+  if (self->source_id == 0)
+    self->source_id = g_timeout_add_seconds_full (G_PRIORITY_HIGH,
+                                                  self->auto_save_timeout,
+                                                  gbp_auto_save_buffer_addin_source_cb,
+                                                  g_object_ref (self),
+                                                  g_object_unref);
+}
+
+static void
+gbp_auto_save_buffer_addin_change_settled_cb (GbpAutoSaveBufferAddin *self,
+                                              IdeBuffer              *buffer)
+{
+  g_assert (GBP_IS_AUTO_SAVE_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  g_clear_handle_id (&self->source_id, g_source_remove);
+  gbp_auto_save_buffer_addin_create_source (self);
+}
+
+static void
+gbp_auto_save_buffer_addin_modified_changed_cb (GbpAutoSaveBufferAddin *self,
+                                                IdeBuffer              *buffer)
+{
+  g_assert (GBP_IS_AUTO_SAVE_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (!gtk_text_buffer_get_modified (GTK_TEXT_BUFFER (buffer)))
+    g_clear_handle_id (&self->source_id, g_source_remove);
+  else
+    gbp_auto_save_buffer_addin_create_source (self);
+}
+
+static void
+gbp_auto_save_buffer_addin_changed_cb (GbpAutoSaveBufferAddin *self,
+                                       const gchar            *key,
+                                       GSettings              *settings)
+{
+  g_assert (GBP_IS_AUTO_SAVE_BUFFER_ADDIN (self));
+  g_assert (G_IS_SETTINGS (settings));
+
+  self->auto_save = g_settings_get_boolean (settings, "auto-save");
+  self->auto_save_timeout = g_settings_get_int (settings, "auto-save-timeout");
+
+  if (self->auto_save_timeout == 0)
+    self->auto_save_timeout = 60;
+
+  g_clear_handle_id (&self->source_id, g_source_remove);
+}
+
+static void
+gbp_auto_save_buffer_addin_load (IdeBufferAddin *addin,
+                                 IdeBuffer      *buffer)
+{
+  GbpAutoSaveBufferAddin *self = (GbpAutoSaveBufferAddin *)addin;
+
+  g_assert (GBP_IS_AUTO_SAVE_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  self->buffer = buffer;
+  self->settings = g_settings_new ("org.gnome.builder.editor");
+
+  self->auto_save = g_settings_get_boolean (self->settings, "auto-save");
+  self->auto_save_timeout = g_settings_get_int (self->settings, "auto-save-timeout");
+
+  g_signal_connect_object (self->settings,
+                           "changed::auto-save",
+                           G_CALLBACK (gbp_auto_save_buffer_addin_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->settings,
+                           "changed::auto-save-timeout",
+                           G_CALLBACK (gbp_auto_save_buffer_addin_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (buffer,
+                           "change-settled",
+                           G_CALLBACK (gbp_auto_save_buffer_addin_change_settled_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (buffer,
+                           "modified-changed",
+                           G_CALLBACK (gbp_auto_save_buffer_addin_modified_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+}
+
+static void
+gbp_auto_save_buffer_addin_unload (IdeBufferAddin *addin,
+                                   IdeBuffer      *buffer)
+{
+  GbpAutoSaveBufferAddin *self = (GbpAutoSaveBufferAddin *)addin;
+
+  g_assert (GBP_IS_AUTO_SAVE_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  g_clear_handle_id (&self->source_id, g_source_remove);
+
+  g_signal_handlers_disconnect_by_func (buffer,
+                                        G_CALLBACK (gbp_auto_save_buffer_addin_change_settled_cb),
+                                        self);
+  g_signal_handlers_disconnect_by_func (buffer,
+                                        G_CALLBACK (gbp_auto_save_buffer_addin_modified_changed_cb),
+                                        self);
+
+  g_clear_object (&self->settings);
+
+  self->buffer = NULL;
+}
+
+static void
+gbp_auto_save_buffer_addin_save_file (IdeBufferAddin *addin,
+                                      IdeBuffer      *buffer,
+                                      GFile          *file)
+{
+  GbpAutoSaveBufferAddin *self = (GbpAutoSaveBufferAddin *)addin;
+  GFile *orig_file;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_AUTO_SAVE_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  orig_file = ide_buffer_get_file (buffer);
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_FILE (orig_file));
+
+  /* If the user requests the buffer save its contents to the original
+   * backing file, then we can drop our auto-save request.
+   */
+  if (g_file_equal (file, orig_file))
+    g_clear_handle_id (&self->source_id, g_source_remove);
+}
+
+static void
+gbp_auto_save_buffer_addin_file_loaded (IdeBufferAddin *addin,
+                                        IdeBuffer      *buffer,
+                                        GFile          *file)
+{
+  GbpAutoSaveBufferAddin *self = (GbpAutoSaveBufferAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_AUTO_SAVE_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+
+  /* Contents just finished loading, clear any queued requests
+   * that happened while loading.
+   */
+  g_clear_handle_id (&self->source_id, g_source_remove);
+}
+
+static void
+buffer_addin_iface_init (IdeBufferAddinInterface *iface)
+{
+  iface->load = gbp_auto_save_buffer_addin_load;
+  iface->unload = gbp_auto_save_buffer_addin_unload;
+  iface->save_file = gbp_auto_save_buffer_addin_save_file;
+  iface->file_loaded = gbp_auto_save_buffer_addin_file_loaded;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpAutoSaveBufferAddin, gbp_auto_save_buffer_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUFFER_ADDIN, buffer_addin_iface_init))
+
+static void
+gbp_auto_save_buffer_addin_class_init (GbpAutoSaveBufferAddinClass *klass)
+{
+}
+
+static void
+gbp_auto_save_buffer_addin_init (GbpAutoSaveBufferAddin *self)
+{
+}
diff --git a/src/plugins/auto-save/gbp-auto-save-buffer-addin.h 
b/src/plugins/auto-save/gbp-auto-save-buffer-addin.h
new file mode 100644
index 000000000..c7fd8d592
--- /dev/null
+++ b/src/plugins/auto-save/gbp-auto-save-buffer-addin.h
@@ -0,0 +1,31 @@
+/* gbp-auto-save-buffer-addin.h
+ *
+ * Copyright 2018-2019 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_AUTO_SAVE_BUFFER_ADDIN (gbp_auto_save_buffer_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpAutoSaveBufferAddin, gbp_auto_save_buffer_addin, GBP, AUTO_SAVE_BUFFER_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/auto-save/meson.build b/src/plugins/auto-save/meson.build
new file mode 100644
index 000000000..8beb1b0ac
--- /dev/null
+++ b/src/plugins/auto-save/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'auto-save-plugin.c',
+  'gbp-auto-save-buffer-addin.c',
+])
+
+plugin_auto_save_resources = gnome.compile_resources(
+  'gbp-auto-save-resources',
+  'auto-save.gresource.xml',
+  c_name: 'gbp_auto_save',
+)
+
+plugins_sources += plugin_auto_save_resources[0]
diff --git a/src/plugins/autotools/autotools-plugin.c b/src/plugins/autotools/autotools-plugin.c
index 358ab351f..e75a1ca9a 100644
--- a/src/plugins/autotools/autotools-plugin.c
+++ b/src/plugins/autotools/autotools-plugin.c
@@ -1,6 +1,6 @@
 /* autotools-plugin.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,19 +14,33 @@
  *
  * 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
  */
 
+#include "config.h"
+
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-foundry.h>
 
 #include "ide-autotools-build-system.h"
+#include "gbp-autotools-build-system-discovery.h"
 #include "ide-autotools-build-target-provider.h"
 #include "ide-autotools-pipeline-addin.h"
 
-void
-ide_autotools_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_ide_autotools_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_PIPELINE_ADDIN, 
IDE_TYPE_AUTOTOOLS_PIPELINE_ADDIN);
-  peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_SYSTEM, 
IDE_TYPE_AUTOTOOLS_BUILD_SYSTEM);
-  peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_TARGET_PROVIDER, 
IDE_TYPE_AUTOTOOLS_BUILD_TARGET_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                              IDE_TYPE_AUTOTOOLS_PIPELINE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_SYSTEM,
+                                              IDE_TYPE_AUTOTOOLS_BUILD_SYSTEM);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_SYSTEM_DISCOVERY,
+                                              GBP_TYPE_AUTOTOOLS_BUILD_SYSTEM_DISCOVERY);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_TARGET_PROVIDER,
+                                              IDE_TYPE_AUTOTOOLS_BUILD_TARGET_PROVIDER);
 }
diff --git a/src/plugins/autotools/autotools.gresource.xml b/src/plugins/autotools/autotools.gresource.xml
index 0da868b96..ab16c25e4 100644
--- a/src/plugins/autotools/autotools.gresource.xml
+++ b/src/plugins/autotools/autotools.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/autotools">
     <file>autotools.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/autotools/autotools.plugin b/src/plugins/autotools/autotools.plugin
index 0e77ccf85..93cbe5b98 100644
--- a/src/plugins/autotools/autotools.plugin
+++ b/src/plugins/autotools/autotools.plugin
@@ -1,10 +1,11 @@
 [Plugin]
-Module=autotools-plugin
-Name=Autotools
-Description=Provides integration with the Autotools build system
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
-Embedded=ide_autotools_register_types
-X-Project-File-Filter-Pattern=configure.ac,configure.in
+Copyright=Copyright © 2015-2018 Christian Hergert
+Description=Provides integration with the Autotools build system
+Embedded=_ide_autotools_register_types
+Hidden=true
+Module=autotools
+Name=Autotools
 X-Project-File-Filter-Name=Autotools Project (configure.ac)
+X-Project-File-Filter-Pattern=configure.ac,configure.in
diff --git a/src/plugins/autotools/gbp-autotools-build-system-discovery.c 
b/src/plugins/autotools/gbp-autotools-build-system-discovery.c
new file mode 100644
index 000000000..31239425f
--- /dev/null
+++ b/src/plugins/autotools/gbp-autotools-build-system-discovery.c
@@ -0,0 +1,49 @@
+/* gbp-autotools-build-system-discovery.c
+ *
+ * Copyright 2018-2019 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-autotools-build-system-discovery"
+
+#include "config.h"
+
+#include "gbp-autotools-build-system-discovery.h"
+
+struct _GbpAutotoolsBuildSystemDiscovery
+{
+  IdeSimpleBuildSystemDiscovery parent_instance;
+};
+
+G_DEFINE_TYPE (GbpAutotoolsBuildSystemDiscovery,
+               gbp_autotools_build_system_discovery,
+               IDE_TYPE_SIMPLE_BUILD_SYSTEM_DISCOVERY)
+
+static void
+gbp_autotools_build_system_discovery_class_init (GbpAutotoolsBuildSystemDiscoveryClass *klass)
+{
+}
+
+static void
+gbp_autotools_build_system_discovery_init (GbpAutotoolsBuildSystemDiscovery *self)
+{
+  g_object_set (self,
+                "glob", "configure.ac",
+                "hint", "autotools",
+                "priority", 0,
+                NULL);
+}
diff --git a/src/plugins/autotools/gbp-autotools-build-system-discovery.h 
b/src/plugins/autotools/gbp-autotools-build-system-discovery.h
new file mode 100644
index 000000000..61238c280
--- /dev/null
+++ b/src/plugins/autotools/gbp-autotools-build-system-discovery.h
@@ -0,0 +1,31 @@
+/* gbp-autotools-build-system-discovery.h
+ *
+ * Copyright 2018-2019 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-foundry.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_AUTOTOOLS_BUILD_SYSTEM_DISCOVERY (gbp_autotools_build_system_discovery_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpAutotoolsBuildSystemDiscovery, gbp_autotools_build_system_discovery, GBP, 
AUTOTOOLS_BUILD_SYSTEM_DISCOVERY, IdeSimpleBuildSystemDiscovery)
+
+G_END_DECLS
diff --git a/src/plugins/autotools/ide-autotools-autogen-stage.c 
b/src/plugins/autotools/ide-autotools-autogen-stage.c
index b1bcfdb31..ed3bceaa1 100644
--- a/src/plugins/autotools/ide-autotools-autogen-stage.c
+++ b/src/plugins/autotools/ide-autotools-autogen-stage.c
@@ -1,6 +1,6 @@
 /* ide-autotools-autogen-stage.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-autotools-autogen-stage"
diff --git a/src/plugins/autotools/ide-autotools-autogen-stage.h 
b/src/plugins/autotools/ide-autotools-autogen-stage.h
index 0785c9803..b3ef5d8c0 100644
--- a/src/plugins/autotools/ide-autotools-autogen-stage.h
+++ b/src/plugins/autotools/ide-autotools-autogen-stage.h
@@ -1,6 +1,6 @@
 /* ide-autotools-autogen-stage.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/autotools/ide-autotools-build-system.c 
b/src/plugins/autotools/ide-autotools-build-system.c
index 9e50f3d51..360d6134e 100644
--- a/src/plugins/autotools/ide-autotools-build-system.c
+++ b/src/plugins/autotools/ide-autotools-build-system.c
@@ -1,6 +1,6 @@
 /* ide-autotools-build-system.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-autotools-build-system"
@@ -22,7 +24,8 @@
 
 #include <gio/gio.h>
 #include <gtksourceview/gtksource.h>
-#include <ide.h>
+#include <libide-foundry.h>
+#include <libide-vcs.h>
 #include <string.h>
 
 #include "ide-autotools-build-system.h"
@@ -202,7 +205,7 @@ invalidate_makecache_stage (gpointer data,
 static void
 evict_makecache (IdeContext *context)
 {
-  IdeBuildManager *build_manager = ide_context_get_build_manager (context);
+  IdeBuildManager *build_manager = ide_build_manager_from_context (context);
   IdeBuildPipeline *pipeline = ide_build_manager_get_pipeline (build_manager);
 
   ide_build_pipeline_foreach_stage (pipeline, invalidate_makecache_stage, NULL);
@@ -213,12 +216,12 @@ looks_like_makefile (IdeBuffer *buffer)
 {
   GtkSourceLanguage *language;
   const gchar *path;
-  IdeFile *file;
+  GFile *file;
 
   g_assert (IDE_IS_BUFFER (buffer));
 
   file = ide_buffer_get_file (buffer);
-  path = ide_file_get_path (file);
+  path = g_file_peek_path (file);
 
   if (path != NULL)
     {
@@ -271,45 +274,32 @@ ide_autotools_build_system__vcs_changed_cb (IdeAutotoolsBuildSystem *self,
 }
 
 static void
-ide_autotools_build_system__context_loaded_cb (IdeAutotoolsBuildSystem *self,
-                                               IdeContext              *context)
-{
-  IdeVcs *vcs;
-
-  IDE_ENTRY;
-
-  g_assert (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (self));
-  g_assert (IDE_IS_CONTEXT (context));
-
-  vcs = ide_context_get_vcs (context);
-
-  g_signal_connect_object (vcs,
-                           "changed",
-                           G_CALLBACK (ide_autotools_build_system__vcs_changed_cb),
-                           self,
-                           G_CONNECT_SWAPPED);
-
-  IDE_EXIT;
-}
-
-static void
-ide_autotools_build_system_constructed (GObject *object)
+ide_autotools_build_system_parent_set (IdeObject *object,
+                                       IdeObject *parent)
 {
   IdeAutotoolsBuildSystem *self = (IdeAutotoolsBuildSystem *)object;
   IdeBufferManager *buffer_manager;
   IdeContext *context;
+  IdeVcs *vcs;
 
-  G_OBJECT_CLASS (ide_autotools_build_system_parent_class)->constructed (object);
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (parent == NULL)
+    return;
 
   context = ide_object_get_context (IDE_OBJECT (self));
   g_assert (IDE_IS_CONTEXT (context));
 
-  buffer_manager = ide_context_get_buffer_manager (context);
+  buffer_manager = ide_buffer_manager_from_context (context);
   g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
 
-  g_signal_connect_object (context,
-                           "loaded",
-                           G_CALLBACK (ide_autotools_build_system__context_loaded_cb),
+  vcs = ide_vcs_from_context (context);
+
+  g_signal_connect_object (vcs,
+                           "changed",
+                           G_CALLBACK (ide_autotools_build_system__vcs_changed_cb),
                            self,
                            G_CONNECT_SWAPPED);
 
@@ -334,7 +324,7 @@ ide_autotools_build_system_constructed (GObject *object)
 static gint
 ide_autotools_build_system_get_priority (IdeBuildSystem *system)
 {
-  return -500;
+  return 0;
 }
 
 static void
@@ -441,7 +431,7 @@ ide_autotools_build_system_get_build_flags_execute_cb (GObject      *object,
 
 static void
 ide_autotools_build_system_get_build_flags_async (IdeBuildSystem      *build_system,
-                                                  IdeFile             *file,
+                                                  GFile               *file,
                                                   GCancellable        *cancellable,
                                                   GAsyncReadyCallback  callback,
                                                   gpointer             user_data)
@@ -454,12 +444,12 @@ ide_autotools_build_system_get_build_flags_async (IdeBuildSystem      *build_sys
   IDE_ENTRY;
 
   g_assert (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (self));
-  g_assert (IDE_IS_FILE (file));
+  g_assert (G_IS_FILE (file));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_autotools_build_system_get_build_flags_async);
-  ide_task_set_task_data (task, g_object_ref (ide_file_get_file (file)), g_object_unref);
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
 
   /*
    * To get the build flags for the file, we first need to get the makecache
@@ -470,10 +460,11 @@ ide_autotools_build_system_get_build_flags_async (IdeBuildSystem      *build_sys
    */
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
 
   ide_build_manager_execute_async (build_manager,
                                    IDE_BUILD_PHASE_CONFIGURE,
+                                   NULL,
                                    cancellable,
                                    ide_autotools_build_system_get_build_flags_execute_cb,
                                    g_steal_pointer (&task));
@@ -512,8 +503,8 @@ ide_autotools_build_system_get_builddir (IdeBuildSystem   *build_system,
    */
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  vcs = ide_vcs_from_context (context);
+  workdir = ide_vcs_get_workdir (vcs);
 
   if (!g_file_is_native (workdir))
     return NULL;
@@ -539,14 +530,14 @@ ide_autotools_build_system_get_display_name (IdeBuildSystem *build_system)
 }
 
 static void
-ide_autotools_build_system_finalize (GObject *object)
+ide_autotools_build_system_destroy (IdeObject *object)
 {
   IdeAutotoolsBuildSystem *self = (IdeAutotoolsBuildSystem *)object;
 
   g_clear_pointer (&self->tarball_name, g_free);
   g_clear_object (&self->project_file);
 
-  G_OBJECT_CLASS (ide_autotools_build_system_parent_class)->finalize (object);
+  IDE_OBJECT_CLASS (ide_autotools_build_system_parent_class)->destroy (object);
 }
 
 static void
@@ -608,12 +599,14 @@ static void
 ide_autotools_build_system_class_init (IdeAutotoolsBuildSystemClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
 
-  object_class->constructed = ide_autotools_build_system_constructed;
-  object_class->finalize = ide_autotools_build_system_finalize;
   object_class->get_property = ide_autotools_build_system_get_property;
   object_class->set_property = ide_autotools_build_system_set_property;
 
+  i_object_class->parent_set = ide_autotools_build_system_parent_set;
+  i_object_class->destroy = ide_autotools_build_system_destroy;
+
   properties [PROP_TARBALL_NAME] =
     g_param_spec_string ("tarball-name",
                          "Tarball Name",
@@ -726,28 +719,23 @@ ide_autotools_build_system_init_async (GAsyncInitable      *initable,
                                        GAsyncReadyCallback  callback,
                                        gpointer             user_data)
 {
-  IdeAutotoolsBuildSystem *system = (IdeAutotoolsBuildSystem *)initable;
+  IdeAutotoolsBuildSystem *self = (IdeAutotoolsBuildSystem *)initable;
   g_autoptr(IdeTask) task = NULL;
-  IdeContext *context;
-  GFile *project_file;
 
   IDE_ENTRY;
 
-  g_return_if_fail (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (system));
+  g_return_if_fail (IDE_IS_AUTOTOOLS_BUILD_SYSTEM (self));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  task = ide_task_new (initable, cancellable, callback, user_data);
+  task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_autotools_build_system_init_async);
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
-  context = ide_object_get_context (IDE_OBJECT (system));
-  project_file = ide_context_get_project_file (context);
-
-  ide_autotools_build_system_discover_file_async (system,
-                                                  project_file,
+  ide_autotools_build_system_discover_file_async (self,
+                                                  self->project_file,
                                                   cancellable,
                                                   discover_file_cb,
-                                                  g_object_ref (task));
+                                                  g_steal_pointer (&task));
 
   IDE_EXIT;
 }
diff --git a/src/plugins/autotools/ide-autotools-build-system.h 
b/src/plugins/autotools/ide-autotools-build-system.h
index 5ec399074..f1340d515 100644
--- a/src/plugins/autotools/ide-autotools-build-system.h
+++ b/src/plugins/autotools/ide-autotools-build-system.h
@@ -1,6 +1,6 @@
 /* ide-autotools-build-system.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/autotools/ide-autotools-build-target-provider.c 
b/src/plugins/autotools/ide-autotools-build-target-provider.c
index 8c3e8837a..422dd13dd 100644
--- a/src/plugins/autotools/ide-autotools-build-target-provider.c
+++ b/src/plugins/autotools/ide-autotools-build-target-provider.c
@@ -1,6 +1,6 @@
 /* ide-autotools-build-target-provider.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-autotools-build-target-provider"
@@ -94,7 +96,7 @@ ide_autotools_build_target_provider_get_targets_async (IdeBuildTargetProvider *p
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
 
   if (!IDE_IS_AUTOTOOLS_BUILD_SYSTEM (build_system))
     {
@@ -105,7 +107,7 @@ ide_autotools_build_target_provider_get_targets_async (IdeBuildTargetProvider *p
       IDE_EXIT;
     }
 
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   pipeline = ide_build_manager_get_pipeline (build_manager);
   builddir = ide_build_pipeline_get_builddir (pipeline);
   builddir_file = g_file_new_for_path (builddir);
diff --git a/src/plugins/autotools/ide-autotools-build-target-provider.h 
b/src/plugins/autotools/ide-autotools-build-target-provider.h
index f3153b76f..9d0700e29 100644
--- a/src/plugins/autotools/ide-autotools-build-target-provider.h
+++ b/src/plugins/autotools/ide-autotools-build-target-provider.h
@@ -1,6 +1,6 @@
 /* ide-autotools-build-target-provider.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/autotools/ide-autotools-build-target.c 
b/src/plugins/autotools/ide-autotools-build-target.c
index 9d3b0ef82..872c21add 100644
--- a/src/plugins/autotools/ide-autotools-build-target.c
+++ b/src/plugins/autotools/ide-autotools-build-target.c
@@ -1,6 +1,6 @@
 /* ide-autotools-build-target.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-autotools-build-target"
diff --git a/src/plugins/autotools/ide-autotools-build-target.h 
b/src/plugins/autotools/ide-autotools-build-target.h
index 4a180014d..ffd415607 100644
--- a/src/plugins/autotools/ide-autotools-build-target.h
+++ b/src/plugins/autotools/ide-autotools-build-target.h
@@ -1,6 +1,6 @@
 /* ide-autotools-build-target.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/autotools/ide-autotools-make-stage.c 
b/src/plugins/autotools/ide-autotools-make-stage.c
index b54edf473..546101a30 100644
--- a/src/plugins/autotools/ide-autotools-make-stage.c
+++ b/src/plugins/autotools/ide-autotools-make-stage.c
@@ -1,6 +1,6 @@
 /* ide-autotools-make-stage.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-autotools-make-stage"
@@ -338,6 +340,7 @@ ide_autotools_make_stage_clean_finish (IdeBuildStage  *stage,
 static void
 ide_autotools_make_stage_query (IdeBuildStage    *stage,
                                 IdeBuildPipeline *pipeline,
+                                GPtrArray        *targets,
                                 GCancellable     *cancellable)
 {
   IDE_ENTRY;
diff --git a/src/plugins/autotools/ide-autotools-make-stage.h 
b/src/plugins/autotools/ide-autotools-make-stage.h
index 777498096..fca10a50d 100644
--- a/src/plugins/autotools/ide-autotools-make-stage.h
+++ b/src/plugins/autotools/ide-autotools-make-stage.h
@@ -1,6 +1,6 @@
 /* ide-autotools-make-stage.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/autotools/ide-autotools-makecache-stage.c 
b/src/plugins/autotools/ide-autotools-makecache-stage.c
index 5ec7455af..81ae9743e 100644
--- a/src/plugins/autotools/ide-autotools-makecache-stage.c
+++ b/src/plugins/autotools/ide-autotools-makecache-stage.c
@@ -1,6 +1,6 @@
 /* ide-autotools-makecache-stage.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-autotools-makecache-stage"
@@ -59,7 +61,9 @@ ide_autotools_makecache_stage_makecache_cb (GObject      *object,
   self = ide_task_get_source_object (task);
   g_assert (IDE_IS_AUTOTOOLS_MAKECACHE_STAGE (self));
 
-  g_clear_object (&self->makecache);
+  ide_clear_and_destroy_object (&self->makecache);
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (makecache));
+
   self->makecache = g_steal_pointer (&makecache);
 
   ide_task_return_boolean (task, TRUE);
@@ -205,13 +209,11 @@ ide_autotools_makecache_stage_new_for_pipeline (IdeBuildPipeline  *pipeline,
   const gchar *make = "make";
   IdeConfiguration *config;
   IdeRuntime *runtime;
-  IdeContext *context;
 
   IDE_ENTRY;
 
   g_return_val_if_fail (IDE_IS_BUILD_PIPELINE (pipeline), NULL);
 
-  context = ide_object_get_context (IDE_OBJECT (pipeline));
   config = ide_build_pipeline_get_configuration (pipeline);
   runtime = ide_configuration_get_runtime (config);
 
@@ -229,7 +231,6 @@ ide_autotools_makecache_stage_new_for_pipeline (IdeBuildPipeline  *pipeline,
   ide_subprocess_launcher_push_argv (launcher, "-s");
 
   stage = g_object_new (IDE_TYPE_AUTOTOOLS_MAKECACHE_STAGE,
-                        "context", context,
                         "launcher", launcher,
                         "ignore-exit-status", TRUE,
                         NULL);
diff --git a/src/plugins/autotools/ide-autotools-makecache-stage.h 
b/src/plugins/autotools/ide-autotools-makecache-stage.h
index c762fd405..98b6a1d55 100644
--- a/src/plugins/autotools/ide-autotools-makecache-stage.h
+++ b/src/plugins/autotools/ide-autotools-makecache-stage.h
@@ -1,6 +1,6 @@
 /* ide-autotools-makecache-stage.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 #include "ide-makecache.h"
 
diff --git a/src/plugins/autotools/ide-autotools-pipeline-addin.c 
b/src/plugins/autotools/ide-autotools-pipeline-addin.c
index 481579140..628819828 100644
--- a/src/plugins/autotools/ide-autotools-pipeline-addin.c
+++ b/src/plugins/autotools/ide-autotools-pipeline-addin.c
@@ -1,6 +1,6 @@
 /* ide-autotools-pipeline-addin.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-autotools-pipeline-addin"
@@ -26,8 +28,6 @@
 #include "ide-autotools-makecache-stage.h"
 #include "ide-autotools-pipeline-addin.h"
 
-#include "toolchain/ide-simple-toolchain.h"
-
 static gboolean
 register_autoreconf_stage (IdeAutotoolsPipelineAddin  *self,
                            IdeBuildPipeline           *pipeline,
@@ -35,7 +35,6 @@ register_autoreconf_stage (IdeAutotoolsPipelineAddin  *self,
 {
   g_autofree gchar *configure_path = NULL;
   g_autoptr(IdeBuildStage) stage = NULL;
-  IdeContext *context;
   const gchar *srcdir;
   gboolean completed;
   guint stage_id;
@@ -43,7 +42,6 @@ register_autoreconf_stage (IdeAutotoolsPipelineAddin  *self,
   g_assert (IDE_IS_AUTOTOOLS_PIPELINE_ADDIN (self));
   g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
   configure_path = ide_build_pipeline_build_srcdir_path (pipeline, "configure", NULL);
   completed = g_file_test (configure_path, G_FILE_TEST_IS_REGULAR);
   srcdir = ide_build_pipeline_get_srcdir (pipeline);
@@ -51,11 +49,10 @@ register_autoreconf_stage (IdeAutotoolsPipelineAddin  *self,
   stage = g_object_new (IDE_TYPE_AUTOTOOLS_AUTOGEN_STAGE,
                         "name", _("Bootstrapping build system"),
                         "completed", completed,
-                        "context", context,
                         "srcdir", srcdir,
                         NULL);
 
-  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_AUTOGEN, 0, stage);
+  stage_id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_AUTOGEN, 0, stage);
 
   ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
 
@@ -97,6 +94,7 @@ compare_mtime (const gchar *path_a,
 static void
 check_configure_status (IdeAutotoolsPipelineAddin *self,
                         IdeBuildPipeline          *pipeline,
+                        GPtrArray                 *targets,
                         GCancellable              *cancellable,
                         IdeBuildStage             *stage)
 {
@@ -289,7 +287,6 @@ register_configure_stage (IdeAutotoolsPipelineAddin  *self,
 
   stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
                         "name", _("Configuring project"),
-                        "context", ide_object_get_context (IDE_OBJECT (self)),
                         "launcher", launcher,
                         NULL);
 
@@ -312,7 +309,7 @@ register_configure_stage (IdeAutotoolsPipelineAddin  *self,
                            self,
                            G_CONNECT_SWAPPED);
 
-  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_CONFIGURE, 0, stage);
+  stage_id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_CONFIGURE, 0, stage);
 
   ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
 
@@ -329,26 +326,23 @@ register_make_stage (IdeAutotoolsPipelineAddin  *self,
 {
   g_autoptr(IdeBuildStage) stage = NULL;
   IdeConfiguration *config;
-  IdeContext *context;
   guint stage_id;
   gint parallel;
 
   g_assert (IDE_IS_AUTOTOOLS_PIPELINE_ADDIN (self));
   g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  context = ide_object_get_context (IDE_OBJECT (pipeline));
   config = ide_build_pipeline_get_configuration (pipeline);
   parallel = ide_configuration_get_parallelism (config);
 
   stage = g_object_new (IDE_TYPE_AUTOTOOLS_MAKE_STAGE,
                         "name", _("Building project"),
                         "clean-target", clean_target,
-                        "context", context,
                         "parallel", parallel,
                         "target", target,
                         NULL);
 
-  stage_id = ide_build_pipeline_connect (pipeline, phase, 0, stage);
+  stage_id = ide_build_pipeline_attach (pipeline, phase, 0, stage);
   ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
 
   return TRUE;
@@ -370,10 +364,10 @@ register_makecache_stage (IdeAutotoolsPipelineAddin  *self,
 
   ide_build_stage_set_name (stage, _("Caching build commands"));
 
-  stage_id = ide_build_pipeline_connect (pipeline,
-                                         IDE_BUILD_PHASE_CONFIGURE | IDE_BUILD_PHASE_AFTER,
-                                         0,
-                                         stage);
+  stage_id = ide_build_pipeline_attach (pipeline,
+                                        IDE_BUILD_PHASE_CONFIGURE | IDE_BUILD_PHASE_AFTER,
+                                        0,
+                                        stage);
   ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
 
   return TRUE;
@@ -392,7 +386,7 @@ ide_autotools_pipeline_addin_load (IdeBuildPipelineAddin *addin,
   g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
   context = ide_object_get_context (IDE_OBJECT (addin));
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
 
   if (!IDE_IS_AUTOTOOLS_BUILD_SYSTEM (build_system))
     return;
diff --git a/src/plugins/autotools/ide-autotools-pipeline-addin.h 
b/src/plugins/autotools/ide-autotools-pipeline-addin.h
index e61c7e06f..988bd3cc3 100644
--- a/src/plugins/autotools/ide-autotools-pipeline-addin.h
+++ b/src/plugins/autotools/ide-autotools-pipeline-addin.h
@@ -1,6 +1,6 @@
 /* ide-autotools-pipeline-addin.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/autotools/ide-makecache-target.c b/src/plugins/autotools/ide-makecache-target.c
index e4520b94a..d60f03af4 100644
--- a/src/plugins/autotools/ide-makecache-target.c
+++ b/src/plugins/autotools/ide-makecache-target.c
@@ -1,6 +1,6 @@
 /* ide-makecache-target.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-makecache-target"
diff --git a/src/plugins/autotools/ide-makecache-target.h b/src/plugins/autotools/ide-makecache-target.h
index aaa159b53..696daefc0 100644
--- a/src/plugins/autotools/ide-makecache-target.h
+++ b/src/plugins/autotools/ide-makecache-target.h
@@ -1,6 +1,6 @@
 /* ide-makecache-target.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/autotools/ide-makecache.c b/src/plugins/autotools/ide-makecache.c
index 5c7d35a94..3aa5ad5a6 100644
--- a/src/plugins/autotools/ide-makecache.c
+++ b/src/plugins/autotools/ide-makecache.c
@@ -1,7 +1,7 @@
 /* ide-makecache.c
  *
  * Copyright 2013 Jesse van den Kieboom <jessevdk gnome org>
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -15,6 +15,8 @@
  *
  * 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 "ide-makecache"
@@ -30,7 +32,9 @@
 #include <glib/gstdio.h>
 #include <string.h>
 #include <unistd.h>
-#include <ide.h>
+
+#include <libide-foundry.h>
+#include <libide-vcs.h>
 
 #include "ide-autotools-build-target.h"
 #include "ide-makecache.h"
@@ -123,8 +127,8 @@ ide_makecache_get_relative_path (IdeMakecache *self,
   g_assert (G_IS_FILE (file));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  vcs = ide_vcs_from_context (context);
+  workdir = ide_vcs_get_workdir (vcs);
 
   return g_file_get_relative_path (workdir, file);
 }
@@ -1106,7 +1110,6 @@ ide_makecache_new_for_cache_file_async (IdeRuntime          *runtime,
   g_autoptr(GMappedFile) mapped = NULL;
   g_autoptr(GError) error = NULL;
   g_autofree gchar *cache_path = NULL;
-  IdeContext *context;
 
   IDE_ENTRY;
 
@@ -1148,11 +1151,7 @@ ide_makecache_new_for_cache_file_async (IdeRuntime          *runtime,
       IDE_EXIT;
     }
 
-  context = ide_object_get_context (IDE_OBJECT (runtime));
-
-  self = g_object_new (IDE_TYPE_MAKECACHE,
-                       "context", context,
-                       NULL);
+  self = g_object_new (IDE_TYPE_MAKECACHE, NULL);
 
   mapped = g_mapped_file_new (cache_path, FALSE, &error);
 
@@ -1482,7 +1481,7 @@ ide_makecache_get_build_targets_worker (GTask        *task,
    */
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  configmgr = ide_context_get_configuration_manager (context);
+  configmgr = ide_configuration_manager_from_context (context);
   config = ide_configuration_manager_get_current (configmgr);
   runtime = ide_configuration_get_runtime (config);
 
@@ -1675,7 +1674,6 @@ ide_makecache_get_build_targets_worker (GTask        *task,
 
               target = g_object_new (IDE_TYPE_AUTOTOOLS_BUILD_TARGET,
                                      "build-directory", makedir,
-                                     "context", context,
                                      "install-directory", installdir,
                                      "name", name,
                                      NULL);
diff --git a/src/plugins/autotools/ide-makecache.h b/src/plugins/autotools/ide-makecache.h
index 290876110..75604b247 100644
--- a/src/plugins/autotools/ide-makecache.h
+++ b/src/plugins/autotools/ide-makecache.h
@@ -1,6 +1,6 @@
 /* ide-makecache.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 #include "ide-makecache-target.h"
 
diff --git a/src/plugins/autotools/meson.build b/src/plugins/autotools/meson.build
index 94506d3dd..93c592742 100644
--- a/src/plugins/autotools/meson.build
+++ b/src/plugins/autotools/meson.build
@@ -1,34 +1,25 @@
-if get_option('with_autotools')
+if get_option('plugin_autotools')
 
-autotools_resources = gnome.compile_resources(    
-  'ide-autotools-resources',                      
-  'autotools.gresource.xml',                      
-  c_name: 'ide_autotools',                        
-)                                           
-
-autotools_sources = [
+plugins_sources += files([
   'autotools-plugin.c',
   'ide-autotools-autogen-stage.c',
-  'ide-autotools-autogen-stage.h',
   'ide-autotools-build-system.c',
-  'ide-autotools-build-system.h',
-  'ide-autotools-build-target.c',
-  'ide-autotools-build-target.h',
+  'gbp-autotools-build-system-discovery.c',
   'ide-autotools-build-target-provider.c',
-  'ide-autotools-build-target-provider.h',
+  'ide-autotools-build-target.c',
   'ide-autotools-make-stage.c',
-  'ide-autotools-make-stage.h',
   'ide-autotools-makecache-stage.c',
-  'ide-autotools-makecache-stage.h',
   'ide-autotools-pipeline-addin.c',
-  'ide-autotools-pipeline-addin.h',
-  'ide-makecache.c',
-  'ide-makecache.h',
   'ide-makecache-target.c',
-  'ide-makecache-target.h',
-]
+  'ide-makecache.c',
+])
+
+plugin_autotools_resources = gnome.compile_resources(
+  'gbp-autotools-resources',
+  'autotools.gresource.xml',
+  c_name: 'gbp_autotools',
+)
 
-gnome_builder_plugins_sources += files(autotools_sources)       
-gnome_builder_plugins_sources += autotools_resources[0]         
+plugins_sources += plugin_autotools_resources[0]
 
 endif
diff --git a/src/plugins/beautifier/beautifier-plugin.c b/src/plugins/beautifier/beautifier-plugin.c
new file mode 100644
index 000000000..ec065e5ef
--- /dev/null
+++ b/src/plugins/beautifier/beautifier-plugin.c
@@ -0,0 +1,34 @@
+/* beautifier-plugin.c
+ *
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include <libide-editor.h>
+#include <libpeas/peas.h>
+
+#include "gb-beautifier-editor-addin.h"
+
+_IDE_EXTERN void
+_gb_beautifier_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_ADDIN,
+                                              GB_TYPE_BEAUTIFIER_EDITOR_ADDIN);
+}
diff --git a/src/plugins/beautifier/beautifier.gresource.xml b/src/plugins/beautifier/beautifier.gresource.xml
new file mode 100644
index 000000000..6e40f6899
--- /dev/null
+++ b/src/plugins/beautifier/beautifier.gresource.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/beautifier">
+    <file>beautifier.plugin</file>
+
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+
+    <file>config/global.ini</file>
+    <file>config/automake/config.ini</file>
+    <file>config/c/config.ini</file>
+    <file>config/c/gnu-indent.cfg</file>
+    <file>config/c/kr.cfg</file>
+    <file>config/c/linux-kernel.cfg</file>
+    <file>config/c-sharp/config.ini</file>
+    <file>config/c-sharp/mono.cfg</file>
+    <file>config/d/config.ini</file>
+    <file>config/d/d.cfg</file>
+    <file>config/html/config.ini</file>
+    <file>config/html/tidy-autoindent.cfg</file>
+    <file>config/html/tidy-indent.cfg</file>
+    <file>config/objc/config.ini</file>
+    <file>config/objc/objc.cfg</file>
+    <file>config/python/config.ini</file>
+    <file>config/xml/config.ini</file>
+
+    <file>self/global.ini</file>
+    <file>self/c/config.ini</file>
+    <file>self/c/gb-clang-format.cfg</file>
+    <file>self/c/gb-uncrustify.cfg</file>
+
+    <file>internal/align_makefile.py</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/beautifier/beautifier.plugin b/src/plugins/beautifier/beautifier.plugin
index d30494dcb..063e23a16 100644
--- a/src/plugins/beautifier/beautifier.plugin
+++ b/src/plugins/beautifier/beautifier.plugin
@@ -1,9 +1,9 @@
 [Plugin]
-Module=beautifier_plugin
-Name=Code Beautifier
-Description=Beautify code according to profiles
 Authors=Sébastien Lafargue <slafargue gnome org>
-Copyright=Copyright © 2016 Sébastien Lafargue
-Depends=editor
 Builtin=true
-Embedded=gb_beautifier_register_types
+Copyright=Copyright © 2016 Sébastien Lafargue
+Depends=editor;
+Description=Beautify code according to profiles
+Embedded=_gb_beautifier_register_types
+Module=beautifier
+Name=Code Beautifier
diff --git a/src/plugins/beautifier/gb-beautifier-config.c b/src/plugins/beautifier/gb-beautifier-config.c
index 8081632dc..81321ce7d 100644
--- a/src/plugins/beautifier/gb-beautifier-config.c
+++ b/src/plugins/beautifier/gb-beautifier-config.c
@@ -14,6 +14,8 @@
  *
  * 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 "beautifier-config"
@@ -22,7 +24,7 @@
 
 #include <glib/gi18n.h>
 #include <glib/gstdio.h>
-#include <ide.h>
+#include <libide-editor.h>
 #include <libpeas/peas.h>
 
 #include "gb-beautifier-helper.h"
@@ -75,8 +77,8 @@ gb_beautifier_config_check_duplicates (GbBeautifierEditorAddin *self,
 {
   g_assert (GB_IS_BEAUTIFIER_EDITOR_ADDIN (self));
   g_assert (entries != NULL);
-  g_assert (!dzl_str_empty0 (lang_id));
-  g_assert (!dzl_str_empty0 (display_name));
+  g_assert (!ide_str_empty0 (lang_id));
+  g_assert (!ide_str_empty0 (display_name));
 
   for (guint i = 0; i < entries->len; ++i)
     {
@@ -101,7 +103,7 @@ gb_beautifier_map_check_duplicates (GbBeautifierEditorAddin *self,
 {
   g_assert (GB_IS_BEAUTIFIER_EDITOR_ADDIN (self));
   g_assert (map != NULL);
-  g_assert (!dzl_str_empty0 (lang_id));
+  g_assert (!ide_str_empty0 (lang_id));
 
   for (guint i = 0; i < map->len; ++i)
     {
@@ -130,8 +132,8 @@ copy_to_tmp_file (GbBeautifierEditorAddin *self,
   g_autofree gchar *tmp_path = NULL;
   gint fd;
 
-  g_assert (!dzl_str_empty0 (tmp_dir));
-  g_assert (!dzl_str_empty0 (source_path));
+  g_assert (!ide_str_empty0 (tmp_dir));
+  g_assert (!ide_str_empty0 (source_path));
 
   tmp_path = g_build_filename (tmp_dir, "XXXXXX.txt", NULL);
   if (-1 != (fd = g_mkstemp (tmp_path)))
@@ -188,9 +190,9 @@ add_entries_from_config_ini_file (GbBeautifierEditorAddin *self,
   gsize nb_profiles;
 
   g_assert (GB_IS_BEAUTIFIER_EDITOR_ADDIN (self));
-  g_assert (!dzl_str_empty0 (base_path));
-  g_assert (!dzl_str_empty0 (lang_id));
-  g_assert (!dzl_str_empty0 (real_lang_id));
+  g_assert (!ide_str_empty0 (base_path));
+  g_assert (!ide_str_empty0 (lang_id));
+  g_assert (!ide_str_empty0 (real_lang_id));
   g_assert (entries != NULL);
 
   *has_default = FALSE;
@@ -317,7 +319,7 @@ add_entries_from_config_ini_file (GbBeautifierEditorAddin *self,
               command = g_key_file_get_string (key_file, profile, "command-pattern", NULL);
               if (g_str_has_prefix (command, "[internal]"))
                 {
-                  command_pattern = g_build_filename 
("resource:///org/gnome/builder/plugins/beautifier_plugin/internal/",
+                  command_pattern = g_build_filename ("resource:///plugins/beautifier/internal/",
                                                       command + 10,
                                                       NULL);
                 }
@@ -435,7 +437,7 @@ add_entries_from_base_path (GbBeautifierEditorAddin *self,
   gboolean ret_has_default = FALSE;
 
   g_assert (GB_IS_BEAUTIFIER_EDITOR_ADDIN (self));
-  g_assert (!dzl_str_empty0 (base_path));
+  g_assert (!ide_str_empty0 (base_path));
   g_assert (entries != NULL);
   g_assert (map != NULL);
 
@@ -524,7 +526,7 @@ gb_beautifier_config_get_map (GbBeautifierEditorAddin *self,
   gsize data_len;
 
   g_assert (GB_IS_BEAUTIFIER_EDITOR_ADDIN (self));
-  g_assert (!dzl_str_empty0 (path));
+  g_assert (!ide_str_empty0 (path));
 
   map = g_array_new (TRUE, TRUE, sizeof (GbBeautifierMapEntry));
   g_array_set_clear_func (map, map_entry_clear_func);
@@ -595,15 +597,14 @@ get_entries_worker (IdeTask      *task,
                     GCancellable *cancellable)
 {
   GbBeautifierEditorAddin *self = (GbBeautifierEditorAddin *)source_object;
-  IdeProject *project;
-  IdeVcs *vcs;
-  GArray *entries;
-  GArray *map = NULL;
-  const gchar *project_name;
+  GbBeautifierEntriesResult *result;
   g_autofree gchar *project_config_path = NULL;
   g_autofree gchar *user_config_path = NULL;
+  g_autofree gchar *project_id = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  GArray *entries;
+  GArray *map = NULL;
   gchar *configdir;
-  GbBeautifierEntriesResult *result;
   gboolean has_default = FALSE;
   gboolean ret_has_default = FALSE;
 
@@ -634,13 +635,14 @@ get_entries_worker (IdeTask      *task,
 
   g_clear_pointer (&map, g_array_unref);
 
+  project_id = ide_context_dup_project_id (self->context);
+
   /* Project wide config */
-  if (NULL != (project = ide_context_get_project (self->context)))
+  if (project_id != NULL)
     {
-      project_name = ide_project_get_name (project);
-      if (dzl_str_equal0 (project_name, "Builder"))
+      if (ide_str_equal0 (project_id, "Builder"))
         {
-          configdir = g_strdup ("resource:///org/gnome/builder/plugins/beautifier_plugin/self/");
+          configdir = g_strdup ("resource:///plugins/beautifier/self/");
           map = gb_beautifier_config_get_map (self, configdir);
           add_entries_from_base_path (self, configdir, entries, map, &ret_has_default);
           has_default |= ret_has_default;
@@ -648,14 +650,9 @@ get_entries_worker (IdeTask      *task,
 
           g_clear_pointer (&map, g_array_unref);
         }
-      else if (NULL != (vcs = ide_context_get_vcs (self->context)))
+      else if ((workdir = ide_context_ref_workdir (self->context)))
         {
-          GFile *workdir;
-          g_autofree gchar *workdir_path = NULL;
-
-          workdir = ide_vcs_get_working_directory (vcs);
-          workdir_path = g_file_get_path (workdir);
-          project_config_path = g_build_filename (workdir_path,
+          project_config_path = g_build_filename (g_file_peek_path (workdir),
                                                   ".beautifier",
                                                   NULL);
           map = gb_beautifier_config_get_map (self, project_config_path);
@@ -667,7 +664,7 @@ get_entries_worker (IdeTask      *task,
     }
 
   /* System wide config */
-  configdir = g_strdup ("resource:///org/gnome/builder/plugins/beautifier_plugin/config/");
+  configdir = g_strdup ("resource:///plugins/beautifier/config/");
 
   map = gb_beautifier_config_get_map (self, configdir);
   add_entries_from_base_path (self, configdir, entries, map, &ret_has_default);
diff --git a/src/plugins/beautifier/gb-beautifier-config.h b/src/plugins/beautifier/gb-beautifier-config.h
index 7759c42e1..39edfba99 100644
--- a/src/plugins/beautifier/gb-beautifier-config.h
+++ b/src/plugins/beautifier/gb-beautifier-config.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/beautifier/gb-beautifier-editor-addin.c 
b/src/plugins/beautifier/gb-beautifier-editor-addin.c
index 6cdb9b133..72b86d33c 100644
--- a/src/plugins/beautifier/gb-beautifier-editor-addin.c
+++ b/src/plugins/beautifier/gb-beautifier-editor-addin.c
@@ -14,6 +14,8 @@
  *
  * 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 "beautifier-plugin"
@@ -24,7 +26,7 @@
 #include <glib.h>
 #include <glib/gi18n.h>
 #include <gtksourceview/gtksource.h>
-#include <ide.h>
+#include <libide-editor.h>
 
 #include "gb-beautifier-editor-addin.h"
 #include "gb-beautifier-helper.h"
@@ -62,7 +64,7 @@ view_activate_beautify_action_cb (GSimpleAction *action,
                                   gpointer       user_data)
 {
   GbBeautifierEditorAddin *self = (GbBeautifierEditorAddin *)user_data;
-  IdeEditorView *view;
+  IdeEditorPage *view;
   IdeSourceView *source_view;
   GtkTextBuffer *buffer;
   GCancellable *cancellable;
@@ -77,10 +79,10 @@ view_activate_beautify_action_cb (GSimpleAction *action,
   g_assert (G_IS_SIMPLE_ACTION (action));
 
   view = g_object_get_data (G_OBJECT (action), "gb-beautifier-editor-addin");
-  if (view == NULL || !IDE_IS_EDITOR_VIEW (view))
+  if (view == NULL || !IDE_IS_EDITOR_PAGE (view))
     return;
 
-  source_view = ide_editor_view_get_view (view);
+  source_view = ide_editor_page_get_view (view);
   if (!GTK_SOURCE_IS_VIEW (source_view))
     {
       ide_object_warning (self, _("Beautifier Plugin: the view is not a GtkSourceView"));
@@ -275,14 +277,14 @@ static void
 setup_view_cb (GtkWidget               *widget,
                GbBeautifierEditorAddin *self)
 {
-  IdeEditorView *view = (IdeEditorView *)widget;
+  IdeEditorPage *view = (IdeEditorPage *)widget;
   IdeSourceView *source_view;
   GActionGroup *actions;
   GAction *action;
 
   g_assert (GB_IS_BEAUTIFIER_EDITOR_ADDIN (self));
 
-  if (!IDE_IS_EDITOR_VIEW (view))
+  if (!IDE_IS_EDITOR_PAGE (view))
     return;
 
   actions = gtk_widget_get_action_group (GTK_WIDGET (view), "view");
@@ -298,7 +300,7 @@ setup_view_cb (GtkWidget               *widget,
 
   g_object_set_data (G_OBJECT (view), "gb-beautifier-editor-addin", self);
 
-  source_view = ide_editor_view_get_view (view);
+  source_view = ide_editor_page_get_view (view);
   g_signal_connect_object (source_view,
                            "populate-popup",
                            G_CALLBACK (view_populate_popup),
@@ -315,12 +317,12 @@ static void
 cleanup_view_cb (GtkWidget               *widget,
                  GbBeautifierEditorAddin *self)
 {
-  IdeEditorView *view = (IdeEditorView *)widget;
+  IdeEditorPage *view = (IdeEditorPage *)widget;
   GActionGroup *actions;
 
   g_assert (GB_IS_BEAUTIFIER_EDITOR_ADDIN (self));
 
-  if (!IDE_IS_EDITOR_VIEW (view))
+  if (!IDE_IS_EDITOR_PAGE (view))
     return;
 
   if (NULL != (actions = gtk_widget_get_action_group (GTK_WIDGET (view), "view")))
@@ -383,7 +385,7 @@ gb_beautifier_editor_addin_async_cb (GObject      *object,
   if (!self->has_default)
     set_default_keybinding (self, "view.beautify-default::none");
 
-  ide_perspective_views_foreach (IDE_PERSPECTIVE (self->editor), (GtkCallback)setup_view_cb, self);
+  ide_surface_foreach_page (IDE_SURFACE (self->editor), (GtkCallback)setup_view_cb, self);
 
   add_shortcut_window_entry (self);
 }
@@ -419,7 +421,7 @@ gb_beautifier_editor_addin_reap_cb (GObject      *object,
 
 static void
 gb_beautifier_editor_addin_load (IdeEditorAddin       *addin,
-                                 IdeEditorPerspective *editor)
+                                 IdeEditorSurface *editor)
 {
   GbBeautifierEditorAddin *self = (GbBeautifierEditorAddin *)addin;
   IdeWorkbench *workbench;
@@ -427,7 +429,7 @@ gb_beautifier_editor_addin_load (IdeEditorAddin       *addin,
   g_autoptr (GFile) tmp_file = NULL;
 
   g_assert (GB_IS_BEAUTIFIER_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
 
   g_set_weak_pointer (&self->editor, editor);
   workbench = ide_widget_get_workbench (GTK_WIDGET (editor));
@@ -449,17 +451,17 @@ gb_beautifier_editor_addin_load (IdeEditorAddin       *addin,
 }
 
 static void
-gb_beautifier_editor_addin_unload (IdeEditorAddin       *addin,
-                                   IdeEditorPerspective *editor)
+gb_beautifier_editor_addin_unload (IdeEditorAddin   *addin,
+                                   IdeEditorSurface *editor)
 {
   GbBeautifierEditorAddin *self = (GbBeautifierEditorAddin *)addin;
   GbBeautifierConfigEntry *entry;
   g_autoptr (GFile) tmp_file = NULL;
 
   g_assert (GB_IS_BEAUTIFIER_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
 
-  ide_perspective_views_foreach (IDE_PERSPECTIVE (self->editor), (GtkCallback)cleanup_view_cb, self);
+  ide_surface_foreach_page (IDE_SURFACE (self->editor), (GtkCallback)cleanup_view_cb, self);
   if (self->entries != NULL)
     {
       for (guint i = 0; i < self->entries->len; i++)
@@ -483,19 +485,19 @@ gb_beautifier_editor_addin_unload (IdeEditorAddin       *addin,
 }
 
 static void
-gb_beautifier_editor_addin_view_set (IdeEditorAddin *addin,
-                                     IdeLayoutView  *view)
+gb_beautifier_editor_addin_page_set (IdeEditorAddin *addin,
+                                     IdePage  *view)
 {
   GbBeautifierEditorAddin *self = (GbBeautifierEditorAddin *)addin;
 
   g_assert (GB_IS_BEAUTIFIER_EDITOR_ADDIN (self));
-  g_assert (!view || IDE_IS_LAYOUT_VIEW (view));
+  g_assert (!view || IDE_IS_PAGE (view));
 
   /* If there is currently a view set, and this is
    * a new view, then we want to clean it up.
    */
 
-  if (!IDE_IS_EDITOR_VIEW (view))
+  if (!IDE_IS_EDITOR_PAGE (view))
     return;
 
   if (self->current_view != NULL)
@@ -530,5 +532,5 @@ editor_addin_iface_init (IdeEditorAddinInterface *iface)
 {
   iface->load = gb_beautifier_editor_addin_load;
   iface->unload = gb_beautifier_editor_addin_unload;
-  iface->view_set = gb_beautifier_editor_addin_view_set;
+  iface->page_set = gb_beautifier_editor_addin_page_set;
 }
diff --git a/src/plugins/beautifier/gb-beautifier-editor-addin.h 
b/src/plugins/beautifier/gb-beautifier-editor-addin.h
index e8eb1965e..880335ea4 100644
--- a/src/plugins/beautifier/gb-beautifier-editor-addin.h
+++ b/src/plugins/beautifier/gb-beautifier-editor-addin.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/beautifier/gb-beautifier-helper.c b/src/plugins/beautifier/gb-beautifier-helper.c
index 9e7968d64..70cf74870 100644
--- a/src/plugins/beautifier/gb-beautifier-helper.c
+++ b/src/plugins/beautifier/gb-beautifier-helper.c
@@ -14,6 +14,8 @@
  *
  * 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 "gb-beautifier-helper"
@@ -22,7 +24,7 @@
 #include <glib/gi18n.h>
 #include <glib/gstdio.h>
 #include <gtksourceview/gtksource.h>
-#include <ide.h>
+#include <libide-editor.h>
 #include <string.h>
 
 #include "gb-beautifier-helper.h"
diff --git a/src/plugins/beautifier/gb-beautifier-helper.h b/src/plugins/beautifier/gb-beautifier-helper.h
index 16b909c98..080e76124 100644
--- a/src/plugins/beautifier/gb-beautifier-helper.h
+++ b/src/plugins/beautifier/gb-beautifier-helper.h
@@ -14,13 +14,15 @@
  *
  * 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>
 
-#include "ide.h"
+#include "libide-editor.h"
 
 #include "gb-beautifier-config.h"
 #include "gb-beautifier-editor-addin.h"
diff --git a/src/plugins/beautifier/gb-beautifier-private.h b/src/plugins/beautifier/gb-beautifier-private.h
index ea70da5c9..5bab0b5b9 100644
--- a/src/plugins/beautifier/gb-beautifier-private.h
+++ b/src/plugins/beautifier/gb-beautifier-private.h
@@ -14,31 +14,32 @@
  *
  * 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>
+#include <libide-editor.h>
 
-#include "ide.h"
 #include "gb-beautifier-editor-addin.h"
 
 G_BEGIN_DECLS
 
 struct _GbBeautifierEditorAddin
 {
-  GObject                parent_instance;
+  GObject            parent_instance;
 
-  IdeContext            *context;
-  IdeEditorPerspective  *editor;
-  IdeLayoutView         *current_view;
-  GArray                *entries;
+  IdeContext        *context;
+  IdeEditorSurface  *editor;
+  IdePage           *current_view;
+  GArray            *entries;
 
-  gchar                 *tmp_dir;
+  gchar             *tmp_dir;
 
-  gboolean               has_default;
+  gboolean           has_default;
 };
 
-GbBeautifierEditorAddin    *gb_beautifier_editor_addin_get_editor_perspective    (GbBeautifierEditorAddin 
*self);
+GbBeautifierEditorAddin *gb_beautifier_editor_addin_get_editor_surface (GbBeautifierEditorAddin *self);
 
 G_END_DECLS
diff --git a/src/plugins/beautifier/gb-beautifier-process.c b/src/plugins/beautifier/gb-beautifier-process.c
index cc3a9361f..8a319553a 100644
--- a/src/plugins/beautifier/gb-beautifier-process.c
+++ b/src/plugins/beautifier/gb-beautifier-process.c
@@ -14,12 +14,14 @@
  *
  * 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
  */
 
 #include <glib.h>
 #include <glib/gi18n.h>
 #include <gtksourceview/gtksource.h>
-#include <ide.h>
+#include <libide-editor.h>
 #include <string.h>
 
 #include "gb-beautifier-private.h"
diff --git a/src/plugins/beautifier/gb-beautifier-process.h b/src/plugins/beautifier/gb-beautifier-process.h
index 4e159b295..9c1848ff0 100644
--- a/src/plugins/beautifier/gb-beautifier-process.h
+++ b/src/plugins/beautifier/gb-beautifier-process.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/beautifier/meson.build b/src/plugins/beautifier/meson.build
index 8e67241a7..268957d2d 100644
--- a/src/plugins/beautifier/meson.build
+++ b/src/plugins/beautifier/meson.build
@@ -1,25 +1,19 @@
-if get_option('with_beautifier')
+if get_option('plugin_beautifier')
 
-beautifier_resources = gnome.compile_resources(
-  'gb-beautifier-resources',
-  'gb-beautifier.gresource.xml',
-  c_name: 'gb_beautifier'
-)
-
-beautifier_sources = [
+plugins_sources += files([
+  'beautifier-plugin.c',
   'gb-beautifier-config.c',
-  'gb-beautifier-config.h',
   'gb-beautifier-helper.c',
-  'gb-beautifier-helper.h',
-  'gb-beautifier-plugin.c',
-  'gb-beautifier-private.h',
   'gb-beautifier-process.c',
-  'gb-beautifier-process.h',
   'gb-beautifier-editor-addin.c',
-  'gb-beautifier-editor-addin.h',
-]
+])
+
+plugin_beautifier_resources = gnome.compile_resources(
+  'beautifier-resources',
+  'beautifier.gresource.xml',
+  c_name: 'gbp_beautifier',
+)
 
-gnome_builder_plugins_sources += files(beautifier_sources)
-gnome_builder_plugins_sources += beautifier_resources[0]
+plugins_sources += plugin_beautifier_resources[0]
 
 endif
diff --git a/src/plugins/buffer-monitor/buffer-monitor-plugin.c 
b/src/plugins/buffer-monitor/buffer-monitor-plugin.c
new file mode 100644
index 000000000..b5509a40d
--- /dev/null
+++ b/src/plugins/buffer-monitor/buffer-monitor-plugin.c
@@ -0,0 +1,36 @@
+/* buffer-monitor-plugin.c
+ *
+ * Copyright 2018-2019 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 "buffer-monitor-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-code.h>
+
+#include "gbp-buffer-monitor-buffer-addin.h"
+
+_IDE_EXTERN void
+_gbp_buffer_monitor_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUFFER_ADDIN,
+                                              GBP_TYPE_BUFFER_MONITOR_BUFFER_ADDIN);
+}
diff --git a/src/plugins/buffer-monitor/buffer-monitor.gresource.xml 
b/src/plugins/buffer-monitor/buffer-monitor.gresource.xml
new file mode 100644
index 000000000..24ebf6a43
--- /dev/null
+++ b/src/plugins/buffer-monitor/buffer-monitor.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/buffer-monitor">
+    <file>buffer-monitor.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/buffer-monitor/buffer-monitor.plugin 
b/src/plugins/buffer-monitor/buffer-monitor.plugin
new file mode 100644
index 000000000..e03a533a4
--- /dev/null
+++ b/src/plugins/buffer-monitor/buffer-monitor.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2018 Christian Hergert
+Depends=editor;
+Description=Watches buffers for changes on disk
+Embedded=_gbp_buffer_monitor_register_types
+Hidden=true
+Module=buffer-monitor
+Name=Buffer Monitor
diff --git a/src/plugins/buffer-monitor/gbp-buffer-monitor-buffer-addin.c 
b/src/plugins/buffer-monitor/gbp-buffer-monitor-buffer-addin.c
new file mode 100644
index 000000000..71edf3c16
--- /dev/null
+++ b/src/plugins/buffer-monitor/gbp-buffer-monitor-buffer-addin.c
@@ -0,0 +1,277 @@
+/* gbp-buffer-monitor-buffer-addin.c
+ *
+ * Copyright 2018-2019 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-buffer-monitor-buffer-addin"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <string.h>
+
+#include "ide-buffer-private.h"
+
+#include "gbp-buffer-monitor-buffer-addin.h"
+
+struct _GbpBufferMonitorBufferAddin
+{
+  GObject       parent_instance;
+
+  IdeBuffer    *buffer;
+  GFileMonitor *monitor;
+
+  GTimeVal      mtime;
+  guint         mtime_set : 1;
+};
+
+static void
+gbp_buffer_monitor_buffer_addin_check_for_change (GbpBufferMonitorBufferAddin *self,
+                                                  GFile                       *file)
+{
+  g_autoptr(GFileInfo) info = NULL;
+
+  g_assert (GBP_IS_BUFFER_MONITOR_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (self->buffer));
+  g_assert (G_IS_FILE (file));
+
+  info = g_file_query_info (file,
+                            G_FILE_ATTRIBUTE_TIME_MODIFIED,
+                            G_FILE_QUERY_INFO_NONE,
+                            NULL,
+                            NULL);
+
+  if (info == NULL)
+    {
+      GtkTextIter iter;
+
+      /* If we get here, the file likely does not exist on disk. We might have
+       * a situation where the file was moved out from under the user. If so,
+       * then we should mark the buffer as modified so that the user can save
+       * it going forward.
+       */
+
+      gtk_text_buffer_get_end_iter (GTK_TEXT_BUFFER (self->buffer), &iter);
+      if (gtk_text_iter_get_offset (&iter) != 0)
+        gtk_text_buffer_set_modified (GTK_TEXT_BUFFER (self->buffer), TRUE);
+    }
+
+  if (!self->mtime_set)
+    return;
+
+  if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_TIME_MODIFIED))
+    {
+      GTimeVal mtime;
+
+      g_file_info_get_modification_time (info, &mtime);
+
+      if (memcmp (&mtime, &self->mtime, sizeof mtime) != 0)
+        {
+          self->mtime_set = FALSE;
+
+          /* Cancel any further requests from being delivered, we don't care
+           * until the file has been re-loaded or saved again.
+           */
+          if (self->monitor != NULL)
+            {
+              g_file_monitor_cancel (self->monitor);
+              g_clear_object (&self->monitor);
+            }
+
+          /* Let the buffer propagate the status to the UI */
+          _ide_buffer_set_changed_on_volume (self->buffer, TRUE);
+        }
+    }
+}
+
+static void
+gbp_buffer_monitor_buffer_addin_file_changed_cb (GbpBufferMonitorBufferAddin *self,
+                                                 GFile                       *file,
+                                                 GFile                       *other_file,
+                                                 GFileMonitorEvent            event,
+                                                 GFileMonitor                *monitor)
+{
+  GFile *expected;
+
+  g_assert (GBP_IS_BUFFER_MONITOR_BUFFER_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!other_file || G_IS_FILE (other_file));
+  g_assert (G_IS_FILE_MONITOR (monitor));
+  g_assert (IDE_IS_BUFFER (self->buffer));
+
+  if (g_file_monitor_is_cancelled (monitor))
+    return;
+
+  expected = ide_buffer_get_file (self->buffer);
+  if (!g_file_equal (expected, file))
+    return;
+
+  if (event == G_FILE_MONITOR_EVENT_CHANGED ||
+      event == G_FILE_MONITOR_EVENT_DELETED)
+    gbp_buffer_monitor_buffer_addin_check_for_change (self, file);
+}
+
+static void
+gbp_buffer_monitor_buffer_addin_setup_monitor (GbpBufferMonitorBufferAddin *self,
+                                               GFile                       *file)
+{
+  g_assert (GBP_IS_BUFFER_MONITOR_BUFFER_ADDIN (self));
+  g_assert (!file || G_IS_FILE (file));
+
+  if (self->monitor != NULL)
+    {
+      g_file_monitor_cancel (self->monitor);
+      g_clear_object (&self->monitor);
+    }
+
+  if (file != NULL)
+    {
+      g_autoptr(GFileInfo) info = NULL;
+
+      info = g_file_query_info (file,
+                                G_FILE_ATTRIBUTE_TIME_MODIFIED","
+                                G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE,
+                                G_FILE_QUERY_INFO_NONE,
+                                NULL,
+                                NULL);
+
+      self->mtime_set = FALSE;
+
+      if (info != NULL)
+        {
+          if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE))
+            _ide_buffer_set_read_only (self->buffer,
+                                       !g_file_info_get_attribute_boolean (info, 
G_FILE_ATTRIBUTE_ACCESS_CAN_WRITE));
+
+          if (g_file_info_has_attribute (info, G_FILE_ATTRIBUTE_TIME_MODIFIED))
+            {
+              g_file_info_get_modification_time (info, &self->mtime);
+              self->mtime_set = TRUE;
+            }
+        }
+
+      self->monitor = g_file_monitor_file (file,
+                                           G_FILE_MONITOR_NONE,
+                                           NULL,
+                                           NULL);
+
+      if (self->monitor != NULL)
+        {
+          g_file_monitor_set_rate_limit (self->monitor, 500);
+          g_signal_connect_object (self->monitor,
+                                   "changed",
+                                   G_CALLBACK (gbp_buffer_monitor_buffer_addin_file_changed_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+        }
+    }
+}
+
+static void
+gbp_buffer_monitor_buffer_addin_load (IdeBufferAddin *addin,
+                                      IdeBuffer      *buffer)
+{
+  GbpBufferMonitorBufferAddin *self = (GbpBufferMonitorBufferAddin *)addin;
+  GFile *file;
+
+  g_assert (GBP_IS_BUFFER_MONITOR_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  self->buffer = buffer;
+
+  file = ide_buffer_get_file (buffer);
+  gbp_buffer_monitor_buffer_addin_setup_monitor (self, file);
+}
+
+static void
+gbp_buffer_monitor_buffer_addin_unload (IdeBufferAddin *addin,
+                                        IdeBuffer      *buffer)
+{
+  GbpBufferMonitorBufferAddin *self = (GbpBufferMonitorBufferAddin *)addin;
+
+  g_assert (GBP_IS_BUFFER_MONITOR_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  gbp_buffer_monitor_buffer_addin_setup_monitor (self, NULL);
+
+  self->buffer = NULL;
+}
+
+static void
+gbp_buffer_monitor_buffer_addin_save_file (IdeBufferAddin *addin,
+                                           IdeBuffer      *buffer,
+                                           GFile          *file)
+{
+  GbpBufferMonitorBufferAddin *self = (GbpBufferMonitorBufferAddin *)addin;
+  GFile *current;
+
+  g_assert (IDE_IS_BUFFER_ADDIN (addin));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+
+  current = ide_buffer_get_file (buffer);
+
+  if (!g_file_equal (file, current))
+    return;
+
+  /* Disable monitors while saving */
+  gbp_buffer_monitor_buffer_addin_setup_monitor (self, NULL);
+}
+
+static void
+gbp_buffer_monitor_buffer_addin_file_saved (IdeBufferAddin *addin,
+                                            IdeBuffer      *buffer,
+                                            GFile          *file)
+{
+  GbpBufferMonitorBufferAddin *self = (GbpBufferMonitorBufferAddin *)addin;
+  GFile *current;
+
+  g_assert (IDE_IS_BUFFER_ADDIN (addin));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+
+  current = ide_buffer_get_file (buffer);
+
+  if (!g_file_equal (file, current))
+    return;
+
+  /* Restore any file monitors */
+  gbp_buffer_monitor_buffer_addin_setup_monitor (self, current);
+}
+
+static void
+buffer_addin_iface_init (IdeBufferAddinInterface *iface)
+{
+  iface->load = gbp_buffer_monitor_buffer_addin_load;
+  iface->unload = gbp_buffer_monitor_buffer_addin_unload;
+  iface->save_file = gbp_buffer_monitor_buffer_addin_save_file;
+  iface->file_saved = gbp_buffer_monitor_buffer_addin_file_saved;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpBufferMonitorBufferAddin, gbp_buffer_monitor_buffer_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUFFER_ADDIN, buffer_addin_iface_init))
+
+static void
+gbp_buffer_monitor_buffer_addin_class_init (GbpBufferMonitorBufferAddinClass *klass)
+{
+}
+
+static void
+gbp_buffer_monitor_buffer_addin_init (GbpBufferMonitorBufferAddin *self)
+{
+}
diff --git a/src/plugins/buffer-monitor/gbp-buffer-monitor-buffer-addin.h 
b/src/plugins/buffer-monitor/gbp-buffer-monitor-buffer-addin.h
new file mode 100644
index 000000000..227ac336c
--- /dev/null
+++ b/src/plugins/buffer-monitor/gbp-buffer-monitor-buffer-addin.h
@@ -0,0 +1,31 @@
+/* gbp-buffer-monitor-buffer-addin.h
+ *
+ * Copyright 2018-2019 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_BUFFER_MONITOR_BUFFER_ADDIN (gbp_buffer_monitor_buffer_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBufferMonitorBufferAddin, gbp_buffer_monitor_buffer_addin, GBP, 
BUFFER_MONITOR_BUFFER_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/buffer-monitor/meson.build b/src/plugins/buffer-monitor/meson.build
new file mode 100644
index 000000000..e4669de4c
--- /dev/null
+++ b/src/plugins/buffer-monitor/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'buffer-monitor-plugin.c',
+  'gbp-buffer-monitor-buffer-addin.c',
+])
+
+plugin_buffer_monitor_resources = gnome.compile_resources(
+  'gbp-buffer-monitor-resources',
+  'buffer-monitor.gresource.xml',
+  c_name: 'gbp_buffer_monitor',
+)
+
+plugins_sources += plugin_buffer_monitor_resources[0]
diff --git a/src/plugins/buildconfig/buildconfig-plugin.c b/src/plugins/buildconfig/buildconfig-plugin.c
new file mode 100644
index 000000000..c85cc12ed
--- /dev/null
+++ b/src/plugins/buildconfig/buildconfig-plugin.c
@@ -0,0 +1,40 @@
+/* buildconfig-plugin.c
+ *
+ * Copyright 2016 Matthew Leeds <mleeds 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 "buildconfig-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-foundry.h>
+
+#include "ide-buildconfig-configuration-provider.h"
+#include "ide-buildconfig-pipeline-addin.h"
+
+_IDE_EXTERN void
+_gbp_buildconfig_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_CONFIGURATION_PROVIDER,
+                                              IDE_TYPE_BUILDCONFIG_CONFIGURATION_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                              IDE_TYPE_BUILDCONFIG_PIPELINE_ADDIN);
+}
diff --git a/src/plugins/buildconfig/buildconfig.gresource.xml 
b/src/plugins/buildconfig/buildconfig.gresource.xml
new file mode 100644
index 000000000..73c7ff6c8
--- /dev/null
+++ b/src/plugins/buildconfig/buildconfig.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/buildconfig">
+    <file>buildconfig.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/buildconfig/buildconfig.plugin b/src/plugins/buildconfig/buildconfig.plugin
new file mode 100644
index 000000000..0bc4662b6
--- /dev/null
+++ b/src/plugins/buildconfig/buildconfig.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2015-2018 Christian Hergert
+Description=Support for .buildconfig files
+Embedded=_gbp_buildconfig_register_types
+Hidden=true
+Module=buildconfig
+Name=Buildconfig Support
diff --git a/src/plugins/buildconfig/ide-buildconfig-configuration-provider.c 
b/src/plugins/buildconfig/ide-buildconfig-configuration-provider.c
new file mode 100644
index 000000000..d7e087d05
--- /dev/null
+++ b/src/plugins/buildconfig/ide-buildconfig-configuration-provider.c
@@ -0,0 +1,768 @@
+/* ide-buildconfig-configuration-provider.c
+ *
+ * Copyright 2016 Matthew Leeds <mleeds 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 "ide-buildconfig-configuration-provider"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+#include <string.h>
+
+#include <libide-foundry.h>
+#include <libide-threading.h>
+
+#include "ide-context.h"
+#include "ide-debug.h"
+
+#include "ide-buildconfig-configuration.h"
+#include "ide-buildconfig-configuration-provider.h"
+
+#define DOT_BUILDCONFIG ".buildconfig"
+
+struct _IdeBuildconfigConfigurationProvider
+{
+  IdeObject  parent_instance;
+
+  /*
+   * A GPtrArray of IdeBuildconfigConfiguration that have been registered.
+   * We append/remove to/from this array in our default signal handler for
+   * the ::added and ::removed signals.
+   */
+  GPtrArray *configs;
+
+  /*
+   * The GKeyFile that was parsed from disk. We keep this around so that
+   * we can persist the changes back without destroying comments.
+   */
+  GKeyFile *key_file;
+
+  /*
+   * If we removed items from the keyfile, we need to know that so that
+   * we persist it back to disk. We only persist back to disk if this bit
+   * is set or if any of our registered configs are "dirty".
+   *
+   * We try hard to avoid writing .buildconfig files unless we know the
+   * user did something to change a config. Otherwise we would liter
+   * everyone's projects with .buildconfig files.
+   */
+  guint key_file_dirty : 1;
+};
+
+static gchar *
+gen_next_id (const gchar *id)
+{
+  g_auto(GStrv) parts = g_strsplit (id, "-", 0);
+  guint len = g_strv_length (parts);
+  const gchar *end;
+  guint64 n64;
+
+  if (len == 0)
+    goto add_suffix;
+
+  end = parts[len - 1];
+
+  n64 = g_ascii_strtoull (end, (gchar **)&end, 10);
+  if (n64 == 0 || n64 == G_MAXUINT64 || *end != 0)
+    goto add_suffix;
+
+  g_free (g_steal_pointer (&parts[len -1]));
+  parts[len -1] = g_strdup_printf ("%"G_GUINT64_FORMAT, n64+1);
+  return g_strjoinv ("-", parts);
+
+add_suffix:
+  return g_strdup_printf ("%s-2", id);
+}
+
+static gchar *
+get_next_id (IdeConfigurationManager *manager,
+             const gchar             *id)
+{
+  g_autoptr(GPtrArray) tries = NULL;
+
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (manager));
+
+  tries = g_ptr_array_new_with_free_func (g_free);
+
+  while (ide_configuration_manager_get_configuration (manager, id))
+    {
+      g_autofree gchar *next = gen_next_id (id);
+      id = next;
+      g_ptr_array_add (tries, g_steal_pointer (&next));
+    }
+
+  return g_strdup (id);
+}
+
+static void
+load_string (IdeConfiguration *config,
+             GKeyFile         *key_file,
+             const gchar      *group,
+             const gchar      *key,
+             const gchar      *property)
+{
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (key_file != NULL);
+  g_assert (group != NULL);
+  g_assert (key != NULL);
+
+  if (g_key_file_has_key (key_file, group, key, NULL))
+    {
+      g_auto(GValue) value = G_VALUE_INIT;
+
+      g_value_init (&value, G_TYPE_STRING);
+      g_value_take_string (&value, g_key_file_get_string (key_file, group, key, NULL));
+      g_object_set_property (G_OBJECT (config), property, &value);
+    }
+}
+
+static void
+load_strv (IdeConfiguration *config,
+           GKeyFile         *key_file,
+           const gchar      *group,
+           const gchar      *key,
+           const gchar      *property)
+{
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (key_file != NULL);
+  g_assert (group != NULL);
+  g_assert (key != NULL);
+
+  if (g_key_file_has_key (key_file, group, key, NULL))
+    {
+      g_auto(GStrv) strv = NULL;
+      g_auto(GValue) value = G_VALUE_INIT;
+
+      strv = g_key_file_get_string_list (key_file, group, key, NULL, NULL);
+      g_value_init (&value, G_TYPE_STRV);
+      g_value_take_boxed (&value, g_steal_pointer (&strv));
+      g_object_set_property (G_OBJECT (config), property, &value);
+    }
+}
+
+static void
+load_environ (IdeConfiguration *config,
+              GKeyFile         *key_file,
+              const gchar      *group)
+{
+  IdeEnvironment *environment;
+  g_auto(GStrv) keys = NULL;
+  gsize len = 0;
+
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (key_file != NULL);
+  g_assert (group != NULL);
+
+  environment = ide_configuration_get_environment (config);
+  keys = g_key_file_get_keys (key_file, group, &len, NULL);
+
+  for (gsize i = 0; i < len; i++)
+    {
+      g_autofree gchar *value = NULL;
+
+      value = g_key_file_get_string (key_file, group, keys[i], NULL);
+      if (value != NULL)
+        ide_environment_setenv (environment, keys [i], value);
+    }
+}
+
+static IdeConfiguration *
+ide_buildconfig_configuration_provider_create (IdeBuildconfigConfigurationProvider *self,
+                                               const gchar                         *config_id)
+{
+  g_autoptr(IdeConfiguration) config = NULL;
+  g_autofree gchar *env_group = NULL;
+
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION_PROVIDER (self));
+  g_assert (self->key_file != NULL);
+  g_assert (config_id != NULL);
+
+  config = g_object_new (IDE_TYPE_BUILDCONFIG_CONFIGURATION,
+                         "id", config_id,
+                         "parent", self,
+                         NULL);
+
+  load_string (config, self->key_file, config_id, "config-opts", "config-opts");
+  load_string (config, self->key_file, config_id, "name", "display-name");
+  load_string (config, self->key_file, config_id, "run-opts", "run-opts");
+  load_string (config, self->key_file, config_id, "runtime", "runtime-id");
+  load_string (config, self->key_file, config_id, "toolchain", "toolchain-id");
+  load_string (config, self->key_file, config_id, "prefix", "prefix");
+  load_string (config, self->key_file, config_id, "app-id", "app-id");
+  load_strv (config, self->key_file, config_id, "prebuild", "prebuild");
+  load_strv (config, self->key_file, config_id, "postbuild", "postbuild");
+
+  if (g_key_file_has_key (self->key_file, config_id, "builddir", NULL))
+    {
+      if (g_key_file_get_boolean (self->key_file, config_id, "builddir", NULL))
+        ide_configuration_set_locality (config, IDE_BUILD_LOCALITY_OUT_OF_TREE);
+      else
+        ide_configuration_set_locality (config, IDE_BUILD_LOCALITY_IN_TREE);
+    }
+
+  env_group = g_strdup_printf ("%s.environment", config_id);
+  if (g_key_file_has_group (self->key_file, env_group))
+    load_environ (config, self->key_file, env_group);
+
+  return g_steal_pointer (&config);
+}
+
+static void
+ide_buildconfig_configuration_provider_load_async (IdeConfigurationProvider *provider,
+                                                   GCancellable             *cancellable,
+                                                   GAsyncReadyCallback       callback,
+                                                   gpointer                  user_data)
+{
+  IdeBuildconfigConfigurationProvider *self = (IdeBuildconfigConfigurationProvider *)provider;
+  g_autoptr(IdeConfiguration) fallback = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *path = NULL;
+  g_auto(GStrv) groups = NULL;
+  IdeContext *context;
+  gsize len;
+
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION_PROVIDER (self));
+  g_assert (self->key_file == NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buildconfig_configuration_provider_load_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  self->key_file = g_key_file_new ();
+
+  /*
+   * We could do this in a thread, but it's not really worth it. We want these
+   * configs loaded ASAP, and nothing can really progress until it's loaded
+   * anyway.
+   */
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  path = ide_context_build_filename (context, DOT_BUILDCONFIG, NULL);
+  if (!g_file_test (path, G_FILE_TEST_IS_REGULAR))
+    goto add_default;
+
+  if (!g_key_file_load_from_file (self->key_file, path, G_KEY_FILE_KEEP_COMMENTS, &error))
+    {
+      g_warning ("Failed to load .buildconfig: %s", error->message);
+      goto add_default;
+    }
+
+  groups = g_key_file_get_groups (self->key_file, &len);
+
+  for (gsize i = 0; i < len; i++)
+    {
+      g_autoptr(IdeConfiguration) config = NULL;
+      const gchar *group = groups[i];
+
+      if (strchr (group, '.') != NULL)
+        continue;
+
+      config = ide_buildconfig_configuration_provider_create (self, group);
+      ide_configuration_set_dirty (config, FALSE);
+      ide_configuration_provider_emit_added (provider, config);
+    }
+
+  if (self->configs->len > 0)
+    goto complete;
+
+add_default:
+  /* "Default" is not translated because .buildconfig can be checked in */
+  fallback = g_object_new (IDE_TYPE_BUILDCONFIG_CONFIGURATION,
+                           "display-name", "Default",
+                           "id", "default",
+                           "parent", self,
+                           "runtime-id", "host",
+                           "toolchain-id", "default",
+                           NULL);
+  ide_configuration_set_dirty (fallback, FALSE);
+  ide_configuration_provider_emit_added (provider, fallback);
+
+complete:
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_buildconfig_configuration_provider_load_finish (IdeConfigurationProvider  *provider,
+                                                    GAsyncResult              *result,
+                                                    GError                   **error)
+{
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+  g_assert (ide_task_is_valid (IDE_TASK (result), provider));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_buildconfig_configuration_provider_save_cb (GObject      *object,
+                                                GAsyncResult *result,
+                                                gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!g_file_replace_contents_finish (file, result, NULL, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_buildconfig_configuration_provider_save_async (IdeConfigurationProvider *provider,
+                                                   GCancellable             *cancellable,
+                                                   GAsyncReadyCallback       callback,
+                                                   gpointer                  user_data)
+{
+  IdeBuildconfigConfigurationProvider *self = (IdeBuildconfigConfigurationProvider *)provider;
+  g_autoptr(GHashTable) group_names = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(GBytes) bytes = NULL;
+  g_autoptr(GError) error = NULL;
+  g_auto(GStrv) groups = NULL;
+  g_autofree gchar *path = NULL;
+  g_autofree gchar *data = NULL;
+  IdeConfigurationManager *manager;
+  IdeContext *context;
+  gboolean dirty = FALSE;
+  gsize length = 0;
+
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION_PROVIDER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (self->key_file != NULL);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_buildconfig_configuration_provider_save_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  dirty = self->key_file_dirty;
+
+  /* If no configs are dirty, short circuit to avoid writing any files to disk. */
+  for (guint i = 0; !dirty && i < self->configs->len; i++)
+    {
+      IdeConfiguration *config = g_ptr_array_index (self->configs, i);
+      dirty |= ide_configuration_get_dirty (config);
+    }
+
+  if (!dirty)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  manager = ide_configuration_manager_from_context (context);
+  path = ide_context_build_filename (context, DOT_BUILDCONFIG, NULL);
+  file = g_file_new_for_path (path);
+
+  /*
+   * We keep the GKeyFile around from when we parsed .buildconfig, so that we
+   * can try to preserve comments and such when writing back.
+   *
+   * This means that we need to fill in all our known configuration sections,
+   * and then remove any that were removed since we were parsed it last.
+   */
+
+  group_names = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+  for (guint i = 0; i < self->configs->len; i++)
+    {
+      IdeConfiguration *config = g_ptr_array_index (self->configs, i);
+      g_autofree gchar *env_group = NULL;
+      const gchar *config_id;
+      IdeEnvironment *env;
+      guint n_items;
+
+      if (!ide_configuration_get_dirty (config))
+        continue;
+
+      config_id = ide_configuration_get_id (config);
+      env_group = g_strdup_printf ("%s.environment", config_id);
+
+      /*
+       * Track our known group names, so we can remove missing names after
+       * we've updated the GKeyFile.
+       */
+      g_hash_table_insert (group_names, g_strdup (config_id), NULL);
+      g_hash_table_insert (group_names, g_strdup (env_group), NULL);
+
+#define PERSIST_STRING_KEY(key, getter) \
+      g_key_file_set_string (self->key_file, config_id, key, \
+                             ide_configuration_##getter (config) ?: "")
+#define PERSIST_STRV_KEY(key, getter) G_STMT_START { \
+      const gchar * const *val = ide_buildconfig_configuration_##getter (IDE_BUILDCONFIG_CONFIGURATION 
(config)); \
+      gsize vlen = val ? g_strv_length ((gchar **)val) : 0; \
+      g_key_file_set_string_list (self->key_file, config_id, key, val, vlen); \
+} G_STMT_END
+
+      PERSIST_STRING_KEY ("name", get_display_name);
+      PERSIST_STRING_KEY ("runtime", get_runtime_id);
+      PERSIST_STRING_KEY ("toolchain", get_toolchain_id);
+      PERSIST_STRING_KEY ("config-opts", get_config_opts);
+      PERSIST_STRING_KEY ("run-opts", get_run_opts);
+      PERSIST_STRING_KEY ("prefix", get_prefix);
+      PERSIST_STRING_KEY ("app-id", get_app_id);
+      PERSIST_STRV_KEY ("postbuild", get_postbuild);
+      PERSIST_STRV_KEY ("prebuild", get_prebuild);
+
+#undef PERSIST_STRING_KEY
+#undef PERSIST_STRV_KEY
+
+      if (ide_configuration_get_locality (config) == IDE_BUILD_LOCALITY_IN_TREE)
+        g_key_file_set_boolean (self->key_file, config_id, "builddir", FALSE);
+      else if (ide_configuration_get_locality (config) == IDE_BUILD_LOCALITY_OUT_OF_TREE)
+        g_key_file_set_boolean (self->key_file, config_id, "builddir", TRUE);
+      else
+        g_key_file_remove_key (self->key_file, config_id, "builddir", NULL);
+
+      if (config == ide_configuration_manager_get_current (manager))
+        g_key_file_set_boolean (self->key_file, config_id, "default", TRUE);
+      else
+        g_key_file_remove_key (self->key_file, config_id, "default", NULL);
+
+      env = ide_configuration_get_environment (config);
+
+      /*
+       * Remove all environment keys that are no longer specified in the
+       * environment. This allows us to just do a single pass of additions
+       * from the environment below.
+       */
+      if (g_key_file_has_group (self->key_file, env_group))
+        {
+          g_auto(GStrv) keys = NULL;
+
+          if (NULL != (keys = g_key_file_get_keys (self->key_file, env_group, NULL, NULL)))
+            {
+              for (guint j = 0; keys [j]; j++)
+                {
+                  if (!ide_environment_getenv (env, keys [j]))
+                    g_key_file_remove_key (self->key_file, env_group, keys [j], NULL);
+                }
+            }
+        }
+
+      n_items = g_list_model_get_n_items (G_LIST_MODEL (env));
+
+      for (guint j = 0; j < n_items; j++)
+        {
+          g_autoptr(IdeEnvironmentVariable) var = NULL;
+          const gchar *key;
+          const gchar *value;
+
+          var = g_list_model_get_item (G_LIST_MODEL (env), j);
+          key = ide_environment_variable_get_key (var);
+          value = ide_environment_variable_get_value (var);
+
+          if (!dzl_str_empty0 (key))
+            g_key_file_set_string (self->key_file, env_group, key, value ?: "");
+        }
+
+      ide_configuration_set_dirty (config, FALSE);
+    }
+
+  /* Now truncate any old groups in the keyfile. */
+  if (NULL != (groups = g_key_file_get_groups (self->key_file, NULL)))
+    {
+      for (guint i = 0; groups [i]; i++)
+        {
+          if (!g_hash_table_contains (group_names, groups [i]))
+            g_key_file_remove_group (self->key_file, groups [i], NULL);
+        }
+    }
+
+  if (!(data = g_key_file_to_data (self->key_file, &length, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self->key_file_dirty = FALSE;
+
+  if (length == 0)
+    {
+      /* Remove the file if it exists, since it would be empty */
+      g_file_delete (file, cancellable, NULL);
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  bytes = g_bytes_new_take (g_steal_pointer (&data), length);
+
+  g_file_replace_contents_bytes_async (file,
+                                       bytes,
+                                       NULL,
+                                       FALSE,
+                                       G_FILE_CREATE_NONE,
+                                       cancellable,
+                                       ide_buildconfig_configuration_provider_save_cb,
+                                       g_steal_pointer (&task));
+}
+
+static gboolean
+ide_buildconfig_configuration_provider_save_finish (IdeConfigurationProvider  *provider,
+                                                    GAsyncResult              *result,
+                                                    GError                   **error)
+{
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+  g_assert (ide_task_is_valid (IDE_TASK (result), provider));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+ide_buildconfig_configuration_provider_delete (IdeConfigurationProvider *provider,
+                                               IdeConfiguration         *config)
+{
+  IdeBuildconfigConfigurationProvider *self = (IdeBuildconfigConfigurationProvider *)provider;
+  g_autoptr(IdeConfiguration) hold = NULL;
+  g_autofree gchar *env = NULL;
+  const gchar *config_id;
+  gboolean had_group;
+
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION_PROVIDER (self));
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION (config));
+  g_assert (self->key_file != NULL);
+  g_assert (self->configs->len > 0);
+
+  hold = g_object_ref (config);
+
+  if (!g_ptr_array_remove (self->configs, hold))
+    {
+      g_critical ("No such configuration %s",
+                  ide_configuration_get_id (hold));
+      return;
+    }
+
+  config_id = ide_configuration_get_id (config);
+  had_group = g_key_file_has_group (self->key_file, config_id);
+  env = g_strdup_printf ("%s.environment", config_id);
+  g_key_file_remove_group (self->key_file, config_id, NULL);
+  g_key_file_remove_group (self->key_file, env, NULL);
+
+  self->key_file_dirty = had_group;
+
+  /*
+   * If we removed our last buildconfig, synthesize a new one to replace it so
+   * that we never have no configurations available. We add it before we remove
+   * @config so that we never have zero configurations available.
+   *
+   * At some point in the future we might want a read only NULL configuration
+   * for fallback, and group configs by type or something.  But until we have
+   * designs for that, this will do.
+   */
+  if (self->configs->len == 0)
+    {
+      g_autoptr(IdeConfiguration) new_config = NULL;
+
+      /* "Default" is not translated because .buildconfig can be checked in */
+      new_config = g_object_new (IDE_TYPE_BUILDCONFIG_CONFIGURATION,
+                                 "display-name", "Default",
+                                 "id", "default",
+                                 "parent", self,
+                                 "runtime-id", "host",
+                                 "toolchain-id", "default",
+                                 NULL);
+
+      /*
+       * Only persist this back if there was data in the keyfile
+       * before we were requested to delete the build-config.
+       */
+      ide_configuration_set_dirty (new_config, had_group);
+      ide_configuration_provider_emit_added (provider, new_config);
+    }
+
+  ide_configuration_provider_emit_removed (provider, hold);
+}
+
+static void
+ide_buildconfig_configuration_provider_duplicate (IdeConfigurationProvider *provider,
+                                                  IdeConfiguration         *config)
+{
+  IdeBuildconfigConfigurationProvider *self = (IdeBuildconfigConfigurationProvider *)provider;
+  g_autoptr(IdeConfiguration) new_config = NULL;
+  g_autofree GParamSpec **pspecs = NULL;
+  g_autofree gchar *new_config_id = NULL;
+  g_autofree gchar *new_name = NULL;
+  IdeConfigurationManager *manager;
+  IdeEnvironment *env;
+  const gchar *config_id;
+  const gchar *name;
+  IdeContext *context;
+  guint n_pspecs = 0;
+
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION_PROVIDER (self));
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION (config));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  manager = ide_configuration_manager_from_context (context);
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (manager));
+
+  config_id = ide_configuration_get_id (config);
+  g_return_if_fail (config_id != NULL);
+
+  new_config_id = get_next_id (manager, config_id);
+  g_return_if_fail (new_config_id != NULL);
+
+  name = ide_configuration_get_display_name (config);
+  /* translators: %s is replaced with the name of the configuration */
+  new_name = g_strdup_printf (_("%s (Copy)"), name);
+
+  env = ide_configuration_get_environment (config);
+
+  new_config = g_object_new (IDE_TYPE_BUILDCONFIG_CONFIGURATION,
+                             "id", new_config_id,
+                             "display-name", new_name,
+                             "parent", self,
+                             NULL);
+
+  ide_environment_copy_into (env, ide_configuration_get_environment (new_config), TRUE);
+
+  pspecs = g_object_class_list_properties (G_OBJECT_GET_CLASS (new_config), &n_pspecs);
+
+  for (guint i = 0; i < n_pspecs; i++)
+    {
+      GParamSpec *pspec = pspecs[i];
+
+      if (g_str_equal (pspec->name, "id") ||
+          g_str_equal (pspec->name, "display-name") ||
+          g_type_is_a (pspec->value_type, G_TYPE_BOXED) ||
+          g_type_is_a (pspec->value_type, G_TYPE_OBJECT))
+        continue;
+
+
+      if ((pspec->flags & G_PARAM_READWRITE) == G_PARAM_READWRITE &&
+          (pspec->flags & G_PARAM_CONSTRUCT_ONLY) == 0)
+        {
+          GValue value = G_VALUE_INIT;
+
+          g_value_init (&value, pspec->value_type);
+          g_object_get_property (G_OBJECT (config), pspec->name, &value);
+          g_object_set_property (G_OBJECT (new_config), pspec->name, &value);
+        }
+    }
+
+  ide_configuration_set_dirty (new_config, TRUE);
+  ide_configuration_provider_emit_added (provider, new_config);
+}
+
+static void
+ide_buildconfig_configuration_provider_unload (IdeConfigurationProvider *provider)
+{
+  IdeBuildconfigConfigurationProvider *self = (IdeBuildconfigConfigurationProvider *)provider;
+  g_autoptr(GPtrArray) configs = NULL;
+
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION_PROVIDER (self));
+  g_assert (self->configs != NULL);
+
+  configs = g_steal_pointer (&self->configs);
+  self->configs = g_ptr_array_new_with_free_func (g_object_unref);
+
+  for (guint i = 0; i < configs->len; i++)
+    {
+      IdeConfiguration *config = g_ptr_array_index (configs, i);
+      ide_configuration_provider_emit_removed (provider, config);
+    }
+}
+
+static void
+ide_buildconfig_configuration_provider_added (IdeConfigurationProvider *provider,
+                                              IdeConfiguration         *config)
+{
+  IdeBuildconfigConfigurationProvider *self = (IdeBuildconfigConfigurationProvider *)provider;
+
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION_PROVIDER (self));
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (self->configs != NULL);
+
+  g_ptr_array_add (self->configs, g_object_ref (config));
+}
+
+static void
+ide_buildconfig_configuration_provider_removed (IdeConfigurationProvider *provider,
+                                                IdeConfiguration         *config)
+{
+  IdeBuildconfigConfigurationProvider *self = (IdeBuildconfigConfigurationProvider *)provider;
+
+  g_assert (IDE_IS_BUILDCONFIG_CONFIGURATION_PROVIDER (self));
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (self->configs != NULL);
+
+  /* It's possible we already removed it by now */
+  g_ptr_array_remove (self->configs, config);
+
+  ide_object_destroy (IDE_OBJECT (config));
+}
+
+static void
+configuration_provider_iface_init (IdeConfigurationProviderInterface *iface)
+{
+  iface->added = ide_buildconfig_configuration_provider_added;
+  iface->removed = ide_buildconfig_configuration_provider_removed;
+  iface->load_async = ide_buildconfig_configuration_provider_load_async;
+  iface->load_finish = ide_buildconfig_configuration_provider_load_finish;
+  iface->save_async = ide_buildconfig_configuration_provider_save_async;
+  iface->save_finish = ide_buildconfig_configuration_provider_save_finish;
+  iface->delete = ide_buildconfig_configuration_provider_delete;
+  iface->duplicate = ide_buildconfig_configuration_provider_duplicate;
+  iface->unload = ide_buildconfig_configuration_provider_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (IdeBuildconfigConfigurationProvider,
+                         ide_buildconfig_configuration_provider,
+                         IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_CONFIGURATION_PROVIDER,
+                                                configuration_provider_iface_init))
+
+static void
+ide_buildconfig_configuration_provider_finalize (GObject *object)
+{
+  IdeBuildconfigConfigurationProvider *self = (IdeBuildconfigConfigurationProvider *)object;
+
+  g_clear_pointer (&self->configs, g_ptr_array_unref);
+  g_clear_pointer (&self->key_file, g_key_file_free);
+
+  G_OBJECT_CLASS (ide_buildconfig_configuration_provider_parent_class)->finalize (object);
+}
+
+static void
+ide_buildconfig_configuration_provider_class_init (IdeBuildconfigConfigurationProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_buildconfig_configuration_provider_finalize;
+}
+
+static void
+ide_buildconfig_configuration_provider_init (IdeBuildconfigConfigurationProvider *self)
+{
+  self->configs = g_ptr_array_new_with_free_func (g_object_unref);
+}
diff --git a/src/plugins/buildconfig/ide-buildconfig-configuration-provider.h 
b/src/plugins/buildconfig/ide-buildconfig-configuration-provider.h
new file mode 100644
index 000000000..83fb0d7f5
--- /dev/null
+++ b/src/plugins/buildconfig/ide-buildconfig-configuration-provider.h
@@ -0,0 +1,31 @@
+/* ide-buildconfig-configuration-provider.h
+ *
+ * Copyright 2016 Matthew Leeds <mleeds 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-core.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILDCONFIG_CONFIGURATION_PROVIDER (ide_buildconfig_configuration_provider_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeBuildconfigConfigurationProvider, ide_buildconfig_configuration_provider, IDE, 
BUILDCONFIG_CONFIGURATION_PROVIDER, IdeObject)
+
+G_END_DECLS
diff --git a/src/plugins/buildconfig/ide-buildconfig-configuration.c 
b/src/plugins/buildconfig/ide-buildconfig-configuration.c
new file mode 100644
index 000000000..f4386a0c4
--- /dev/null
+++ b/src/plugins/buildconfig/ide-buildconfig-configuration.c
@@ -0,0 +1,172 @@
+/* ide-buildconfig-configuration.c
+ *
+ * Copyright 2017-2019 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 "ide-buildconfig-configuration"
+
+#include "config.h"
+
+#include "ide-buildconfig-configuration.h"
+
+struct _IdeBuildconfigConfiguration
+{
+  IdeConfiguration   parent_instance;
+
+  gchar            **prebuild;
+  gchar            **postbuild;
+};
+
+enum {
+  PROP_0,
+  PROP_PREBUILD,
+  PROP_POSTBUILD,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeBuildconfigConfiguration, ide_buildconfig_configuration, IDE_TYPE_CONFIGURATION)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_buildconfig_configuration_finalize (GObject *object)
+{
+  IdeBuildconfigConfiguration *self = (IdeBuildconfigConfiguration *)object;
+
+  g_clear_pointer (&self->prebuild, g_strfreev);
+  g_clear_pointer (&self->postbuild, g_strfreev);
+
+  G_OBJECT_CLASS (ide_buildconfig_configuration_parent_class)->finalize (object);
+}
+
+static void
+ide_buildconfig_configuration_get_property (GObject    *object,
+                                            guint       prop_id,
+                                            GValue     *value,
+                                            GParamSpec *pspec)
+{
+  IdeBuildconfigConfiguration *self = (IdeBuildconfigConfiguration *)object;
+
+  switch (prop_id)
+    {
+    case PROP_PREBUILD:
+      g_value_set_boxed (value, ide_buildconfig_configuration_get_prebuild (self));
+      break;
+
+    case PROP_POSTBUILD:
+      g_value_set_boxed (value, ide_buildconfig_configuration_get_postbuild (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buildconfig_configuration_set_property (GObject      *object,
+                                            guint         prop_id,
+                                            const GValue *value,
+                                            GParamSpec   *pspec)
+{
+  IdeBuildconfigConfiguration *self = (IdeBuildconfigConfiguration *)object;
+
+  switch (prop_id)
+    {
+    case PROP_PREBUILD:
+      ide_buildconfig_configuration_set_prebuild (self, g_value_get_boxed (value));
+      break;
+
+    case PROP_POSTBUILD:
+      ide_buildconfig_configuration_set_postbuild (self, g_value_get_boxed (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_buildconfig_configuration_class_init (IdeBuildconfigConfigurationClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = ide_buildconfig_configuration_finalize;
+  object_class->get_property = ide_buildconfig_configuration_get_property;
+  object_class->set_property = ide_buildconfig_configuration_set_property;
+
+  properties [PROP_PREBUILD] =
+    g_param_spec_boxed ("prebuild", NULL, NULL,
+                        G_TYPE_STRV,
+                        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_POSTBUILD] =
+    g_param_spec_boxed ("postbuild", NULL, NULL,
+                        G_TYPE_STRV,
+                        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+ide_buildconfig_configuration_init (IdeBuildconfigConfiguration *self)
+{
+}
+
+const gchar * const *
+ide_buildconfig_configuration_get_prebuild (IdeBuildconfigConfiguration *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILDCONFIG_CONFIGURATION (self), NULL);
+
+  return (const gchar * const *)self->prebuild;
+}
+
+const gchar * const *
+ide_buildconfig_configuration_get_postbuild (IdeBuildconfigConfiguration *self)
+{
+  g_return_val_if_fail (IDE_IS_BUILDCONFIG_CONFIGURATION (self), NULL);
+
+  return (const gchar * const *)self->postbuild;
+}
+
+void
+ide_buildconfig_configuration_set_prebuild (IdeBuildconfigConfiguration *self,
+                                            const gchar * const         *prebuild)
+{
+  g_return_if_fail (IDE_IS_BUILDCONFIG_CONFIGURATION (self));
+
+  if (self->prebuild != (gchar **)prebuild)
+    {
+      g_strfreev (self->prebuild);
+      self->prebuild = g_strdupv ((gchar **)prebuild);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PREBUILD]);
+    }
+}
+
+void
+ide_buildconfig_configuration_set_postbuild (IdeBuildconfigConfiguration *self,
+                                             const gchar * const         *postbuild)
+{
+  g_return_if_fail (IDE_IS_BUILDCONFIG_CONFIGURATION (self));
+
+  if (self->postbuild != (gchar **)postbuild)
+    {
+      g_strfreev (self->postbuild);
+      self->postbuild = g_strdupv ((gchar **)postbuild);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_POSTBUILD]);
+    }
+}
diff --git a/src/plugins/buildconfig/ide-buildconfig-configuration.h 
b/src/plugins/buildconfig/ide-buildconfig-configuration.h
new file mode 100644
index 000000000..f7700e698
--- /dev/null
+++ b/src/plugins/buildconfig/ide-buildconfig-configuration.h
@@ -0,0 +1,38 @@
+/* ide-buildconfig-configuration.h
+ *
+ * Copyright 2017-2019 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-foundry.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILDCONFIG_CONFIGURATION (ide_buildconfig_configuration_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeBuildconfigConfiguration, ide_buildconfig_configuration, IDE, 
BUILDCONFIG_CONFIGURATION, IdeConfiguration)
+
+const gchar * const *ide_buildconfig_configuration_get_prebuild  (IdeBuildconfigConfiguration *self);
+void                 ide_buildconfig_configuration_set_prebuild  (IdeBuildconfigConfiguration *self,
+                                                                  const gchar * const         *prebuild);
+const gchar * const *ide_buildconfig_configuration_get_postbuild (IdeBuildconfigConfiguration *self);
+void                 ide_buildconfig_configuration_set_postbuild (IdeBuildconfigConfiguration *self,
+                                                                  const gchar * const         *postbuild);
+
+G_END_DECLS
diff --git a/src/plugins/buildconfig/ide-buildconfig-pipeline-addin.c 
b/src/plugins/buildconfig/ide-buildconfig-pipeline-addin.c
new file mode 100644
index 000000000..f75032e68
--- /dev/null
+++ b/src/plugins/buildconfig/ide-buildconfig-pipeline-addin.c
@@ -0,0 +1,117 @@
+/* ide-buildconfig-pipeline-addin.c
+ *
+ * Copyright 2017-2019 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 "ide-buildconfig-pipeline-addin"
+
+#include "config.h"
+
+#include <libide-foundry.h>
+#include <libide-threading.h>
+
+#include "ide-buildconfig-configuration.h"
+#include "ide-buildconfig-pipeline-addin.h"
+
+static void
+add_command (IdeBuildPipelineAddin  *addin,
+             IdeBuildPipeline       *pipeline,
+             IdeBuildPhase           phase,
+             gint                    priority,
+             const gchar            *command_text,
+             gchar                 **env)
+{
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_auto(GStrv) argv = NULL;
+  guint stage_id;
+  gint argc = 0;
+
+  if (!g_shell_parse_argv (command_text, &argc, &argv, &error))
+    {
+      g_warning ("%s", error->message);
+      return;
+    }
+
+  launcher = ide_build_pipeline_create_launcher (pipeline, NULL);
+
+  if (launcher == NULL)
+    {
+      g_warning ("Failed to create launcher for build command");
+      return;
+    }
+
+  for (guint i = 0; i < argc; i++)
+    ide_subprocess_launcher_push_argv (launcher, argv[i]);
+
+  ide_subprocess_launcher_set_environ (launcher, (const gchar * const *)env);
+
+  stage_id = ide_build_pipeline_attach_launcher (pipeline, phase, priority, launcher);
+  ide_build_pipeline_addin_track (addin, stage_id);
+}
+
+static void
+ide_buildconfig_pipeline_addin_load (IdeBuildPipelineAddin *addin,
+                                     IdeBuildPipeline      *pipeline)
+{
+  const gchar * const *prebuild;
+  const gchar * const *postbuild;
+  IdeConfiguration *config;
+  g_auto(GStrv) env = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILDCONFIG_PIPELINE_ADDIN (addin));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  config = ide_build_pipeline_get_configuration (pipeline);
+
+  if (!IDE_IS_BUILDCONFIG_CONFIGURATION (config))
+    return;
+
+  env = ide_configuration_get_environ (config);
+
+  prebuild = ide_buildconfig_configuration_get_prebuild (IDE_BUILDCONFIG_CONFIGURATION (config));
+  postbuild = ide_buildconfig_configuration_get_postbuild (IDE_BUILDCONFIG_CONFIGURATION (config));
+
+  if (prebuild != NULL)
+    {
+      for (guint i = 0; prebuild[i]; i++)
+        add_command (addin, pipeline, IDE_BUILD_PHASE_BUILD|IDE_BUILD_PHASE_BEFORE, i, prebuild[i], env);
+    }
+
+  if (postbuild != NULL)
+    {
+      for (guint i = 0; postbuild[i]; i++)
+        add_command (addin, pipeline, IDE_BUILD_PHASE_BUILD|IDE_BUILD_PHASE_AFTER, i, postbuild[i], env);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+pipeline_addin_init (IdeBuildPipelineAddinInterface *iface)
+{
+  iface->load = ide_buildconfig_pipeline_addin_load;
+}
+
+struct _IdeBuildconfigPipelineAddin { IdeObject parent_instance; };
+G_DEFINE_TYPE_EXTENDED (IdeBuildconfigPipelineAddin, ide_buildconfig_pipeline_addin, IDE_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_PIPELINE_ADDIN, pipeline_addin_init))
+static void ide_buildconfig_pipeline_addin_class_init (IdeBuildconfigPipelineAddinClass *klass) { }
+static void ide_buildconfig_pipeline_addin_init (IdeBuildconfigPipelineAddin *self) { }
diff --git a/src/plugins/buildconfig/ide-buildconfig-pipeline-addin.h 
b/src/plugins/buildconfig/ide-buildconfig-pipeline-addin.h
new file mode 100644
index 000000000..2d01f444c
--- /dev/null
+++ b/src/plugins/buildconfig/ide-buildconfig-pipeline-addin.h
@@ -0,0 +1,31 @@
+/* ide-buildconfig-pipeline-addin.h
+ *
+ * Copyright 2017-2019 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-foundry.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_BUILDCONFIG_PIPELINE_ADDIN (ide_buildconfig_pipeline_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeBuildconfigPipelineAddin, ide_buildconfig_pipeline_addin, IDE, 
BUILDCONFIG_PIPELINE_ADDIN, IdeObject)
+
+G_END_DECLS
diff --git a/src/plugins/buildconfig/meson.build b/src/plugins/buildconfig/meson.build
new file mode 100644
index 000000000..d1e51c7ae
--- /dev/null
+++ b/src/plugins/buildconfig/meson.build
@@ -0,0 +1,14 @@
+plugins_sources += files([
+  'buildconfig-plugin.c',
+  'ide-buildconfig-configuration.c',
+  'ide-buildconfig-configuration-provider.c',
+  'ide-buildconfig-pipeline-addin.c',
+])
+
+plugin_buildconfig_resources = gnome.compile_resources(
+  'buildconfig-resources',
+  'buildconfig.gresource.xml',
+  c_name: 'gbp_buildconfig',
+)
+
+plugins_sources += plugin_buildconfig_resources[0]
diff --git a/src/plugins/buildsystem/buildsystem-plugin.c b/src/plugins/buildsystem/buildsystem-plugin.c
new file mode 100644
index 000000000..be2511922
--- /dev/null
+++ b/src/plugins/buildsystem/buildsystem-plugin.c
@@ -0,0 +1,37 @@
+/* buildsystem-plugin.c
+ *
+ * Copyright 2018-2019 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 "buildsystem-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+#include <libide-foundry.h>
+
+#include "gbp-buildsystem-workbench-addin.h"
+
+_IDE_EXTERN void
+_gbp_buildsystem_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GBP_TYPE_BUILDSYSTEM_WORKBENCH_ADDIN);
+}
diff --git a/src/plugins/buildsystem/buildsystem.gresource.xml 
b/src/plugins/buildsystem/buildsystem.gresource.xml
new file mode 100644
index 000000000..da5669249
--- /dev/null
+++ b/src/plugins/buildsystem/buildsystem.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/buildsystem">
+    <file>buildsystem.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/buildsystem/buildsystem.plugin b/src/plugins/buildsystem/buildsystem.plugin
new file mode 100644
index 000000000..d50f00a19
--- /dev/null
+++ b/src/plugins/buildsystem/buildsystem.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2015-2018 Christian Hergert
+Description=Initializes build system support
+Embedded=_gbp_buildsystem_register_types
+Hidden=true
+Module=buildsystem
+Name=Buildsystem Support
diff --git a/src/plugins/buildsystem/gbp-buildsystem-workbench-addin.c 
b/src/plugins/buildsystem/gbp-buildsystem-workbench-addin.c
new file mode 100644
index 000000000..14f25b949
--- /dev/null
+++ b/src/plugins/buildsystem/gbp-buildsystem-workbench-addin.c
@@ -0,0 +1,298 @@
+/* gbp-buildsystem-workbench-addin.c
+ *
+ * Copyright 2018-2019 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-buildsystem-workbench-addin"
+
+#include "config.h"
+
+#include <libide-gui.h>
+#include <libide-foundry.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+
+#include "gbp-buildsystem-workbench-addin.h"
+
+struct _GbpBuildsystemWorkbenchAddin
+{
+  GObject       parent_instance;
+  IdeWorkbench *workbench;
+};
+
+typedef struct
+{
+  IdeExtensionSetAdapter *set;
+  GFile                  *directory;
+  const gchar            *best_match;
+  gint                    best_match_priority;
+  guint                   n_active;
+} Discovery;
+
+static void
+discovery_free (Discovery *state)
+{
+  g_assert (state);
+  g_assert (state->n_active == 0);
+
+  g_clear_object (&state->directory);
+  ide_clear_and_destroy_object (&state->set);
+  g_slice_free (Discovery, state);
+}
+
+static void
+discovery_foreach_cb (IdeExtensionSetAdapter *set,
+                      PeasPluginInfo         *plugin_info,
+                      PeasExtension          *exten,
+                      gpointer                user_data)
+{
+  IdeBuildSystemDiscovery *addin = (IdeBuildSystemDiscovery *)exten;
+  Discovery *state = user_data;
+  g_autofree gchar *ret = NULL;
+  gint priority = 0;
+
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_BUILD_SYSTEM_DISCOVERY (addin));
+  g_assert (state != NULL);
+
+  if ((ret = ide_build_system_discovery_discover (addin,
+                                                  state->directory,
+                                                  NULL,
+                                                  &priority,
+                                                  NULL)))
+    {
+      if (priority < state->best_match_priority || state->best_match == NULL)
+        {
+          state->best_match = g_intern_string (ret);
+          state->best_match_priority = priority;
+        }
+    }
+}
+
+static void
+discovery_worker (IdeTask      *task,
+                  gpointer      source_object,
+                  gpointer      task_data,
+                  GCancellable *cancellable)
+{
+  Discovery *state = task_data;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_BUILDSYSTEM_WORKBENCH_ADDIN (source_object));
+  g_assert (state != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_extension_set_adapter_foreach (state->set, discovery_foreach_cb, state);
+
+  if (state->best_match != NULL)
+    ide_task_return_pointer (task, (gpointer)state->best_match, NULL);
+  else
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_NOT_SUPPORTED,
+                               "Failed to discover a build system");
+}
+
+static void
+discover_async (GbpBuildsystemWorkbenchAddin *self,
+                GFile                        *directory,
+                GCancellable                 *cancellable,
+                GAsyncReadyCallback           callback,
+                gpointer                      user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  IdeContext *context;
+  Discovery *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDSYSTEM_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_FILE (directory));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (IDE_IS_WORKBENCH (self->workbench));
+
+  context = ide_workbench_get_context (self->workbench);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, discover_async);
+
+  state = g_slice_new0 (Discovery);
+  state->directory = g_file_dup (directory);
+  state->set = ide_extension_set_adapter_new (IDE_OBJECT (context),
+                                              peas_engine_get_default (),
+                                              IDE_TYPE_BUILD_SYSTEM_DISCOVERY,
+                                              NULL, NULL);
+  ide_task_set_task_data (task, state, discovery_free);
+  ide_task_run_in_thread (task, discovery_worker);
+}
+
+static const gchar *
+discover_finish (GbpBuildsystemWorkbenchAddin  *self,
+                 GAsyncResult                  *result,
+                 GError                       **error)
+{
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+discover_cb (GObject      *object,
+             GAsyncResult *result,
+             gpointer      user_data)
+{
+  GbpBuildsystemWorkbenchAddin *self = (GbpBuildsystemWorkbenchAddin *)object;
+  g_autoptr(IdeBuildSystem) build_system = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  PeasPluginInfo *plugin_info;
+  IdeProjectInfo *project_info;
+  const gchar *plugin_name;
+  PeasEngine *engine;
+  GFile *file;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDSYSTEM_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!(plugin_name = discover_finish (self, result, &error)))
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  if (self->workbench == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_CANCELLED,
+                                 "Workbench was destroyed");
+      return;
+    }
+
+  engine = peas_engine_get_default ();
+
+  if (!(plugin_info = peas_engine_get_plugin_info (engine, plugin_name)) ||
+      !peas_engine_provides_extension (engine, plugin_info, IDE_TYPE_BUILD_SYSTEM))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_FOUND,
+                                 "Failed to locate build system plugin %s",
+                                 plugin_name);
+      return;
+    }
+
+  project_info = ide_task_get_task_data (task);
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+
+  file = ide_project_info_get_file (project_info);
+  g_assert (G_IS_FILE (file));
+
+  build_system = (IdeBuildSystem *)
+    peas_engine_create_extension (engine,
+                                  plugin_info,
+                                  IDE_TYPE_BUILD_SYSTEM,
+                                  "project-file", file,
+                                  NULL);
+
+  ide_workbench_set_build_system (self->workbench, build_system);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_buildsystem_workbench_addin_load_project_async (IdeWorkbenchAddin   *addin,
+                                                    IdeProjectInfo      *project_info,
+                                                    GCancellable        *cancellable,
+                                                    GAsyncReadyCallback  callback,
+                                                    gpointer             user_data)
+{
+  GbpBuildsystemWorkbenchAddin *self = (GbpBuildsystemWorkbenchAddin *)addin;
+  g_autoptr(IdeTask) task = NULL;
+  GFile *directory;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDSYSTEM_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (IDE_IS_WORKBENCH (self->workbench));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_buildsystem_workbench_addin_load_project_async);
+  ide_task_set_task_data (task, g_object_ref (project_info), g_object_unref);
+
+  directory = ide_project_info_get_directory (project_info);
+
+  discover_async (self,
+                  directory,
+                  cancellable,
+                  discover_cb,
+                  g_steal_pointer (&task));
+}
+
+static gboolean
+gbp_buildsystem_workbench_addin_load_project_finish (IdeWorkbenchAddin  *addin,
+                                                     GAsyncResult       *result,
+                                                     GError            **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDSYSTEM_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+gbp_buildsystem_workbench_addin_load (IdeWorkbenchAddin *addin,
+                                      IdeWorkbench      *workbench)
+{
+  GBP_BUILDSYSTEM_WORKBENCH_ADDIN (addin)->workbench = workbench;
+}
+
+static void
+gbp_buildsystem_workbench_addin_unload (IdeWorkbenchAddin *addin,
+                                        IdeWorkbench      *workbench)
+{
+  GBP_BUILDSYSTEM_WORKBENCH_ADDIN (addin)->workbench = NULL;
+}
+
+static void
+workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load = gbp_buildsystem_workbench_addin_load;
+  iface->unload = gbp_buildsystem_workbench_addin_unload;
+  iface->load_project_async = gbp_buildsystem_workbench_addin_load_project_async;
+  iface->load_project_finish = gbp_buildsystem_workbench_addin_load_project_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpBuildsystemWorkbenchAddin,
+                         gbp_buildsystem_workbench_addin,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKBENCH_ADDIN,
+                                                workbench_addin_iface_init))
+
+static void
+gbp_buildsystem_workbench_addin_class_init (GbpBuildsystemWorkbenchAddinClass *klass)
+{
+}
+
+static void
+gbp_buildsystem_workbench_addin_init (GbpBuildsystemWorkbenchAddin *self)
+{
+}
diff --git a/src/plugins/buildsystem/gbp-buildsystem-workbench-addin.h 
b/src/plugins/buildsystem/gbp-buildsystem-workbench-addin.h
new file mode 100644
index 000000000..5477e349f
--- /dev/null
+++ b/src/plugins/buildsystem/gbp-buildsystem-workbench-addin.h
@@ -0,0 +1,31 @@
+/* gbp-buildsystem-workbench-addin.h
+ *
+ * Copyright 2018-2019 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_BUILDSYSTEM_WORKBENCH_ADDIN (gbp_buildsystem_workbench_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuildsystemWorkbenchAddin, gbp_buildsystem_workbench_addin, GBP, 
BUILDSYSTEM_WORKBENCH_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/buildsystem/meson.build b/src/plugins/buildsystem/meson.build
new file mode 100644
index 000000000..2de1e508c
--- /dev/null
+++ b/src/plugins/buildsystem/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'buildsystem-plugin.c',
+  'gbp-buildsystem-workbench-addin.c',
+])
+
+plugin_buildsystem_resources = gnome.compile_resources(
+  'buildsystem-resources',
+  'buildsystem.gresource.xml',
+  c_name: 'gbp_buildsystem',
+)
+
+plugins_sources += plugin_buildsystem_resources[0]
diff --git a/src/plugins/buildui/buildui-plugin.c b/src/plugins/buildui/buildui-plugin.c
new file mode 100644
index 000000000..cf395308f
--- /dev/null
+++ b/src/plugins/buildui/buildui-plugin.c
@@ -0,0 +1,45 @@
+/* buildui-plugin.c
+ *
+ * Copyright 2018-2019 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 "buildui-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+#include <libide-tree.h>
+
+#include "gbp-buildui-config-view-addin.h"
+#include "gbp-buildui-workspace-addin.h"
+#include "gbp-buildui-tree-addin.h"
+
+_IDE_EXTERN void
+_gbp_buildui_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_CONFIG_VIEW_ADDIN,
+                                              GBP_TYPE_BUILDUI_CONFIG_VIEW_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_BUILDUI_WORKSPACE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_TREE_ADDIN,
+                                              GBP_TYPE_BUILDUI_TREE_ADDIN);
+}
diff --git a/src/plugins/buildui/buildui.gresource.xml b/src/plugins/buildui/buildui.gresource.xml
new file mode 100644
index 000000000..71dc36377
--- /dev/null
+++ b/src/plugins/buildui/buildui.gresource.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/buildui">
+    <file>buildui.plugin</file>
+    <file preprocess="xml-stripblanks">gbp-buildui-config-surface.ui</file>
+    <file preprocess="xml-stripblanks">gbp-buildui-log-pane.ui</file>
+    <file preprocess="xml-stripblanks">gbp-buildui-omni-bar-section.ui</file>
+    <file preprocess="xml-stripblanks">gbp-buildui-pane.ui</file>
+    <file preprocess="xml-stripblanks">gbp-buildui-stage-row.ui</file>
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+    <file>themes/shared.css</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/buildui/buildui.plugin b/src/plugins/buildui/buildui.plugin
new file mode 100644
index 000000000..e5b4cc3a7
--- /dev/null
+++ b/src/plugins/buildui/buildui.plugin
@@ -0,0 +1,11 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2015-2018 Christian Hergert
+Depends=editor;project-tree;
+Description=Provides user interface components to display the build settings.
+Embedded=_gbp_buildui_register_types
+Hidden=true
+Module=buildui
+Name=Build user interface components
+X-Workspace-Kind=primary;
diff --git a/src/plugins/buildui/gbp-buildui-config-surface.c 
b/src/plugins/buildui/gbp-buildui-config-surface.c
new file mode 100644
index 000000000..df3646dbf
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-config-surface.c
@@ -0,0 +1,335 @@
+/* gbp-buildui-config-surface.c
+ *
+ * Copyright 2018-2019 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-buildui-config-surface"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+#include <libide-foundry.h>
+
+#include "gbp-buildui-config-surface.h"
+
+struct _GbpBuilduiConfigSurface
+{
+  IdeSurface               parent_instance;
+
+  IdeConfigurationManager *config_manager;
+
+  GtkListBox              *config_list_box;
+  GtkPaned                *paned;
+  DzlPreferences          *preferences;
+
+  /* raw pointer, only use for comparison */
+  GtkListBoxRow           *last;
+};
+
+typedef struct
+{
+  DzlPreferences   *view;
+  IdeConfiguration *config;
+} AddinState;
+
+G_DEFINE_TYPE (GbpBuilduiConfigSurface, gbp_buildui_config_surface, IDE_TYPE_SURFACE)
+
+enum {
+  PROP_0,
+  PROP_CONFIG_MANAGER,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_buildui_config_surface_foreach_cb (PeasExtensionSet *set,
+                                       PeasPluginInfo   *plugin_info,
+                                       PeasExtension    *exten,
+                                       gpointer          user_data)
+{
+  IdeConfigViewAddin *addin = (IdeConfigViewAddin *)exten;
+  AddinState *state = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_CONFIG_VIEW_ADDIN (addin));
+  g_assert (state != NULL);
+
+  ide_config_view_addin_load (addin, state->view, state->config);
+}
+
+static void
+gbp_buildui_config_surface_row_selected_cb (GbpBuilduiConfigSurface *self,
+                                            GtkListBoxRow           *row,
+                                            GtkListBox              *list_box)
+{
+  g_autoptr(PeasExtensionSet) set = NULL;
+  IdeConfiguration *config;
+  AddinState state = {0};
+  GtkWidget *child2;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_CONFIG_SURFACE (self));
+  g_assert (!row || GTK_IS_LIST_BOX_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  /* Prevent double applying settings so we don't lose state */
+  if (row == self->last)
+    return;
+  self->last = row;
+
+  /* Clear out any previous view/empty-state */
+  child2 = gtk_paned_get_child2 (self->paned);
+  if (child2 != NULL)
+    gtk_widget_destroy (child2);
+  g_assert (self->preferences == NULL);
+
+  /* If no row was selected, add empty-state view */
+  if (row == NULL)
+    {
+      GtkWidget *empty;
+
+      /* Add an empty selection state instead of preferences view */
+      empty = g_object_new (DZL_TYPE_EMPTY_STATE,
+                            "icon-name", "builder-build-symbolic",
+                            "title", _("No build configuration"),
+                            "subtitle", _("Select a build configuration from the sidebar to modify."),
+                            "visible", TRUE,
+                            "hexpand", TRUE,
+                            NULL);
+      gtk_container_add (GTK_CONTAINER (self->paned), empty);
+
+      return;
+    }
+
+  /* We have a configuration to display, so do it */
+  self->preferences = g_object_new (DZL_TYPE_PREFERENCES_VIEW,
+                                    "use-sidebar", FALSE,
+                                    "visible", TRUE,
+                                    NULL);
+  g_signal_connect (self->preferences,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->preferences);
+  gtk_container_add (GTK_CONTAINER (self->paned), GTK_WIDGET (self->preferences));
+
+  config = g_object_get_data (G_OBJECT (row), "CONFIG");
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  set = peas_extension_set_new (peas_engine_get_default (),
+                                IDE_TYPE_CONFIG_VIEW_ADDIN,
+                                NULL);
+
+  state.view = self->preferences;
+  state.config = config;
+
+  peas_extension_set_foreach (set,
+                              gbp_buildui_config_surface_foreach_cb,
+                              &state);
+}
+
+static GtkWidget *
+gbp_buildui_config_surface_create_row_cb (gpointer item,
+                                          gpointer user_data)
+{
+  GbpBuilduiConfigSurface *self = user_data;
+  IdeConfiguration *config = item;
+  const gchar *title;
+  GtkWidget *row;
+  GtkWidget *label;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_CONFIG_SURFACE (self));
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  title = ide_configuration_get_display_name (config);
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "visible", TRUE,
+                        "label", title,
+                        "xalign", 0.0f,
+                        "margin", 6,
+                        NULL);
+  gtk_container_add (GTK_CONTAINER (row), GTK_WIDGET (label));
+
+  g_object_set_data_full (G_OBJECT (row),
+                          "CONFIG",
+                          g_object_ref (config),
+                          g_object_unref);
+
+  return row;
+}
+
+static void
+gbp_buildui_config_surface_set_config_manager (GbpBuilduiConfigSurface *self,
+                                               IdeConfigurationManager *config_manager)
+{
+  g_assert (GBP_IS_BUILDUI_CONFIG_SURFACE (self));
+  g_assert (IDE_IS_CONFIGURATION_MANAGER (config_manager));
+  g_assert (self->config_manager == NULL);
+
+  g_set_object (&self->config_manager, config_manager);
+
+  gtk_list_box_bind_model (self->config_list_box,
+                           G_LIST_MODEL (config_manager),
+                           gbp_buildui_config_surface_create_row_cb,
+                           g_object_ref (self),
+                           g_object_unref);
+}
+
+static void
+header_func_cb (GtkListBoxRow *row,
+                GtkListBoxRow *before,
+                gpointer       user_data)
+{
+  if (before == NULL)
+    {
+      PangoAttrList *attrs;
+      GtkWidget *header;
+
+      attrs = pango_attr_list_new ();
+      pango_attr_list_insert (attrs, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+      pango_attr_list_insert (attrs, pango_attr_foreground_alpha_new (.55 * G_MAXUSHORT));
+
+      header = g_object_new (GTK_TYPE_LABEL,
+                             "attributes", attrs,
+                             "label", _("Build Configurations"),
+                             "xalign", 0.0f,
+                             "visible", TRUE,
+                             NULL);
+      dzl_gtk_widget_add_style_class (header, "header");
+
+      gtk_list_box_row_set_header (row, header);
+
+      pango_attr_list_unref (attrs);
+    }
+}
+
+static void
+gbp_buildui_config_surface_get_property (GObject    *object,
+                                         guint       prop_id,
+                                         GValue     *value,
+                                         GParamSpec *pspec)
+{
+  GbpBuilduiConfigSurface *self = GBP_BUILDUI_CONFIG_SURFACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONFIG_MANAGER:
+      g_value_set_object (value, self->config_manager);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_buildui_config_surface_set_property (GObject      *object,
+                                         guint         prop_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec)
+{
+  GbpBuilduiConfigSurface *self = GBP_BUILDUI_CONFIG_SURFACE (object);
+
+  switch (prop_id)
+    {
+    case PROP_CONFIG_MANAGER:
+      gbp_buildui_config_surface_set_config_manager (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_buildui_config_surface_class_init (GbpBuilduiConfigSurfaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = gbp_buildui_config_surface_get_property;
+  object_class->set_property = gbp_buildui_config_surface_set_property;
+
+  properties [PROP_CONFIG_MANAGER] =
+    g_param_spec_object ("config-manager",
+                         "Config Manager",
+                         "The configuration manager",
+                         IDE_TYPE_CONFIGURATION_MANAGER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/buildui/gbp-buildui-config-surface.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiConfigSurface, config_list_box);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiConfigSurface, paned);
+}
+
+static void
+gbp_buildui_config_surface_init (GbpBuilduiConfigSurface *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_list_box_set_header_func (self->config_list_box, header_func_cb, NULL, NULL);
+
+  g_signal_connect_object (self->config_list_box,
+                           "row-selected",
+                           G_CALLBACK (gbp_buildui_config_surface_row_selected_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+gbp_buildui_config_surface_set_config_cb (GtkWidget *widget,
+                                          gpointer   user_data)
+{
+  IdeConfiguration *config = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GTK_IS_LIST_BOX_ROW (widget));
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  if (g_object_get_data (G_OBJECT (widget), "CONFIG") == (gpointer)config)
+    {
+      GtkListBox *list_box = GTK_LIST_BOX (gtk_widget_get_parent (widget));
+
+      gtk_list_box_select_row (list_box, GTK_LIST_BOX_ROW (widget));
+    }
+}
+
+void
+gbp_buildui_config_surface_set_config (GbpBuilduiConfigSurface *self,
+                                       IdeConfiguration        *config)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (GBP_IS_BUILDUI_CONFIG_SURFACE (self));
+  g_return_if_fail (IDE_IS_CONFIGURATION (config));
+
+  gtk_container_foreach (GTK_CONTAINER (self->config_list_box),
+                         gbp_buildui_config_surface_set_config_cb,
+                         config);
+}
diff --git a/src/plugins/buildui/gbp-buildui-config-surface.h 
b/src/plugins/buildui/gbp-buildui-config-surface.h
new file mode 100644
index 000000000..0f931dba2
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-config-surface.h
@@ -0,0 +1,35 @@
+/* gbp-buildui-config-surface.h
+ *
+ * Copyright 2018-2019 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-foundry.h>
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_BUILDUI_CONFIG_SURFACE (gbp_buildui_config_surface_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuilduiConfigSurface, gbp_buildui_config_surface, GBP, BUILDUI_CONFIG_SURFACE, 
IdeSurface)
+
+void gbp_buildui_config_surface_set_config (GbpBuilduiConfigSurface *self,
+                                            IdeConfiguration        *config);
+
+G_END_DECLS
diff --git a/src/plugins/buildui/gbp-buildui-config-surface.ui 
b/src/plugins/buildui/gbp-buildui-config-surface.ui
new file mode 100644
index 000000000..df511de6d
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-config-surface.ui
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpBuilduiConfigSurface" parent="IdeSurface">
+    <style>
+      <class name="buildui"/>
+    </style>
+    <child>
+      <object class="GtkPaned" id="paned">
+        <property name="position">275</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkScrolledWindow">
+            <property name="propagate-natural-width">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkListBox" id="config_list_box">
+                <property name="visible">true</property>
+                <style>
+                  <class name="sidebar"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/buildui/gbp-buildui-config-view-addin.c 
b/src/plugins/buildui/gbp-buildui-config-view-addin.c
new file mode 100644
index 000000000..97d9da31b
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-config-view-addin.c
@@ -0,0 +1,517 @@
+/* gbp-buildui-config-view-addin.c
+ *
+ * Copyright 2018-2019 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-buildui-config-view-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+
+#include "gbp-buildui-config-view-addin.h"
+#include "gbp-buildui-runtime-categories.h"
+#include "gbp-buildui-runtime-row.h"
+
+struct _GbpBuilduiConfigViewAddin
+{
+  GObject parent_instance;
+};
+
+static gboolean
+treat_null_as_empty (GBinding     *binding,
+                     const GValue *from_value,
+                     GValue       *to_value,
+                     gpointer      user_data)
+{
+  const gchar *str = g_value_get_string (from_value);
+  g_value_set_string (to_value, str ?: "");
+  return TRUE;
+}
+
+static void
+add_description_row (DzlPreferences *preferences,
+                     const gchar    *page,
+                     const gchar    *group,
+                     const gchar    *title,
+                     const gchar    *value,
+                     GtkWidget      *value_widget)
+{
+  GtkWidget *widget;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  widget = g_object_new (GTK_TYPE_LABEL,
+                         "xalign", 0.0f,
+                         "label", title,
+                         "visible", TRUE,
+                         "margin-right", 12,
+                         NULL);
+  dzl_gtk_widget_add_style_class (widget, "dim-label");
+
+  if (value_widget == NULL)
+    value_widget = g_object_new (GTK_TYPE_LABEL,
+                                 "hexpand", TRUE,
+                                 "label", value,
+                                 "xalign", 0.0f,
+                                 "visible", TRUE,
+                                 NULL);
+
+  dzl_preferences_add_table_row (preferences, page, group, widget, value_widget, NULL);
+}
+
+static GtkWidget *
+create_stack_list_row (gpointer item,
+                       gpointer user_data)
+{
+  IdeConfiguration *config = user_data;
+  GtkWidget *row;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  if (IDE_IS_RUNTIME (item))
+    return gbp_buildui_runtime_row_new (item, config);
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+  g_object_set_data_full (G_OBJECT (row),
+                          "ITEM",
+                          g_object_ref (item),
+                          g_object_unref);
+
+  if (GBP_IS_BUILDUI_RUNTIME_CATEGORIES (item))
+    {
+      const gchar *category = gbp_buildui_runtime_categories_get_name (item);
+      GtkWidget *label;
+
+      label = g_object_new (GTK_TYPE_LABEL,
+                            "label", category,
+                            "margin", 10,
+                            "use-markup", TRUE,
+                            "visible", TRUE,
+                            "xalign", 0.0f,
+                            NULL);
+      gtk_container_add (GTK_CONTAINER (row), label);
+    }
+  else if (DZL_IS_LIST_MODEL_FILTER (item))
+    {
+      const gchar *category = g_object_get_data (item, "CATEGORY");
+      GtkWidget *label;
+
+      label = g_object_new (GTK_TYPE_LABEL,
+                            "label", category,
+                            "margin", 10,
+                            "use-markup", TRUE,
+                            "visible", TRUE,
+                            "xalign", 0.0f,
+                            NULL);
+      gtk_container_add (GTK_CONTAINER (row), label);
+    }
+
+  return row;
+}
+
+static void
+on_runtime_row_activated_cb (DzlStackList  *stack_list,
+                             GtkListBoxRow *row,
+                             gpointer       user_data)
+{
+  IdeConfiguration *config = user_data;
+  gpointer item;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (DZL_IS_STACK_LIST (stack_list));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  if (GBP_IS_BUILDUI_RUNTIME_ROW (row))
+    {
+      const gchar *id;
+
+      id = gbp_buildui_runtime_row_get_id (GBP_BUILDUI_RUNTIME_ROW (row));
+      ide_configuration_set_runtime_id (config, id);
+
+      {
+        GtkWidget *box;
+
+        if ((box = gtk_widget_get_ancestor (GTK_WIDGET (row), GTK_TYPE_LIST_BOX)))
+          gtk_list_box_unselect_all (GTK_LIST_BOX (box));
+      }
+
+      return;
+    }
+
+  item = g_object_get_data (G_OBJECT (row), "ITEM");
+  g_assert (G_IS_LIST_MODEL (item) || IDE_IS_RUNTIME (item));
+
+  if (GBP_IS_BUILDUI_RUNTIME_CATEGORIES (item))
+    {
+      dzl_stack_list_push (stack_list,
+                           create_stack_list_row (item, config),
+                           G_LIST_MODEL (item),
+                           create_stack_list_row,
+                           g_object_ref (config),
+                           g_object_unref);
+    }
+  else if (G_IS_LIST_MODEL (item))
+    {
+      dzl_stack_list_push (stack_list,
+                           create_stack_list_row (item, config),
+                           G_LIST_MODEL (item),
+                           create_stack_list_row,
+                           g_object_ref (config),
+                           g_object_unref);
+    }
+}
+
+static GtkWidget *
+create_runtime_box (IdeConfiguration  *config,
+                    IdeRuntimeManager *runtime_manager)
+{
+  g_autoptr(GbpBuilduiRuntimeCategories) filter = NULL;
+  DzlStackList *stack;
+  const gchar *category;
+  IdeRuntime *runtime;
+  GtkWidget *header;
+  GtkWidget *frame;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (IDE_IS_RUNTIME_MANAGER (runtime_manager));
+
+  filter = gbp_buildui_runtime_categories_new (runtime_manager, NULL);
+
+  frame = g_object_new (GTK_TYPE_FRAME,
+                        "visible", TRUE,
+                        NULL);
+
+  header = g_object_new (GTK_TYPE_LABEL,
+                         "label", _("All Runtimes"),
+                         "margin", 10,
+                         "visible", TRUE,
+                         "xalign", 0.0f,
+                         NULL);
+
+  stack = g_object_new (DZL_TYPE_STACK_LIST,
+                        "visible", TRUE,
+                        NULL);
+  dzl_stack_list_push (stack,
+                       header,
+                       G_LIST_MODEL (filter),
+                       create_stack_list_row,
+                       g_object_ref (config),
+                       g_object_unref);
+  gtk_container_add (GTK_CONTAINER (frame), GTK_WIDGET (stack));
+
+  g_signal_connect_object (stack,
+                           "row-activated",
+                           G_CALLBACK (on_runtime_row_activated_cb),
+                           config,
+                           0);
+
+  if ((runtime = ide_configuration_get_runtime (config)) &&
+      (category = ide_runtime_get_category (runtime)))
+    {
+      g_autoptr(GString) prefix = g_string_new (NULL);
+      g_auto(GStrv) parts = g_strsplit (category, "/", 0);
+
+      for (guint i = 0; parts[i]; i++)
+        {
+          g_autoptr(GListModel) model = NULL;
+
+          g_string_append (prefix, parts[i]);
+          if (parts[i+1])
+            g_string_append_c (prefix, '/');
+
+          model = gbp_buildui_runtime_categories_create_child_model (filter, prefix->str);
+
+          dzl_stack_list_push (stack,
+                               create_stack_list_row (model, config),
+                               model,
+                               create_stack_list_row,
+                               g_object_ref (config),
+                               g_object_unref);
+        }
+    }
+
+  return GTK_WIDGET (frame);
+}
+
+static void
+notify_toolchain_id (IdeConfiguration *config,
+                     GParamSpec       *pspec,
+                     GtkImage         *image)
+{
+  const gchar *toolchain_id;
+  const gchar *current;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (GTK_IS_IMAGE (image));
+
+  toolchain_id = ide_configuration_get_toolchain_id (config);
+  current = g_object_get_data (G_OBJECT (image), "TOOLCHAIN_ID");
+
+  gtk_widget_set_visible (GTK_WIDGET (image), ide_str_equal0 (toolchain_id, current));
+}
+
+static GtkWidget *
+create_toolchain_row (gpointer item,
+                      gpointer user_data)
+{
+  IdeToolchain *toolchain = item;
+  IdeConfiguration *config = user_data;
+  const gchar *toolchain_id;
+  GtkWidget *label;
+  GtkWidget *row;
+  GtkWidget *box;
+  GtkImage *image;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TOOLCHAIN (toolchain));
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  toolchain_id = ide_toolchain_get_id (toolchain);
+
+  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
+                      "visible", TRUE,
+                      NULL);
+  g_object_set_data_full (G_OBJECT (row), "TOOLCHAIN_ID", g_strdup (toolchain_id), g_free);
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "spacing", 6,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (row), box);
+
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "label", ide_toolchain_get_display_name (toolchain),
+                        "visible", TRUE,
+                        "xalign", 0.0f,
+                        NULL);
+  gtk_container_add (GTK_CONTAINER (box), label);
+
+  image = g_object_new (GTK_TYPE_IMAGE,
+                        "icon-name", "object-select-symbolic",
+                        "halign", GTK_ALIGN_START,
+                        "hexpand", TRUE,
+                        NULL);
+  g_object_set_data_full (G_OBJECT (image), "TOOLCHAIN_ID", g_strdup (toolchain_id), g_free);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (image));
+
+  g_signal_connect_object (config,
+                           "notify::toolchain-id",
+                           G_CALLBACK (notify_toolchain_id),
+                           image,
+                           0);
+  notify_toolchain_id (config, NULL, image);
+
+  return row;
+}
+
+static void
+on_toolchain_row_activated_cb (GtkListBox       *list_box,
+                               GtkListBoxRow    *row,
+                               IdeConfiguration *config)
+{
+  const gchar *toolchain_id;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GTK_IS_LIST_BOX (list_box));
+  g_assert (GTK_IS_LIST_BOX_ROW (row));
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  if ((toolchain_id = g_object_get_data (G_OBJECT (row), "TOOLCHAIN_ID")))
+    ide_configuration_set_toolchain_id (config, toolchain_id);
+
+  gtk_list_box_unselect_all (list_box);
+}
+
+static GtkWidget *
+create_toolchain_box (IdeConfiguration    *config,
+                      IdeToolchainManager *toolchain_manager)
+{
+  GtkScrolledWindow *scroller;
+  GtkListBox *list_box;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONFIGURATION (config));
+  g_assert (IDE_IS_TOOLCHAIN_MANAGER (toolchain_manager));
+
+  scroller = g_object_new (GTK_TYPE_SCROLLED_WINDOW,
+                           "propagate-natural-height", TRUE,
+                           "shadow-type", GTK_SHADOW_IN,
+                           "visible", TRUE,
+                           NULL);
+
+  list_box = g_object_new (GTK_TYPE_LIST_BOX,
+                           "visible", TRUE,
+                           NULL);
+  g_signal_connect_object (list_box,
+                           "row-activated",
+                           G_CALLBACK (on_toolchain_row_activated_cb),
+                           config,
+                           0);
+  gtk_container_add (GTK_CONTAINER (scroller), GTK_WIDGET (list_box));
+
+  gtk_list_box_bind_model (list_box,
+                           G_LIST_MODEL (toolchain_manager),
+                           create_toolchain_row,
+                           g_object_ref (config),
+                           g_object_unref);
+
+  return GTK_WIDGET (scroller);
+}
+
+static void
+gbp_buildui_config_view_addin_load (IdeConfigViewAddin *addin,
+                                    DzlPreferences     *preferences,
+                                    IdeConfiguration   *config)
+{
+  GbpBuilduiConfigViewAddin *self = (GbpBuilduiConfigViewAddin *)addin;
+  IdeToolchainManager *toolchain_manager;
+  IdeRuntimeManager *runtime_manager;
+  g_autoptr(GFile) workdir = NULL;
+  IdeBuildSystem *build_system;
+  IdeEnvironment *environ;
+  IdeContext *context;
+  GtkWidget *box;
+  GtkWidget *entry;
+  static const struct {
+    const gchar *label;
+    const gchar *action;
+    const gchar *tooltip;
+    const gchar *style_class;
+  } actions[] = {
+    { N_("Make active"), "config-manager.current", N_("Select this configuration as the active 
configuration.") },
+    { N_("Duplicate"), "config-manager.duplicate", N_("Duplicating the configuration allows making changes 
without modifying this configuration.") },
+    { N_("Remove"), "config-manager.delete", N_("Removes the configuration and cannot be undone."), 
"destructive-action" },
+  };
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_CONFIG_VIEW_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  /* Get manager objects */
+  context = ide_object_get_context (IDE_OBJECT (config));
+  runtime_manager = ide_runtime_manager_from_context (context);
+  toolchain_manager = ide_toolchain_manager_from_context (context);
+  build_system = ide_build_system_from_context (context);
+  workdir = ide_context_ref_workdir (context);
+
+  /* Add our pages */
+  dzl_preferences_add_page (preferences, "general", _("General"), 0);
+  dzl_preferences_add_page (preferences, "environ", _("Environment"), 20);
+
+  /* Add groups to pages */
+  dzl_preferences_add_list_group (preferences, "general", "general", _("Overview"), GTK_SELECTION_NONE, 0);
+  dzl_preferences_add_group (preferences, "general", "buttons", NULL, 0);
+  dzl_preferences_add_group (preferences, "environ", "build", _("Build Environment"), 0);
+
+  /* actions button box */
+  box = g_object_new (GTK_TYPE_BOX,
+                      "homogeneous", TRUE,
+                      "spacing", 12,
+                      "visible", TRUE,
+                      NULL);
+  for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
+    {
+      GtkWidget *button;
+
+      button = g_object_new (GTK_TYPE_BUTTON,
+                             "visible", TRUE,
+                             "action-name", actions[i].action,
+                             "action-target", g_variant_new_string (ide_configuration_get_id (config)),
+                             "label", g_dgettext (GETTEXT_PACKAGE, actions[i].label),
+                             "tooltip-text", g_dgettext (GETTEXT_PACKAGE, actions[i].tooltip),
+                             NULL);
+      if (actions[i].style_class)
+        dzl_gtk_widget_add_style_class (button, actions[i].style_class);
+      gtk_container_add (GTK_CONTAINER (box), button);
+    }
+
+  /* Add description info */
+  add_description_row (preferences, "general", "general", _("Name"), ide_configuration_get_display_name 
(config), NULL);
+  add_description_row (preferences, "general", "general", _("Source Directory"), g_file_peek_path (workdir), 
NULL);
+  add_description_row (preferences, "general", "general", _("Build System"), 
ide_build_system_get_display_name (build_system), NULL);
+
+  entry = g_object_new (GTK_TYPE_ENTRY,
+                        "visible", TRUE,
+                        "hexpand", TRUE,
+                        NULL);
+  g_object_bind_property_full (config, "prefix", entry, "text",
+                               G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
+                               treat_null_as_empty, NULL, NULL, NULL);
+  add_description_row (preferences, "general", "general", _("Install Prefix"), NULL, entry);
+
+  entry = g_object_new (GTK_TYPE_ENTRY,
+                        "visible", TRUE,
+                        "hexpand", TRUE,
+                        NULL);
+  g_object_bind_property_full (config, "config-opts", entry, "text",
+                               G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL,
+                               treat_null_as_empty, NULL, NULL, NULL);
+  add_description_row (preferences, "general", "general", _("Configure Options"), NULL, entry);
+
+  dzl_preferences_add_custom (preferences, "general", "buttons", box, NULL, 5);
+
+  /* Setup runtime selection */
+  dzl_preferences_add_group (preferences, "general", "runtime", _("Application Runtime"), 10);
+  dzl_preferences_add_custom (preferences, "general", "runtime", create_runtime_box (config, 
runtime_manager), NULL, 10);
+
+  /* Setup toolchain selection */
+  dzl_preferences_add_group (preferences, "general", "toolchain", _("Build Toolchain"), 20);
+  dzl_preferences_add_custom (preferences, "general", "toolchain", create_toolchain_box (config, 
toolchain_manager), NULL, 10);
+
+  /* Add environment selector */
+  environ = ide_configuration_get_environment (config);
+  dzl_preferences_add_custom (preferences, "environ", "build",
+                              g_object_new (GTK_TYPE_FRAME,
+                                            "visible", TRUE,
+                                            "child", g_object_new (IDE_TYPE_ENVIRONMENT_EDITOR,
+                                                                   "environment", environ,
+                                                                   "visible", TRUE,
+                                                                   NULL),
+                                            NULL),
+                              NULL, 0);
+}
+
+static void
+config_view_addin_iface_init (IdeConfigViewAddinInterface *iface)
+{
+  iface->load = gbp_buildui_config_view_addin_load;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpBuilduiConfigViewAddin, gbp_buildui_config_view_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_CONFIG_VIEW_ADDIN, config_view_addin_iface_init))
+
+static void
+gbp_buildui_config_view_addin_class_init (GbpBuilduiConfigViewAddinClass *klass)
+{
+}
+
+static void
+gbp_buildui_config_view_addin_init (GbpBuilduiConfigViewAddin *self)
+{
+}
diff --git a/src/plugins/buildui/gbp-buildui-config-view-addin.h 
b/src/plugins/buildui/gbp-buildui-config-view-addin.h
new file mode 100644
index 000000000..7c8f1d1c4
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-config-view-addin.h
@@ -0,0 +1,31 @@
+/* gbp-buildui-config-view-addin.h
+ *
+ * Copyright 2018-2019 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_BUILDUI_CONFIG_VIEW_ADDIN (gbp_buildui_config_view_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuilduiConfigViewAddin, gbp_buildui_config_view_addin, GBP, 
BUILDUI_CONFIG_VIEW_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/buildui/gbp-buildui-log-pane.c b/src/plugins/buildui/gbp-buildui-log-pane.c
new file mode 100644
index 000000000..7bdab1908
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-log-pane.c
@@ -0,0 +1,378 @@
+/* gbp-buildui-log-pane.c
+ *
+ * Copyright 2015-2019 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-buildui-log-pane"
+
+#include "config.h"
+
+#include <libide-terminal.h>
+#include <glib/gi18n.h>
+
+#include "ide-build-private.h"
+
+#include "gbp-buildui-log-pane.h"
+
+struct _GbpBuilduiLogPane
+{
+  IdePane            parent_instance;
+
+  IdeBuildPipeline  *pipeline;
+
+  GtkScrollbar      *scrollbar;
+  IdeTerminal       *terminal;
+
+  guint              log_observer;
+};
+
+enum {
+  PROP_0,
+  PROP_PIPELINE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (GbpBuilduiLogPane, gbp_buildui_log_pane, IDE_TYPE_PANE)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_buildui_log_pane_reset_view (GbpBuilduiLogPane *self)
+{
+  g_assert (GBP_IS_BUILDUI_LOG_PANE (self));
+
+  vte_terminal_reset (VTE_TERMINAL (self->terminal), TRUE, TRUE);
+}
+
+static void
+gbp_buildui_log_pane_log_observer (IdeBuildLogStream  stream,
+                                   const gchar       *message,
+                                   gssize             message_len,
+                                   gpointer           user_data)
+{
+  GbpBuilduiLogPane *self = user_data;
+
+  g_assert (GBP_IS_BUILDUI_LOG_PANE (self));
+  g_assert (message != NULL);
+  g_assert (message_len >= 0);
+  g_assert (message[message_len] == '\0');
+
+  vte_terminal_feed (VTE_TERMINAL (self->terminal), message, -1);
+  vte_terminal_feed (VTE_TERMINAL (self->terminal), "\r\n", -1);
+}
+
+static void
+gbp_buildui_log_pane_notify_pty (GbpBuilduiLogPane *self,
+                                 GParamSpec        *pspec,
+                                 IdeBuildPipeline  *pipeline)
+{
+  g_assert (GBP_IS_BUILDUI_LOG_PANE (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  vte_terminal_set_pty (VTE_TERMINAL (self->terminal),
+                        ide_build_pipeline_get_pty (pipeline));
+}
+
+void
+gbp_buildui_log_pane_set_pipeline (GbpBuilduiLogPane *self,
+                                   IdeBuildPipeline  *pipeline)
+{
+  g_return_if_fail (GBP_IS_BUILDUI_LOG_PANE (self));
+  g_return_if_fail (!pipeline || IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (pipeline != self->pipeline)
+    {
+      if (self->pipeline != NULL)
+        {
+          g_signal_handlers_disconnect_by_func (self->pipeline,
+                                                G_CALLBACK (gbp_buildui_log_pane_notify_pty),
+                                                self);
+          ide_build_pipeline_remove_log_observer (self->pipeline, self->log_observer);
+          self->log_observer = 0;
+          g_clear_object (&self->pipeline);
+          vte_terminal_set_pty (VTE_TERMINAL (self->terminal), NULL);
+        }
+
+      if (pipeline != NULL)
+        {
+          self->pipeline = g_object_ref (pipeline);
+          self->log_observer =
+            ide_build_pipeline_add_log_observer (self->pipeline,
+                                                 gbp_buildui_log_pane_log_observer,
+                                                 self,
+                                                 NULL);
+          vte_terminal_reset (VTE_TERMINAL (self->terminal), TRUE, TRUE);
+          vte_terminal_set_pty (VTE_TERMINAL (self->terminal),
+                                ide_build_pipeline_get_pty (pipeline));
+          g_signal_connect_object (pipeline,
+                                   "notify::pty",
+                                   G_CALLBACK (gbp_buildui_log_pane_notify_pty),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+        }
+    }
+}
+
+static void
+gbp_buildui_log_pane_window_title_changed (GbpBuilduiLogPane *self,
+                                           IdeTerminal       *terminal)
+{
+  g_assert (GBP_IS_BUILDUI_LOG_PANE (self));
+  g_assert (VTE_IS_TERMINAL (terminal));
+
+  if (self->pipeline != NULL)
+    {
+      const gchar *title;
+
+      title = vte_terminal_get_window_title (VTE_TERMINAL (terminal));
+      _ide_build_pipeline_set_message (self->pipeline, title);
+    }
+}
+
+static void
+gbp_buildui_log_pane_grab_focus (GtkWidget *widget)
+{
+  GbpBuilduiLogPane *self = (GbpBuilduiLogPane *)widget;
+
+  g_assert (GBP_IS_BUILDUI_LOG_PANE (self));
+
+  if (self->terminal != NULL)
+    gtk_widget_grab_focus (GTK_WIDGET (self->terminal));
+}
+
+static void
+gbp_buildui_log_pane_finalize (GObject *object)
+{
+  GbpBuilduiLogPane *self = (GbpBuilduiLogPane *)object;
+
+  g_clear_object (&self->pipeline);
+
+  G_OBJECT_CLASS (gbp_buildui_log_pane_parent_class)->finalize (object);
+}
+
+static void
+gbp_buildui_log_pane_dispose (GObject *object)
+{
+  GbpBuilduiLogPane *self = (GbpBuilduiLogPane *)object;
+
+  gbp_buildui_log_pane_set_pipeline (self, NULL);
+
+  G_OBJECT_CLASS (gbp_buildui_log_pane_parent_class)->dispose (object);
+}
+
+static void
+gbp_buildui_log_pane_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  GbpBuilduiLogPane *self = GBP_BUILDUI_LOG_PANE (object);
+
+  switch (prop_id)
+    {
+    case PROP_PIPELINE:
+      g_value_set_object (value, self->pipeline);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_buildui_log_pane_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  GbpBuilduiLogPane *self = GBP_BUILDUI_LOG_PANE (object);
+
+  switch (prop_id)
+    {
+    case PROP_PIPELINE:
+      gbp_buildui_log_pane_set_pipeline (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_buildui_log_pane_class_init (GbpBuilduiLogPaneClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = gbp_buildui_log_pane_dispose;
+  object_class->finalize = gbp_buildui_log_pane_finalize;
+  object_class->get_property = gbp_buildui_log_pane_get_property;
+  object_class->set_property = gbp_buildui_log_pane_set_property;
+
+  widget_class->grab_focus = gbp_buildui_log_pane_grab_focus;
+
+  gtk_widget_class_set_css_name (widget_class, "buildlogpanel");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/buildui/gbp-buildui-log-pane.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiLogPane, scrollbar);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiLogPane, terminal);
+
+  properties [PROP_PIPELINE] =
+    g_param_spec_object ("pipeline",
+                         "Result",
+                         "Result",
+                         IDE_TYPE_BUILD_PIPELINE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_buildui_log_pane_clear_activate (GSimpleAction *action,
+                                     GVariant      *param,
+                                     gpointer       user_data)
+{
+  GbpBuilduiLogPane *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_BUILDUI_LOG_PANE (self));
+
+  gbp_buildui_log_pane_reset_view (self);
+}
+
+static void
+gbp_buildui_log_pane_save_in_file (GSimpleAction *action,
+                                   GVariant      *param,
+                                   gpointer       user_data)
+{
+  GbpBuilduiLogPane *self = user_data;
+  g_autoptr(GtkFileChooserNative) native = NULL;
+  GtkWidget *window;
+  gint res;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_BUILDUI_LOG_PANE (self));
+
+  window = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);
+  native = gtk_file_chooser_native_new (_("Save File"),
+                                        GTK_WINDOW (window),
+                                        GTK_FILE_CHOOSER_ACTION_SAVE,
+                                        _("_Save"),
+                                        _("_Cancel"));
+
+  res = gtk_native_dialog_run (GTK_NATIVE_DIALOG (native));
+
+  if (res == GTK_RESPONSE_ACCEPT)
+    {
+      g_autoptr(GFile) file = NULL;
+
+      file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (native));
+
+      if (file != NULL)
+        {
+          g_autoptr(GFileOutputStream) stream = NULL;
+          g_autoptr(GError) error = NULL;
+
+          stream = g_file_replace (file,
+                                   NULL,
+                                   FALSE,
+                                   G_FILE_CREATE_REPLACE_DESTINATION,
+                                   NULL,
+                                   &error);
+
+          if (stream != NULL)
+            {
+              vte_terminal_write_contents_sync (VTE_TERMINAL (self->terminal),
+                                                G_OUTPUT_STREAM (stream),
+                                                VTE_WRITE_DEFAULT,
+                                                NULL,
+                                                &error);
+              g_output_stream_close (G_OUTPUT_STREAM (stream), NULL, NULL);
+            }
+
+          if (error != NULL)
+            g_warning ("Failed to write contents: %s", error->message);
+        }
+    }
+
+  IDE_EXIT;
+}
+
+static void
+terminal_size_allocate (GbpBuilduiLogPane *self,
+                        GtkAllocation     *allocation,
+                        IdeTerminal       *terminal)
+{
+  VtePty *pty;
+  gint rows = 0;
+  gint columns = 0;
+
+  g_assert (GBP_IS_BUILDUI_LOG_PANE (self));
+  g_assert (allocation != NULL);
+  g_assert (IDE_IS_TERMINAL (terminal));
+
+  pty = vte_terminal_get_pty (VTE_TERMINAL (self->terminal));
+
+  if (self->pipeline != NULL && pty != NULL)
+    {
+      if (vte_pty_get_size (pty, &rows, &columns, NULL))
+        _ide_build_pipeline_set_pty_size (self->pipeline, rows, columns);
+    }
+}
+
+static void
+gbp_buildui_log_pane_init (GbpBuilduiLogPane *self)
+{
+  g_autoptr(GSimpleActionGroup) actions = NULL;
+  static const GActionEntry entries[] = {
+    { "clear", gbp_buildui_log_pane_clear_activate },
+    { "save", gbp_buildui_log_pane_save_in_file },
+  };
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  dzl_dock_widget_set_icon_name (DZL_DOCK_WIDGET (self), "builder-build-symbolic");
+
+  g_signal_connect_object (self->terminal,
+                           "size-allocate",
+                           G_CALLBACK (terminal_size_allocate),
+                           self,
+                           G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+  g_signal_connect_object (self->terminal,
+                           "window-title-changed",
+                           G_CALLBACK (gbp_buildui_log_pane_window_title_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_range_set_adjustment (GTK_RANGE (self->scrollbar),
+                            gtk_scrollable_get_vadjustment (GTK_SCROLLABLE (self->terminal)));
+
+  vte_terminal_set_scrollback_lines (VTE_TERMINAL (self->terminal), 1000);
+  vte_terminal_set_scroll_on_output (VTE_TERMINAL (self->terminal), FALSE);
+  vte_terminal_set_scroll_on_keystroke (VTE_TERMINAL (self->terminal), TRUE);
+
+  dzl_dock_widget_set_title (DZL_DOCK_WIDGET (self), _("Build Output"));
+
+  gbp_buildui_log_pane_reset_view (self);
+
+  actions = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (actions), entries, G_N_ELEMENTS (entries), self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self), "build-log", G_ACTION_GROUP (actions));
+}
diff --git a/src/plugins/buildui/gbp-buildui-log-pane.h b/src/plugins/buildui/gbp-buildui-log-pane.h
new file mode 100644
index 000000000..31f1ff628
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-log-pane.h
@@ -0,0 +1,36 @@
+/* gbp-buildui-log-pane.h
+ *
+ * Copyright 2015-2019 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 <dazzle.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_BUILDUI_LOG_PANE (gbp_buildui_log_pane_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuilduiLogPane, gbp_buildui_log_pane, GBP, BUILDUI_LOG_PANE, IdePane)
+
+void gbp_buildui_log_pane_set_pipeline (GbpBuilduiLogPane *self,
+                                        IdeBuildPipeline  *pipeline);
+
+G_END_DECLS
diff --git a/src/plugins/buildui/gbp-buildui-log-pane.ui b/src/plugins/buildui/gbp-buildui-log-pane.ui
new file mode 100644
index 000000000..820d9ac72
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-log-pane.ui
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpBuilduiLogPane" parent="IdePane">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">horizontal</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="IdeTerminal" id="terminal">
+            <property name="audible-bell">false</property>
+            <property name="expand">true</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkScrollbar" id="scrollbar">
+            <property name="orientation">vertical</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="border-width">2</property>
+            <property name="hexpand">false</property>
+            <property name="orientation">vertical</property>
+            <property name="spacing">2</property>
+            <property name="vexpand">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkButton" id="clear_button">
+                <property name="action-name">build-log.clear</property>
+                <property name="expand">false</property>
+                <property name="tooltip-text" translatable="yes">Clear build log</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="flat"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">edit-clear-all-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="stop_button">
+                <property name="action-name">build-manager.cancel</property>
+                <property name="expand">false</property>
+                <property name="tooltip-text" translatable="yes">Cancel build</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="flat"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">process-stop-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="save_button">
+                <property name="action-name">build-log.save</property>
+                <property name="expand">false</property>
+                <property name="tooltip-text" translatable="yes">Save build log</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="flat"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="icon-name">document-save-symbolic</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/buildui/gbp-buildui-omni-bar-section.c 
b/src/plugins/buildui/gbp-buildui-omni-bar-section.c
new file mode 100644
index 000000000..103816dd8
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-omni-bar-section.c
@@ -0,0 +1,367 @@
+/* gbp-buildui-omni-bar-section.c
+ *
+ * Copyright 2018-2019 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-buildui-omni-bar-section"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+#include <libide-vcs.h>
+
+#include "gbp-buildui-omni-bar-section.h"
+
+struct _GbpBuilduiOmniBarSection
+{
+  GtkBin          parent_instance;
+
+  DzlSignalGroup *build_manager_signals;
+
+  GtkButton      *configure_button;
+  GtkLabel       *config_ready_label;
+  GtkLabel       *popover_branch_label;
+  GtkLabel       *popover_build_message;
+  GtkLabel       *popover_build_result_label;
+  GtkLabel       *popover_config_label;
+  GtkLabel       *popover_errors_label;
+  GtkLabel       *popover_last_build_time_label;
+  GtkLabel       *popover_project_label;
+  GtkLabel       *popover_runtime_label;
+  GtkLabel       *popover_warnings_label;
+
+  GtkRevealer    *popover_details_revealer;
+};
+
+G_DEFINE_TYPE (GbpBuilduiOmniBarSection, gbp_buildui_omni_bar_section, GTK_TYPE_BIN)
+
+static void
+gbp_buildui_omni_bar_section_notify_can_build (GbpBuilduiOmniBarSection *self,
+                                               GParamSpec               *pspec,
+                                               IdeBuildManager          *build_manager)
+{
+  gboolean can_build;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  can_build = ide_build_manager_get_can_build (build_manager);
+
+  gtk_widget_set_visible (GTK_WIDGET (self->config_ready_label), !can_build);
+}
+
+static void
+gbp_buildui_omni_bar_section_notify_pipeline (GbpBuilduiOmniBarSection *self,
+                                              GParamSpec               *pspec,
+                                              IdeBuildManager          *build_manager)
+{
+  IdeBuildPipeline *pipeline;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  if ((pipeline = ide_build_manager_get_pipeline (build_manager)))
+    {
+      IdeConfiguration *config = ide_build_pipeline_get_configuration (pipeline);
+      const gchar *config_id = ide_configuration_get_id (config);
+      const gchar *display_name = ide_configuration_get_display_name (config);
+      IdeRuntime *runtime = ide_configuration_get_runtime (config);
+      const gchar *name = NULL;
+
+      gtk_label_set_label (self->popover_config_label, display_name);
+
+      if (runtime != NULL)
+        name = ide_runtime_get_display_name (runtime);
+
+      if (name == NULL)
+        name = ide_runtime_get_id (runtime);
+
+      gtk_label_set_label (self->popover_runtime_label, name);
+
+      gtk_actionable_set_action_target (GTK_ACTIONABLE (self->configure_button), "s", config_id);
+    }
+}
+
+static void
+gbp_buildui_omni_bar_section_notify_error_count (GbpBuilduiOmniBarSection *self,
+                                                 GParamSpec               *pspec,
+                                                 IdeBuildManager          *build_manager)
+{
+  gchar str[12];
+  guint count;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  count = ide_build_manager_get_error_count (build_manager);
+  g_snprintf (str, sizeof str, "%u", count);
+  gtk_label_set_label (self->popover_errors_label, str);
+}
+
+static void
+gbp_buildui_omni_bar_section_notify_warning_count (GbpBuilduiOmniBarSection *self,
+                                                   GParamSpec               *pspec,
+                                                   IdeBuildManager          *build_manager)
+{
+  gchar str[12];
+  guint count;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  count = ide_build_manager_get_warning_count (build_manager);
+  g_snprintf (str, sizeof str, "%u", count);
+  gtk_label_set_label (self->popover_warnings_label, str);
+}
+
+static void
+gbp_buildui_omni_bar_section_notify_last_build_time (GbpBuilduiOmniBarSection *self,
+                                                     GParamSpec               *pspec,
+                                                     IdeBuildManager          *build_manager)
+{
+  g_autofree gchar *formatted = NULL;
+  GDateTime *last_build_time;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  if ((last_build_time = ide_build_manager_get_last_build_time (build_manager)))
+    formatted = g_date_time_format (last_build_time, "%X");
+
+  gtk_label_set_label (self->popover_last_build_time_label, formatted);
+}
+
+static void
+gbp_buildui_omni_bar_section_notify_message (GbpBuilduiOmniBarSection *self,
+                                             GParamSpec               *pspec,
+                                             IdeBuildManager          *build_manager)
+{
+  g_autofree gchar *message = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  message = ide_build_manager_get_message (build_manager);
+
+  gtk_label_set_label (self->popover_build_message, message);
+}
+
+static void
+gbp_buildui_omni_bar_section_build_started (GbpBuilduiOmniBarSection *self,
+                                            IdeBuildPipeline         *pipeline,
+                                            IdeBuildManager          *build_manager)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  gtk_revealer_set_reveal_child (self->popover_details_revealer, TRUE);
+
+  gtk_label_set_label (self->popover_build_result_label, _("Building…"));
+  dzl_gtk_widget_remove_style_class (GTK_WIDGET (self->popover_build_result_label), "error");
+
+  IDE_EXIT;
+}
+
+static void
+gbp_buildui_omni_bar_section_build_failed (GbpBuilduiOmniBarSection *self,
+                                           IdeBuildPipeline         *pipeline,
+                                           IdeBuildManager          *build_manager)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  gtk_label_set_label (self->popover_build_result_label, _("Failed"));
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->popover_build_result_label), "error");
+
+  IDE_EXIT;
+}
+
+static void
+gbp_buildui_omni_bar_section_build_finished (GbpBuilduiOmniBarSection *self,
+                                             IdeBuildPipeline         *pipeline,
+                                             IdeBuildManager          *build_manager)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  gtk_label_set_label (self->popover_build_result_label, _("Success"));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_buildui_omni_bar_section_bind_build_manager (GbpBuilduiOmniBarSection *self,
+                                                 IdeBuildManager          *build_manager,
+                                                 DzlSignalGroup           *signals)
+{
+  IdeContext *context;
+  IdeVcs *vcs;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+  g_assert (DZL_IS_SIGNAL_GROUP (signals));
+
+  gbp_buildui_omni_bar_section_notify_can_build (self, NULL, build_manager);
+  gbp_buildui_omni_bar_section_notify_pipeline (self, NULL, build_manager);
+  gbp_buildui_omni_bar_section_notify_message (self, NULL, build_manager);
+  gbp_buildui_omni_bar_section_notify_error_count (self, NULL, build_manager);
+  gbp_buildui_omni_bar_section_notify_warning_count (self, NULL, build_manager);
+  gbp_buildui_omni_bar_section_notify_last_build_time (self, NULL, build_manager);
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  vcs = ide_vcs_from_context (context);
+
+  g_object_bind_property (context, "title",
+                          self->popover_project_label, "label",
+                          G_BINDING_SYNC_CREATE);
+
+  g_object_bind_property (vcs, "branch-name",
+                          self->popover_branch_label, "label",
+                          G_BINDING_SYNC_CREATE);
+}
+
+static void
+gbp_buildui_omni_bar_section_destroy (GtkWidget *widget)
+{
+  GbpBuilduiOmniBarSection *self = (GbpBuilduiOmniBarSection *)widget;
+
+  g_assert (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+
+  if (self->build_manager_signals)
+    {
+      dzl_signal_group_set_target (self->build_manager_signals, NULL);
+      g_clear_object (&self->build_manager_signals);
+    }
+
+  GTK_WIDGET_CLASS (gbp_buildui_omni_bar_section_parent_class)->destroy (widget);
+}
+
+static void
+gbp_buildui_omni_bar_section_class_init (GbpBuilduiOmniBarSectionClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->destroy = gbp_buildui_omni_bar_section_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/buildui/gbp-buildui-omni-bar-section.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, config_ready_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, configure_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, popover_branch_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, popover_build_message);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, popover_build_result_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, popover_config_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, popover_details_revealer);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, popover_errors_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, 
popover_last_build_time_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, popover_project_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, popover_runtime_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiOmniBarSection, popover_warnings_label);
+}
+
+static void
+gbp_buildui_omni_bar_section_init (GbpBuilduiOmniBarSection *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->build_manager_signals = dzl_signal_group_new (IDE_TYPE_BUILD_MANAGER);
+  g_signal_connect_object (self->build_manager_signals,
+                           "bind",
+                           G_CALLBACK (gbp_buildui_omni_bar_section_bind_build_manager),
+                           self,
+                           G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "notify::can-build",
+                                   G_CALLBACK (gbp_buildui_omni_bar_section_notify_can_build),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "notify::message",
+                                   G_CALLBACK (gbp_buildui_omni_bar_section_notify_message),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "notify::pipeline",
+                                   G_CALLBACK (gbp_buildui_omni_bar_section_notify_pipeline),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "notify::error-count",
+                                   G_CALLBACK (gbp_buildui_omni_bar_section_notify_error_count),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "notify::warning-count",
+                                   G_CALLBACK (gbp_buildui_omni_bar_section_notify_warning_count),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "notify::last-build-time",
+                                   G_CALLBACK (gbp_buildui_omni_bar_section_notify_last_build_time),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "build-started",
+                                   G_CALLBACK (gbp_buildui_omni_bar_section_build_started),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "build-failed",
+                                   G_CALLBACK (gbp_buildui_omni_bar_section_build_failed),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "build-finished",
+                                   G_CALLBACK (gbp_buildui_omni_bar_section_build_finished),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+}
+
+void
+gbp_buildui_omni_bar_section_set_context (GbpBuilduiOmniBarSection *self,
+                                          IdeContext               *context)
+{
+  IdeBuildManager *build_manager;
+
+  g_return_if_fail (GBP_IS_BUILDUI_OMNI_BAR_SECTION (self));
+  g_return_if_fail (IDE_IS_CONTEXT (context));
+
+  build_manager = ide_build_manager_from_context (context);
+  dzl_signal_group_set_target (self->build_manager_signals, build_manager);
+}
diff --git a/src/plugins/buildui/gbp-buildui-omni-bar-section.h 
b/src/plugins/buildui/gbp-buildui-omni-bar-section.h
new file mode 100644
index 000000000..a064b44f3
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-omni-bar-section.h
@@ -0,0 +1,35 @@
+/* gbp-buildui-omni-bar-section.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_BUILDUI_OMNI_BAR_SECTION (gbp_buildui_omni_bar_section_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuilduiOmniBarSection, gbp_buildui_omni_bar_section, GBP, BUILDUI_OMNI_BAR_SECTION, 
GtkBin)
+
+void gbp_buildui_omni_bar_section_set_context (GbpBuilduiOmniBarSection *self,
+                                               IdeContext               *context);
+
+G_END_DECLS
diff --git a/src/plugins/buildui/gbp-buildui-omni-bar-section.ui 
b/src/plugins/buildui/gbp-buildui-omni-bar-section.ui
new file mode 100644
index 000000000..c45c23241
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-omni-bar-section.ui
@@ -0,0 +1,530 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <template class="GbpBuilduiOmniBarSection" parent="GtkBin">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_start">24</property>
+            <property name="margin_end">24</property>
+            <property name="margin_top">24</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <property name="spacing">12</property>
+                <child>
+                  <object class="GtkLabel" id="popover_project_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="valign">baseline</property>
+                    <property name="hexpand">True</property>
+                    <property name="xalign">0</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="focus_on_click">False</property>
+                    <property name="receives_default">False</property>
+                    <property name="tooltip_text" translatable="yes">Update project dependencies</property>
+                    <property name="valign">baseline</property>
+                    <property name="action_name">win.update-dependencies</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="icon_name">software-update-available-symbolic</property>
+                      </object>
+                    </child>
+                    <style>
+                      <class name="image-button"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkButton" id="configure_button">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="focus_on_click">False</property>
+                    <property name="receives_default">False</property>
+                    <property name="tooltip_text" translatable="yes">Configure build preferences</property>
+                    <property name="valign">baseline</property>
+                    <property name="action_name">win.edit-config</property>
+                    <property name="action_target">''</property>
+                    <child>
+                      <object class="GtkImage">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="icon_name">builder-build-configure-symbolic</property>
+                      </object>
+                    </child>
+                    <style>
+                      <class name="image-button"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkGrid">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="row_spacing">6</property>
+                <property name="column_spacing">18</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">Branch</property>
+                    <property name="xalign">1</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="popover_branch_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="hexpand">True</property>
+                    <property name="xalign">0</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="build_profile_title">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">Build Profile</property>
+                    <property name="xalign">1</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="popover_config_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="hexpand">True</property>
+                    <property name="xalign">0</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="runtime_title">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="label" translatable="yes">Runtime</property>
+                    <property name="xalign">1</property>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                  <packing>
+                    <property name="left_attach">0</property>
+                    <property name="top_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="popover_runtime_label">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="hexpand">True</property>
+                    <property name="use_markup">True</property>
+                    <property name="xalign">0</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="top_attach">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="config_ready_label">
+            <property name="can_focus">False</property>
+            <property name="margin_top">12</property>
+            <property name="label" translatable="yes">There is a problem with the current build 
configuration.</property>
+            <property name="xalign">0.5</property>
+            <attributes>
+              <attribute name="weight" value="bold"/>
+            </attributes>
+            <style>
+              <class name="warning"/>
+            </style>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkRevealer" id="popover_details_revealer">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <child>
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_start">24</property>
+                <property name="margin_end">24</property>
+                <property name="margin_top">24</property>
+                <property name="orientation">vertical</property>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="hexpand">True</property>
+                    <property name="spacing">12</property>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="valign">baseline</property>
+                        <property name="label" translatable="yes">Build status</property>
+                        <property name="xalign">0</property>
+                        <attributes>
+                          <attribute name="weight" value="bold"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="popover_build_message">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="valign">baseline</property>
+                        <property name="hexpand">True</property>
+                        <property name="ellipsize">end</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkButton">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="focus_on_click">False</property>
+                        <property name="receives_default">False</property>
+                        <property name="tooltip_text" translatable="yes">View build console 
contents</property>
+                        <property name="halign">end</property>
+                        <property name="valign">baseline</property>
+                        <property name="action_name">win.view-output</property>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="visible">True</property>
+                            <property name="can_focus">False</property>
+                            <property name="icon_name">utilities-terminal-symbolic</property>
+                          </object>
+                        </child>
+                        <style>
+                          <class name="image-button"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">True</property>
+                        <property name="position">2</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkGrid" id="build_status_grid">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="row_spacing">6</property>
+                    <property name="column_spacing">18</property>
+                    <child>
+                      <object class="GtkLabel" id="last_build_title">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="hexpand">False</property>
+                        <property name="label" translatable="yes">Last build</property>
+                        <property name="xalign">1</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="popover_last_build_time_label">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="hexpand">True</property>
+                        <property name="width_chars">10</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="build_result_title">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="hexpand">False</property>
+                        <property name="label" translatable="yes">Build result</property>
+                        <property name="xalign">1</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="left_attach">0</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="popover_build_result_label">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="hexpand">True</property>
+                        <property name="width_chars">10</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">1</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="error_title">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="hexpand">False</property>
+                        <property name="label" translatable="yes">Errors</property>
+                        <property name="xalign">1</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="left_attach">2</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="popover_errors_label">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="hexpand">True</property>
+                        <property name="label">0</property>
+                        <property name="width_chars">10</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">3</property>
+                        <property name="top_attach">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="warning_title">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="hexpand">False</property>
+                        <property name="label" translatable="yes">Warnings</property>
+                        <property name="xalign">1</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="left_attach">2</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="popover_warnings_label">
+                        <property name="visible">True</property>
+                        <property name="can_focus">False</property>
+                        <property name="hexpand">True</property>
+                        <property name="label">0</property>
+                        <property name="width_chars">10</property>
+                        <property name="xalign">0</property>
+                      </object>
+                      <packing>
+                        <property name="left_attach">3</property>
+                        <property name="top_attach">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">True</property>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">2</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_start">24</property>
+            <property name="margin_end">24</property>
+            <property name="margin_top">24</property>
+            <property name="spacing">6</property>
+            <!-- translators: valid values are 'true' or 'false', untranslated. If the buttons in the build 
popover are too large because of translations, set to false to disable homogeneous sizing -->
+            <property name="homogeneous">true</property>
+            <child>
+              <object class="GtkButton">
+                <property name="label" translatable="yes">Build</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="action_name">build-manager.build</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="label" translatable="yes">Rebuild</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="action_name">build-manager.rebuild</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="label" translatable="yes">Clean</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="action_name">build-manager.clean</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton">
+                <property name="label" translatable="yes">Export Bundle</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="action_name">build-manager.export</property>
+                <style>
+                  <class name="suggested-action"/>
+                </style>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">3</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">3</property>
+          </packing>
+        </child>
+        <style>
+          <class name="popover-content-area"/>
+        </style>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/buildui/gbp-buildui-pane.c b/src/plugins/buildui/gbp-buildui-pane.c
new file mode 100644
index 000000000..f2d43a880
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-pane.c
@@ -0,0 +1,702 @@
+/* gbp-buildui-pane.c
+ *
+ * Copyright 2015-2019 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-buildui-pane"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-editor.h>
+#include <libide-foundry.h>
+
+#include "ide-build-stage-private.h"
+
+#include "gbp-buildui-pane.h"
+#include "gbp-buildui-stage-row.h"
+
+struct _GbpBuilduiPane
+{
+  IdePane              parent_instance;
+
+  /* Owned references */
+  GHashTable          *diags_hash;
+  IdeBuildPipeline    *pipeline;
+  DzlSignalGroup      *pipeline_signals;
+
+  /* Template widgets */
+  GtkLabel            *build_status_label;
+  GtkLabel            *time_completed_label;
+  GtkNotebook         *notebook;
+  GtkScrolledWindow   *errors_page;
+  IdeFancyTreeView    *errors_tree_view;
+  GtkScrolledWindow   *warnings_page;
+  IdeFancyTreeView    *warnings_tree_view;
+  GtkListStore        *diagnostics_store;
+  GtkListBox          *stages_list_box;
+
+  guint                error_count;
+  guint                warning_count;
+};
+
+G_DEFINE_TYPE (GbpBuilduiPane, gbp_buildui_pane, IDE_TYPE_PANE)
+
+enum {
+  COLUMN_DIAGNOSTIC,
+  LAST_COLUMN
+};
+
+enum {
+  PROP_0,
+  PROP_PIPELINE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+set_warnings_label (GbpBuilduiPane *self,
+                    const gchar   *label)
+{
+  gtk_container_child_set (GTK_CONTAINER (self->notebook), GTK_WIDGET (self->warnings_page),
+                           "tab-label", label,
+                           NULL);
+}
+
+static void
+set_errors_label (GbpBuilduiPane *self,
+                  const gchar   *label)
+{
+  gtk_container_child_set (GTK_CONTAINER (self->notebook), GTK_WIDGET (self->errors_page),
+                           "tab-label", label,
+                           NULL);
+}
+
+static void
+gbp_buildui_pane_diagnostic (GbpBuilduiPane   *self,
+                             IdeDiagnostic    *diagnostic,
+                             IdeBuildPipeline *pipeline)
+{
+  IdeDiagnosticSeverity severity;
+  guint hash;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_BUILDUI_PANE (self));
+  g_assert (diagnostic != NULL);
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  severity = ide_diagnostic_get_severity (diagnostic);
+
+  if (severity == IDE_DIAGNOSTIC_WARNING)
+    {
+      g_autofree gchar *label = NULL;
+
+      self->warning_count++;
+
+      label = g_strdup_printf ("%s (%u)", _("Warnings"), self->warning_count);
+      set_warnings_label (self, label);
+    }
+  else if (severity == IDE_DIAGNOSTIC_ERROR || severity == IDE_DIAGNOSTIC_FATAL)
+    {
+      g_autofree gchar *label = NULL;
+
+      self->error_count++;
+
+      label = g_strdup_printf ("%s (%u)", _("Errors"), self->error_count);
+      set_errors_label (self, label);
+    }
+  else
+    {
+      /* TODO: Figure out design for "Others" Column like Notes? */
+    }
+
+  hash = ide_diagnostic_hash (diagnostic);
+
+  if (g_hash_table_insert (self->diags_hash, GUINT_TO_POINTER (hash), NULL))
+    {
+      GtkTreeIter iter;
+
+      dzl_gtk_list_store_insert_sorted (self->diagnostics_store,
+                                        &iter,
+                                        diagnostic,
+                                        COLUMN_DIAGNOSTIC,
+                                        (GCompareDataFunc)ide_diagnostic_compare,
+                                        NULL);
+      gtk_list_store_set (self->diagnostics_store, &iter,
+                          COLUMN_DIAGNOSTIC, diagnostic,
+                          -1);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+gbp_buildui_pane_update_running_time (GbpBuilduiPane *self)
+{
+  g_autofree gchar *text = NULL;
+
+  g_assert (GBP_IS_BUILDUI_PANE (self));
+
+  if (self->pipeline != NULL)
+    {
+      IdeBuildManager *build_manager;
+      IdeContext *context;
+      GTimeSpan span;
+
+      context = ide_widget_get_context (GTK_WIDGET (self));
+      build_manager = ide_build_manager_from_context (context);
+
+      span = ide_build_manager_get_running_time (build_manager);
+      text = dzl_g_time_span_to_label (span);
+      gtk_label_set_label (self->time_completed_label, text);
+    }
+  else
+    gtk_label_set_label (self->time_completed_label, "—");
+}
+
+static void
+gbp_buildui_pane_started (GbpBuilduiPane   *self,
+                          IdeBuildPhase     phase,
+                          IdeBuildPipeline *pipeline)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_BUILDUI_PANE (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (phase >= IDE_BUILD_PHASE_BUILD)
+    {
+      self->error_count = 0;
+      self->warning_count = 0;
+
+      set_warnings_label (self, _("Warnings"));
+      set_errors_label (self, _("Errors"));
+
+      gtk_list_store_clear (self->diagnostics_store);
+      g_hash_table_remove_all (self->diags_hash);
+    }
+
+  IDE_EXIT;
+}
+
+static GtkWidget *
+gbp_buildui_pane_create_stage_row_cb (gpointer data,
+                                     gpointer user_data)
+{
+  IdeBuildStage *stage = data;
+
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+  g_assert (GBP_IS_BUILDUI_PANE (user_data));
+
+  return gbp_buildui_stage_row_new (stage);
+}
+
+static void
+gbp_buildui_pane_bind_pipeline (GbpBuilduiPane   *self,
+                                IdeBuildPipeline *pipeline,
+                                DzlSignalGroup   *signals)
+{
+  g_assert (GBP_IS_BUILDUI_PANE (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (G_IS_LIST_MODEL (pipeline));
+  g_assert (self->pipeline == NULL);
+  g_assert (DZL_IS_SIGNAL_GROUP (signals));
+
+  self->pipeline = g_object_ref (pipeline);
+  self->error_count = 0;
+  self->warning_count = 0;
+
+  set_warnings_label (self, _("Warnings"));
+  set_errors_label (self, _("Errors"));
+
+  gtk_label_set_label (self->time_completed_label, "—");
+  gtk_label_set_label (self->build_status_label, "—");
+
+  gtk_list_box_bind_model (self->stages_list_box,
+                           G_LIST_MODEL (pipeline),
+                           gbp_buildui_pane_create_stage_row_cb,
+                           self, NULL);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PIPELINE]);
+}
+
+static void
+gbp_buildui_pane_unbind_pipeline (GbpBuilduiPane *self,
+                                  DzlSignalGroup *signals)
+{
+  g_return_if_fail (GBP_IS_BUILDUI_PANE (self));
+  g_return_if_fail (!self->pipeline || IDE_IS_BUILD_PIPELINE (self->pipeline));
+
+  g_clear_object (&self->pipeline);
+
+  if (!gtk_widget_in_destruction (GTK_WIDGET (self)))
+    {
+      g_hash_table_remove_all (self->diags_hash);
+      gtk_list_store_clear (self->diagnostics_store);
+      gtk_container_foreach (GTK_CONTAINER (self->stages_list_box),
+                             (GtkCallback) gtk_widget_destroy,
+                             NULL);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PIPELINE]);
+    }
+}
+
+void
+gbp_buildui_pane_set_pipeline (GbpBuilduiPane   *self,
+                               IdeBuildPipeline *pipeline)
+{
+  g_return_if_fail (GBP_IS_BUILDUI_PANE (self));
+  g_return_if_fail (!pipeline || IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (self->pipeline_signals != NULL)
+    dzl_signal_group_set_target (self->pipeline_signals, pipeline);
+}
+
+static void
+gbp_buildui_pane_diagnostic_activated (GbpBuilduiPane    *self,
+                                       GtkTreePath       *path,
+                                       GtkTreeViewColumn *colun,
+                                       GtkTreeView       *tree_view)
+{
+  g_autoptr(IdeDiagnostic) diagnostic = NULL;
+  IdeWorkspace *workspace;
+  IdeLocation *loc;
+  GtkTreeModel *model;
+  IdeSurface *surface;
+  GtkTreeIter iter;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_BUILDUI_PANE (self));
+  g_assert (path != NULL);
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (colun));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  model = gtk_tree_view_get_model (tree_view);
+  if (!gtk_tree_model_get_iter (model, &iter, path))
+    IDE_EXIT;
+
+  gtk_tree_model_get (model, &iter,
+                      COLUMN_DIAGNOSTIC, &diagnostic,
+                      -1);
+
+  if (diagnostic == NULL || !(loc = ide_diagnostic_get_location (diagnostic)))
+    IDE_EXIT;
+
+  workspace = ide_widget_get_workspace (GTK_WIDGET (self));
+  surface = ide_workspace_get_surface_by_name (workspace, "editor");
+  ide_editor_surface_focus_location (IDE_EDITOR_SURFACE (surface), loc);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_buildui_pane_text_func (GtkCellLayout   *layout,
+                            GtkCellRenderer *renderer,
+                            GtkTreeModel    *model,
+                            GtkTreeIter     *iter,
+                            gpointer         user_data)
+{
+  IdeCellRendererFancy *fancy = (IdeCellRendererFancy *)renderer;
+  g_autoptr(IdeDiagnostic) diagnostic = NULL;
+
+  gtk_tree_model_get (model, iter,
+                      COLUMN_DIAGNOSTIC, &diagnostic,
+                      -1);
+
+  if G_LIKELY (diagnostic != NULL)
+    {
+      g_autofree gchar *title = NULL;
+      g_autofree gchar *name = NULL;
+      IdeLocation *location;
+      const gchar *text;
+      GFile *file = NULL;
+      guint line = 0;
+      guint column = 0;
+
+      location = ide_diagnostic_get_location (diagnostic);
+
+      if (location != NULL)
+        {
+          if ((file = ide_location_get_file (location)))
+            {
+              name = g_file_get_basename (file);
+              line = ide_location_get_line (location);
+              column = ide_location_get_line_offset (location);
+            }
+        }
+
+      title = g_strdup_printf ("%s:%u:%u", name ?: "", line + 1, column + 1);
+      ide_cell_renderer_fancy_take_title (fancy, g_steal_pointer (&title));
+
+      text = ide_diagnostic_get_text (diagnostic);
+      ide_cell_renderer_fancy_set_body (fancy, text);
+    }
+  else
+    {
+      ide_cell_renderer_fancy_set_title (fancy, NULL);
+      ide_cell_renderer_fancy_set_body (fancy, NULL);
+    }
+}
+
+static void
+gbp_buildui_pane_notify_message (GbpBuilduiPane  *self,
+                                 GParamSpec      *pspec,
+                                 IdeBuildManager *build_manager)
+{
+  g_autofree gchar *message = NULL;
+  IdeBuildPipeline *pipeline;
+  GtkStyleContext *style;
+
+  g_assert (GBP_IS_BUILDUI_PANE (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  message = ide_build_manager_get_message (build_manager);
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+
+  gtk_label_set_label (self->build_status_label, message);
+
+  style = gtk_widget_get_style_context (GTK_WIDGET (self->build_status_label));
+
+  if (ide_build_pipeline_get_phase (pipeline) == IDE_BUILD_PHASE_FAILED)
+    gtk_style_context_add_class (style, GTK_STYLE_CLASS_ERROR);
+  else
+    gtk_style_context_remove_class (style, GTK_STYLE_CLASS_ERROR);
+}
+
+static void
+gbp_buildui_pane_context_handler (GtkWidget  *widget,
+                                  IdeContext *context)
+{
+  GbpBuilduiPane *self = (GbpBuilduiPane *)widget;
+  IdeBuildManager *build_manager;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_BUILDUI_PANE (self));
+  g_assert (!context || IDE_IS_CONTEXT (context));
+
+  if (context == NULL)
+    IDE_EXIT;
+
+  build_manager = ide_build_manager_from_context (context);
+
+  g_signal_connect_object (build_manager,
+                           "notify::message",
+                           G_CALLBACK (gbp_buildui_pane_notify_message),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (build_manager,
+                           "notify::running-time",
+                           G_CALLBACK (gbp_buildui_pane_update_running_time),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (build_manager,
+                           "build-started",
+                           G_CALLBACK (gbp_buildui_pane_update_running_time),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (build_manager,
+                           "build-finished",
+                           G_CALLBACK (gbp_buildui_pane_update_running_time),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (build_manager,
+                           "build-failed",
+                           G_CALLBACK (gbp_buildui_pane_update_running_time),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  IDE_EXIT;
+}
+
+static gboolean
+gbp_buildui_pane_diagnostic_tooltip (GbpBuilduiPane *self,
+                                     gint            x,
+                                     gint            y,
+                                     gboolean        keyboard_mode,
+                                     GtkTooltip     *tooltip,
+                                     GtkTreeView    *tree_view)
+{
+  GtkTreeModel *model = NULL;
+  GtkTreeIter iter;
+
+  g_assert (GBP_IS_BUILDUI_PANE (self));
+  g_assert (GTK_IS_TOOLTIP (tooltip));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  if (gtk_tree_view_get_tooltip_context (tree_view, &x, &y, keyboard_mode, &model, NULL, &iter))
+    {
+      g_autoptr(IdeDiagnostic) diag = NULL;
+
+      gtk_tree_model_get (model, &iter,
+                          COLUMN_DIAGNOSTIC, &diag,
+                          -1);
+
+      if (diag != NULL)
+        {
+          g_autofree gchar *text = ide_diagnostic_get_text_for_display (diag);
+
+          gtk_tooltip_set_text (tooltip, text);
+
+          return TRUE;
+        }
+    }
+
+  return FALSE;
+}
+
+static gboolean
+diagnostic_is_warning (GtkTreeModel *model,
+                       GtkTreeIter  *iter,
+                       gpointer      user_data)
+{
+  g_autoptr(IdeDiagnostic) diag = NULL;
+  IdeDiagnosticSeverity severity = 0;
+
+  gtk_tree_model_get (model, iter,
+                      COLUMN_DIAGNOSTIC, &diag,
+                      -1);
+
+  if (diag != NULL)
+    severity = ide_diagnostic_get_severity (diag);
+
+  return severity <= IDE_DIAGNOSTIC_WARNING;
+}
+
+static gboolean
+diagnostic_is_error (GtkTreeModel *model,
+                     GtkTreeIter  *iter,
+                     gpointer      user_data)
+{
+  g_autoptr(IdeDiagnostic) diag = NULL;
+  IdeDiagnosticSeverity severity = 0;
+
+  gtk_tree_model_get (model, iter,
+                      COLUMN_DIAGNOSTIC, &diag,
+                      -1);
+
+  if (diag != NULL)
+    severity = ide_diagnostic_get_severity (diag);
+
+  return severity > IDE_DIAGNOSTIC_WARNING;
+}
+
+static void
+gbp_buildui_pane_stage_row_activated (GbpBuilduiPane     *self,
+                                      GbpBuilduiStageRow *row,
+                                      GtkListBox         *list_box)
+{
+  IdeBuildStage *stage;
+  IdeBuildPhase phase;
+
+  g_assert (GBP_IS_BUILDUI_PANE (self));
+  g_assert (GBP_IS_BUILDUI_STAGE_ROW (row));
+  g_assert (GTK_IS_LIST_BOX (list_box));
+
+  if (self->pipeline == NULL)
+    return;
+
+  stage = gbp_buildui_stage_row_get_stage (row);
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+
+  phase = _ide_build_stage_get_phase (stage);
+
+  ide_build_pipeline_build_async (self->pipeline,
+                                  phase & IDE_BUILD_PHASE_MASK,
+                                  NULL, NULL, NULL);
+}
+
+static void
+gbp_buildui_pane_destroy (GtkWidget *widget)
+{
+  GbpBuilduiPane *self = (GbpBuilduiPane *)widget;
+
+  if (self->pipeline_signals != NULL)
+    dzl_signal_group_set_target (self->pipeline_signals, NULL);
+
+  g_clear_pointer (&self->diags_hash, g_hash_table_unref);
+
+  g_clear_object (&self->pipeline_signals);
+  g_clear_object (&self->pipeline);
+
+  GTK_WIDGET_CLASS (gbp_buildui_pane_parent_class)->destroy (widget);
+}
+
+static void
+gbp_buildui_pane_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  GbpBuilduiPane *self = GBP_BUILDUI_PANE (object);
+
+  switch (prop_id)
+    {
+    case PROP_PIPELINE:
+      g_value_set_object (value, self->pipeline);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_buildui_pane_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  GbpBuilduiPane *self = GBP_BUILDUI_PANE (object);
+
+  switch (prop_id)
+    {
+    case PROP_PIPELINE:
+      gbp_buildui_pane_set_pipeline (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_buildui_pane_class_init (GbpBuilduiPaneClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  widget_class->destroy = gbp_buildui_pane_destroy;
+
+  object_class->get_property = gbp_buildui_pane_get_property;
+  object_class->set_property = gbp_buildui_pane_set_property;
+
+  properties [PROP_PIPELINE] =
+    g_param_spec_object ("pipeline",
+                         NULL,
+                         NULL,
+                         IDE_TYPE_BUILD_PIPELINE,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/buildui/gbp-buildui-pane.ui");
+  gtk_widget_class_set_css_name (widget_class, "buildpanel");
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiPane, build_status_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiPane, time_completed_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiPane, notebook);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiPane, errors_page);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiPane, errors_tree_view);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiPane, warnings_page);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiPane, warnings_tree_view);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiPane, diagnostics_store);
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiPane, stages_list_box);
+
+  g_type_ensure (IDE_TYPE_CELL_RENDERER_FANCY);
+  g_type_ensure (IDE_TYPE_DIAGNOSTIC);
+  g_type_ensure (IDE_TYPE_FANCY_TREE_VIEW);
+}
+
+static void
+gbp_buildui_pane_init (GbpBuilduiPane *self)
+{
+  GtkTreeModel *filter;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->pipeline_signals = dzl_signal_group_new (IDE_TYPE_BUILD_PIPELINE);
+
+  g_signal_connect_object (self->pipeline_signals,
+                           "bind",
+                           G_CALLBACK (gbp_buildui_pane_bind_pipeline),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->pipeline_signals,
+                           "unbind",
+                           G_CALLBACK (gbp_buildui_pane_unbind_pipeline),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->pipeline_signals,
+                                   "diagnostic",
+                                   G_CALLBACK (gbp_buildui_pane_diagnostic),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->pipeline_signals,
+                                   "started",
+                                   G_CALLBACK (gbp_buildui_pane_started),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  self->diags_hash = g_hash_table_new (NULL, NULL);
+
+  g_object_set (self, "title", _("Build Issues"), NULL);
+
+  ide_widget_set_context_handler (self, gbp_buildui_pane_context_handler);
+
+  g_signal_connect_swapped (self->warnings_tree_view,
+                           "row-activated",
+                           G_CALLBACK (gbp_buildui_pane_diagnostic_activated),
+                           self);
+
+  g_signal_connect_swapped (self->warnings_tree_view,
+                           "query-tooltip",
+                           G_CALLBACK (gbp_buildui_pane_diagnostic_tooltip),
+                           self);
+
+  g_signal_connect_swapped (self->errors_tree_view,
+                           "row-activated",
+                           G_CALLBACK (gbp_buildui_pane_diagnostic_activated),
+                           self);
+
+  g_signal_connect_swapped (self->errors_tree_view,
+                           "query-tooltip",
+                           G_CALLBACK (gbp_buildui_pane_diagnostic_tooltip),
+                           self);
+
+  filter = gtk_tree_model_filter_new (GTK_TREE_MODEL (self->diagnostics_store), NULL);
+  gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (filter),
+                                          diagnostic_is_warning, NULL, NULL);
+  gtk_tree_view_set_model (GTK_TREE_VIEW (self->warnings_tree_view), GTK_TREE_MODEL (filter));
+  g_object_unref (filter);
+
+  filter = gtk_tree_model_filter_new (GTK_TREE_MODEL (self->diagnostics_store), NULL);
+  gtk_tree_model_filter_set_visible_func (GTK_TREE_MODEL_FILTER (filter),
+                                          diagnostic_is_error, NULL, NULL);
+  gtk_tree_view_set_model (GTK_TREE_VIEW (self->errors_tree_view), GTK_TREE_MODEL (filter));
+  g_object_unref (filter);
+
+  ide_fancy_tree_view_set_data_func (IDE_FANCY_TREE_VIEW (self->warnings_tree_view),
+                                     gbp_buildui_pane_text_func, self, NULL);
+
+  ide_fancy_tree_view_set_data_func (IDE_FANCY_TREE_VIEW (self->errors_tree_view),
+                                     gbp_buildui_pane_text_func, self, NULL);
+
+  g_signal_connect_swapped (self->stages_list_box,
+                            "row-activated",
+                            G_CALLBACK (gbp_buildui_pane_stage_row_activated),
+                            self);
+}
diff --git a/src/plugins/buildui/gbp-buildui-pane.h b/src/plugins/buildui/gbp-buildui-pane.h
new file mode 100644
index 000000000..f7c1efcc8
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-pane.h
@@ -0,0 +1,35 @@
+/* gbp-buildui-pane.h
+ *
+ * Copyright 2015-2019 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-gui.h>
+#include <libide-foundry.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_BUILDUI_PANE (gbp_buildui_pane_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuilduiPane, gbp_buildui_pane, GBP, BUILDUI_PANE, IdePane)
+
+void gbp_buildui_pane_set_pipeline (GbpBuilduiPane   *self,
+                                    IdeBuildPipeline *pipeline);
+
+G_END_DECLS
diff --git a/src/plugins/buildui/gbp-buildui-pane.ui b/src/plugins/buildui/gbp-buildui-pane.ui
new file mode 100644
index 000000000..8511ca473
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-pane.ui
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpBuilduiPane" parent="IdePane">
+    <child>
+      <object class="DzlMultiPaned">
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkGrid">
+            <property name="margin">6</property>
+            <property name="column-spacing">6</property>
+            <property name="row-spacing">6</property>
+            <property name="expand">false</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkLabel">
+                <property name="label" translatable="yes">Build status:</property>
+                <property name="visible">true</property>
+                <property name="xalign">1.0</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+                <attributes>
+                  <attribute name="scale" value="0.8333"/>
+                </attributes>
+              </object>
+              <packing>
+                <property name="top-attach">0</property>
+                <property name="left-attach">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel">
+                <property name="label" translatable="yes">Time completed:</property>
+                <property name="visible">true</property>
+                <property name="xalign">1.0</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+                <attributes>
+                  <attribute name="scale" value="0.8333"/>
+                </attributes>
+              </object>
+              <packing>
+                <property name="top-attach">1</property>
+                <property name="left-attach">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="build_status_label">
+                <property name="label" translatable="yes">—</property>
+                <property name="hexpand">true</property>
+                <property name="visible">true</property>
+                <property name="xalign">0.0</property>
+                <attributes>
+                  <attribute name="scale" value="0.8333"/>
+                </attributes>
+              </object>
+              <packing>
+                <property name="top-attach">0</property>
+                <property name="left-attach">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="time_completed_label">
+                <property name="label" translatable="yes">—</property>
+                <property name="hexpand">true</property>
+                <property name="visible">true</property>
+                <property name="xalign">0.0</property>
+                <attributes>
+                  <attribute name="scale" value="0.8333"/>
+                </attributes>
+              </object>
+              <packing>
+                <property name="top-attach">1</property>
+                <property name="left-attach">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkExpander">
+                <property name="visible">true</property>
+                <property name="label" translatable="yes">Build Details</property>
+                <child>
+                  <object class="GtkScrolledWindow">
+                    <property name="propagate-natural-height">true</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkListBox" id="stages_list_box">
+                        <property name="selection-mode">none</property>
+                        <property name="visible">true</property>
+                        <child type="placeholder">
+                          <object class="GtkLabel">
+                            <style>
+                              <class name="dim-label"/>
+                            </style>
+                            <property name="label" translatable="yes">Build pipeline is empty</property>
+                            <property name="xalign">0.5</property>
+                            <property name="ypad">12</property>
+                            <property name="visible">true</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="top-attach">2</property>
+                <property name="left-attach">0</property>
+                <property name="width">2</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkNotebook" id="notebook">
+            <property name="show-border">false</property>
+            <property name="vexpand">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkScrolledWindow" id="warnings_page">
+                <property name="visible">true</property>
+                <child>
+                  <object class="IdeFancyTreeView" id="warnings_tree_view">
+                    <property name="has-tooltip">true</property>
+                    <property name="visible">true</property>
+                    <style>
+                      <class name="i-wanna-be-listbox"/>
+                    </style>
+                    <child internal-child="selection">
+                      <object class="GtkTreeSelection">
+                        <property name="mode">none</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="position">0</property>
+                <property name="tab-label" translatable="yes">Warnings</property>
+                <property name="tab-expand">true</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="errors_page">
+                <property name="visible">true</property>
+                <child>
+                  <object class="IdeFancyTreeView" id="errors_tree_view">
+                    <property name="has-tooltip">true</property>
+                    <property name="visible">true</property>
+                    <child internal-child="selection">
+                      <object class="GtkTreeSelection">
+                        <property name="mode">none</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="position">1</property>
+                <property name="tab-label" translatable="yes">Errors</property>
+                <property name="tab-expand">true</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkListStore" id="diagnostics_store">
+    <columns>
+      <column type="IdeDiagnostic"/>
+    </columns>
+  </object>
+</interface>
diff --git a/src/plugins/buildui/gbp-buildui-runtime-categories.c 
b/src/plugins/buildui/gbp-buildui-runtime-categories.c
new file mode 100644
index 000000000..e4a693559
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-runtime-categories.c
@@ -0,0 +1,251 @@
+/* gbp-buildui-runtime-categories.c
+ *
+ * Copyright 2018-2019 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-buildui-runtime-categories"
+
+#include "config.h"
+
+#include <string.h>
+
+#include "gbp-buildui-runtime-categories.h"
+
+struct _GbpBuilduiRuntimeCategories
+{
+  GObject            parent_instance;
+  GPtrArray         *items;
+  gchar             *prefix;
+  gchar             *name;
+  IdeRuntimeManager *runtime_manager;
+};
+
+static gboolean
+filter_by_category (GObject *object,
+                    gpointer user_data)
+{
+  const gchar *category = user_data;
+  IdeRuntime *runtime = IDE_RUNTIME (object);
+
+  return ide_str_equal0 (category, ide_runtime_get_category (runtime));
+}
+
+static GType
+gbp_buildui_runtime_categories_get_item_type (GListModel *model)
+{
+  return G_TYPE_OBJECT;
+}
+
+static guint
+gbp_buildui_runtime_categories_get_n_items (GListModel *model)
+{
+  GbpBuilduiRuntimeCategories *self = (GbpBuilduiRuntimeCategories *)model;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_RUNTIME_CATEGORIES (self));
+
+  return GBP_BUILDUI_RUNTIME_CATEGORIES (model)->items->len;
+}
+
+static gpointer
+gbp_buildui_runtime_categories_get_item (GListModel *model,
+                                         guint       position)
+{
+  GbpBuilduiRuntimeCategories *self = GBP_BUILDUI_RUNTIME_CATEGORIES (model);
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_RUNTIME_CATEGORIES (self));
+
+  if (self->items->len > position)
+    {
+      const gchar *category = g_ptr_array_index (self->items, position);
+      return gbp_buildui_runtime_categories_create_child_model (self, category);
+    }
+
+  return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = gbp_buildui_runtime_categories_get_item_type;
+  iface->get_n_items = gbp_buildui_runtime_categories_get_n_items;
+  iface->get_item = gbp_buildui_runtime_categories_get_item;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpBuilduiRuntimeCategories, gbp_buildui_runtime_categories, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static void
+gbp_buildui_runtime_categories_finalize (GObject *object)
+{
+  GbpBuilduiRuntimeCategories *self = (GbpBuilduiRuntimeCategories *)object;
+
+  g_clear_object (&self->runtime_manager);
+  g_clear_pointer (&self->items, g_ptr_array_unref);
+  g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->prefix, g_free);
+
+  G_OBJECT_CLASS (gbp_buildui_runtime_categories_parent_class)->finalize (object);
+}
+
+static void
+gbp_buildui_runtime_categories_class_init (GbpBuilduiRuntimeCategoriesClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_buildui_runtime_categories_finalize;
+}
+
+static void
+gbp_buildui_runtime_categories_init (GbpBuilduiRuntimeCategories *self)
+{
+  self->items = g_ptr_array_new_with_free_func (g_free);
+}
+
+static gint
+sort_by_name (const gchar **name_a,
+              const gchar **name_b)
+{
+  return g_strcmp0 (*name_a, *name_b);
+}
+
+static void
+on_items_changed_cb (GbpBuilduiRuntimeCategories *self,
+                     guint                        position,
+                     guint                        added,
+                     guint                        removed,
+                     IdeRuntimeManager           *runtime_manager)
+{
+  g_autoptr(GHashTable) found = NULL;
+  guint old_len;
+  guint n_items;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_RUNTIME_CATEGORIES (self));
+  g_assert (IDE_IS_RUNTIME_MANAGER (runtime_manager));
+
+  old_len = self->items->len;
+
+  if (old_len > 0)
+    g_ptr_array_remove_range (self->items, 0, old_len);
+
+  found = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (runtime_manager));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeRuntime) runtime = g_list_model_get_item (G_LIST_MODEL (runtime_manager), i);
+      g_autofree gchar *word = NULL;
+      const gchar *category = ide_runtime_get_category (runtime);
+      const gchar *next = category;
+      const gchar *slash;
+
+      if (self->prefix != NULL && !g_str_has_prefix (category, self->prefix))
+        continue;
+
+      if (self->prefix)
+        next += strlen (self->prefix);
+
+      if ((slash = strchr (next, '/')))
+        next = word = g_strndup (next, slash + 1 - next);
+
+      if (!g_hash_table_contains (found, next))
+        {
+          g_hash_table_insert (found, g_strdup (next), NULL);
+          g_ptr_array_add (self->items, g_strdup (next));
+        }
+    }
+
+  g_ptr_array_sort (self->items, (GCompareFunc)sort_by_name);
+
+  g_list_model_items_changed (G_LIST_MODEL (self), 0, old_len, self->items->len);
+}
+
+GbpBuilduiRuntimeCategories *
+gbp_buildui_runtime_categories_new (IdeRuntimeManager *runtime_manager,
+                                    const gchar       *prefix)
+{
+  GbpBuilduiRuntimeCategories *ret;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_RUNTIME_MANAGER (runtime_manager), NULL);
+
+  ret = g_object_new (GBP_TYPE_BUILDUI_RUNTIME_CATEGORIES, NULL);
+  ret->runtime_manager = g_object_ref (runtime_manager);
+  ret->prefix = g_strdup (prefix);
+  ret->name = prefix ? g_path_get_basename (prefix) : NULL;
+
+  g_signal_connect_object (runtime_manager,
+                           "items-changed",
+                           G_CALLBACK (on_items_changed_cb),
+                           ret,
+                           G_CONNECT_SWAPPED);
+
+  on_items_changed_cb (ret, 0, 0, 0, runtime_manager);
+
+  return g_steal_pointer (&ret);
+}
+
+const gchar *
+gbp_buildui_runtime_categories_get_name (GbpBuilduiRuntimeCategories *self)
+{
+  g_return_val_if_fail (GBP_IS_BUILDUI_RUNTIME_CATEGORIES (self), NULL);
+
+  return self->name;
+}
+
+const gchar *
+gbp_buildui_runtime_categories_get_prefix (GbpBuilduiRuntimeCategories *self)
+{
+  g_return_val_if_fail (GBP_IS_BUILDUI_RUNTIME_CATEGORIES (self), NULL);
+
+  return self->prefix;
+}
+
+GListModel *
+gbp_buildui_runtime_categories_create_child_model (GbpBuilduiRuntimeCategories *self,
+                                                   const gchar                 *category)
+{
+  g_autofree gchar *prefix = NULL;
+  g_autofree gchar *name = NULL;
+  DzlListModelFilter *filter;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_RUNTIME_CATEGORIES (self));
+  g_assert (category != NULL);
+
+  if (self->prefix == NULL)
+    prefix = g_strdup (category);
+  else
+    prefix = g_strdup_printf ("%s%s", self->prefix, category);
+
+  name = g_path_get_basename (prefix);
+
+  if (g_str_has_suffix (category, "/"))
+    return G_LIST_MODEL (gbp_buildui_runtime_categories_new (self->runtime_manager, prefix));
+
+  filter = dzl_list_model_filter_new (G_LIST_MODEL (self->runtime_manager));
+  g_object_set_data_full (G_OBJECT (filter), "CATEGORY", g_steal_pointer (&name), g_free);
+  dzl_list_model_filter_set_filter_func (filter,
+                                         filter_by_category,
+                                         g_strdup (prefix),
+                                         g_free);
+
+  return G_LIST_MODEL (g_steal_pointer (&filter));
+}
diff --git a/src/plugins/buildui/gbp-buildui-runtime-categories.h 
b/src/plugins/buildui/gbp-buildui-runtime-categories.h
new file mode 100644
index 000000000..ae70b360e
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-runtime-categories.h
@@ -0,0 +1,38 @@
+/* gbp-buildui-runtime-categories.h
+ *
+ * Copyright 2018-2019 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-foundry.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_BUILDUI_RUNTIME_CATEGORIES (gbp_buildui_runtime_categories_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuilduiRuntimeCategories, gbp_buildui_runtime_categories, GBP, 
BUILDUI_RUNTIME_CATEGORIES, GObject)
+
+GbpBuilduiRuntimeCategories *gbp_buildui_runtime_categories_new                (IdeRuntimeManager           
*runtime_manager,
+                                                                                const gchar                 
*prefix);
+GListModel                  *gbp_buildui_runtime_categories_create_child_model (GbpBuilduiRuntimeCategories 
*self,
+                                                                                const gchar                 
*category);
+const gchar                 *gbp_buildui_runtime_categories_get_prefix         (GbpBuilduiRuntimeCategories 
*self);
+const gchar                 *gbp_buildui_runtime_categories_get_name           (GbpBuilduiRuntimeCategories 
*self);
+
+G_END_DECLS
diff --git a/src/plugins/buildui/gbp-buildui-runtime-row.c b/src/plugins/buildui/gbp-buildui-runtime-row.c
new file mode 100644
index 000000000..ec7bedb4f
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-runtime-row.c
@@ -0,0 +1,137 @@
+/* gbp-buildui-runtime-row.c
+ *
+ * Copyright 2018-2019 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-buildui-runtime-row"
+
+#include "config.h"
+
+#include "gbp-buildui-runtime-row.h"
+
+struct _GbpBuilduiRuntimeRow
+{
+  GtkListBoxRow  parent_instance;
+
+  gchar         *runtime_id;
+
+  GtkLabel      *label;
+  GtkImage      *image;
+};
+
+G_DEFINE_TYPE (GbpBuilduiRuntimeRow, gbp_buildui_runtime_row, GTK_TYPE_LIST_BOX_ROW)
+
+static void
+gbp_buildui_runtime_row_finalize (GObject *object)
+{
+  GbpBuilduiRuntimeRow *self = (GbpBuilduiRuntimeRow *)object;
+
+  g_clear_pointer (&self->runtime_id, g_free);
+
+  G_OBJECT_CLASS (gbp_buildui_runtime_row_parent_class)->finalize (object);
+}
+
+static void
+gbp_buildui_runtime_row_class_init (GbpBuilduiRuntimeRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_buildui_runtime_row_finalize;
+}
+
+static void
+gbp_buildui_runtime_row_init (GbpBuilduiRuntimeRow *self)
+{
+  GtkWidget *box;
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "margin", 10,
+                      "orientation", GTK_ORIENTATION_HORIZONTAL,
+                      "spacing", 6,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (self), box);
+
+  self->label = g_object_new (GTK_TYPE_LABEL,
+                              "visible", TRUE,
+                              "use-markup", TRUE,
+                              "xalign", 0.0f,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (self->label));
+
+  self->image = g_object_new (GTK_TYPE_IMAGE,
+                              "visible", TRUE,
+                              "halign", GTK_ALIGN_START,
+                              "hexpand", TRUE,
+                              "icon-name", "object-select-symbolic",
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (self->image));
+}
+
+static void
+notify_config_runtime_id (GbpBuilduiRuntimeRow *self,
+                          GParamSpec           *pspec,
+                          IdeConfiguration     *config)
+{
+  g_assert (GBP_IS_BUILDUI_RUNTIME_ROW (self));
+  g_assert (IDE_IS_CONFIGURATION (config));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->image),
+                          ide_str_equal0 (self->runtime_id,
+                                          ide_configuration_get_runtime_id (config)));
+}
+
+GtkWidget *
+gbp_buildui_runtime_row_new (IdeRuntime       *runtime,
+                             IdeConfiguration *config)
+{
+  GbpBuilduiRuntimeRow *self;
+  gboolean sensitive;
+
+  g_return_val_if_fail (IDE_IS_RUNTIME (runtime), NULL);
+  g_return_val_if_fail (IDE_IS_CONFIGURATION (config), NULL);
+
+  sensitive = ide_configuration_supports_runtime (config, runtime);
+
+  self = g_object_new (GBP_TYPE_BUILDUI_RUNTIME_ROW,
+                       "sensitive", sensitive,
+                       "visible", TRUE,
+                       NULL);
+  self->runtime_id = g_strdup (ide_runtime_get_id (runtime));
+  gtk_label_set_label (self->label,
+                       ide_runtime_get_display_name (runtime));
+
+  g_signal_connect_object (config,
+                           "notify::runtime-id",
+                           G_CALLBACK (notify_config_runtime_id),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_widget_set_visible (GTK_WIDGET (self->image),
+                          ide_str_equal0 (self->runtime_id,
+                                          ide_configuration_get_runtime_id (config)));
+
+  return GTK_WIDGET (self);
+}
+
+const gchar *
+gbp_buildui_runtime_row_get_id (GbpBuilduiRuntimeRow *self)
+{
+  g_return_val_if_fail (GBP_IS_BUILDUI_RUNTIME_ROW (self), NULL);
+
+  return self->runtime_id;
+}
diff --git a/src/plugins/buildui/gbp-buildui-runtime-row.h b/src/plugins/buildui/gbp-buildui-runtime-row.h
new file mode 100644
index 000000000..64fe8b5a0
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-runtime-row.h
@@ -0,0 +1,36 @@
+/* gbp-buildui-runtime-row.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+#include <libide-foundry.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_BUILDUI_RUNTIME_ROW (gbp_buildui_runtime_row_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuilduiRuntimeRow, gbp_buildui_runtime_row, GBP, BUILDUI_RUNTIME_ROW, GtkListBoxRow)
+
+GtkWidget   *gbp_buildui_runtime_row_new    (IdeRuntime           *runtime,
+                                             IdeConfiguration     *config);
+const gchar *gbp_buildui_runtime_row_get_id (GbpBuilduiRuntimeRow *self);
+
+G_END_DECLS
diff --git a/src/plugins/buildui/gbp-buildui-stage-row.c b/src/plugins/buildui/gbp-buildui-stage-row.c
new file mode 100644
index 000000000..6cfdc9f3d
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-stage-row.c
@@ -0,0 +1,198 @@
+/* gbp-buildui-stage-row.c
+ *
+ * Copyright 2017-2019 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-buildui-stage-row"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "gbp-buildui-stage-row.h"
+
+struct _GbpBuilduiStageRow
+{
+  GtkListBoxRow    parent_instance;
+
+  IdeBuildStage   *stage;
+
+  DzlBoldingLabel *label;
+};
+
+enum {
+  PROP_0,
+  PROP_STAGE,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (GbpBuilduiStageRow, gbp_buildui_stage_row, GTK_TYPE_LIST_BOX_ROW)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_buildui_stage_row_notify_completed (GbpBuilduiStageRow *row,
+                                      GParamSpec       *pspec,
+                                      IdeBuildStage    *stage)
+{
+  g_assert (GBP_IS_BUILDUI_STAGE_ROW (row));
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+
+  if (ide_build_stage_get_completed (stage))
+    dzl_gtk_widget_add_style_class (GTK_WIDGET (row->label), "dim-label");
+  else
+    dzl_gtk_widget_remove_style_class (GTK_WIDGET (row->label), "dim-label");
+}
+
+static void
+gbp_buildui_stage_row_set_stage (GbpBuilduiStageRow *self,
+                               IdeBuildStage    *stage)
+{
+  const gchar *name;
+
+  g_return_if_fail (GBP_IS_BUILDUI_STAGE_ROW (self));
+  g_return_if_fail (IDE_IS_BUILD_STAGE (stage));
+
+  g_set_object (&self->stage, stage);
+
+  name = ide_build_stage_get_name (stage);
+
+  if (name == NULL)
+    name = G_OBJECT_TYPE_NAME (stage);
+
+  gtk_label_set_label (GTK_LABEL (self->label), name);
+
+  g_signal_connect_object (stage,
+                           "notify::completed",
+                           G_CALLBACK (gbp_buildui_stage_row_notify_completed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_object_bind_property (stage, "disabled", self, "sensitive", G_BINDING_DEFAULT);
+  g_object_bind_property (stage, "active", self->label, "bold", G_BINDING_DEFAULT);
+
+  gbp_buildui_stage_row_notify_completed (self, NULL, stage);
+}
+
+static void
+gbp_buildui_stage_row_destroy (GtkWidget *widget)
+{
+  GbpBuilduiStageRow *self = (GbpBuilduiStageRow *)widget;
+
+  g_clear_object (&self->stage);
+
+  GTK_WIDGET_CLASS (gbp_buildui_stage_row_parent_class)->destroy (widget);
+}
+
+static void
+gbp_buildui_stage_row_get_property (GObject    *object,
+                                  guint       prop_id,
+                                  GValue     *value,
+                                  GParamSpec *pspec)
+{
+  GbpBuilduiStageRow *self = GBP_BUILDUI_STAGE_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_STAGE:
+      g_value_set_object (value, gbp_buildui_stage_row_get_stage (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_buildui_stage_row_set_property (GObject      *object,
+                                  guint         prop_id,
+                                  const GValue *value,
+                                  GParamSpec   *pspec)
+{
+  GbpBuilduiStageRow *self = GBP_BUILDUI_STAGE_ROW (object);
+
+  switch (prop_id)
+    {
+    case PROP_STAGE:
+      gbp_buildui_stage_row_set_stage (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_buildui_stage_row_class_init (GbpBuilduiStageRowClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = gbp_buildui_stage_row_get_property;
+  object_class->set_property = gbp_buildui_stage_row_set_property;
+
+  widget_class->destroy = gbp_buildui_stage_row_destroy;
+
+  properties [PROP_STAGE] =
+    g_param_spec_object ("stage",
+                         "Stage",
+                         "The stage for the row",
+                         IDE_TYPE_BUILD_STAGE,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/plugins/buildui/gbp-buildui-stage-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpBuilduiStageRow, label);
+}
+
+static void
+gbp_buildui_stage_row_init (GbpBuilduiStageRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget *
+gbp_buildui_stage_row_new (IdeBuildStage *stage)
+{
+  g_return_val_if_fail (IDE_IS_BUILD_STAGE (stage), NULL);
+
+  return g_object_new (GBP_TYPE_BUILDUI_STAGE_ROW,
+                       "stage", stage,
+                       "visible", TRUE,
+                       NULL);
+}
+
+/**
+ * gbp_buildui_stage_row_get_stage:
+ * @self: a #GbpBuilduiStageRow
+ *
+ * Gets the stage for the row.
+ *
+ * Returns: (transfer none): an #IdeBuildStage
+ *
+ * Since: 3.32
+ */
+IdeBuildStage *
+gbp_buildui_stage_row_get_stage (GbpBuilduiStageRow *self)
+{
+  g_return_val_if_fail (GBP_IS_BUILDUI_STAGE_ROW (self), NULL);
+
+  return self->stage;
+}
diff --git a/src/plugins/buildui/gbp-buildui-stage-row.h b/src/plugins/buildui/gbp-buildui-stage-row.h
new file mode 100644
index 000000000..d5bca66ad
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-stage-row.h
@@ -0,0 +1,35 @@
+/* gbp-buildui-stage-row.h
+ *
+ * Copyright 2017-2019 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 <gtk/gtk.h>
+#include <libide-foundry.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_BUILDUI_STAGE_ROW (gbp_buildui_stage_row_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuilduiStageRow, gbp_buildui_stage_row, GBP, BUILDUI_STAGE_ROW, GtkListBoxRow)
+
+GtkWidget     *gbp_buildui_stage_row_new       (IdeBuildStage    *stage);
+IdeBuildStage *gbp_buildui_stage_row_get_stage (GbpBuilduiStageRow *self);
+
+G_END_DECLS
diff --git a/src/plugins/buildui/gbp-buildui-stage-row.ui b/src/plugins/buildui/gbp-buildui-stage-row.ui
new file mode 100644
index 000000000..9eb96f579
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-stage-row.ui
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpBuilduiStageRow" parent="GtkListBoxRow">
+    <child>
+      <object class="GtkBox">
+        <property name="visible">true</property>
+        <property name="orientation">horizontal</property>
+        <child>
+          <object class="DzlBoldingLabel" id="label">
+            <property name="visible">true</property>
+            <property name="xalign">0.0</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/buildui/gbp-buildui-tree-addin.c b/src/plugins/buildui/gbp-buildui-tree-addin.c
new file mode 100644
index 000000000..6c8c337df
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-tree-addin.c
@@ -0,0 +1,381 @@
+/* gbp-buildui-tree-addin.c
+ *
+ * Copyright 2018-2019 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-buildui-tree-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+#include <libide-tree.h>
+
+#include "gbp-buildui-tree-addin.h"
+
+struct _GbpBuilduiTreeAddin
+{
+  GObject       parent_instance;
+  IdeTree      *tree;
+  IdeTreeModel *model;
+};
+
+typedef struct
+{
+  IdeTreeNode *node;
+  guint        n_active;
+} BuildTargets;
+
+static void
+build_targets_free (BuildTargets *state)
+{
+  g_clear_object (&state->node);
+  g_slice_free (BuildTargets, state);
+}
+
+static void
+get_targets_cb (GObject      *object,
+                GAsyncResult *result,
+                gpointer      user_data)
+{
+  IdeBuildTargetProvider *provider = (IdeBuildTargetProvider *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GPtrArray) targets = NULL;
+  BuildTargets *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUILD_TARGET_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+
+  if ((targets = ide_build_target_provider_get_targets_finish (provider, result, &error)))
+    {
+      for (guint i = 0; i < targets->len; i++)
+        {
+          IdeBuildTarget *target = g_ptr_array_index (targets, i);
+          g_autoptr(IdeTreeNode) node = NULL;
+          g_autofree gchar *name = NULL;
+
+          name = ide_build_target_get_name (target);
+          node = g_object_new (IDE_TYPE_TREE_NODE,
+                               "destroy-item", TRUE,
+                               "display-name", name,
+                               "icon-name", "builder-build-symbolic",
+                               "item", target,
+                               NULL);
+          ide_tree_node_append (state->node, node);
+        }
+    }
+
+  state->n_active--;
+
+  if (state->n_active == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+build_targets_cb (IdeExtensionSetAdapter *set,
+                  PeasPluginInfo         *plugin_info,
+                  PeasExtension          *exten,
+                  gpointer                user_data)
+{
+  IdeBuildTargetProvider *provider = (IdeBuildTargetProvider *)exten;
+  IdeTask *task = user_data;
+  BuildTargets *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EXTENSION_SET_ADAPTER (set));
+  g_assert (IDE_IS_BUILD_TARGET_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (task));
+
+  state = ide_task_get_task_data (task);
+  state->n_active++;
+
+  ide_build_target_provider_get_targets_async (provider,
+                                               ide_task_get_cancellable (task),
+                                               get_targets_cb,
+                                               g_object_ref (task));
+}
+
+static void
+gbp_buildui_tree_addin_build_children_async (IdeTreeAddin        *addin,
+                                             IdeTreeNode         *node,
+                                             GCancellable        *cancellable,
+                                             GAsyncReadyCallback  callback,
+                                             gpointer             user_data)
+{
+  GbpBuilduiTreeAddin *self = (GbpBuilduiTreeAddin *)addin;
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_buildui_tree_addin_build_children_async);
+
+  if (ide_tree_node_holds (node, IDE_TYPE_CONTEXT))
+    {
+      g_autoptr(IdeTreeNode) targets = NULL;
+
+      targets = g_object_new (IDE_TYPE_TREE_NODE,
+                              "icon-name", "builder-build-symbolic",
+                              "item", NULL,
+                              "display-name", _("Build Targets"),
+                              "children-possible", TRUE,
+                              "tag", "BUILD_TARGETS",
+                              NULL);
+      ide_tree_node_prepend (node, targets);
+    }
+  else if (ide_tree_node_is_tag (node, "BUILD_TARGETS"))
+    {
+      g_autoptr(IdeExtensionSetAdapter) set = NULL;
+      BuildTargets *state;
+
+      state = g_slice_new0 (BuildTargets);
+      state->node = g_object_ref (node);
+      state->n_active = 0;
+      ide_task_set_task_data (task, state, build_targets_free);
+
+      set = ide_extension_set_adapter_new (IDE_OBJECT (self->model),
+                                           peas_engine_get_default (),
+                                           IDE_TYPE_BUILD_TARGET_PROVIDER,
+                                           NULL, NULL);
+      ide_extension_set_adapter_foreach (set, build_targets_cb, task);
+
+      if (state->n_active > 0)
+        return;
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_buildui_tree_addin_build_children_finish (IdeTreeAddin  *addin,
+                                              GAsyncResult  *result,
+                                              GError       **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+gbp_buildui_tree_addin_action_build (GSimpleAction *action,
+                                     GVariant      *param,
+                                     gpointer       user_data)
+{
+  GbpBuilduiTreeAddin *self = user_data;
+  g_autoptr(GPtrArray) targets = NULL;
+  IdeBuildManager *build_manager;
+  IdeBuildTarget *target;
+  IdeTreeNode *node;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_BUILDUI_TREE_ADDIN (self));
+
+  if (!(context = ide_widget_get_context (GTK_WIDGET (self->tree))) ||
+      !(build_manager = ide_build_manager_from_context (context)) ||
+      !(node = ide_tree_get_selected_node (self->tree)) ||
+      !ide_tree_node_holds (node, IDE_TYPE_BUILD_TARGET) ||
+      !(target = ide_tree_node_get_item (node)))
+    return;
+
+  targets = g_ptr_array_new_full (1, g_object_unref);
+  g_ptr_array_add (targets, g_object_ref (target));
+
+  ide_build_manager_execute_async (build_manager, IDE_BUILD_PHASE_BUILD, targets, NULL, NULL, NULL);
+}
+
+static void
+gbp_buildui_tree_addin_action_rebuild (GSimpleAction *action,
+                                       GVariant      *param,
+                                       gpointer       user_data)
+{
+  GbpBuilduiTreeAddin *self = user_data;
+  g_autoptr(GPtrArray) targets = NULL;
+  IdeBuildManager *build_manager;
+  IdeBuildTarget *target;
+  IdeTreeNode *node;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_BUILDUI_TREE_ADDIN (self));
+
+  if (!(context = ide_widget_get_context (GTK_WIDGET (self->tree))) ||
+      !(build_manager = ide_build_manager_from_context (context)) ||
+      !(node = ide_tree_get_selected_node (self->tree)) ||
+      !ide_tree_node_holds (node, IDE_TYPE_BUILD_TARGET) ||
+      !(target = ide_tree_node_get_item (node)))
+    return;
+
+  targets = g_ptr_array_new_full (1, g_object_unref);
+  g_ptr_array_add (targets, g_object_ref (target));
+
+  ide_build_manager_rebuild_async (build_manager, IDE_BUILD_PHASE_BUILD, targets, NULL, NULL, NULL);
+}
+
+static void
+gbp_buildui_tree_addin_action_run (GSimpleAction *action,
+                                   GVariant      *param,
+                                   gpointer       user_data)
+{
+  GbpBuilduiTreeAddin *self = user_data;
+  g_autoptr(GPtrArray) targets = NULL;
+  IdeBuildManager *build_manager;
+  IdeBuildTarget *target;
+  IdeRunManager *run_manager;
+  IdeTreeNode *node;
+  IdeContext *context;
+  const gchar *handler;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+  g_assert (GBP_IS_BUILDUI_TREE_ADDIN (self));
+
+  if (!(context = ide_widget_get_context (GTK_WIDGET (self->tree))) ||
+      !(build_manager = ide_build_manager_from_context (context)) ||
+      !(node = ide_tree_get_selected_node (self->tree)) ||
+      !ide_tree_node_holds (node, IDE_TYPE_BUILD_TARGET) ||
+      !(target = ide_tree_node_get_item (node)))
+    return;
+
+  run_manager = ide_run_manager_from_context (context);
+  handler = g_variant_get_string (param, NULL);
+
+  if (ide_str_empty0 (handler))
+    ide_run_manager_set_handler (run_manager, NULL);
+  else
+    ide_run_manager_set_handler (run_manager, handler);
+
+  ide_run_manager_run_async (run_manager,
+                             target,
+                             NULL,
+                             NULL,
+                             NULL);
+}
+
+static void
+gbp_buildui_tree_addin_load (IdeTreeAddin *addin,
+                             IdeTree      *tree,
+                             IdeTreeModel *model)
+{
+  GbpBuilduiTreeAddin *self = (GbpBuilduiTreeAddin *)addin;
+  g_autoptr(GSimpleActionGroup) group = NULL;
+  static const GActionEntry actions[] = {
+    { "build", gbp_buildui_tree_addin_action_build },
+    { "rebuild", gbp_buildui_tree_addin_action_rebuild },
+    { "run-with-handler", gbp_buildui_tree_addin_action_run, "s" },
+  };
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  self->model = model;
+  self->tree = tree;
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (tree), "buildui", G_ACTION_GROUP (group));
+}
+
+static void
+gbp_buildui_tree_addin_unload (IdeTreeAddin *addin,
+                               IdeTree      *tree,
+                               IdeTreeModel *model)
+{
+  GbpBuilduiTreeAddin *self = (GbpBuilduiTreeAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (tree), "buildui", NULL);
+
+  self->model = NULL;
+  self->tree = NULL;
+}
+
+static void
+gbp_buildui_tree_addin_selection_changed (IdeTreeAddin *addin,
+                                          IdeTreeNode  *node)
+{
+  GbpBuilduiTreeAddin *self = (GbpBuilduiTreeAddin *)addin;
+  IdeBuildTarget *target;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_TREE_ADDIN (self));
+  g_assert (!node || IDE_IS_TREE_NODE (node));
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "buildui", "build",
+                             "enabled", node && ide_tree_node_holds (node, IDE_TYPE_BUILD_TARGET),
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "buildui", "rebuild",
+                             "enabled", node && ide_tree_node_holds (node, IDE_TYPE_BUILD_TARGET),
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "buildui", "run-with-handler",
+                             "enabled", node &&
+                                        ide_tree_node_holds (node, IDE_TYPE_BUILD_TARGET) &&
+                                        (target = ide_tree_node_get_item (node)) &&
+                                        ide_build_target_get_install (target) &&
+                                        ide_build_target_get_kind (target) == IDE_ARTIFACT_KIND_EXECUTABLE,
+                             NULL);
+}
+
+static void
+tree_addin_iface_init (IdeTreeAddinInterface *iface)
+{
+  iface->build_children_async = gbp_buildui_tree_addin_build_children_async;
+  iface->build_children_finish = gbp_buildui_tree_addin_build_children_finish;
+  iface->selection_changed = gbp_buildui_tree_addin_selection_changed;
+  iface->load = gbp_buildui_tree_addin_load;
+  iface->unload = gbp_buildui_tree_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpBuilduiTreeAddin, gbp_buildui_tree_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_TREE_ADDIN, tree_addin_iface_init))
+
+static void
+gbp_buildui_tree_addin_class_init (GbpBuilduiTreeAddinClass *klass)
+{
+}
+
+static void
+gbp_buildui_tree_addin_init (GbpBuilduiTreeAddin *self)
+{
+}
diff --git a/src/plugins/buildui/gbp-buildui-tree-addin.h b/src/plugins/buildui/gbp-buildui-tree-addin.h
new file mode 100644
index 000000000..ef7005605
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-tree-addin.h
@@ -0,0 +1,31 @@
+/* gbp-buildui-tree-addin.h
+ *
+ * Copyright 2018-2019 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_BUILDUI_TREE_ADDIN (gbp_buildui_tree_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuilduiTreeAddin, gbp_buildui_tree_addin, GBP, BUILDUI_TREE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/buildui/gbp-buildui-workspace-addin.c 
b/src/plugins/buildui/gbp-buildui-workspace-addin.c
new file mode 100644
index 000000000..e36cd1bd1
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-workspace-addin.c
@@ -0,0 +1,428 @@
+/* gbp-buildui-workspace-addin.c
+ *
+ * Copyright 2018-2019 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-buildui-workspace-addin"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-editor.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+
+#include "gbp-buildui-config-surface.h"
+#include "gbp-buildui-log-pane.h"
+#include "gbp-buildui-omni-bar-section.h"
+#include "gbp-buildui-pane.h"
+#include "gbp-buildui-workspace-addin.h"
+
+struct _GbpBuilduiWorkspaceAddin
+{
+  GObject                   parent_instance;
+
+  /* Borrowed references */
+  IdeWorkspace             *workspace;
+  GbpBuilduiConfigSurface  *surface;
+  GbpBuilduiOmniBarSection *omni_bar_section;
+  GbpBuilduiLogPane        *log_pane;
+  GbpBuilduiPane           *pane;
+  GtkBox                   *diag_box;
+  GtkImage                 *error_image;
+  GtkLabel                 *error_label;
+  GtkImage                 *warning_image;
+  GtkLabel                 *warning_label;
+  GtkButton                *build_button;
+  GtkButton                *cancel_button;
+
+  /* Owned references */
+  DzlSignalGroup           *build_manager_signals;
+};
+
+static void
+gbp_buildui_workspace_addin_notify_error_count (GbpBuilduiWorkspaceAddin *self,
+                                                GParamSpec               *pspec,
+                                                IdeBuildManager          *build_manager)
+{
+  gchar str[12];
+  guint count;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  if (!(count = ide_build_manager_get_error_count (build_manager)))
+    {
+      gtk_widget_set_visible (GTK_WIDGET (self->error_label), FALSE);
+      gtk_widget_set_visible (GTK_WIDGET (self->error_image), FALSE);
+      gtk_label_set_label (self->error_label, NULL);
+      return;
+    }
+
+  g_snprintf (str, sizeof str, "%u", count);
+  gtk_label_set_label (self->error_label, str);
+  gtk_widget_set_visible (GTK_WIDGET (self->error_label), TRUE);
+  gtk_widget_set_visible (GTK_WIDGET (self->error_image), TRUE);
+}
+
+static void
+gbp_buildui_workspace_addin_notify_warning_count (GbpBuilduiWorkspaceAddin *self,
+                                                  GParamSpec               *pspec,
+                                                  IdeBuildManager          *build_manager)
+{
+  gchar str[12];
+  guint count;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  if (!(count = ide_build_manager_get_warning_count (build_manager)))
+    {
+      gtk_widget_set_visible (GTK_WIDGET (self->warning_label), FALSE);
+      gtk_widget_set_visible (GTK_WIDGET (self->warning_image), FALSE);
+      gtk_label_set_label (self->warning_label, NULL);
+      return;
+    }
+
+  g_snprintf (str, sizeof str, "%u", count);
+  gtk_label_set_label (self->warning_label, str);
+  gtk_widget_set_visible (GTK_WIDGET (self->warning_label), TRUE);
+  gtk_widget_set_visible (GTK_WIDGET (self->warning_image), TRUE);
+}
+
+static void
+gbp_buildui_workspace_addin_notify_pipeline (GbpBuilduiWorkspaceAddin *self,
+                                             GParamSpec               *pspec,
+                                             IdeBuildManager          *build_manager)
+{
+  IdeBuildPipeline *pipeline;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+  gbp_buildui_log_pane_set_pipeline (self->log_pane, pipeline);
+  gbp_buildui_pane_set_pipeline (self->pane, pipeline);
+}
+
+static void
+gbp_buildui_workspace_addin_notify_busy (GbpBuilduiWorkspaceAddin *self,
+                                         GParamSpec               *pspec,
+                                         IdeBuildManager          *build_manager)
+{
+  gboolean busy;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  busy = ide_build_manager_get_busy (build_manager);
+
+  gtk_widget_set_visible (GTK_WIDGET (self->build_button), !busy);
+  gtk_widget_set_visible (GTK_WIDGET (self->cancel_button), busy);
+}
+
+static void
+gbp_buildui_workspace_addin_bind_build_manager (GbpBuilduiWorkspaceAddin *self,
+                                                IdeBuildManager          *build_manager,
+                                                DzlSignalGroup           *signals)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+  g_assert (DZL_IS_SIGNAL_GROUP (signals));
+
+  gbp_buildui_workspace_addin_notify_busy (self, NULL, build_manager);
+  gbp_buildui_workspace_addin_notify_pipeline (self, NULL, build_manager);
+  gbp_buildui_workspace_addin_notify_error_count (self, NULL, build_manager);
+  gbp_buildui_workspace_addin_notify_warning_count (self, NULL, build_manager);
+}
+
+static void
+on_view_output_cb (GSimpleAction *action,
+                   GVariant      *param,
+                   gpointer       user_data)
+{
+  GbpBuilduiWorkspaceAddin *self = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_BUILDUI_WORKSPACE_ADDIN (self));
+
+  ide_widget_reveal_and_grab (GTK_WIDGET (self->log_pane));
+}
+
+static void
+on_edit_config_cb (GSimpleAction *action,
+                   GVariant      *param,
+                   gpointer       user_data)
+{
+  GbpBuilduiWorkspaceAddin *self = user_data;
+  IdeConfigurationManager *config_manager;
+  IdeConfiguration *config;
+  IdeContext *context;
+  const gchar *id;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_BUILDUI_WORKSPACE_ADDIN (self));
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+
+  ide_workspace_set_visible_surface_name (self->workspace, "buildui");
+
+  context = ide_widget_get_context (GTK_WIDGET (self->workspace));
+  config_manager = ide_configuration_manager_from_context (context);
+  id = g_variant_get_string (param, NULL);
+  config = ide_configuration_manager_get_configuration (config_manager, id);
+
+  if (config != NULL)
+    gbp_buildui_config_surface_set_config (self->surface, config);
+}
+
+static const GActionEntry actions[] = {
+  { "edit-config", on_edit_config_cb, "s" },
+  { "view-output", on_view_output_cb },
+};
+
+static void
+gbp_buildui_workspace_addin_load (IdeWorkspaceAddin *addin,
+                                  IdeWorkspace      *workspace)
+{
+  GbpBuilduiWorkspaceAddin *self = (GbpBuilduiWorkspaceAddin *)addin;
+  IdeConfigurationManager *config_manager;
+  PangoAttrList *small_attrs = NULL;
+  IdeEditorSidebar *sidebar;
+  IdeBuildManager *build_manager;
+  IdeWorkbench *workbench;
+  IdeHeaderBar *headerbar;
+  IdeSurface *surface;
+  IdeOmniBar *omnibar;
+  IdeContext *context;
+  GtkWidget *utilities;
+
+  g_assert (GBP_IS_BUILDUI_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace));
+
+  self->workspace = workspace;
+
+  g_action_map_add_action_entries (G_ACTION_MAP (workspace),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+
+  headerbar = ide_workspace_get_header_bar (workspace);
+  omnibar = IDE_OMNI_BAR (gtk_header_bar_get_custom_title (GTK_HEADER_BAR (headerbar)));
+  workbench = ide_widget_get_workbench (GTK_WIDGET (workspace));
+  context = ide_workbench_get_context (workbench);
+  build_manager = ide_build_manager_from_context (context);
+  config_manager = ide_configuration_manager_from_context (context);
+
+  small_attrs = pango_attr_list_new ();
+  pango_attr_list_insert (small_attrs, pango_attr_scale_new (0.833333));
+
+  self->diag_box = g_object_new (GTK_TYPE_BOX,
+                                 "orientation", GTK_ORIENTATION_HORIZONTAL,
+                                 "visible", TRUE,
+                                 NULL);
+  g_signal_connect (self->diag_box,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->diag_box);
+  ide_omni_bar_add_status_icon (omnibar, GTK_WIDGET (self->diag_box), 0);
+
+  self->error_image = g_object_new (GTK_TYPE_IMAGE,
+                                    "icon-name", "dialog-error-symbolic",
+                                    "margin-end", 2,
+                                    "margin-start", 4,
+                                    "pixel-size", 12,
+                                    "valign", GTK_ALIGN_BASELINE,
+                                    NULL);
+  gtk_container_add (GTK_CONTAINER (self->diag_box), GTK_WIDGET (self->error_image));
+
+  self->error_label = g_object_new (GTK_TYPE_LABEL,
+                                    "attributes", small_attrs,
+                                    "margin-end", 2,
+                                    "margin-start", 2,
+                                    "valign", GTK_ALIGN_BASELINE,
+                                    NULL);
+  gtk_container_add (GTK_CONTAINER (self->diag_box), GTK_WIDGET (self->error_label));
+
+  self->warning_image = g_object_new (GTK_TYPE_IMAGE,
+                                      "icon-name", "dialog-warning-symbolic",
+                                      "margin-end", 2,
+                                      "margin-start", 4,
+                                      "pixel-size", 12,
+                                      "valign", GTK_ALIGN_BASELINE,
+                                      NULL);
+  gtk_container_add (GTK_CONTAINER (self->diag_box), GTK_WIDGET (self->warning_image));
+
+  self->warning_label = g_object_new (GTK_TYPE_LABEL,
+                                      "attributes", small_attrs,
+                                      "margin-end", 2,
+                                      "margin-start", 2,
+                                      "valign", GTK_ALIGN_BASELINE,
+                                      NULL);
+  gtk_container_add (GTK_CONTAINER (self->diag_box), GTK_WIDGET (self->warning_label));
+
+  g_clear_pointer (&small_attrs, pango_attr_list_unref);
+
+  self->omni_bar_section = g_object_new (GBP_TYPE_BUILDUI_OMNI_BAR_SECTION,
+                                         "visible", TRUE,
+                                         NULL);
+  g_signal_connect (self->omni_bar_section,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->omni_bar_section);
+  ide_omni_bar_add_popover_section (omnibar, GTK_WIDGET (self->omni_bar_section), 0);
+  gbp_buildui_omni_bar_section_set_context (self->omni_bar_section, context);
+
+  self->build_button = g_object_new (GTK_TYPE_BUTTON,
+                                     "action-name", "build-manager.build",
+                                     "child", g_object_new (GTK_TYPE_IMAGE,
+                                                            "icon-name", "builder-build-symbolic",
+                                                            "visible", TRUE,
+                                                            NULL),
+                                     "focus-on-click", FALSE,
+                                     "has-tooltip", TRUE,
+                                     "visible", TRUE,
+                                     NULL);
+  ide_omni_bar_add_button (omnibar, GTK_WIDGET (self->build_button), GTK_PACK_END, 0);
+
+  self->cancel_button = g_object_new (GTK_TYPE_BUTTON,
+                                      "action-name", "build-manager.cancel",
+                                      "child", g_object_new (GTK_TYPE_IMAGE,
+                                                             "icon-name", "process-stop-symbolic",
+                                                             "visible", TRUE,
+                                                             NULL),
+                                      "focus-on-click", FALSE,
+                                      "has-tooltip", TRUE,
+                                      "visible", TRUE,
+                                      NULL);
+  ide_omni_bar_add_button (omnibar, GTK_WIDGET (self->cancel_button), GTK_PACK_END, 0);
+
+  surface = ide_workspace_get_surface_by_name (workspace, "editor");
+  utilities = ide_editor_surface_get_utilities (IDE_EDITOR_SURFACE (surface));
+  sidebar = ide_editor_surface_get_sidebar (IDE_EDITOR_SURFACE (surface));
+
+  self->log_pane = g_object_new (GBP_TYPE_BUILDUI_LOG_PANE,
+                                 "visible", TRUE,
+                                 NULL);
+  gtk_container_add (GTK_CONTAINER (utilities), GTK_WIDGET (self->log_pane));
+
+  self->pane = g_object_new (GBP_TYPE_BUILDUI_PANE,
+                             "visible", TRUE,
+                             NULL);
+  ide_editor_sidebar_add_section (sidebar,
+                                  "build-issues",
+                                  _("Build Issues"),
+                                  "builder-build-symbolic",
+                                  NULL, NULL,
+                                  GTK_WIDGET (self->pane),
+                                  100);
+
+  self->surface = g_object_new (GBP_TYPE_BUILDUI_CONFIG_SURFACE,
+                                "config-manager", config_manager,
+                                "icon-name", "builder-build-configure-symbolic",
+                                "title", _("Build Preferences"),
+                                "name", "buildui",
+                                "visible", TRUE,
+                                NULL);
+  g_signal_connect (self->surface,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->surface);
+  ide_workspace_add_surface (workspace, IDE_SURFACE (self->surface));
+
+  self->build_manager_signals = dzl_signal_group_new (IDE_TYPE_BUILD_MANAGER);
+  g_signal_connect_object (self->build_manager_signals,
+                           "bind",
+                           G_CALLBACK (gbp_buildui_workspace_addin_bind_build_manager),
+                           self,
+                           G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "notify::error-count",
+                                   G_CALLBACK (gbp_buildui_workspace_addin_notify_error_count),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "notify::warning-count",
+                                   G_CALLBACK (gbp_buildui_workspace_addin_notify_warning_count),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "notify::pipeline",
+                                   G_CALLBACK (gbp_buildui_workspace_addin_notify_pipeline),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->build_manager_signals,
+                                   "notify::busy",
+                                   G_CALLBACK (gbp_buildui_workspace_addin_notify_busy),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_set_target (self->build_manager_signals, build_manager);
+}
+
+static void
+gbp_buildui_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                    IdeWorkspace      *workspace)
+{
+  GbpBuilduiWorkspaceAddin *self = (GbpBuilduiWorkspaceAddin *)addin;
+
+  g_assert (GBP_IS_BUILDUI_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace));
+
+  for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
+    g_action_map_remove_action (G_ACTION_MAP (workspace), actions[i].name);
+
+  if (self->omni_bar_section)
+    gtk_widget_destroy (GTK_WIDGET (self->omni_bar_section));
+
+  if (self->diag_box)
+    gtk_widget_destroy (GTK_WIDGET (self->diag_box));
+
+  if (self->surface)
+    gtk_widget_destroy (GTK_WIDGET (self->surface));
+
+  dzl_signal_group_set_target (self->build_manager_signals, NULL);
+  g_clear_object (&self->build_manager_signals);
+
+  self->workspace = NULL;
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_buildui_workspace_addin_load;
+  iface->unload = gbp_buildui_workspace_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpBuilduiWorkspaceAddin, gbp_buildui_workspace_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
+
+static void
+gbp_buildui_workspace_addin_class_init (GbpBuilduiWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_buildui_workspace_addin_init (GbpBuilduiWorkspaceAddin *self)
+{
+}
diff --git a/src/plugins/buildui/gbp-buildui-workspace-addin.h 
b/src/plugins/buildui/gbp-buildui-workspace-addin.h
new file mode 100644
index 000000000..4279292c9
--- /dev/null
+++ b/src/plugins/buildui/gbp-buildui-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-buildui-workspace-addin.h
+ *
+ * Copyright 2018-2019 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_BUILDUI_WORKSPACE_ADDIN (gbp_buildui_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpBuilduiWorkspaceAddin, gbp_buildui_workspace_addin, GBP, BUILDUI_WORKSPACE_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/buildui/gtk/menus.ui b/src/plugins/buildui/gtk/menus.ui
new file mode 100644
index 000000000..7741b8491
--- /dev/null
+++ b/src/plugins/buildui/gtk/menus.ui
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-primary-workspace-surfaces-menu">
+    <section id="ide-primary-workspace-surfaces-menu-section">
+      <item>
+        <attribute name="accel">&lt;alt&gt;2</attribute>
+        <attribute name="id">surface-menu-config</attribute>
+        <attribute name="label" translatable="yes">Build Preferences</attribute>
+        <attribute name="role">normal</attribute>
+        <attribute name="action">win.surface</attribute>
+        <attribute name="target">buildui</attribute>
+        <attribute name="verb-icon-name">builder-build-configure-symbolic</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="project-tree-menu">
+    <section id="project-tree-menu-placeholder3">
+      <item>
+        <attribute name="id">project-tree-menu-build</attribute>
+        <attribute name="label" translatable="yes">Build</attribute>
+        <attribute name="action">buildui.build</attribute>
+      </item>
+      <item>
+        <attribute name="id">project-tree-menu-rebuild</attribute>
+        <attribute name="label" translatable="yes">Rebuild</attribute>
+        <attribute name="action">buildui.rebuild</attribute>
+      </item>
+      <item>
+        <attribute name="id">project-tree-menu-run</attribute>
+        <attribute name="label" translatable="yes">Run</attribute>
+        <attribute name="action">buildui.run-with-handler</attribute>
+        <attribute name="target" type="s">''</attribute>
+      </item>
+      <submenu id="project-tree-run-with-submenu">
+        <attribute name="label" translatable="yes">Run With…</attribute>
+        <section id="project-tree-menu-run-with-section">
+        </section>
+      </submenu>
+    </section>
+  </menu>
+</interface>
diff --git a/src/plugins/buildui/meson.build b/src/plugins/buildui/meson.build
new file mode 100644
index 000000000..fe69f85e4
--- /dev/null
+++ b/src/plugins/buildui/meson.build
@@ -0,0 +1,21 @@
+plugins_sources += files([
+  'buildui-plugin.c',
+  'gbp-buildui-config-surface.c',
+  'gbp-buildui-config-view-addin.c',
+  'gbp-buildui-log-pane.c',
+  'gbp-buildui-omni-bar-section.c',
+  'gbp-buildui-pane.c',
+  'gbp-buildui-runtime-categories.c',
+  'gbp-buildui-runtime-row.c',
+  'gbp-buildui-stage-row.c',
+  'gbp-buildui-tree-addin.c',
+  'gbp-buildui-workspace-addin.c',
+])
+
+plugin_buildui_resources = gnome.compile_resources(
+  'buildui-resources',
+  'buildui.gresource.xml',
+  c_name: 'gbp_buildui',
+)
+
+plugins_sources += plugin_buildui_resources[0]
diff --git a/src/plugins/buildui/themes/shared.css b/src/plugins/buildui/themes/shared.css
new file mode 100644
index 000000000..a4f1baea0
--- /dev/null
+++ b/src/plugins/buildui/themes/shared.css
@@ -0,0 +1,9 @@
+.buildui .sidebar label.header {
+  padding: 8px;
+  border-bottom: 1px solid alpha(@borders, 0.5);
+}
+
+.omnibar label.error {
+  color: @error_color;
+  font-weight: bold;
+}
diff --git a/src/plugins/c-pack/c-pack-plugin.c b/src/plugins/c-pack/c-pack-plugin.c
index 5836e3e00..82bd8b2d2 100644
--- a/src/plugins/c-pack/c-pack-plugin.c
+++ b/src/plugins/c-pack/c-pack-plugin.c
@@ -1,6 +1,6 @@
 /* c-pack-plugin.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,18 +14,30 @@
  *
  * 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 "c-pack-plugin"
+
+#include <libide-editor.h>
+#include <libide-sourceview.h>
 #include <libpeas/peas.h>
 
 #include "ide-c-indenter.h"
 #include "cpack-completion-provider.h"
-#include "cpack-editor-view-addin.h"
+#include "cpack-editor-page-addin.h"
 
-void
-ide_c_pack_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_ide_c_pack_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module, IDE_TYPE_INDENTER, IDE_TYPE_C_INDENTER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_EDITOR_VIEW_ADDIN, 
CPACK_TYPE_EDITOR_VIEW_ADDIN);
-  peas_object_module_register_extension_type (module, IDE_TYPE_COMPLETION_PROVIDER, 
CPACK_TYPE_COMPLETION_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_INDENTER,
+                                              IDE_TYPE_C_INDENTER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                              CPACK_TYPE_EDITOR_PAGE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_COMPLETION_PROVIDER,
+                                              CPACK_TYPE_COMPLETION_PROVIDER);
 }
diff --git a/src/plugins/c-pack/c-pack.gresource.xml b/src/plugins/c-pack/c-pack.gresource.xml
index b7417949b..9cda0e405 100644
--- a/src/plugins/c-pack/c-pack.gresource.xml
+++ b/src/plugins/c-pack/c-pack.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/c-pack">
     <file>c-pack.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/c-pack/c-pack.plugin b/src/plugins/c-pack/c-pack.plugin
index e8e0a0585..c99764211 100644
--- a/src/plugins/c-pack/c-pack.plugin
+++ b/src/plugins/c-pack/c-pack.plugin
@@ -1,11 +1,12 @@
 [Plugin]
-Module=c-pack-plugin
-Name=C Language Enablement
-Description=Provides language support for the C programming language.
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
-X-Indenter-Languages=c,chdr
-X-Indenter-Languages-Priority=0
+Copyright=Copyright © 2015-2018 Christian Hergert
+Depends=editor;
+Description=Provides language support for the C programming language.
+Embedded=_ide_c_pack_register_types
+Module=c-pack
+Name=C Language Enablement
 X-Completion-Provider-Languages=c,chdr,cpp,cpphdr
-Embedded=ide_c_pack_register_types
+X-Indenter-Languages-Priority=0
+X-Indenter-Languages=c,chdr
diff --git a/src/plugins/c-pack/c-parse-helper.c b/src/plugins/c-pack/c-parse-helper.c
index be645c161..76f0b3462 100644
--- a/src/plugins/c-pack/c-parse-helper.c
+++ b/src/plugins/c-pack/c-parse-helper.c
@@ -1,6 +1,6 @@
 /* c-parse-helper.c
  *
- * Copyright 2014 Christian Hergert <christian hergert me>
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "c-parser"
diff --git a/src/plugins/c-pack/c-parse-helper.h b/src/plugins/c-pack/c-parse-helper.h
index 52f960df5..c2a8b133f 100644
--- a/src/plugins/c-pack/c-parse-helper.h
+++ b/src/plugins/c-pack/c-parse-helper.h
@@ -1,6 +1,6 @@
 /* c-parse-helper.h
  *
- * Copyright 2014 Christian Hergert <christian hergert me>
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/c-pack/cpack-completion-item.c b/src/plugins/c-pack/cpack-completion-item.c
index 6c4296263..0d064e7dc 100644
--- a/src/plugins/c-pack/cpack-completion-item.c
+++ b/src/plugins/c-pack/cpack-completion-item.c
@@ -1,6 +1,6 @@
 /* cpack-completion-item.c
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,13 +14,15 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "cpack-completion-item"
 
-#include <ide.h>
+#include "config.h"
+
+#include <libide-sourceview.h>
 
 #include "cpack-completion-item.h"
 
diff --git a/src/plugins/c-pack/cpack-completion-item.h b/src/plugins/c-pack/cpack-completion-item.h
index 024c2daf1..6252b9558 100644
--- a/src/plugins/c-pack/cpack-completion-item.h
+++ b/src/plugins/c-pack/cpack-completion-item.h
@@ -1,6 +1,6 @@
 /* cpack-completion-item.h
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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>
+#include <libide-sourceview.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/c-pack/cpack-completion-provider.c b/src/plugins/c-pack/cpack-completion-provider.c
index 3baf7bc4c..a2bb5325a 100644
--- a/src/plugins/c-pack/cpack-completion-provider.c
+++ b/src/plugins/c-pack/cpack-completion-provider.c
@@ -1,6 +1,6 @@
 /* cpack-completion-provider.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,15 @@
  *
  * 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 "cpack-completion-provider"
+
 #include "config.h"
 
-#define G_LOG_DOMAIN "cpack-completion-provider"
+#include <libide-sourceview.h>
 
 #include "cpack-completion-item.h"
 #include "cpack-completion-provider.h"
@@ -44,6 +48,7 @@ cpack_completion_provider_init (CpackCompletionProvider *self)
 {
 }
 
+#if 0
 static void
 cpack_completion_provider_populate_cb (GObject      *object,
                                        GAsyncResult *result,
@@ -101,6 +106,7 @@ cpack_completion_provider_get_build_flags_cb (GObject      *object,
                                            cpack_completion_provider_populate_cb,
                                            g_object_ref (task));
 }
+#endif
 
 static void
 cpack_completion_provider_populate_async (IdeCompletionProvider *provider,
@@ -155,6 +161,12 @@ query_filesystem:
 
   g_assert (IDE_IS_BUFFER (buffer));
 
+  ide_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "TODO need access to build flags");
+
+#if 0
   /*
    * First step is to get our list of include paths from the CFLAGS for the
    * file. After that, we can start looking for matches on the file-system
@@ -165,6 +177,7 @@ query_filesystem:
                                     cancellable,
                                     cpack_completion_provider_get_build_flags_cb,
                                     g_steal_pointer (&task));
+#endif
 }
 
 static GListModel *
diff --git a/src/plugins/c-pack/cpack-completion-provider.h b/src/plugins/c-pack/cpack-completion-provider.h
index fa2097fab..283ba12a2 100644
--- a/src/plugins/c-pack/cpack-completion-provider.h
+++ b/src/plugins/c-pack/cpack-completion-provider.h
@@ -1,6 +1,6 @@
 /* cpack-completion-provider.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/c-pack/cpack-completion-results.c b/src/plugins/c-pack/cpack-completion-results.c
index 087439a37..9d4d31f36 100644
--- a/src/plugins/c-pack/cpack-completion-results.c
+++ b/src/plugins/c-pack/cpack-completion-results.c
@@ -1,6 +1,6 @@
 /* cpack-completion-results.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -18,10 +18,10 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "cpack-completion-results"
 
+#include "config.h"
+
 #include <string.h>
 
 #include "cpack-completion-item.h"
diff --git a/src/plugins/c-pack/cpack-completion-results.h b/src/plugins/c-pack/cpack-completion-results.h
index 0d130f3b6..9872dca1a 100644
--- a/src/plugins/c-pack/cpack-completion-results.h
+++ b/src/plugins/c-pack/cpack-completion-results.h
@@ -1,6 +1,6 @@
 /* cpack-completion-results.h
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-sourceview.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/c-pack/cpack-editor-page-addin.c b/src/plugins/c-pack/cpack-editor-page-addin.c
new file mode 100644
index 000000000..02577abee
--- /dev/null
+++ b/src/plugins/c-pack/cpack-editor-page-addin.c
@@ -0,0 +1,115 @@
+/* cpack-editor-page-addin.c
+ *
+ * Copyright 2018-2019 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 "cpack-editor-page-addin"
+
+#include "config.h"
+
+#include <libide-editor.h>
+
+#include "cpack-editor-page-addin.h"
+#include "hdr-format.h"
+
+struct _CpackEditorPageAddin
+{
+  GObject parent_instance;
+};
+
+static void
+format_decls_cb (GSimpleAction *action,
+                 GVariant      *param,
+                 gpointer       user_data)
+{
+  IdeEditorPage *view = user_data;
+  g_autofree gchar *input = NULL;
+  g_autofree gchar *output = NULL;
+  IdeBuffer *buffer;
+  IdeSourceView *sourceview;
+  GtkTextIter begin, end;
+
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  buffer = ide_editor_page_get_buffer (view);
+  sourceview = ide_editor_page_get_view (view);
+
+  /* We require a selection */
+  if (!gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end))
+    return;
+
+  input = gtk_text_iter_get_slice (&begin, &end);
+  output = hdr_format_string (input, -1);
+
+  if (output != NULL)
+    {
+      gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer));
+      gtk_text_buffer_delete (GTK_TEXT_BUFFER (buffer), &begin, &end);
+      gtk_text_buffer_insert (GTK_TEXT_BUFFER (buffer), &begin, output, -1);
+      gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer));
+      g_signal_emit_by_name (sourceview, "reset");
+    }
+}
+
+static GActionEntry entries[] = {
+  { "format-decls", format_decls_cb },
+};
+
+static void
+cpack_editor_page_addin_load (IdeEditorPageAddin *addin,
+                              IdeEditorPage      *view)
+{
+  g_autoptr(GActionMap) group = NULL;
+
+  g_assert (CPACK_IS_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  group = G_ACTION_MAP (g_simple_action_group_new ());
+  g_action_map_add_action_entries (group, entries, G_N_ELEMENTS (entries), view);
+  gtk_widget_insert_action_group (GTK_WIDGET (view), "cpack", G_ACTION_GROUP (group));
+}
+
+static void
+cpack_editor_page_addin_unload (IdeEditorPageAddin *addin,
+                                IdeEditorPage      *view)
+{
+  g_assert (CPACK_IS_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (view), "cpack", NULL);
+}
+
+static void
+iface_init (IdeEditorPageAddinInterface *iface)
+{
+  iface->load = cpack_editor_page_addin_load;
+  iface->unload = cpack_editor_page_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (CpackEditorPageAddin, cpack_editor_page_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_PAGE_ADDIN, iface_init))
+
+static void
+cpack_editor_page_addin_class_init (CpackEditorPageAddinClass *klass)
+{
+}
+
+static void
+cpack_editor_page_addin_init (CpackEditorPageAddin *self)
+{
+}
diff --git a/src/plugins/c-pack/cpack-editor-page-addin.h b/src/plugins/c-pack/cpack-editor-page-addin.h
new file mode 100644
index 000000000..58f136870
--- /dev/null
+++ b/src/plugins/c-pack/cpack-editor-page-addin.h
@@ -0,0 +1,31 @@
+/* cpack-editor-page-addin.h
+ *
+ * Copyright 2018-2019 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 CPACK_TYPE_EDITOR_PAGE_ADDIN (cpack_editor_page_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (CpackEditorPageAddin, cpack_editor_page_addin, CPACK, EDITOR_PAGE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/c-pack/hdr-format.c b/src/plugins/c-pack/hdr-format.c
index 920c01d30..397e930ff 100644
--- a/src/plugins/c-pack/hdr-format.c
+++ b/src/plugins/c-pack/hdr-format.c
@@ -1,6 +1,6 @@
 /* hdr-format.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,12 +14,13 @@
  *
  * 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 "hdr-format"
 
-#include <glib.h>
-#include <ide.h>
+#include <gtksourceview/gtksource.h>
 #include <string.h>
 
 #include "c-parse-helper.h"
@@ -209,7 +210,7 @@ push_chunk (GArray      *ar,
   str = pos;
 
   chunk.return_type = g_strstrip (g_steal_pointer (&return_type));
-  
+
   if (!(ident = getword (str, &pos)))
     goto failure;
   if (*ident != '_' && !g_ascii_isalpha (*ident))
@@ -407,6 +408,12 @@ hdr_format_string (const gchar *data,
               break;
             }
 
+          if (p->type == NULL)
+            {
+              g_warning ("Unexpected NULL value for type");
+              continue;
+            }
+
           g_string_append (out, p->type);
 
           for (guint j = strlen (p->type); j < long_ptype; j++)
diff --git a/src/plugins/c-pack/hdr-format.h b/src/plugins/c-pack/hdr-format.h
index 4065cbf28..1c7d029d5 100644
--- a/src/plugins/c-pack/hdr-format.h
+++ b/src/plugins/c-pack/hdr-format.h
@@ -1,6 +1,6 @@
 /* hdr-format.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/c-pack/ide-c-indenter.c b/src/plugins/c-pack/ide-c-indenter.c
index 5b17cdaeb..c0e8ef46e 100644
--- a/src/plugins/c-pack/ide-c-indenter.c
+++ b/src/plugins/c-pack/ide-c-indenter.c
@@ -1,6 +1,6 @@
 /* ide-c-indenter.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,12 +14,15 @@
  *
  * 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 "cindent"
 
 #include <glib/gi18n.h>
 #include <libpeas/peas.h>
+#include <libide-sourceview.h>
 
 #include "c-parse-helper.h"
 #include "ide-c-indenter.h"
@@ -1356,7 +1359,7 @@ ide_c_indenter_format (IdeIndenter    *indenter,
     ret = c_indenter_indent (c, view, buffer, begin);
     *begin = begin_copy;
 
-    if (!dzl_str_empty0 (ret))
+    if (!ide_str_empty0 (ret))
       {
         /*
          * If we have additional space after where our new indentation
diff --git a/src/plugins/c-pack/ide-c-indenter.h b/src/plugins/c-pack/ide-c-indenter.h
index c07018751..774cc4864 100644
--- a/src/plugins/c-pack/ide-c-indenter.h
+++ b/src/plugins/c-pack/ide-c-indenter.h
@@ -1,6 +1,6 @@
 /* ide-c-indenter.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/c-pack/meson.build b/src/plugins/c-pack/meson.build
index a97560126..a1f60fc9f 100644
--- a/src/plugins/c-pack/meson.build
+++ b/src/plugins/c-pack/meson.build
@@ -1,23 +1,35 @@
-if get_option('with_c_pack')
+if get_option('plugin_c_pack')
 
-c_pack_resources = gnome.compile_resources(
-  'c-pack-resources',
-  'c-pack.gresource.xml',
-  c_name: 'ide_c',
-)
-
-c_pack_sources = [
+plugins_sources += files([
   'c-pack-plugin.c',
   'c-parse-helper.c',
-  'hdr-format.c',
-  'ide-c-indenter.c',
   'cpack-completion-item.c',
   'cpack-completion-provider.c',
   'cpack-completion-results.c',
-  'cpack-editor-view-addin.c',
-]
+  'cpack-editor-page-addin.c',
+  'hdr-format.c',
+  'ide-c-indenter.c',
+])
 
-gnome_builder_plugins_sources += files(c_pack_sources)
-gnome_builder_plugins_sources += c_pack_resources[0]
+plugin_c_pack_resources = gnome.compile_resources(
+  'c-pack-resources',
+  'c-pack.gresource.xml',
+  c_name: 'ide_c',
+)
+
+plugins_sources += plugin_c_pack_resources[0]
+
+test_cpack = executable('test-cpack',
+  'test-cpack.c', 'c-parse-helper.c',
+        c_args: test_cflags,
+  dependencies: [ libide_projects_dep ],
+)
+test('test-cpack', test_cpack, env: test_env)
+
+test_hdr_format = executable('test-hdr-format',
+  'test-hdr-format.c', 'c-parse-helper.c',
+        c_args: test_cflags,
+  dependencies: [ libide_sourceview_dep ],
+)
 
 endif
diff --git a/src/plugins/c-pack/test-cpack.c b/src/plugins/c-pack/test-cpack.c
new file mode 100644
index 000000000..41ca6cebc
--- /dev/null
+++ b/src/plugins/c-pack/test-cpack.c
@@ -0,0 +1,80 @@
+/* test-cpack.c
+ *
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
+ *
+ * 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/>.
+ */
+
+#include "c-parse-helper.h"
+
+static void
+test_parse_parameters1 (void)
+{
+  gsize i;
+  GSList *ret;
+  GSList *iter;
+
+  static const struct
+  {
+    const gchar *type;
+    guint n_star;
+    const gchar *name;
+    guint ellipsis;
+  } result[]=
+  {
+    { "Item", 1, "a", 0 },
+    { "Item", 2, "b", 0 },
+    { "gpointer", 0, "u", 0 },
+    { "GError", 2, "error", 0 },
+    { NULL, 0, NULL, 1}
+  };
+
+  ret = parse_parameters ("Item *a , Item **b, gpointer u, GError ** error, ...");
+  g_assert_cmpint (5, ==, g_slist_length (ret));
+
+  for (i = 0, iter = ret; i < G_N_ELEMENTS (result); i++, iter = iter->next)
+    {
+      Parameter *p;
+
+      p = iter->data;
+      g_assert_cmpstr (p->type, ==, result[i].type);
+      g_assert_cmpint (p->n_star, ==, result[i].n_star);
+      g_assert_cmpstr (p->name, ==, result[i].name);
+      g_assert_cmpint (p->ellipsis, ==, result[i].ellipsis);
+    }
+
+  g_assert (!iter);
+
+  g_slist_foreach (ret, (GFunc)parameter_free, NULL);
+  g_slist_free (ret);
+}
+
+static void
+test_parse_parameters2 (void)
+{
+  GSList *ret;
+
+  ret = parse_parameters ("abc, def, ghi");
+  g_assert (!ret);
+}
+
+int
+main (int argc,
+      char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  g_test_add_func ("/Parser/C/parse_parameters1", test_parse_parameters1);
+  g_test_add_func ("/Parser/C/parse_parameters2", test_parse_parameters2);
+  return g_test_run ();
+}
diff --git a/src/plugins/c-pack/test-hdr-format.c b/src/plugins/c-pack/test-hdr-format.c
new file mode 100644
index 000000000..83295e70a
--- /dev/null
+++ b/src/plugins/c-pack/test-hdr-format.c
@@ -0,0 +1,47 @@
+/* test-hdr-format.c
+ *
+ * Copyright 2018-2019 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/>.
+ */
+
+#include "hdr-format.c"
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+  g_autofree gchar *contents = NULL;
+  g_autofree gchar *ret = NULL;
+  g_autoptr(GError) error = NULL;
+  gsize len;
+
+  if (argc < 2)
+    {
+      g_printerr ("usage: %s FILENAME\n", argv[0]);
+      return 1;
+    }
+
+  if (!g_file_get_contents (argv[1], &contents, &len, &error))
+    {
+      g_printerr ("%s\n", error->message);
+      return 1;
+    }
+
+  ret = hdr_format_string (contents, len);
+
+  g_print ("%s\n", ret);
+
+  return 0;
+}
diff --git a/src/plugins/cargo/cargo.plugin b/src/plugins/cargo/cargo.plugin
index 29adfd29d..14d8a9f34 100644
--- a/src/plugins/cargo/cargo.plugin
+++ b/src/plugins/cargo/cargo.plugin
@@ -1,10 +1,12 @@
 [Plugin]
-Module=cargo_plugin
-Name=Cargo
-Loader=python3
-Description=Provides integration with the Cargo build system
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2016 Christian Hergert
 Builtin=true
-X-Project-File-Filter-Pattern=Cargo.toml
+Copyright=Copyright © 2016 Christian Hergert
+Description=Provides integration with the Cargo build system
+Hidden=true
+Loader=python3
+Module=cargo_plugin
+Name=Cargo
 X-Project-File-Filter-Name=Cargo (Cargo.toml)
+X-Project-File-Filter-Pattern=Cargo.toml
+X-Builder-ABI=@PACKAGE_ABI@
diff --git a/src/plugins/cargo/cargo_plugin.py b/src/plugins/cargo/cargo_plugin.py
index 67a3400b5..b3f4b6db2 100644
--- a/src/plugins/cargo/cargo_plugin.py
+++ b/src/plugins/cargo/cargo_plugin.py
@@ -19,12 +19,9 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-import gi
 import threading
 import os
 
-gi.require_version('Ide', '1.0')
-
 from gi.repository import Gio
 from gi.repository import GLib
 from gi.repository import GObject
@@ -39,7 +36,14 @@ _ERROR_FORMAT_REGEX = ("(?<filename>[a-zA-Z0-9\\+\\-\\.\\/_]+):"
                        "(?<level>[\\w\\[a-zA-Z0-9\\]\\s]+): "
                        "(?<message>.*)")
 
-class CargoBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
+class CargoBuildSystemDiscovery(Ide.SimpleBuildSystemDiscovery):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.props.glob = 'Cargo.toml'
+        self.props.hint = 'cargo_plugin'
+        self.props.priority = -200
+
+class CargoBuildSystem(Ide.Object, Ide.BuildSystem):
     project_file = GObject.Property(type=Gio.File)
 
     def do_get_id(self):
@@ -48,33 +52,6 @@ class CargoBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
     def do_get_display_name(self):
         return 'Cargo'
 
-    def do_init_async(self, io_priority, cancellable, callback, data):
-        task = Gio.Task.new(self, cancellable, callback)
-
-        # This is all done synchronously, doing it in a thread would probably
-        # be somewhat ideal although unnecessary at this time.
-
-        try:
-            # Maybe this is a Cargo.toml
-            if self.props.project_file.get_basename() in ('Cargo.toml',):
-                task.return_boolean(True)
-                return
-
-            # Maybe this is a directory with a Cargo.toml
-            if self.props.project_file.query_file_type(0) == Gio.FileType.DIRECTORY:
-                child = self.props.project_file.get_child('Cargo.toml')
-                if child.query_exists(None):
-                    self.props.project_file = child
-                    task.return_boolean(True)
-                    return
-        except Exception as ex:
-            task.return_error(ex)
-
-        task.return_error(Ide.NotSupportedError())
-
-    def do_init_finish(self, task):
-        return task.propagate_boolean()
-
     def do_get_priority(self):
         return -200
 
@@ -100,7 +77,7 @@ class CargoPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
 
     def do_load(self, pipeline):
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         # Always register the error regex
         self.error_format_id = pipeline.add_error_format(_ERROR_FORMAT_REGEX,
@@ -127,7 +104,7 @@ class CargoPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
         fetch_launcher.push_argv('fetch')
         fetch_launcher.push_argv('--manifest-path')
         fetch_launcher.push_argv(cargo_toml)
-        self.track(pipeline.connect_launcher(Ide.BuildPhase.DOWNLOADS, 0, fetch_launcher))
+        self.track(pipeline.attach_launcher(Ide.BuildPhase.DOWNLOADS, 0, fetch_launcher))
 
         # Now create our launcher to build the project
         build_launcher = pipeline.create_launcher()
@@ -166,13 +143,13 @@ class CargoPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
         build_stage.set_name(_("Building project"))
         build_stage.set_clean_launcher(clean_launcher)
         build_stage.connect('query', self._query)
-        self.track(pipeline.connect(Ide.BuildPhase.BUILD, 0, build_stage))
+        self.track(pipeline.attach(Ide.BuildPhase.BUILD, 0, build_stage))
 
     def do_unload(self, pipeline):
         if self.error_format_id:
             pipeline.remove_error_format(self.error_format_id)
 
-    def _query(self, stage, pipeline, cancellable):
+    def _query(self, stage, pipeline, targets, cancellable):
         # Always defer to cargo to check if build is needed
         stage.set_completed(False)
 
@@ -213,7 +190,7 @@ class CargoBuildTargetProvider(Ide.Object, Ide.BuildTargetProvider):
         task.set_priority(GLib.PRIORITY_LOW)
 
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != CargoBuildSystem:
             task.return_error(GLib.Error('Not cargo build system',
@@ -235,14 +212,14 @@ class CargoDependencyUpdater(Ide.Object, Ide.DependencyUpdater):
         task.set_priority(GLib.PRIORITY_LOW)
 
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         # Short circuit if not using cargo
         if type(build_system) != CargoBuildSystem:
             task.return_boolean(True)
             return
 
-        build_manager = context.get_build_manager()
+        build_manager = Ide.BuildManager.from_context(context)
         pipeline = build_manager.get_pipeline()
         if not pipeline:
             task.return_error(GLib.Error('Cannot update dependencies without build pipeline',
diff --git a/src/plugins/cargo/meson.build b/src/plugins/cargo/meson.build
index 6e7a360fa..4b975cbea 100644
--- a/src/plugins/cargo/meson.build
+++ b/src/plugins/cargo/meson.build
@@ -1,11 +1,11 @@
-if get_option('with_cargo')
+if get_option('plugin_cargo')
 
 install_data('cargo_plugin.py', install_dir: plugindir)
 
 configure_file(
           input: 'cargo.plugin',
          output: 'cargo.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/clang/clang-plugin.c b/src/plugins/clang/clang-plugin.c
index eaca349e1..c8cac1bba 100644
--- a/src/plugins/clang/clang-plugin.c
+++ b/src/plugins/clang/clang-plugin.c
@@ -1,6 +1,6 @@
 /* clang-plugin.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,10 +14,18 @@
  *
  * 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 "clang-plugin"
+
+#include "config.h"
+
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-code.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
 
 #include "ide-clang-client.h"
 #include "ide-clang-code-indexer.h"
@@ -31,8 +39,8 @@
 #include "ide-clang-symbol-resolver.h"
 #include "ide-clang-symbol-tree.h"
 
-void
-ide_clang_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_ide_clang_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_CODE_INDEXER,
@@ -43,9 +51,6 @@ ide_clang_register_types (PeasObjectModule *module)
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_SYMBOL_RESOLVER,
                                               IDE_TYPE_CLANG_SYMBOL_RESOLVER);
-  peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_SERVICE,
-                                              IDE_TYPE_CLANG_CLIENT);
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_DIAGNOSTIC_PROVIDER,
                                               IDE_TYPE_CLANG_DIAGNOSTIC_PROVIDER);
diff --git a/src/plugins/clang/clang.gresource.xml b/src/plugins/clang/clang.gresource.xml
index a2a31083f..0514f91bb 100644
--- a/src/plugins/clang/clang.gresource.xml
+++ b/src/plugins/clang/clang.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/clang">
     <file>clang.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/clang/clang.plugin b/src/plugins/clang/clang.plugin
index a016adc73..9dbcfa80e 100644
--- a/src/plugins/clang/clang.plugin
+++ b/src/plugins/clang/clang.plugin
@@ -1,17 +1,19 @@
 [Plugin]
-Module=clang-plugin
-Name=Clang
-Description=Provides integration with Clang
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
-Embedded=ide_clang_register_types
+Copyright=Copyright © 2015 Christian Hergert
+Description=Provides integration with Clang
+Depends=editor;
+Embedded=_ide_clang_register_types
+Hidden=true
+Module=clang
+Name=Clang
+X-Code-Indexer-Languages-Priority=100
+X-Code-Indexer-Languages=c,chdr,cpp,cpphdr,objc
 X-Completion-Provider-Languages=c,chdr,cpp,cpphdr,objc
-X-Highlighter-Languages=c,chdr,cpp,cpphdr,objc
+X-Diagnostic-Provider-Languages-Priority=100
+X-Diagnostic-Provider-Languages=c,chdr,cpp,cpphdr,objc
 X-Highlighter-Languages-Priority=100
-X-Symbol-Resolver-Languages=c,chdr,cpp,cpphdr,objc
+X-Highlighter-Languages=c,chdr,cpp,cpphdr,objc
 X-Symbol-Resolver-Languages-Priority=100
-X-Diagnostic-Provider-Languages=c,chdr,cpp,cpphdr,objc
-X-Diagnostic-Provider-Languages-Priority=100
-X-Code-Indexer-Languages=c,chdr,cpp,cpphdr,objc
-X-Code-Indexer-Languages-Priority=100
+X-Symbol-Resolver-Languages=c,chdr,cpp,cpphdr,objc
diff --git a/src/plugins/clang/gnome-builder-clang.c b/src/plugins/clang/gnome-builder-clang.c
index 90a9c4177..f16de61c1 100644
--- a/src/plugins/clang/gnome-builder-clang.c
+++ b/src/plugins/clang/gnome-builder-clang.c
@@ -1,6 +1,6 @@
 /* gnome-builder-clang.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 /* Prologue {{{1 */
@@ -27,7 +29,7 @@
 #include <gio/gunixoutputstream.h>
 #include <glib-unix.h>
 #include <jsonrpc-glib.h>
-#include <ide.h>
+#include <libide-code.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
@@ -420,7 +422,7 @@ handle_diagnose_cb (IdeClang     *clang,
       return;
     }
 
-  IDE_PTR_ARRAY_SET_FREE_FUNC (diagnostics, ide_diagnostic_unref);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (diagnostics, ide_object_unref_and_destroy);
 
   g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
 
diff --git a/src/plugins/clang/ide-clang-autocleanups.h b/src/plugins/clang/ide-clang-autocleanups.h
index 75429419e..19902ec25 100644
--- a/src/plugins/clang/ide-clang-autocleanups.h
+++ b/src/plugins/clang/ide-clang-autocleanups.h
@@ -1,6 +1,6 @@
 /* ide-clang-autocleanups.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/clang/ide-clang-client.c b/src/plugins/clang/ide-clang-client.c
index d89b4bf84..07eb43934 100644
--- a/src/plugins/clang/ide-clang-client.c
+++ b/src/plugins/clang/ide-clang-client.c
@@ -1,6 +1,6 @@
 /* ide-clang-client.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-clang-client"
@@ -23,6 +25,9 @@
 #include <gio/gunixinputstream.h>
 #include <gio/gunixoutputstream.h>
 #include <glib-unix.h>
+#include <libide-code.h>
+#include <libide-foundry.h>
+#include <libide-vcs.h>
 #include <jsonrpc-glib.h>
 
 #include "ide-clang-client.h"
@@ -56,10 +61,7 @@ typedef struct
   gulong          cancel_id;
 } Call;
 
-static void service_iface_init (IdeServiceInterface *iface);
-
-G_DEFINE_TYPE_EXTENDED (IdeClangClient, ide_clang_client, IDE_TYPE_OBJECT, 0,
-                        G_IMPLEMENT_INTERFACE (IDE_TYPE_SERVICE, service_iface_init))
+G_DEFINE_TYPE (IdeClangClient, ide_clang_client, IDE_TYPE_OBJECT)
 
 static void
 call_free (gpointer data)
@@ -102,7 +104,7 @@ ide_clang_client_sync_buffers (IdeClangClient *self)
    */
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  ufs = ide_context_get_unsaved_files (context);
+  ufs = ide_unsaved_files_from_context (context);
   ar = ide_unsaved_files_to_array (ufs);
   IDE_PTR_ARRAY_SET_FREE_FUNC (ar, ide_unsaved_file_unref);
 
@@ -295,8 +297,7 @@ ide_clang_client_buffer_saved (IdeClangClient   *self,
                                IdeBuffer        *buffer,
                                IdeBufferManager *bufmgr)
 {
-  IdeFile *file;
-  GFile *gfile;
+  GFile *file;
 
   g_assert (IDE_IS_CLANG_CLIENT (self));
   g_assert (IDE_BUFFER (buffer));
@@ -309,20 +310,20 @@ ide_clang_client_buffer_saved (IdeClangClient   *self,
    */
 
   file = ide_buffer_get_file (buffer);
-  gfile = ide_file_get_file (file);
   if (self->seq_by_file != NULL)
-    g_hash_table_remove (self->seq_by_file, gfile);
+    g_hash_table_remove (self->seq_by_file, file);
 
   /* skip if thereis no peer */
   if (self->rpc_client == NULL)
     return;
 
-  if (gfile != NULL)
-    ide_clang_client_set_buffer_async (self, gfile, NULL, NULL, NULL, NULL);
+  if (file != NULL)
+    ide_clang_client_set_buffer_async (self, file, NULL, NULL, NULL, NULL);
 }
 
 static void
-ide_clang_client_constructed (GObject *object)
+ide_clang_client_parent_set (IdeObject *object,
+                             IdeObject *parent)
 {
   IdeClangClient *self = (IdeClangClient *)object;
   g_autoptr(IdeSubprocessLauncher) launcher = NULL;
@@ -332,10 +333,16 @@ ide_clang_client_constructed (GObject *object)
   IdeVcs *vcs;
   GFile *workdir;
 
+  g_assert (IDE_IS_CLANG_CLIENT (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (parent == NULL)
+    return;
+
   context = ide_object_get_context (IDE_OBJECT (self));
-  bufmgr = ide_context_get_buffer_manager (context);
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  bufmgr = ide_buffer_manager_from_context (context);
+  vcs = ide_vcs_from_context (context);
+  workdir = ide_vcs_get_workdir (vcs);
 
   self->root_uri = g_object_ref (workdir);
 
@@ -374,12 +381,10 @@ ide_clang_client_constructed (GObject *object)
                            G_CALLBACK (ide_clang_client_buffer_saved),
                            self,
                            G_CONNECT_SWAPPED);
-
-  G_OBJECT_CLASS (ide_clang_client_parent_class)->constructed (object);
 }
 
 static void
-ide_clang_client_dispose (GObject *object)
+ide_clang_client_destroy (IdeObject *object)
 {
   IdeClangClient *self = (IdeClangClient *)object;
   GList *queued;
@@ -416,7 +421,7 @@ ide_clang_client_dispose (GObject *object)
 
   g_list_free_full (queued, g_object_unref);
 
-  G_OBJECT_CLASS (ide_clang_client_parent_class)->dispose (object);
+  IDE_OBJECT_CLASS (ide_clang_client_parent_class)->destroy (object);
 }
 
 static void
@@ -440,10 +445,12 @@ static void
 ide_clang_client_class_init (IdeClangClientClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
 
-  object_class->constructed = ide_clang_client_constructed;
-  object_class->dispose = ide_clang_client_dispose;
   object_class->finalize = ide_clang_client_finalize;
+
+  i_object_class->parent_set = ide_clang_client_parent_set;
+  i_object_class->destroy = ide_clang_client_destroy;;
 }
 
 static void
@@ -681,7 +688,7 @@ ide_clang_client_index_file_async (IdeClangClient      *self,
  * Returns: (transfer full): a #GVariant containing the indexed data
  *   or %NULL in case of failure.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 GVariant *
 ide_clang_client_index_file_finish (IdeClangClient  *self,
@@ -815,7 +822,7 @@ ide_clang_client_find_nearest_scope_cb (GObject      *object,
   else
     ide_task_return_pointer (task,
                              g_steal_pointer (&ret),
-                             (GDestroyNotify)ide_symbol_unref);
+                             g_object_unref);
 }
 
 void
@@ -910,7 +917,7 @@ ide_clang_client_locate_symbol_cb (GObject      *object,
   else
     ide_task_return_pointer (task,
                              g_steal_pointer (&ret),
-                             (GDestroyNotify)ide_symbol_unref);
+                             g_object_unref);
 }
 
 void
@@ -983,20 +990,18 @@ ide_clang_client_get_symbol_tree_cb (GObject      *object,
   g_autoptr(GVariant) reply = NULL;
   g_autoptr(IdeTask) task = user_data;
   g_autoptr(GError) error = NULL;
-  IdeContext *context;
   GFile *file;
 
   g_assert (IDE_IS_CLANG_CLIENT (self));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
   file = ide_task_get_task_data (task);
 
   if (!ide_clang_client_call_finish (self, result, &reply, &error))
     ide_task_return_error (task, g_steal_pointer (&error));
   else
-    ide_task_return_object (task, ide_clang_symbol_tree_new (context, file, reply));
+    ide_task_return_object (task, ide_clang_symbol_tree_new (file, reply));
 }
 
 void
@@ -1080,7 +1085,7 @@ ide_clang_client_diagnose_cb (GObject      *object,
       return;
     }
 
-  ret = ide_diagnostics_new (NULL);
+  ret = ide_diagnostics_new ();
 
   g_variant_iter_init (&iter, reply);
 
@@ -1096,7 +1101,7 @@ ide_clang_client_diagnose_cb (GObject      *object,
 
   ide_task_return_pointer (task,
                            g_steal_pointer (&ret),
-                           (GDestroyNotify) ide_diagnostics_unref);
+                           g_object_unref);
 }
 
 void
@@ -1398,17 +1403,3 @@ ide_clang_client_set_buffer_finish (IdeClangClient  *self,
 
   return ide_task_propagate_boolean (IDE_TASK (result), error);
 }
-
-static void
-ide_clang_client_stop (IdeService *service)
-{
-  g_assert (IDE_IS_CLANG_CLIENT (service));
-
-  g_object_run_dispose (G_OBJECT (service));
-}
-
-static void
-service_iface_init (IdeServiceInterface *iface)
-{
-  iface->stop = ide_clang_client_stop;
-}
diff --git a/src/plugins/clang/ide-clang-client.h b/src/plugins/clang/ide-clang-client.h
index afd3c2d2b..6c4826363 100644
--- a/src/plugins/clang/ide-clang-client.h
+++ b/src/plugins/clang/ide-clang-client.h
@@ -1,6 +1,6 @@
 /* ide-clang-client.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/clang/ide-clang-code-index-entries.c 
b/src/plugins/clang/ide-clang-code-index-entries.c
index 7009b9e64..46e69c4ed 100644
--- a/src/plugins/clang/ide-clang-code-index-entries.c
+++ b/src/plugins/clang/ide-clang-code-index-entries.c
@@ -1,7 +1,7 @@
 /* ide-clang-code-index-entries.c
  *
  * Copyright 2017 Anoop Chandu <anoopchandu96 gmail com>
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -15,6 +15,8 @@
  *
  * 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 "ide-clang-code-index-entries"
@@ -105,7 +107,7 @@ ide_clang_code_index_entries_worker (IdeTask      *task,
                                  &begin.line, &begin.column,
                                  &end.line, &end.column);
 
-          if (dzl_str_empty0 (key))
+          if (ide_str_empty0 (key))
             key = NULL;
 
           ide_code_index_entry_builder_set_name (builder, name);
diff --git a/src/plugins/clang/ide-clang-code-index-entries.h 
b/src/plugins/clang/ide-clang-code-index-entries.h
index af0ee7e73..f62f749dd 100644
--- a/src/plugins/clang/ide-clang-code-index-entries.h
+++ b/src/plugins/clang/ide-clang-code-index-entries.h
@@ -1,7 +1,7 @@
 /* ide-clang-code-index-entries.h
  *
  * Copyright 2017 Anoop Chandu <anoopchandu96 gmail com>
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/clang/ide-clang-code-indexer.c b/src/plugins/clang/ide-clang-code-indexer.c
index 92ee43d3f..bf0b44687 100644
--- a/src/plugins/clang/ide-clang-code-indexer.c
+++ b/src/plugins/clang/ide-clang-code-indexer.c
@@ -14,6 +14,8 @@
  *
  * 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 "ide-clang-code-indexer"
@@ -69,7 +71,7 @@ ide_clang_code_indexer_index_file_async (IdeCodeIndexer      *indexer,
 {
   IdeClangCodeIndexer *self = (IdeClangCodeIndexer *)indexer;
   g_autoptr(IdeTask) task = NULL;
-  IdeClangClient *client;
+  g_autoptr(IdeClangClient) client = NULL;
   IdeContext *context;
 
   g_assert (IDE_IS_CLANG_CODE_INDEXER (self));
@@ -93,7 +95,7 @@ ide_clang_code_indexer_index_file_async (IdeCodeIndexer      *indexer,
   ide_task_set_task_data (task, g_file_get_path (file), g_free);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  client = ide_context_get_service_typed (context, IDE_TYPE_CLANG_CLIENT);
+  client = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_CLANG_CLIENT);
 
   ide_clang_client_index_file_async (client,
                                      file,
@@ -136,7 +138,7 @@ ide_clang_code_indexer_generate_key_cb (GObject       *object,
 
 static void
 ide_clang_code_indexer_generate_key_async (IdeCodeIndexer       *indexer,
-                                           IdeSourceLocation    *location,
+                                           IdeLocation    *location,
                                            const gchar * const  *args,
                                            GCancellable         *cancellable,
                                            GAsyncReadyCallback   callback,
@@ -144,9 +146,8 @@ ide_clang_code_indexer_generate_key_async (IdeCodeIndexer       *indexer,
 {
   IdeClangCodeIndexer *self = (IdeClangCodeIndexer *)indexer;
   g_autoptr(IdeTask) task = NULL;
-  IdeClangClient *client;
+  g_autoptr(IdeClangClient) client = NULL;
   IdeContext *context;
-  IdeFile *ifile;
   GFile *file;
   guint line;
   guint column;
@@ -161,12 +162,11 @@ ide_clang_code_indexer_generate_key_async (IdeCodeIndexer       *indexer,
   ide_task_set_kind (task, IDE_TASK_KIND_INDEXER);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  client = ide_context_get_service_typed (context, IDE_TYPE_CLANG_CLIENT);
+  client = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_CLANG_CLIENT);
 
-  ifile = ide_source_location_get_file (location);
-  file = ide_file_get_file (ifile);
-  line = ide_source_location_get_line (location);
-  column = ide_source_location_get_line_offset (location);
+  file = ide_location_get_file (location);
+  line = ide_location_get_line (location);
+  column = ide_location_get_line_offset (location);
 
   ide_clang_client_get_index_key_async (client,
                                         file,
diff --git a/src/plugins/clang/ide-clang-code-indexer.h b/src/plugins/clang/ide-clang-code-indexer.h
index e7ba6b7f7..61a86c9d3 100644
--- a/src/plugins/clang/ide-clang-code-indexer.h
+++ b/src/plugins/clang/ide-clang-code-indexer.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/clang/ide-clang-completion-item.c b/src/plugins/clang/ide-clang-completion-item.c
index 7ecbc5fab..93ca4d391 100644
--- a/src/plugins/clang/ide-clang-completion-item.c
+++ b/src/plugins/clang/ide-clang-completion-item.c
@@ -1,6 +1,6 @@
 /* ide-clang-completion-item.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,12 +14,15 @@
  *
  * 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 "ide-clang-completion"
 
 #include <clang-c/Index.h>
 #include <glib/gi18n.h>
+#include <libide-foundry.h>
 
 #include "ide-clang-completion-item.h"
 
@@ -53,24 +56,24 @@ ide_clang_completion_item_do_init (IdeClangCompletionItem *self)
     case CXCursor_ObjCClassMethodDecl:
     case CXCursor_ObjCInstanceMethodDecl:
       self->icon_name = "lang-method-symbolic";
-      self->kind = IDE_SYMBOL_METHOD;
+      self->kind = IDE_SYMBOL_KIND_METHOD;
       break;
 
     case CXCursor_ConversionFunction:
     case CXCursor_FunctionDecl:
     case CXCursor_FunctionTemplate:
       self->icon_name = "lang-function-symbolic";
-      self->kind = IDE_SYMBOL_FUNCTION;
+      self->kind = IDE_SYMBOL_KIND_FUNCTION;
       break;
 
     case CXCursor_FieldDecl:
       self->icon_name = "lang-struct-field-symbolic";
-      self->kind = IDE_SYMBOL_FIELD;
+      self->kind = IDE_SYMBOL_KIND_FIELD;
       break;
 
     case CXCursor_VarDecl:
       self->icon_name = "lang-variable-symbolic";
-      self->kind = IDE_SYMBOL_VARIABLE;
+      self->kind = IDE_SYMBOL_KIND_VARIABLE;
       /* local? */
       break;
 
@@ -78,7 +81,7 @@ ide_clang_completion_item_do_init (IdeClangCompletionItem *self)
     case CXCursor_NamespaceAlias:
     case CXCursor_NamespaceRef:
       self->icon_name = "lang-namespace-symbolic";
-      self->kind = IDE_SYMBOL_NAMESPACE;
+      self->kind = IDE_SYMBOL_KIND_NAMESPACE;
       break;
 
     case CXCursor_ParmDecl:
@@ -90,12 +93,12 @@ ide_clang_completion_item_do_init (IdeClangCompletionItem *self)
 
     case CXCursor_StructDecl:
       self->icon_name = "lang-struct-symbolic";
-      self->kind = IDE_SYMBOL_STRUCT;
+      self->kind = IDE_SYMBOL_KIND_STRUCT;
       break;
 
     case CXCursor_UnionDecl:
       self->icon_name  = "lang-union-symbolic";
-      self->kind = IDE_SYMBOL_UNION;
+      self->kind = IDE_SYMBOL_KIND_UNION;
       break;
 
     case CXCursor_ClassDecl:
@@ -114,23 +117,23 @@ ide_clang_completion_item_do_init (IdeClangCompletionItem *self)
     case CXCursor_TemplateTypeParameter:
     case CXCursor_TemplateTemplateParameter:
       self->icon_name  = "lang-class-symbolic";
-      self->kind = IDE_SYMBOL_CLASS;
+      self->kind = IDE_SYMBOL_KIND_CLASS;
       break;
 
     case CXCursor_MacroDefinition:
     case CXCursor_MacroExpansion:
       self->icon_name = "lang-define-symbolic";
-      self->kind = IDE_SYMBOL_MACRO;
+      self->kind = IDE_SYMBOL_KIND_MACRO;
       break;
 
     case CXCursor_EnumConstantDecl:
       self->icon_name = "lang-enum-value-symbolic";
-      self->kind = IDE_SYMBOL_ENUM_VALUE;
+      self->kind = IDE_SYMBOL_KIND_ENUM_VALUE;
       break;
 
     case CXCursor_EnumDecl:
       self->icon_name = "lang-enum-symbolic";
-      self->kind = IDE_SYMBOL_ENUM;
+      self->kind = IDE_SYMBOL_KIND_ENUM;
       break;
 
     case CXCursor_NotImplemented:
@@ -183,7 +186,7 @@ ide_clang_completion_item_do_init (IdeClangCompletionItem *self)
           break;
 
         case CXCompletionChunk_Informative:
-          if (dzl_str_equal0 (text, "const "))
+          if (ide_str_equal0 (text, "const "))
             g_string_append (markup, text);
           break;
 
@@ -434,6 +437,8 @@ ide_clang_completion_item_init (IdeClangCompletionItem *self)
  * Gets the #IdeSnippet to be inserted when expanding this completion item.
  *
  * Returns: (transfer full): An #IdeSnippet.
+ *
+ * Since: 3.32
  */
 IdeSnippet *
 ide_clang_completion_item_get_snippet (IdeClangCompletionItem *self,
@@ -454,6 +459,8 @@ ide_clang_completion_item_get_snippet (IdeClangCompletionItem *self,
  * The @keyword parameter is not copied, it is expected to be valid
  * string found within @variant (and therefore associated with its
  * life-cycle).
+ *
+ * Since: 3.32
  */
 IdeClangCompletionItem *
 ide_clang_completion_item_new (GVariant    *variant,
diff --git a/src/plugins/clang/ide-clang-completion-item.h b/src/plugins/clang/ide-clang-completion-item.h
index 302a927f9..4b32a9667 100644
--- a/src/plugins/clang/ide-clang-completion-item.h
+++ b/src/plugins/clang/ide-clang-completion-item.h
@@ -1,6 +1,6 @@
 /* ide-clang-completion-item.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,14 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
+#include <libide-sourceview.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/clang/ide-clang-completion-provider.c 
b/src/plugins/clang/ide-clang-completion-provider.c
index 445a39190..c6fa63f89 100644
--- a/src/plugins/clang/ide-clang-completion-provider.c
+++ b/src/plugins/clang/ide-clang-completion-provider.c
@@ -1,6 +1,6 @@
 /* ide-clang-completion-provider.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-clang-completion-provider"
@@ -101,7 +103,7 @@ ide_clang_completion_provider_key_activates (IdeCompletionProvider *provider,
   IdeClangCompletionItem *item = IDE_CLANG_COMPLETION_ITEM (proposal);
 
   /* Try to dereference field/variable */
-  if (item->kind == IDE_SYMBOL_FIELD || item->kind == IDE_SYMBOL_VARIABLE)
+  if (item->kind == IDE_SYMBOL_KIND_FIELD || item->kind == IDE_SYMBOL_KIND_VARIABLE)
     return key->keyval == GDK_KEY_period;
 
 #if 0
@@ -127,9 +129,9 @@ ide_clang_completion_provider_activate_proposal (IdeCompletionProvider *provider
   IdeClangCompletionItem *item = (IdeClangCompletionItem *)proposal;
   g_autofree gchar *word = NULL;
   g_autoptr(IdeSnippet) snippet = NULL;
+  IdeFileSettings *file_settings;
   GtkTextBuffer *buffer;
   GtkTextView *view;
-  IdeFile *file;
   GtkTextIter begin, end;
 
   g_assert (IDE_IS_CLANG_COMPLETION_PROVIDER (provider));
@@ -142,8 +144,7 @@ ide_clang_completion_provider_activate_proposal (IdeCompletionProvider *provider
   g_assert (IDE_IS_BUFFER (buffer));
   g_assert (IDE_IS_SOURCE_VIEW (view));
 
-  file = ide_buffer_get_file (IDE_BUFFER (buffer));
-  g_assert (IDE_IS_FILE (file));
+  file_settings = ide_buffer_get_file_settings (IDE_BUFFER (buffer));
 
   /*
    * If the typed text matches the typed text of the item, and the user
@@ -153,7 +154,7 @@ ide_clang_completion_provider_activate_proposal (IdeCompletionProvider *provider
     {
       if ((word = ide_completion_context_get_word (context)))
         {
-          if (dzl_str_equal0 (word, item->typed_text))
+          if (ide_str_equal0 (word, item->typed_text))
             {
               ide_completion_context_get_bounds (context, &begin, &end);
               gtk_text_buffer_insert (buffer, &end, "\n", -1);
@@ -168,13 +169,13 @@ ide_clang_completion_provider_activate_proposal (IdeCompletionProvider *provider
   if (ide_completion_context_get_bounds (context, &begin, &end))
     gtk_text_buffer_delete (buffer, &begin, &end);
 
-  snippet = ide_clang_completion_item_get_snippet (item, ide_file_peek_settings (file));
+  snippet = ide_clang_completion_item_get_snippet (item, file_settings);
 
   /*
    * If we are completing field or variable types, we might want to add
    * a . or -> to the snippet based on the input character.
    */
-  if (item->kind == IDE_SYMBOL_FIELD || item->kind == IDE_SYMBOL_VARIABLE)
+  if (item->kind == IDE_SYMBOL_KIND_FIELD || item->kind == IDE_SYMBOL_KIND_VARIABLE)
     {
       if (key->keyval == GDK_KEY_period || key->keyval == GDK_KEY_minus)
         {
@@ -236,12 +237,12 @@ ide_clang_completion_provider_load (IdeCompletionProvider *provider,
                                     IdeContext            *context)
 {
   IdeClangCompletionProvider *self = (IdeClangCompletionProvider *)provider;
-  IdeClangClient *client;
+  g_autoptr(IdeClangClient) client = NULL;
 
   g_assert (IDE_IS_CLANG_COMPLETION_PROVIDER (self));
   g_assert (IDE_IS_CONTEXT (context));
 
-  client = ide_context_get_service_typed (context, IDE_TYPE_CLANG_CLIENT);
+  client = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_CLANG_CLIENT);
   g_set_object (&self->client, client);
 }
 
diff --git a/src/plugins/clang/ide-clang-completion-provider.h 
b/src/plugins/clang/ide-clang-completion-provider.h
index d40e19b86..408ad51cf 100644
--- a/src/plugins/clang/ide-clang-completion-provider.h
+++ b/src/plugins/clang/ide-clang-completion-provider.h
@@ -1,6 +1,6 @@
 /* ide-clang-completion-provider.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/clang/ide-clang-diagnostic-provider.c 
b/src/plugins/clang/ide-clang-diagnostic-provider.c
index a86f54f5e..e5d9fb256 100644
--- a/src/plugins/clang/ide-clang-diagnostic-provider.c
+++ b/src/plugins/clang/ide-clang-diagnostic-provider.c
@@ -1,6 +1,6 @@
 /* ide-clang-diagnostic-provider.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,14 @@
  *
  * 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 "ide-clang-diagnostic-provider"
 
 #include <glib/gi18n.h>
+#include <libide-foundry.h>
 
 #include "ide-clang-client.h"
 #include "ide-clang-diagnostic-provider.h"
@@ -59,8 +62,8 @@ diagnose_get_build_flags_cb (GObject      *object,
 {
   IdeBuildSystem *build_system = (IdeBuildSystem *)object;
   g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeClangClient) client = NULL;
   g_auto(GStrv) flags = NULL;
-  IdeClangClient *client;
   GCancellable *cancellable;
   IdeContext *context;
   GFile *file;
@@ -71,7 +74,7 @@ diagnose_get_build_flags_cb (GObject      *object,
 
   flags = ide_build_system_get_build_flags_finish (build_system, result, NULL);
   context = ide_object_get_context (IDE_OBJECT (build_system));
-  client = ide_context_get_service_typed (context, IDE_TYPE_CLANG_CLIENT);
+  client = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_CLANG_CLIENT);
   file = ide_task_get_task_data (task);
   cancellable = ide_task_get_cancellable (task);
 
@@ -85,8 +88,9 @@ diagnose_get_build_flags_cb (GObject      *object,
 
 static void
 ide_clang_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
-                                              IdeFile               *file,
-                                              IdeBuffer             *buffer,
+                                              GFile                 *file,
+                                              GBytes                *contents,
+                                              const gchar           *lang_id,
                                               GCancellable          *cancellable,
                                               GAsyncReadyCallback    callback,
                                               gpointer               user_data)
@@ -95,18 +99,16 @@ ide_clang_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
   g_autoptr(IdeTask) task = NULL;
   IdeBuildSystem *build_system;
   IdeContext *context;
-  GFile *gfile;
-
-  g_return_if_fail (IDE_IS_CLANG_DIAGNOSTIC_PROVIDER (self));
 
-  gfile = ide_file_get_file (file);
+  g_assert (IDE_IS_CLANG_DIAGNOSTIC_PROVIDER (self));
+  g_assert (IDE_IS_CLANG_DIAGNOSTIC_PROVIDER (self));
 
   task = ide_task_new (self, cancellable, callback, user_data);
-  ide_task_set_task_data (task, g_object_ref (gfile), g_object_unref);
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
   ide_task_set_kind (task, IDE_TASK_KIND_COMPILER);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
 
   ide_build_system_get_build_flags_async (build_system,
                                           file,
diff --git a/src/plugins/clang/ide-clang-diagnostic-provider.h 
b/src/plugins/clang/ide-clang-diagnostic-provider.h
index 35dc9bc52..d11314769 100644
--- a/src/plugins/clang/ide-clang-diagnostic-provider.h
+++ b/src/plugins/clang/ide-clang-diagnostic-provider.h
@@ -1,6 +1,6 @@
 /* ide-clang-diagnostic-provider.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/clang/ide-clang-highlighter.c b/src/plugins/clang/ide-clang-highlighter.c
index b48064bbd..3bcb60f24 100644
--- a/src/plugins/clang/ide-clang-highlighter.c
+++ b/src/plugins/clang/ide-clang-highlighter.c
@@ -1,6 +1,6 @@
 /* ide-clang-highlighter.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,9 +14,16 @@
  *
  * 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 "ide-clang-highlighter"
+
+#include "config.h"
+
 #include <glib/gi18n.h>
+#include <libide-foundry.h>
 
 #include "ide-clang-client.h"
 #include "ide-clang-highlighter.h"
@@ -111,12 +118,12 @@ get_index_flags_cb (GObject      *object,
 {
   IdeBuildSystem *build_system = (IdeBuildSystem *)object;
   g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeClangClient) client = NULL;
   g_autoptr(GError) error = NULL;
   g_auto(GStrv) flags = NULL;
-  IdeClangClient *client;
   GCancellable *cancellable;
   IdeContext *context;
-  IdeFile *file;
+  GFile *file;
 
   g_assert (IDE_IS_BUILD_SYSTEM (build_system));
   g_assert (G_IS_ASYNC_RESULT (result));
@@ -124,12 +131,12 @@ get_index_flags_cb (GObject      *object,
 
   flags = ide_build_system_get_build_flags_finish (build_system, result, &error);
   context = ide_object_get_context (IDE_OBJECT (build_system));
-  client = ide_context_get_service_typed (context, IDE_TYPE_CLANG_CLIENT);
+  client = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_CLANG_CLIENT);
   file = ide_task_get_task_data (task);
   cancellable = ide_task_get_cancellable (task);
 
   ide_clang_client_get_highlight_index_async (client,
-                                              ide_file_get_file (file),
+                                              file,
                                               (const gchar * const *)flags,
                                               cancellable,
                                               get_highlight_index_cb,
@@ -307,11 +314,11 @@ static gboolean
 ide_clang_highlighter_do_update (IdeClangHighlighter *self)
 {
   g_autoptr(IdeTask) task = NULL;
+  g_autoptr(IdeClangClient) client = NULL;
   IdeBuildSystem *build_system;
-  IdeClangClient *client;
   IdeContext *context;
   IdeBuffer *buffer;
-  IdeFile *file;
+  GFile *file;
 
   g_assert (IDE_IS_CLANG_HIGHLIGHTER (self));
 
@@ -321,14 +328,14 @@ ide_clang_highlighter_do_update (IdeClangHighlighter *self)
       !(buffer = ide_highlight_engine_get_buffer (self->engine)) ||
       !(file = ide_buffer_get_file (buffer)) ||
       !(context = ide_object_get_context (IDE_OBJECT (self))) ||
-      !(client = ide_context_get_service_typed (context, IDE_TYPE_CLANG_CLIENT)))
+      !(client = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_CLANG_CLIENT)))
     return G_SOURCE_REMOVE;
 
   task = ide_task_new (self, NULL, NULL, NULL);
   ide_task_set_source_tag (task, ide_clang_highlighter_get_index);
   ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
 
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
 
   ide_build_system_get_build_flags_async (build_system,
                                           file,
diff --git a/src/plugins/clang/ide-clang-highlighter.h b/src/plugins/clang/ide-clang-highlighter.h
index 712f37087..6c7a64b2c 100644
--- a/src/plugins/clang/ide-clang-highlighter.h
+++ b/src/plugins/clang/ide-clang-highlighter.h
@@ -1,6 +1,6 @@
 /* ide-clang-highlighter.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/clang/ide-clang-preferences-addin.c b/src/plugins/clang/ide-clang-preferences-addin.c
index fbe2db9f7..06571beb5 100644
--- a/src/plugins/clang/ide-clang-preferences-addin.c
+++ b/src/plugins/clang/ide-clang-preferences-addin.c
@@ -1,6 +1,6 @@
 /* ide-clang-preferences-addin.c
  *
- * Copyright 2016 Christian Hergert <christian hergert me>
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,10 +14,13 @@
  *
  * 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
  */
 
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-code.h>
+#include <libide-gui.h>
 
 #include "ide-clang-preferences-addin.h"
 
diff --git a/src/plugins/clang/ide-clang-preferences-addin.h b/src/plugins/clang/ide-clang-preferences-addin.h
index 9d4075865..3eb42b8a9 100644
--- a/src/plugins/clang/ide-clang-preferences-addin.h
+++ b/src/plugins/clang/ide-clang-preferences-addin.h
@@ -1,6 +1,6 @@
 /* ide-clang-preferences-addin.h
  *
- * Copyright 2016 Christian Hergert <christian hergert me>
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/clang/ide-clang-proposals.c b/src/plugins/clang/ide-clang-proposals.c
index e4ca89a90..9106c9941 100644
--- a/src/plugins/clang/ide-clang-proposals.c
+++ b/src/plugins/clang/ide-clang-proposals.c
@@ -1,6 +1,6 @@
 /* ide-clang-proposals.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,19 +14,24 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-clang-proposals"
 
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-foundry.h>
+#include <libide-sourceview.h>
 #include <clang-c/Index.h>
 
+#include "ide-buffer-private.h"
+
 #include "ide-clang-completion-item.h"
 #include "ide-clang-proposals.h"
 
-#include "sourceview/ide-text-iter.h"
-
 struct _IdeClangProposals
 {
   GObject parent_instance;
@@ -214,7 +219,7 @@ ide_clang_proposals_class_init (IdeClangProposalsClass *klass)
                          "The client to the clang worker process",
                          IDE_TYPE_CLANG_CLIENT,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-  
+
   g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
@@ -512,7 +517,7 @@ ide_clang_proposals_query_build_flags_cb (GObject      *object,
 
 static void
 ide_clang_proposals_query_async (IdeClangProposals   *self,
-                                 IdeFile             *file,
+                                 GFile               *file,
                                  guint                line,
                                  guint                column,
                                  GCancellable        *cancellable,
@@ -525,15 +530,15 @@ ide_clang_proposals_query_async (IdeClangProposals   *self,
   Query *q;
 
   g_assert (IDE_IS_CLANG_PROPOSALS (self));
-  g_assert (IDE_IS_FILE (file));
+  g_assert (G_IS_FILE (file));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self->client));
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
 
   q = g_slice_new0 (Query);
   q->client = g_object_ref (self->client);
-  q->file = g_object_ref (ide_file_get_file (file));
+  q->file = g_object_ref (file);
   q->line = line;
   q->column = column;
   q->query_id = ++self->query_id;
@@ -611,7 +616,7 @@ ide_clang_proposals_populate_async (IdeClangProposals   *self,
   GtkTextBuffer *buffer;
   GtkTextIter begin;
   GtkTextIter previous;
-  IdeFile *file;
+  GFile *file;
 
   IDE_ENTRY;
 
@@ -653,7 +658,7 @@ ide_clang_proposals_populate_async (IdeClangProposals   *self,
     }
 
   /* Unlikely, but is this the exact same query as before? */
-  if (dzl_str_equal0 (self->filter, word))
+  if (ide_str_equal0 (self->filter, word))
     {
       ide_task_return_boolean (task, TRUE);
       IDE_EXIT;
@@ -685,7 +690,7 @@ ide_clang_proposals_populate_async (IdeClangProposals   *self,
 
 query_client:
 
-  ide_buffer_sync_to_unsaved_files (IDE_BUFFER (buffer));
+  _ide_buffer_sync_to_unsaved_files (IDE_BUFFER (buffer));
   file = ide_buffer_get_file (IDE_BUFFER (buffer));
 
   prev_cancellable = g_steal_pointer (&self->cancellable);
diff --git a/src/plugins/clang/ide-clang-proposals.h b/src/plugins/clang/ide-clang-proposals.h
index 784e7dd99..b2bc677be 100644
--- a/src/plugins/clang/ide-clang-proposals.h
+++ b/src/plugins/clang/ide-clang-proposals.h
@@ -1,6 +1,6 @@
 /* ide-clang-proposals.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 #include "ide-clang-client.h"
 
diff --git a/src/plugins/clang/ide-clang-rename-provider.c b/src/plugins/clang/ide-clang-rename-provider.c
index 80708e692..7ccc7cb58 100644
--- a/src/plugins/clang/ide-clang-rename-provider.c
+++ b/src/plugins/clang/ide-clang-rename-provider.c
@@ -1,6 +1,6 @@
 /* ide-clang-rename-provider.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,17 @@
  *
  * 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 "ide-clang-rename-provider"
+
 #include "config.h"
 
-#define G_LOG_DOMAIN "ide-clang-rename-provider"
+#include <libide-code.h>
+#include <libide-foundry.h>
+#include <libide-vcs.h>
 
 #include "ide-clang-rename-provider.h"
 
@@ -43,10 +49,10 @@ ide_clang_rename_provider_communicate_cb (GObject      *object,
 {
   IdeSubprocess *subprocess = (IdeSubprocess *)object;
   g_autoptr(IdeTask) task = user_data;
-  g_autoptr(IdeProjectEdit) edit = NULL;
-  g_autoptr(IdeSourceLocation) begin = NULL;
-  g_autoptr(IdeSourceLocation) end = NULL;
-  g_autoptr(IdeSourceRange) range = NULL;
+  g_autoptr(IdeTextEdit) edit = NULL;
+  g_autoptr(IdeLocation) begin = NULL;
+  g_autoptr(IdeLocation) end = NULL;
+  g_autoptr(IdeRange) range = NULL;
   g_autoptr(GError) error = NULL;
   g_autoptr(GPtrArray) edits = NULL;
   g_autofree gchar *stdout_buf = NULL;
@@ -68,7 +74,7 @@ ide_clang_rename_provider_communicate_cb (GObject      *object,
   if (ide_task_return_error_if_cancelled (task))
     IDE_EXIT;
 
-  if (dzl_str_empty0 (stdout_buf) || (stdout_buf[0] == '\n' && stdout_buf[1] == 0))
+  if (ide_str_empty0 (stdout_buf) || (stdout_buf[0] == '\n' && stdout_buf[1] == 0))
     {
       /* Don't allow deleting the buffer contents */
       ide_task_return_new_error (task,
@@ -98,17 +104,14 @@ ide_clang_rename_provider_communicate_cb (GObject      *object,
   gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (buffer), &begin_iter, &end_iter);
   begin = ide_buffer_get_iter_location (buffer, &begin_iter);
   end = ide_buffer_get_iter_location (buffer, &end_iter);
-  range = ide_source_range_new (begin, end);
+  range = ide_range_new (begin, end);
 
   /*
    * We just get the single replacement buffer from clang-rename instead
-   * of individual file-edits, so create IdeProjectEdit to reflect that.
+   * of individual file-edits, so create IdeTextEdit to reflect that.
    */
 
-  edit = ide_project_edit_new ();
-  ide_project_edit_set_range (edit, range);
-  ide_project_edit_set_replacement (edit, stdout_buf);
-
+  edit = ide_text_edit_new (range, stdout_buf);
   edits = g_ptr_array_new_full (1, g_object_unref);
   g_ptr_array_add (edits, g_steal_pointer (&edit));
 
@@ -119,7 +122,7 @@ ide_clang_rename_provider_communicate_cb (GObject      *object,
 
 static void
 ide_clang_rename_provider_rename_async (IdeRenameProvider   *provider,
-                                        IdeSourceLocation   *location,
+                                        IdeLocation         *location,
                                         const gchar         *new_name,
                                         GCancellable        *cancellable,
                                         GAsyncReadyCallback  callback,
@@ -137,8 +140,7 @@ ide_clang_rename_provider_rename_async (IdeRenameProvider   *provider,
   IdeBuildManager *build_manager;
   const gchar *builddir = NULL;
   IdeContext *context;
-  IdeFile *file;
-  GFile *gfile;
+  GFile *file;
   guint offset;
 
   IDE_ENTRY;
@@ -146,7 +148,7 @@ ide_clang_rename_provider_rename_async (IdeRenameProvider   *provider,
   g_assert (IDE_IS_CLANG_RENAME_PROVIDER (self));
   g_assert (IDE_IS_BUFFER (self->buffer));
   g_assert (location != NULL);
-  g_assert (!dzl_str_empty0 (new_name));
+  g_assert (!ide_str_empty0 (new_name));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   /* TODO: For build systems that don't support compile_commands.json,
@@ -159,13 +161,12 @@ ide_clang_rename_provider_rename_async (IdeRenameProvider   *provider,
   ide_task_set_task_data (task, g_object_ref (self->buffer), g_object_unref);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   if ((pipeline = ide_build_manager_get_pipeline (build_manager)))
     builddir = ide_build_pipeline_get_builddir (pipeline);
 
-  file = ide_source_location_get_file (location);
-  gfile = ide_file_get_file (file);
-  path = g_file_get_path (gfile);
+  file = ide_location_get_file (location);
+  path = g_file_get_path (file);
 
   if (path == NULL)
     {
@@ -176,7 +177,7 @@ ide_clang_rename_provider_rename_async (IdeRenameProvider   *provider,
       IDE_EXIT;
     }
 
-  offset = ide_source_location_get_offset (location);
+  offset = ide_location_get_offset (location);
 
   position_arg = g_strdup_printf ("-offset=%u", offset);
   new_name_arg = g_strdup_printf ("-new-name=%s", new_name);
@@ -187,8 +188,8 @@ ide_clang_rename_provider_rename_async (IdeRenameProvider   *provider,
     ide_subprocess_launcher_set_cwd (launcher, builddir);
   else
     {
-      IdeVcs *vcs = ide_context_get_vcs (context);
-      GFile *workdir = ide_vcs_get_working_directory (vcs);
+      IdeVcs *vcs = ide_vcs_from_context (context);
+      GFile *workdir = ide_vcs_get_workdir (vcs);
       g_autofree gchar *srcdir = g_file_get_path (workdir);
 
       /* fallback to srcdir */
diff --git a/src/plugins/clang/ide-clang-rename-provider.h b/src/plugins/clang/ide-clang-rename-provider.h
index f344d6970..dde3de58e 100644
--- a/src/plugins/clang/ide-clang-rename-provider.h
+++ b/src/plugins/clang/ide-clang-rename-provider.h
@@ -1,6 +1,6 @@
 /* ide-clang-rename-provider.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/clang/ide-clang-symbol-node.c b/src/plugins/clang/ide-clang-symbol-node.c
index 5a1a8abc8..92cbe32d6 100644
--- a/src/plugins/clang/ide-clang-symbol-node.c
+++ b/src/plugins/clang/ide-clang-symbol-node.c
@@ -1,6 +1,6 @@
 /* ide-clang-symbol-node.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-clang-symbol-node"
@@ -33,15 +35,13 @@ struct _IdeClangSymbolNode
 G_DEFINE_TYPE (IdeClangSymbolNode, ide_clang_symbol_node, IDE_TYPE_SYMBOL_NODE)
 
 IdeSymbolNode *
-ide_clang_symbol_node_new (IdeContext *context,
-                           GVariant   *node)
+ide_clang_symbol_node_new (GVariant *node)
 {
   g_autoptr(IdeSymbol) symbol = NULL;
   g_autoptr(GVariant) children = NULL;
   IdeClangSymbolNode *self;
   const gchar *name;
 
-  g_return_val_if_fail (!context || IDE_IS_CONTEXT (context), NULL);
   g_return_val_if_fail (node != NULL, NULL);
 
   if (!(symbol = ide_symbol_new_from_variant (node)))
@@ -50,10 +50,9 @@ ide_clang_symbol_node_new (IdeContext *context,
   name = ide_symbol_get_name (symbol);
 
   self = g_object_new (IDE_TYPE_CLANG_SYMBOL_NODE,
-                       "context", context,
                        "kind", ide_symbol_get_kind (symbol),
                        "flags", ide_symbol_get_flags (symbol),
-                       "name", dzl_str_empty0 (name) ? _("anonymous") : name,
+                       "name", ide_str_empty0 (name) ? _("anonymous") : name,
                        NULL);
 
   self->symbol = g_steal_pointer (&symbol);
@@ -78,7 +77,7 @@ ide_clang_symbol_node_get_location_async (IdeSymbolNode       *symbol_node,
 {
   IdeClangSymbolNode *self = (IdeClangSymbolNode *)symbol_node;
   g_autoptr(IdeTask) task = NULL;
-  IdeSourceLocation *location;
+  IdeLocation *location;
 
   g_return_if_fail (IDE_IS_CLANG_SYMBOL_NODE (self));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
@@ -88,20 +87,19 @@ ide_clang_symbol_node_get_location_async (IdeSymbolNode       *symbol_node,
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
   if (self->symbol == NULL ||
-      (!(location = ide_symbol_get_definition_location (self->symbol)) &&
-       !(location = ide_symbol_get_declaration_location (self->symbol)) &&
-       !(location = ide_symbol_get_canonical_location (self->symbol))))
+      (!(location = ide_symbol_get_location (self->symbol)) &&
+       !(location = ide_symbol_get_header_location (self->symbol))))
     ide_task_return_new_error (task,
                                G_IO_ERROR,
                                G_IO_ERROR_NOT_FOUND,
                                "Failed to locate location for symbol");
   else
     ide_task_return_pointer (task,
-                             ide_source_location_ref (location),
-                             (GDestroyNotify) ide_source_location_unref);
+                             g_object_ref (location),
+                             g_object_unref);
 }
 
-static IdeSourceLocation *
+static IdeLocation *
 ide_clang_symbol_node_get_location_finish (IdeSymbolNode  *symbol_node,
                                            GAsyncResult   *result,
                                            GError        **error)
@@ -117,7 +115,7 @@ ide_clang_symbol_node_finalize (GObject *object)
 {
   IdeClangSymbolNode *self = (IdeClangSymbolNode *)object;
 
-  g_clear_pointer (&self->symbol, ide_symbol_unref);
+  g_clear_object (&self->symbol);
   g_clear_pointer (&self->children, g_variant_unref);
 
   G_OBJECT_CLASS (ide_clang_symbol_node_parent_class)->finalize (object);
@@ -153,15 +151,13 @@ ide_clang_symbol_node_get_nth_child (IdeClangSymbolNode *self,
                                      guint               nth)
 {
   g_autoptr(GVariant) child = NULL;
-  IdeContext *context;
 
   g_return_val_if_fail (IDE_IS_CLANG_SYMBOL_NODE (self), NULL);
 
   if (self->children == NULL || g_variant_n_children (self->children) <= nth)
     return NULL;
 
-  context = ide_object_get_context (IDE_OBJECT (self));
   child = g_variant_get_child_value (self->children, nth);
 
-  return ide_clang_symbol_node_new (context, child);
+  return ide_clang_symbol_node_new (child);
 }
diff --git a/src/plugins/clang/ide-clang-symbol-node.h b/src/plugins/clang/ide-clang-symbol-node.h
index b18b02fca..6966a8b37 100644
--- a/src/plugins/clang/ide-clang-symbol-node.h
+++ b/src/plugins/clang/ide-clang-symbol-node.h
@@ -1,6 +1,6 @@
 /* ide-clang-symbol-node.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
@@ -26,8 +28,7 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeClangSymbolNode, ide_clang_symbol_node, IDE, CLANG_SYMBOL_NODE, IdeSymbolNode)
 
-IdeSymbolNode *ide_clang_symbol_node_new            (IdeContext         *context,
-                                                     GVariant           *variant);
+IdeSymbolNode *ide_clang_symbol_node_new            (GVariant           *variant);
 guint          ide_clang_symbol_node_get_n_children (IdeClangSymbolNode *self);
 IdeSymbolNode *ide_clang_symbol_node_get_nth_child  (IdeClangSymbolNode *self,
                                                      guint               nth);
diff --git a/src/plugins/clang/ide-clang-symbol-resolver.c b/src/plugins/clang/ide-clang-symbol-resolver.c
index 0601c594c..33bf47314 100644
--- a/src/plugins/clang/ide-clang-symbol-resolver.c
+++ b/src/plugins/clang/ide-clang-symbol-resolver.c
@@ -1,6 +1,6 @@
 /* ide-clang-symbol-resolver.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,10 +14,16 @@
  *
  * 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 "clang-symbol-resolver"
 
+#include "config.h"
+
+#include <libide-foundry.h>
+
 #include "ide-clang-client.h"
 #include "ide-clang-symbol-resolver.h"
 
@@ -52,7 +58,7 @@ ide_clang_symbol_resolver_lookup_symbol_cb (GObject      *object,
   else
     ide_task_return_pointer (task,
                              g_steal_pointer (&symbol),
-                             (GDestroyNotify) ide_symbol_unref);
+                             g_object_unref);
 
   IDE_EXIT;
 }
@@ -64,13 +70,12 @@ lookup_symbol_flags_cb (GObject      *object,
 {
   IdeBuildSystem *build_system = (IdeBuildSystem *)object;
   g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeClangClient) client = NULL;
   g_auto(GStrv) flags = NULL;
-  IdeSourceLocation *location;
-  IdeClangClient *client;
+  IdeLocation *location;
   GCancellable *cancellable;
   IdeContext *context;
-  IdeFile *file;
-  GFile *gfile;
+  GFile *file;
   guint line;
   guint column;
 
@@ -82,16 +87,15 @@ lookup_symbol_flags_cb (GObject      *object,
 
   flags = ide_build_system_get_build_flags_finish (build_system, result, NULL);
   context = ide_object_get_context (IDE_OBJECT (build_system));
-  client = ide_context_get_service_typed (context, IDE_TYPE_CLANG_CLIENT);
+  client = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_CLANG_CLIENT);
   cancellable = ide_task_get_cancellable (task);
   location = ide_task_get_task_data (task);
-  file = ide_source_location_get_file (location);
-  gfile = ide_file_get_file (file);
-  line = ide_source_location_get_line (location);
-  column = ide_source_location_get_line_offset (location);
+  file = ide_location_get_file (location);
+  line = ide_location_get_line (location);
+  column = ide_location_get_line_offset (location);
 
   ide_clang_client_locate_symbol_async (client,
-                                        gfile,
+                                        file,
                                         (const gchar * const *)flags,
                                         line + 1,
                                         column + 1,
@@ -104,7 +108,7 @@ lookup_symbol_flags_cb (GObject      *object,
 
 static void
 ide_clang_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolver,
-                                               IdeSourceLocation   *location,
+                                               IdeLocation   *location,
                                                GCancellable        *cancellable,
                                                GAsyncReadyCallback  callback,
                                                gpointer             user_data)
@@ -113,7 +117,7 @@ ide_clang_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolver,
   g_autoptr(IdeTask) task = NULL;
   IdeBuildSystem *build_system;
   IdeContext *context;
-  IdeFile *file;
+  GFile *file;
 
   IDE_ENTRY;
 
@@ -125,12 +129,12 @@ ide_clang_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolver,
   ide_task_set_priority (task, G_PRIORITY_LOW);
   ide_task_set_source_tag (task, ide_clang_symbol_resolver_lookup_symbol_async);
   ide_task_set_task_data (task,
-                          ide_source_location_ref (location),
-                          ide_source_location_unref);
+                          g_object_ref (location),
+                          g_object_unref);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_system = ide_context_get_build_system (context);
-  file = ide_source_location_get_file (location);
+  build_system = ide_build_system_from_context (context);
+  file = ide_location_get_file (location);
 
   ide_build_system_get_build_flags_async (build_system,
                                           file,
@@ -189,8 +193,8 @@ get_symbol_tree_flags_cb (GObject      *object,
 {
   IdeBuildSystem *build_system = (IdeBuildSystem *)object;
   g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeClangClient) client = NULL;
   g_auto(GStrv) flags = NULL;
-  IdeClangClient *client;
   GCancellable *cancellable;
   IdeContext *context;
   GFile *file;
@@ -201,7 +205,7 @@ get_symbol_tree_flags_cb (GObject      *object,
 
   flags = ide_build_system_get_build_flags_finish (build_system, result, NULL);
   context = ide_object_get_context (IDE_OBJECT (build_system));
-  client = ide_context_get_service_typed (context, IDE_TYPE_CLANG_CLIENT);
+  client = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_CLANG_CLIENT);
   cancellable = ide_task_get_cancellable (task);
   file = ide_task_get_task_data (task);
 
@@ -216,14 +220,13 @@ get_symbol_tree_flags_cb (GObject      *object,
 static void
 ide_clang_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *resolver,
                                                  GFile               *file,
-                                                 IdeBuffer           *buffer,
+                                                 GBytes              *content,
                                                  GCancellable        *cancellable,
                                                  GAsyncReadyCallback  callback,
                                                  gpointer             user_data)
 {
   IdeClangSymbolResolver *self = (IdeClangSymbolResolver *)resolver;
   g_autoptr(IdeTask) task = NULL;
-  g_autoptr(IdeFile) ifile = NULL;
   IdeBuildSystem *build_system;
   IdeContext *context;
 
@@ -231,7 +234,6 @@ ide_clang_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *resolver,
 
   g_return_if_fail (IDE_IS_CLANG_SYMBOL_RESOLVER (self));
   g_return_if_fail (G_IS_FILE (file));
-  g_return_if_fail (IDE_IS_BUFFER (buffer));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
@@ -240,11 +242,10 @@ ide_clang_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *resolver,
   ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_system = ide_context_get_build_system (context);
-  ifile = ide_file_new (context, file);
+  build_system = ide_build_system_from_context (context);
 
   ide_build_system_get_build_flags_async (build_system,
-                                          ifile,
+                                          file,
                                           cancellable,
                                           get_symbol_tree_flags_cb,
                                           g_steal_pointer (&task));
@@ -288,7 +289,7 @@ ide_clang_symbol_resolver_find_scope_cb (GObject      *object,
   else
     ide_task_return_pointer (task,
                              g_steal_pointer (&symbol),
-                             (GDestroyNotify)ide_symbol_unref);
+                             g_object_unref);
 }
 
 static void
@@ -298,13 +299,12 @@ find_nearest_scope_flags_cb (GObject      *object,
 {
   IdeBuildSystem *build_system = (IdeBuildSystem *)object;
   g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeClangClient) client = NULL;
   g_auto(GStrv) flags = NULL;
-  IdeSourceLocation *location;
-  IdeClangClient *client;
+  IdeLocation *location;
   GCancellable *cancellable;
   IdeContext *context;
-  IdeFile *file;
-  GFile *gfile;
+  GFile *file;
   guint line;
   guint column;
 
@@ -314,16 +314,15 @@ find_nearest_scope_flags_cb (GObject      *object,
 
   flags = ide_build_system_get_build_flags_finish (build_system, result, NULL);
   context = ide_object_get_context (IDE_OBJECT (build_system));
-  client = ide_context_get_service_typed (context, IDE_TYPE_CLANG_CLIENT);
+  client = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_CLANG_CLIENT);
   cancellable = ide_task_get_cancellable (task);
   location = ide_task_get_task_data (task);
-  file = ide_source_location_get_file (location);
-  gfile = ide_file_get_file (file);
-  line = ide_source_location_get_line (location);
-  column = ide_source_location_get_line_offset (location);
+  file = ide_location_get_file (location);
+  line = ide_location_get_line (location);
+  column = ide_location_get_line_offset (location);
 
   ide_clang_client_find_nearest_scope_async (client,
-                                             gfile,
+                                             file,
                                              (const gchar * const *)flags,
                                              line + 1,
                                              column + 1,
@@ -334,7 +333,7 @@ find_nearest_scope_flags_cb (GObject      *object,
 
 static void
 ide_clang_symbol_resolver_find_nearest_scope_async (IdeSymbolResolver   *symbol_resolver,
-                                                    IdeSourceLocation   *location,
+                                                    IdeLocation   *location,
                                                     GCancellable        *cancellable,
                                                     GAsyncReadyCallback  callback,
                                                     gpointer             user_data)
@@ -343,7 +342,7 @@ ide_clang_symbol_resolver_find_nearest_scope_async (IdeSymbolResolver   *symbol_
   g_autoptr(IdeTask) task = NULL;
   IdeBuildSystem *build_system;
   IdeContext *context;
-  IdeFile *file;
+  GFile *file;
 
   IDE_ENTRY;
 
@@ -354,12 +353,12 @@ ide_clang_symbol_resolver_find_nearest_scope_async (IdeSymbolResolver   *symbol_
   ide_task_set_priority (task, G_PRIORITY_LOW);
   ide_task_set_source_tag (task, ide_clang_symbol_resolver_find_nearest_scope_async);
   ide_task_set_task_data (task,
-                          ide_source_location_ref (location),
-                          ide_source_location_unref);
+                          g_object_ref (location),
+                          g_object_unref);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_system = ide_context_get_build_system (context);
-  file = ide_source_location_get_file (location);
+  build_system = ide_build_system_from_context (context);
+  file = ide_location_get_file (location);
 
   ide_build_system_get_build_flags_async (build_system,
                                           file,
diff --git a/src/plugins/clang/ide-clang-symbol-resolver.h b/src/plugins/clang/ide-clang-symbol-resolver.h
index 0bfc2a2c4..d545b7776 100644
--- a/src/plugins/clang/ide-clang-symbol-resolver.h
+++ b/src/plugins/clang/ide-clang-symbol-resolver.h
@@ -1,6 +1,6 @@
 /* ide-clang-symbol-resolver.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/clang/ide-clang-symbol-tree.c b/src/plugins/clang/ide-clang-symbol-tree.c
index 7c711f45b..82e369266 100644
--- a/src/plugins/clang/ide-clang-symbol-tree.c
+++ b/src/plugins/clang/ide-clang-symbol-tree.c
@@ -1,6 +1,6 @@
 /* ide-clang-symbol-tree.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-clang-symbol-tree"
@@ -33,7 +35,7 @@ struct _IdeClangSymbolTree
 
 static void symbol_tree_iface_init (IdeSymbolTreeInterface *iface);
 
-G_DEFINE_TYPE_WITH_CODE (IdeClangSymbolTree, ide_clang_symbol_tree, IDE_TYPE_OBJECT,
+G_DEFINE_TYPE_WITH_CODE (IdeClangSymbolTree, ide_clang_symbol_tree, G_TYPE_OBJECT,
                          G_IMPLEMENT_INTERFACE (IDE_TYPE_SYMBOL_TREE, symbol_tree_iface_init))
 
 enum {
@@ -51,6 +53,8 @@ static GParamSpec *properties [N_PROPS];
  * Gets the #IdeClangSymbolTree:file property.
  *
  * Returns: (transfer none): a #GFile.
+ *
+ * Since: 3.32
  */
 GFile *
 ide_clang_symbol_tree_get_file (IdeClangSymbolTree *self)
@@ -83,7 +87,6 @@ ide_clang_symbol_tree_get_nth_child (IdeSymbolTree *symbol_tree,
   IdeClangSymbolTree *self = (IdeClangSymbolTree *)symbol_tree;
   g_autoptr(GVariant) node = NULL;
   IdeSymbolNode *ret;
-  IdeContext *context;
 
   g_assert (IDE_IS_CLANG_SYMBOL_TREE (self));
   g_assert (!parent || IDE_IS_CLANG_SYMBOL_NODE (parent));
@@ -97,9 +100,8 @@ ide_clang_symbol_tree_get_nth_child (IdeSymbolTree *symbol_tree,
   if (nth >= g_variant_n_children (self->tree))
     g_return_val_if_reached (NULL);
 
-  context = ide_object_get_context (IDE_OBJECT (self));
   node = g_variant_get_child_value (self->tree, nth);
-  ret = ide_clang_symbol_node_new (context, node);
+  ret = ide_clang_symbol_node_new (node);
 
   g_return_val_if_fail (IDE_IS_CLANG_SYMBOL_NODE (ret), NULL);
 
@@ -180,8 +182,7 @@ ide_clang_symbol_tree_init (IdeClangSymbolTree *self)
 }
 
 IdeClangSymbolTree *
-ide_clang_symbol_tree_new (IdeContext *context,
-                           GFile      *file,
+ide_clang_symbol_tree_new (GFile      *file,
                            GVariant   *tree)
 {
   IdeClangSymbolTree *self;
@@ -193,7 +194,6 @@ ide_clang_symbol_tree_new (IdeContext *context,
                         NULL);
 
   self = g_object_new (IDE_TYPE_CLANG_SYMBOL_TREE,
-                       "context", context,
                        "file", file,
                        NULL);
 
diff --git a/src/plugins/clang/ide-clang-symbol-tree.h b/src/plugins/clang/ide-clang-symbol-tree.h
index a01819ed9..0485ad158 100644
--- a/src/plugins/clang/ide-clang-symbol-tree.h
+++ b/src/plugins/clang/ide-clang-symbol-tree.h
@@ -1,6 +1,6 @@
 /* ide-clang-symbol-tree.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,21 +14,22 @@
  *
  * 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>
-#include <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
 #define IDE_TYPE_CLANG_SYMBOL_TREE (ide_clang_symbol_tree_get_type())
 
-G_DECLARE_FINAL_TYPE (IdeClangSymbolTree, ide_clang_symbol_tree, IDE, CLANG_SYMBOL_TREE, IdeObject)
+G_DECLARE_FINAL_TYPE (IdeClangSymbolTree, ide_clang_symbol_tree, IDE, CLANG_SYMBOL_TREE, GObject)
 
-IdeClangSymbolTree *ide_clang_symbol_tree_new      (IdeContext         *context,
-                                                    GFile              *file,
+IdeClangSymbolTree *ide_clang_symbol_tree_new      (GFile              *file,
                                                     GVariant           *tree);
 GFile              *ide_clang_symbol_tree_get_file (IdeClangSymbolTree *self);
 
diff --git a/src/plugins/clang/ide-clang-util.h b/src/plugins/clang/ide-clang-util.h
index 755dae2e6..432c16756 100644
--- a/src/plugins/clang/ide-clang-util.h
+++ b/src/plugins/clang/ide-clang-util.h
@@ -1,6 +1,6 @@
 /* ide-clang-util.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 #include "ide-clang-autocleanups.h"
 
@@ -30,50 +32,50 @@ ide_clang_translate_kind (enum CXCursorKind cursor_kind)
   switch ((int)cursor_kind)
     {
     case CXCursor_StructDecl:
-      return IDE_SYMBOL_STRUCT;
+      return IDE_SYMBOL_KIND_STRUCT;
 
     case CXCursor_UnionDecl:
-      return IDE_SYMBOL_UNION;
+      return IDE_SYMBOL_KIND_UNION;
 
     case CXCursor_ClassDecl:
-      return IDE_SYMBOL_CLASS;
+      return IDE_SYMBOL_KIND_CLASS;
 
     case CXCursor_EnumDecl:
-      return IDE_SYMBOL_ENUM;
+      return IDE_SYMBOL_KIND_ENUM;
 
     case CXCursor_FieldDecl:
-      return IDE_SYMBOL_FIELD;
+      return IDE_SYMBOL_KIND_FIELD;
 
     case CXCursor_EnumConstantDecl:
-      return IDE_SYMBOL_ENUM_VALUE;
+      return IDE_SYMBOL_KIND_ENUM_VALUE;
 
     case CXCursor_FunctionDecl:
-      return IDE_SYMBOL_FUNCTION;
+      return IDE_SYMBOL_KIND_FUNCTION;
 
     case CXCursor_CXXMethod:
-      return IDE_SYMBOL_METHOD;
+      return IDE_SYMBOL_KIND_METHOD;
 
     case CXCursor_VarDecl:
     case CXCursor_ParmDecl:
-      return IDE_SYMBOL_VARIABLE;
+      return IDE_SYMBOL_KIND_VARIABLE;
 
     case CXCursor_TypedefDecl:
     case CXCursor_NamespaceAlias:
     case CXCursor_TypeAliasDecl:
-      return IDE_SYMBOL_ALIAS;
+      return IDE_SYMBOL_KIND_ALIAS;
 
     case CXCursor_Namespace:
-      return IDE_SYMBOL_NAMESPACE;
+      return IDE_SYMBOL_KIND_NAMESPACE;
 
     case CXCursor_FunctionTemplate:
     case CXCursor_ClassTemplate:
-      return IDE_SYMBOL_TEMPLATE;
+      return IDE_SYMBOL_KIND_TEMPLATE;
 
     case CXCursor_MacroDefinition:
-      return IDE_SYMBOL_MACRO;
+      return IDE_SYMBOL_KIND_MACRO;
 
     default:
-      return IDE_SYMBOL_NONE;
+      return IDE_SYMBOL_KIND_NONE;
     }
 }
 
diff --git a/src/plugins/clang/ide-clang.c b/src/plugins/clang/ide-clang.c
index edfd87031..808a73537 100644
--- a/src/plugins/clang/ide-clang.c
+++ b/src/plugins/clang/ide-clang.c
@@ -1,6 +1,6 @@
 /* ide-clang.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,13 +14,15 @@
  *
  * 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
  */
 
 /* Prologue {{{1 */
 
 #define G_LOG_DOMAIN "ide-clang"
 
-#include <ide.h>
+#include <libide-code.h>
 
 #include "ide-clang.h"
 #include "ide-clang-util.h"
@@ -288,54 +290,54 @@ ide_clang_get_symbol_kind (CXCursor        cursor,
   switch ((int)cxkind)
     {
     case CXCursor_StructDecl:
-      kind = IDE_SYMBOL_STRUCT;
+      kind = IDE_SYMBOL_KIND_STRUCT;
       break;
 
     case CXCursor_UnionDecl:
-      kind = IDE_SYMBOL_UNION;
+      kind = IDE_SYMBOL_KIND_UNION;
       break;
 
     case CXCursor_ClassDecl:
-      kind = IDE_SYMBOL_CLASS;
+      kind = IDE_SYMBOL_KIND_CLASS;
       break;
 
     case CXCursor_FunctionDecl:
-      kind = IDE_SYMBOL_FUNCTION;
+      kind = IDE_SYMBOL_KIND_FUNCTION;
       break;
 
     case CXCursor_EnumDecl:
-      kind = IDE_SYMBOL_ENUM;
+      kind = IDE_SYMBOL_KIND_ENUM;
       break;
 
     case CXCursor_EnumConstantDecl:
-      kind = IDE_SYMBOL_ENUM_VALUE;
+      kind = IDE_SYMBOL_KIND_ENUM_VALUE;
       break;
 
     case CXCursor_FieldDecl:
-      kind = IDE_SYMBOL_FIELD;
+      kind = IDE_SYMBOL_KIND_FIELD;
       break;
 
     case CXCursor_InclusionDirective:
-      kind = IDE_SYMBOL_HEADER;
+      kind = IDE_SYMBOL_KIND_HEADER;
       break;
 
     case CXCursor_VarDecl:
-      kind = IDE_SYMBOL_VARIABLE;
+      kind = IDE_SYMBOL_KIND_VARIABLE;
       break;
 
     case CXCursor_NamespaceAlias:
-      kind = IDE_SYMBOL_NAMESPACE;
+      kind = IDE_SYMBOL_KIND_NAMESPACE;
       break;
 
     case CXCursor_CXXMethod:
     case CXCursor_Destructor:
     case CXCursor_Constructor:
-      kind = IDE_SYMBOL_METHOD;
+      kind = IDE_SYMBOL_KIND_METHOD;
       break;
 
     case CXCursor_MacroDefinition:
     case CXCursor_MacroExpansion:
-      kind = IDE_SYMBOL_MACRO;
+      kind = IDE_SYMBOL_KIND_MACRO;
       break;
 
     default:
@@ -354,8 +356,7 @@ create_symbol (const gchar  *path,
 {
   g_auto(CXString) cxname = {0};
   g_autoptr(GFile) gfile = NULL;
-  g_autoptr(IdeFile) ifile = NULL;
-  g_autoptr(IdeSourceLocation) srcloc = NULL;
+  g_autoptr(IdeLocation) srcloc = NULL;
   IdeSymbolKind symkind;
   IdeSymbolFlags symflags = 0;
   CXSourceLocation loc;
@@ -374,21 +375,15 @@ create_symbol (const gchar  *path,
   loc = clang_getCursorLocation (cursor);
   clang_getExpansionLocation (loc, NULL, &line, &column, NULL);
   gfile = g_file_new_for_path (path);
-  ifile = ide_file_new (NULL, gfile);
 
   if (line) line--;
   if (column) column--;
 
-  srcloc = ide_source_location_new (ifile, line, column, 0);
+  srcloc = ide_location_new (gfile, line, column);
   cxname = clang_getCursorSpelling (cursor);
   symkind = ide_clang_get_symbol_kind (cursor, &symflags);
 
-  return ide_symbol_new (clang_getCString (cxname),
-                         symkind,
-                         symflags,
-                         NULL,
-                         NULL,
-                         srcloc);
+  return ide_symbol_new (clang_getCString (cxname), symkind, symflags, srcloc, srcloc);
 }
 
 static void
@@ -470,28 +465,28 @@ ide_clang_index_symbol_prefix (IdeSymbolKind kind)
 {
   switch ((int)kind)
     {
-    case IDE_SYMBOL_FUNCTION:
+    case IDE_SYMBOL_KIND_FUNCTION:
       return "f\x1F";
 
-    case IDE_SYMBOL_STRUCT:
+    case IDE_SYMBOL_KIND_STRUCT:
       return "s\x1F";
 
-    case IDE_SYMBOL_VARIABLE:
+    case IDE_SYMBOL_KIND_VARIABLE:
       return "v\x1F";
 
-    case IDE_SYMBOL_UNION:
+    case IDE_SYMBOL_KIND_UNION:
       return "u\x1F";
 
-    case IDE_SYMBOL_ENUM:
+    case IDE_SYMBOL_KIND_ENUM:
       return "e\x1F";
 
-    case IDE_SYMBOL_CLASS:
+    case IDE_SYMBOL_KIND_CLASS:
       return "c\x1F";
 
-    case IDE_SYMBOL_ENUM_VALUE:
+    case IDE_SYMBOL_KIND_ENUM_VALUE:
       return "a\x1F";
 
-    case IDE_SYMBOL_MACRO:
+    case IDE_SYMBOL_KIND_MACRO:
       return "m\x1F";
 
     default:
@@ -527,7 +522,7 @@ ide_clang_index_file_visitor (CXCursor     cursor,
   cxpath = clang_getFileName (file);
   path = clang_getCString (cxpath);
 
-  if (dzl_str_equal0 (path, state->path))
+  if (ide_str_equal0 (path, state->path))
     {
       enum CXCursorKind cursor_kind = clang_getCursorKind (cursor);
 
@@ -552,6 +547,8 @@ ide_clang_index_file_visitor (CXCursor     cursor,
  * from queue, else this will do Breadth first traversal on AST till it finds a
  * declaration.  On next request when decl_cursors is empty it will continue
  * traversal from where it has stopped in previously.
+ *
+ * Since: 3.32
  */
 static IdeCodeIndexEntry *
 ide_clang_index_file_next_entry (IndexFile                *state,
@@ -564,7 +561,7 @@ ide_clang_index_file_next_entry (IndexFile                *state,
   g_auto(CXString) usr = {0};
   CXSourceLocation location;
   IdeSymbolFlags flags = IDE_SYMBOL_FLAGS_NONE;
-  IdeSymbolKind kind = IDE_SYMBOL_NONE;
+  IdeSymbolKind kind = IDE_SYMBOL_KIND_NONE;
   enum CXLinkageKind linkage;
   enum CXCursorKind cursor_kind;
   const gchar *cname = NULL;
@@ -611,7 +608,7 @@ ide_clang_index_file_next_entry (IndexFile                *state,
    */
   cxname = clang_getCursorSpelling (*cursor);
   cname = clang_getCString (cxname);
-  if (dzl_str_empty0 (cname))
+  if (ide_str_empty0 (cname))
     return NULL;
 
   /*
@@ -732,7 +729,7 @@ ide_clang_index_file_worker (IdeTask      *task,
 
       break;
     }
-  
+
   ide_task_return_pointer (task,
                            g_steal_pointer (&state->entries),
                            (GDestroyNotify)g_ptr_array_unref);
@@ -751,7 +748,7 @@ ide_clang_index_file_worker (IdeTask      *task,
  * found at @path. The results (an array of #IdeCodeIndexEntry) can be accessed
  * via ide_clang_index_file_finish() using the result provided to @callback
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_clang_index_file_async (IdeClang            *self,
@@ -798,7 +795,7 @@ ide_clang_index_file_async (IdeClang            *self,
  *
  * Returns: (transfer full): a #GPtrArray of #IdeCodeIndexEntry
  *
- * Since: 3.30
+ * Since: 3.32
  */
 GPtrArray *
 ide_clang_index_file_finish (IdeClang      *self,
@@ -881,14 +878,13 @@ get_path (GFile       *workdir,
   return path_or_uri (child);
 }
 
-static IdeSourceLocation *
+static IdeLocation *
 create_location (GFile             *workdir,
                  CXSourceLocation   cxloc,
-                 IdeSourceLocation *alternate)
+                 IdeLocation *alternate)
 {
   g_autofree gchar *path = NULL;
-  g_autoptr(IdeFile) file = NULL;
-  g_autoptr(GFile) gfile = NULL;
+  g_autoptr(GFile) file = NULL;
   g_auto(CXString) str = {0};
   CXFile cxfile = NULL;
   unsigned line;
@@ -902,7 +898,7 @@ create_location (GFile             *workdir,
   str = clang_getFileName (cxfile);
 
   if (line == 0 || clang_getCString (str) == NULL)
-    return alternate ? ide_source_location_ref (alternate) : NULL;
+    return alternate ? g_object_ref (alternate) : NULL;
 
   if (line > 0)
     line--;
@@ -910,24 +906,21 @@ create_location (GFile             *workdir,
   if (column > 0)
     column--;
 
-  /* TODO: Remove IdeFile from IdeSourceLocation */
-
   path = get_path (workdir, clang_getCString (str));
-  gfile = g_file_new_for_path (path);
-  file = ide_file_new (NULL, gfile);
+  file = g_file_new_for_path (path);
 
-  return ide_source_location_new (file, line, column, offset);
+  return ide_location_new (file, line, column);
 }
 
-static IdeSourceRange *
+static IdeRange *
 create_range (GFile         *workdir,
               CXSourceRange  cxrange)
 {
-  IdeSourceRange *range = NULL;
+  IdeRange *range = NULL;
   CXSourceLocation cxbegin;
   CXSourceLocation cxend;
-  g_autoptr(IdeSourceLocation) begin = NULL;
-  g_autoptr(IdeSourceLocation) end = NULL;
+  g_autoptr(IdeLocation) begin = NULL;
+  g_autoptr(IdeLocation) end = NULL;
 
   g_assert (G_IS_FILE (workdir));
 
@@ -935,13 +928,13 @@ create_range (GFile         *workdir,
   cxend = clang_getRangeEnd (cxrange);
 
   /* Sometimes the end location does not have a file associated with it,
-   * so we force it to have the IdeFile of the first location.
+   * so we force it to have the GFile of the first location.
    */
   begin = create_location (workdir, cxbegin, NULL);
   end = create_location (workdir, cxend, begin);
 
   if ((begin != NULL) && (end != NULL))
-    range = ide_source_range_new (begin, end);
+    range = ide_range_new (begin, end);
 
   return range;
 }
@@ -951,7 +944,7 @@ create_diagnostic (GFile        *workdir,
                    GFile        *target,
                    CXDiagnostic *cxdiag)
 {
-  g_autoptr(IdeSourceLocation) loc = NULL;
+  g_autoptr(IdeLocation) loc = NULL;
   enum CXDiagnosticSeverity cxseverity;
   IdeDiagnosticSeverity severity;
   IdeDiagnostic *diag;
@@ -996,7 +989,7 @@ create_diagnostic (GFile        *workdir,
   for (guint i = 0; i < num_ranges; i++)
     {
       CXSourceRange cxrange;
-      IdeSourceRange *range;
+      IdeRange *range;
 
       cxrange = clang_getDiagnosticRange (cxdiag, i);
       range = create_range (workdir, cxrange);
@@ -1085,7 +1078,7 @@ ide_clang_diagnose_worker (IdeTask      *task,
  *
  * This generates diagnostics related to the file after parsing it.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 void
 ide_clang_diagnose_async (IdeClang            *self,
@@ -1118,7 +1111,7 @@ ide_clang_diagnose_async (IdeClang            *self,
   else
     state->workdir = g_file_new_for_path ((parent = g_path_get_dirname (path)));
 
-  IDE_PTR_ARRAY_SET_FREE_FUNC (state->diagnostics, ide_diagnostic_unref);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (state->diagnostics, ide_object_unref_and_destroy);
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_clang_diagnose_async);
@@ -1136,7 +1129,7 @@ ide_clang_diagnose_async (IdeClang            *self,
  * Returns: (transfer full) (element-type Ide.Diagnostic):
  *   a #GPtrArray of #IdeDiagnostic
  *
- * Since: 3.30
+ * Since: 3.32
  */
 GPtrArray *
 ide_clang_diagnose_finish (IdeClang      *self,
@@ -1150,9 +1143,7 @@ ide_clang_diagnose_finish (IdeClang      *self,
 
   ret = ide_task_propagate_pointer (IDE_TASK (result), error);
 
-  IDE_PTR_ARRAY_CLEAR_FREE_FUNC (ret);
-
-  return ret;
+  return IDE_PTR_ARRAY_STEAL_FULL (&ret);
 }
 
 /* Completion {{{1 */
@@ -1453,7 +1444,7 @@ ide_clang_find_nearest_scope_worker (IdeTask      *task,
   else
     ide_task_return_pointer (task,
                              g_steal_pointer (&ret),
-                             (GDestroyNotify)ide_symbol_unref);
+                             g_object_unref);
 }
 
 void
@@ -1536,9 +1527,8 @@ ide_clang_locate_symbol_worker (IdeTask      *task,
                                 GCancellable *cancellable)
 {
   LocateSymbol *state = task_data;
-  g_autoptr(IdeSourceLocation) declaration = NULL;
-  g_autoptr(IdeSourceLocation) definition = NULL;
-  g_autoptr(IdeSourceLocation) canonical = NULL;
+  g_autoptr(IdeLocation) declaration = NULL;
+  g_autoptr(IdeLocation) definition = NULL;
   g_autoptr(IdeSymbol) ret = NULL;
   g_auto(CXTranslationUnit) unit = NULL;
   g_auto(CXString) cxstr = {0};
@@ -1615,7 +1605,7 @@ ide_clang_locate_symbol_worker (IdeTask      *task,
 
   symkind = ide_clang_get_symbol_kind (cursor, &symflags);
 
-  if (symkind == IDE_SYMBOL_HEADER)
+  if (symkind == IDE_SYMBOL_KIND_HEADER)
     {
       g_auto(CXString) included_file_name = {0};
       CXFile included_file;
@@ -1627,24 +1617,19 @@ ide_clang_locate_symbol_worker (IdeTask      *task,
 
       if (path != NULL)
         {
-          g_autoptr(IdeFile) file = NULL;
-          g_autoptr(GFile) gfile = NULL;
-
-          gfile = g_file_new_for_path (path);
-          file = ide_file_new (NULL, gfile);
+          g_autoptr(GFile) file = g_file_new_for_path (path);
 
-          g_clear_pointer (&definition, ide_source_location_unref);
-          declaration = ide_source_location_new (file, 0, 0, 0);
+          g_clear_object (&definition);
+          declaration = ide_location_new (file, -1, -1);
         }
     }
 
   cxstr = clang_getCursorDisplayName (cursor);
-  ret = ide_symbol_new (clang_getCString (cxstr), symkind, symflags,
-                        declaration, definition, canonical);
+  ret = ide_symbol_new (clang_getCString (cxstr), symkind, symflags, declaration, definition);
 
   ide_task_return_pointer (task,
                            g_steal_pointer (&ret),
-                           (GDestroyNotify)ide_symbol_unref);
+                           g_object_unref);
 }
 
 void
@@ -1758,7 +1743,7 @@ cursor_is_recognized (GetSymbolTree *state,
         cxloc = clang_getCursorLocation (cursor);
         clang_getFileLocation (cxloc, &file, NULL, NULL, NULL);
         filename = clang_getFileName (file);
-        ret = dzl_str_equal0 (clang_getCString (filename), state->path);
+        ret = ide_str_equal0 (clang_getCString (filename), state->path);
       }
       break;
 
@@ -2220,6 +2205,8 @@ ide_clang_get_index_key_async (IdeClang            *self,
  * at a given source location.
  *
  * Returns: (transfer full): the key, or %NULL and @error is set
+ *
+ * Since: 3.32
  */
 gchar *
 ide_clang_get_index_key_finish (IdeClang      *self,
diff --git a/src/plugins/clang/ide-clang.h b/src/plugins/clang/ide-clang.h
index 28f857652..d1a96fc57 100644
--- a/src/plugins/clang/ide-clang.h
+++ b/src/plugins/clang/ide-clang.h
@@ -1,6 +1,6 @@
 /* ide-clang.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/clang/meson.build b/src/plugins/clang/meson.build
index ba5c46a28..01381b74f 100644
--- a/src/plugins/clang/meson.build
+++ b/src/plugins/clang/meson.build
@@ -1,12 +1,6 @@
-if get_option('with_clang')
+if get_option('plugin_clang')
 
-clang_resources = gnome.compile_resources(    
-  'ide-clang-resources',                      
-  'clang.gresource.xml',                      
-  c_name: 'ide_clang',                        
-)                                           
-
-clang_sources = [
+plugins_sources += files([
   'clang-plugin.c',
   'ide-clang-client.c',
   'ide-clang-client.h',
@@ -34,13 +28,21 @@ clang_sources = [
   'ide-clang-symbol-resolver.h',
   'ide-clang-symbol-tree.c',
   'ide-clang-symbol-tree.h',
-]
+])
 
 gnome_builder_clang_sources = [
   'gnome-builder-clang.c',
   'ide-clang.c',
 ]
 
+plugin_clang_resources = gnome.compile_resources(
+  'clang-resources',
+  'clang.gresource.xml',
+  c_name: 'gbp_clang',
+)
+
+plugins_sources += plugin_clang_resources[0]
+
 add_languages('cpp') # Needed for llvm dep
 llvm_dep = dependency('llvm', version: '>= 3.5')
 clang_include = llvm_dep.get_configtool_variable('includedir')
@@ -59,11 +61,14 @@ clang_includes_dep = declare_dependency(
   include_directories: include_directories(clang_include),
 )
 
-gnome_builder_plugins_deps += [clang_includes_dep]
-gnome_builder_plugins_sources += files(clang_sources)
-gnome_builder_plugins_sources += clang_resources[0]
+plugins_deps += [clang_includes_dep]
+
+gnome_builder_clang_deps = [
+  clang_dep,
+  libjsonrpc_glib_dep,
 
-gnome_builder_clang_deps = [ clang_dep, libide_deps, libide_dep ]
+  libide_code_dep,
+]
 
 executable('gnome-builder-clang', gnome_builder_clang_sources,
       dependencies: gnome_builder_clang_deps,
diff --git a/src/plugins/cmake/cmake-plugin.c b/src/plugins/cmake/cmake-plugin.c
index a4c562162..f59624847 100644
--- a/src/plugins/cmake/cmake-plugin.c
+++ b/src/plugins/cmake/cmake-plugin.c
@@ -14,19 +14,33 @@
  *
  * 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
  */
 
+#include "config.h"
+
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-foundry.h>
 
 #include "gbp-cmake-build-system.h"
+#include "gbp-cmake-build-system-discovery.h"
 #include "gbp-cmake-pipeline-addin.h"
 #include "gbp-cmake-toolchain-provider.h"
 
-void
-gbp_cmake_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_gbp_cmake_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_PIPELINE_ADDIN, 
GBP_TYPE_CMAKE_PIPELINE_ADDIN);
-  peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_SYSTEM, GBP_TYPE_CMAKE_BUILD_SYSTEM);
-  peas_object_module_register_extension_type (module, IDE_TYPE_TOOLCHAIN_PROVIDER, 
GBP_TYPE_CMAKE_TOOLCHAIN_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                              GBP_TYPE_CMAKE_PIPELINE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_SYSTEM,
+                                              GBP_TYPE_CMAKE_BUILD_SYSTEM);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_SYSTEM_DISCOVERY,
+                                              GBP_TYPE_CMAKE_BUILD_SYSTEM_DISCOVERY);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_TOOLCHAIN_PROVIDER,
+                                              GBP_TYPE_CMAKE_TOOLCHAIN_PROVIDER);
 }
diff --git a/src/plugins/cmake/cmake.gresource.xml b/src/plugins/cmake/cmake.gresource.xml
index df2cf043d..df918ea9b 100644
--- a/src/plugins/cmake/cmake.gresource.xml
+++ b/src/plugins/cmake/cmake.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/cmake">
     <file>cmake.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/cmake">
     <file>CMakeLists.txt</file>
     <file>toolchain-info.ini.cmake</file>
   </gresource>
diff --git a/src/plugins/cmake/cmake.plugin b/src/plugins/cmake/cmake.plugin
index 2edfe3810..f7e1b41ec 100644
--- a/src/plugins/cmake/cmake.plugin
+++ b/src/plugins/cmake/cmake.plugin
@@ -1,10 +1,11 @@
 [Plugin]
-Module=cmake-plugin
-Name=CMake
-Description=Provides integration with the CMake build system
 Authors=Martin Blanchard <tchaik gmx com>
-Copyright=Copyright © 2017 Martin Blanchard
 Builtin=true
-X-Project-File-Filter-Pattern=CMakeLists.txt
+Copyright=Copyright © 2017 Martin Blanchard
+Description=Provides integration with the CMake build system
+Embedded=_gbp_cmake_register_types
+Hidden=true
+Module=cmake
+Name=CMake
 X-Project-File-Filter-Name=CMake Project (CMakeLists.txt)
-Embedded=gbp_cmake_register_types
+X-Project-File-Filter-Pattern=CMakeLists.txt
diff --git a/src/plugins/cmake/gbp-cmake-build-stage-cross-file.c 
b/src/plugins/cmake/gbp-cmake-build-stage-cross-file.c
index 1c545e671..e5b84a4ec 100644
--- a/src/plugins/cmake/gbp-cmake-build-stage-cross-file.c
+++ b/src/plugins/cmake/gbp-cmake-build-stage-cross-file.c
@@ -16,6 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gbp-cmake-build-stage-cross-file"
@@ -63,6 +65,7 @@ _gbp_cmake_file_set_quoted (gchar       **content,
 static void
 gbp_cmake_build_stage_cross_file_query (IdeBuildStage    *stage,
                                         IdeBuildPipeline *pipeline,
+                                        GPtrArray        *targets,
                                         GCancellable     *cancellable)
 {
   GbpCMakeBuildStageCrossFile *self = (GbpCMakeBuildStageCrossFile *)stage;
@@ -192,7 +195,6 @@ gbp_cmake_build_stage_cross_file_class_init (GbpCMakeBuildStageCrossFileClass *k
 static void
 gbp_cmake_build_stage_cross_file_init (GbpCMakeBuildStageCrossFile *self)
 {
-  
 }
 
 GbpCMakeBuildStageCrossFile *
diff --git a/src/plugins/cmake/gbp-cmake-build-stage-cross-file.h 
b/src/plugins/cmake/gbp-cmake-build-stage-cross-file.h
index 1156ca62a..2256848e0 100644
--- a/src/plugins/cmake/gbp-cmake-build-stage-cross-file.h
+++ b/src/plugins/cmake/gbp-cmake-build-stage-cross-file.h
@@ -16,11 +16,13 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/cmake/gbp-cmake-build-system-discovery.c 
b/src/plugins/cmake/gbp-cmake-build-system-discovery.c
new file mode 100644
index 000000000..d06cfa91a
--- /dev/null
+++ b/src/plugins/cmake/gbp-cmake-build-system-discovery.c
@@ -0,0 +1,49 @@
+/* gbp-cmake-build-system-discovery.c
+ *
+ * Copyright 2018-2019 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-cmake-build-system-discovery"
+
+#include "config.h"
+
+#include "gbp-cmake-build-system-discovery.h"
+
+struct _GbpCmakeBuildSystemDiscovery
+{
+  IdeSimpleBuildSystemDiscovery parent;
+};
+
+G_DEFINE_TYPE (GbpCmakeBuildSystemDiscovery,
+               gbp_cmake_build_system_discovery,
+               IDE_TYPE_SIMPLE_BUILD_SYSTEM_DISCOVERY)
+
+static void
+gbp_cmake_build_system_discovery_class_init (GbpCmakeBuildSystemDiscoveryClass *klass)
+{
+}
+
+static void
+gbp_cmake_build_system_discovery_init (GbpCmakeBuildSystemDiscovery *self)
+{
+  g_object_set (self,
+                "hint", "cmake",
+                "glob", "CMakeLists.txt",
+                "priority", 100,
+                NULL);
+}
diff --git a/src/plugins/cmake/gbp-cmake-build-system-discovery.h 
b/src/plugins/cmake/gbp-cmake-build-system-discovery.h
new file mode 100644
index 000000000..5bbbc4542
--- /dev/null
+++ b/src/plugins/cmake/gbp-cmake-build-system-discovery.h
@@ -0,0 +1,31 @@
+/* gbp-cmake-build-system-discovery.h
+ *
+ * Copyright 2018-2019 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-foundry.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_CMAKE_BUILD_SYSTEM_DISCOVERY (gbp_cmake_build_system_discovery_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCmakeBuildSystemDiscovery, gbp_cmake_build_system_discovery, GBP, 
CMAKE_BUILD_SYSTEM_DISCOVERY, IdeSimpleBuildSystemDiscovery)
+
+G_END_DECLS
diff --git a/src/plugins/cmake/gbp-cmake-build-system.c b/src/plugins/cmake/gbp-cmake-build-system.c
index 5504f77a3..c2cb5bc66 100644
--- a/src/plugins/cmake/gbp-cmake-build-system.c
+++ b/src/plugins/cmake/gbp-cmake-build-system.c
@@ -1,6 +1,6 @@
 /* gbp-cmake-build-system.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  * Copyright 2017 Martin Blanchard <tchaik gmx com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -15,6 +15,8 @@
  *
  * 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-cmake-build-system"
@@ -130,10 +132,11 @@ gbp_cmake_build_system_ensure_config_async (GbpCMakeBuildSystem *self,
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
 
   ide_build_manager_execute_async (build_manager,
                                    IDE_BUILD_PHASE_CONFIGURE,
+                                   NULL,
                                    cancellable,
                                    gbp_cmake_build_system_ensure_config_cb,
                                    g_steal_pointer (&task));
@@ -205,7 +208,7 @@ gbp_cmake_build_system_load_commands_config_cb (GObject      *object,
     }
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   pipeline = ide_build_manager_get_pipeline (build_manager);
 
   if (pipeline == NULL)
@@ -282,7 +285,7 @@ gbp_cmake_build_system_load_commands_async (GbpCMakeBuildSystem *self,
    */
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   pipeline = ide_build_manager_get_pipeline (build_manager);
 
   if (pipeline != NULL)
@@ -461,9 +464,9 @@ gbp_cmake_build_system_get_build_flags_cb (GObject      *object,
 
   /* Get non-standard system includes */
   context = ide_object_get_context (IDE_OBJECT (self));
-  config_manager = ide_context_get_configuration_manager (context);
+  config_manager = ide_configuration_manager_from_context (context);
   config = ide_configuration_manager_get_current (config_manager);
-  if (NULL != (runtime = ide_configuration_get_runtime (config)))
+  if ((runtime = ide_configuration_get_runtime (config)))
     system_includes = ide_runtime_get_system_include_dirs (runtime);
 
   build_flags = ide_compile_commands_lookup (compile_commands,
@@ -480,27 +483,24 @@ gbp_cmake_build_system_get_build_flags_cb (GObject      *object,
 
 static void
 gbp_cmake_build_system_get_build_flags_async (IdeBuildSystem      *build_system,
-                                              IdeFile             *file,
+                                              GFile               *file,
                                               GCancellable        *cancellable,
                                               GAsyncReadyCallback  callback,
                                               gpointer             user_data)
 {
   GbpCMakeBuildSystem *self = (GbpCMakeBuildSystem *)build_system;
   g_autoptr(IdeTask) task = NULL;
-  GFile *gfile;
 
   IDE_ENTRY;
 
   g_assert (GBP_IS_CMAKE_BUILD_SYSTEM (self));
-  g_assert (IDE_IS_FILE (file));
+  g_assert (G_IS_FILE (file));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  gfile = ide_file_get_file (file);
-
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_priority (task, G_PRIORITY_LOW);
   ide_task_set_source_tag (task, gbp_cmake_build_system_get_build_flags_async);
-  ide_task_set_task_data (task, g_object_ref (gfile), g_object_unref);
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
 
   gbp_cmake_build_system_load_commands_async (self,
                                               cancellable,
@@ -574,7 +574,7 @@ gbp_cmake_build_system_init_worker (IdeTask      *task,
 
   name = g_file_get_basename (project_file);
 
-  if (dzl_str_equal0 (name, "CMakeLists.txt"))
+  if (ide_str_equal0 (name, "CMakeLists.txt"))
     {
       ide_task_return_pointer (task, g_object_ref (project_file), g_object_unref);
       IDE_EXIT;
@@ -621,7 +621,7 @@ gbp_cmake_build_system_init_async (GAsyncInitable      *initable,
   context = ide_object_get_context (IDE_OBJECT (self));
   g_assert (IDE_IS_CONTEXT (context));
 
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
 
   task = ide_task_new (self, cancellable, callback, user_data);
diff --git a/src/plugins/cmake/gbp-cmake-build-system.h b/src/plugins/cmake/gbp-cmake-build-system.h
index 56bcdd39c..16632c290 100644
--- a/src/plugins/cmake/gbp-cmake-build-system.h
+++ b/src/plugins/cmake/gbp-cmake-build-system.h
@@ -1,6 +1,6 @@
 /* gbp-cmake-build-system.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  * Copyright 2017 Martin Blanchard <tchaik gmx com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/cmake/gbp-cmake-build-target.c b/src/plugins/cmake/gbp-cmake-build-target.c
index 5f7ab06d1..6fb7fbdfd 100644
--- a/src/plugins/cmake/gbp-cmake-build-target.c
+++ b/src/plugins/cmake/gbp-cmake-build-target.c
@@ -1,6 +1,6 @@
 /* gbp-cmake-build-target.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  * Copyright 2017 Martin Blanchard <tchaik gmx com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -15,6 +15,8 @@
  *
  * 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-cmake-build-target"
diff --git a/src/plugins/cmake/gbp-cmake-build-target.h b/src/plugins/cmake/gbp-cmake-build-target.h
index 0b852e893..fb299dd3e 100644
--- a/src/plugins/cmake/gbp-cmake-build-target.h
+++ b/src/plugins/cmake/gbp-cmake-build-target.h
@@ -1,6 +1,6 @@
 /* gbp-cmake-build-target.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  * Copyright 2017 Martin Blanchard <tchaik gmx com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/cmake/gbp-cmake-pipeline-addin.c b/src/plugins/cmake/gbp-cmake-pipeline-addin.c
index b6a0d7f62..a60fa47ef 100644
--- a/src/plugins/cmake/gbp-cmake-pipeline-addin.c
+++ b/src/plugins/cmake/gbp-cmake-pipeline-addin.c
@@ -1,6 +1,6 @@
 /* gbp-cmake-pipeline-addin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  * Copyright 2017 Martin Blanchard <tchaik gmx com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -15,6 +15,8 @@
  *
  * 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-cmake-pipeline-addin"
@@ -51,6 +53,7 @@ gbp_cmake_pipeline_addin_init (GbpCMakePipelineAddin *self)
 static void
 gbp_cmake_pipeline_addin_stage_query_cb (IdeBuildStage    *stage,
                                          IdeBuildPipeline *pipeline,
+                                         GPtrArray        *targets,
                                          GCancellable     *cancellable)
 {
   g_assert (IDE_IS_BUILD_STAGE (stage));
@@ -77,7 +80,7 @@ gbp_cmake_pipeline_addin_load (IdeBuildPipelineAddin *addin,
   g_autofree gchar *prefix_option = NULL;
   g_autofree gchar *build_ninja = NULL;
   g_autofree gchar *crossbuild_file = NULL;
-  GFile *project_file;
+  g_autoptr(GFile) project_file = NULL;
   g_autofree gchar *project_file_name = NULL;
   g_autofree gchar *srcdir = NULL;
   IdeBuildSystem *build_system;
@@ -99,11 +102,11 @@ gbp_cmake_pipeline_addin_load (IdeBuildPipelineAddin *addin,
 
   context = ide_object_get_context (IDE_OBJECT (self));
 
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
   if (!GBP_IS_CMAKE_BUILD_SYSTEM (build_system))
     IDE_GOTO (failure);
 
-  project_file = ide_context_get_project_file (context);
+  g_object_get (build_system, "project-file", &project_file, NULL);
   project_file_name = g_file_get_basename (project_file);
 
   configuration = ide_build_pipeline_get_configuration (pipeline);
@@ -156,7 +159,7 @@ gbp_cmake_pipeline_addin_load (IdeBuildPipelineAddin *addin,
       cross_file_stage = gbp_cmake_build_stage_cross_file_new (context, toolchain);
       crossbuild_file = gbp_cmake_build_stage_cross_file_get_path (cross_file_stage, pipeline);
 
-      id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_PREPARE, 0, IDE_BUILD_STAGE 
(cross_file_stage));
+      id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_PREPARE, 0, IDE_BUILD_STAGE 
(cross_file_stage));
       ide_build_pipeline_addin_track (addin, id);
     }
 
@@ -197,7 +200,7 @@ gbp_cmake_pipeline_addin_load (IdeBuildPipelineAddin *addin,
   if (g_file_test (build_ninja, G_FILE_TEST_IS_REGULAR))
     ide_build_stage_set_completed (configure_stage, TRUE);
 
-  id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_CONFIGURE, 0, configure_stage);
+  id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_CONFIGURE, 0, configure_stage);
   ide_build_pipeline_addin_track (addin, id);
 
   /* Setup our build stage */
@@ -226,7 +229,7 @@ gbp_cmake_pipeline_addin_load (IdeBuildPipelineAddin *addin,
                     G_CALLBACK (gbp_cmake_pipeline_addin_stage_query_cb),
                     NULL);
 
-  id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_BUILD, 0, build_stage);
+  id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_BUILD, 0, build_stage);
   ide_build_pipeline_addin_track (addin, id);
 
   /* Setup our install stage */
@@ -242,7 +245,7 @@ gbp_cmake_pipeline_addin_load (IdeBuildPipelineAddin *addin,
                     G_CALLBACK (gbp_cmake_pipeline_addin_stage_query_cb),
                     NULL);
 
-  id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_INSTALL, 0, install_stage);
+  id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_INSTALL, 0, install_stage);
   ide_build_pipeline_addin_track (addin, id);
 
   IDE_EXIT;
diff --git a/src/plugins/cmake/gbp-cmake-pipeline-addin.h b/src/plugins/cmake/gbp-cmake-pipeline-addin.h
index 0d7c26d06..0f82e5a3d 100644
--- a/src/plugins/cmake/gbp-cmake-pipeline-addin.h
+++ b/src/plugins/cmake/gbp-cmake-pipeline-addin.h
@@ -1,6 +1,6 @@
 /* gbp-cmake-pipeline-addin.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 Christian Hergert <chergert redhat com>
  * Copyright 2017 Martin Blanchard <tchaik gmx com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/cmake/gbp-cmake-toolchain-provider.c 
b/src/plugins/cmake/gbp-cmake-toolchain-provider.c
index ef6b376c0..e33ba8bda 100644
--- a/src/plugins/cmake/gbp-cmake-toolchain-provider.c
+++ b/src/plugins/cmake/gbp-cmake-toolchain-provider.c
@@ -16,6 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gbp-cmake-toolchain-provider"
@@ -138,9 +140,8 @@ gbp_cmake_toolchain_provider_load_async (IdeToolchainProvider     *provider,
 {
   GbpCMakeToolchainProvider *self = (GbpCMakeToolchainProvider *)provider;
   g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) workdir = NULL;
   IdeContext *context;
-  IdeVcs *vcs;
-  GFile *workdir;
 
   IDE_ENTRY;
 
@@ -149,8 +150,7 @@ gbp_cmake_toolchain_provider_load_async (IdeToolchainProvider     *provider,
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  workdir = ide_context_ref_workdir (context);
 
   task = ide_task_new (provider, cancellable, callback, user_data);
   ide_task_set_source_tag (task, gbp_cmake_toolchain_provider_load_async);
@@ -233,5 +233,5 @@ gbp_cmake_toolchain_provider_class_init (GbpCMakeToolchainProviderClass *klass)
 static void
 gbp_cmake_toolchain_provider_init (GbpCMakeToolchainProvider *self)
 {
-  
+
 }
diff --git a/src/plugins/cmake/gbp-cmake-toolchain-provider.h 
b/src/plugins/cmake/gbp-cmake-toolchain-provider.h
index 23b9ec2e9..cbf3f1d5f 100644
--- a/src/plugins/cmake/gbp-cmake-toolchain-provider.h
+++ b/src/plugins/cmake/gbp-cmake-toolchain-provider.h
@@ -16,11 +16,13 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/cmake/gbp-cmake-toolchain.c b/src/plugins/cmake/gbp-cmake-toolchain.c
index 3974fa852..a21452ad6 100644
--- a/src/plugins/cmake/gbp-cmake-toolchain.c
+++ b/src/plugins/cmake/gbp-cmake-toolchain.c
@@ -16,6 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gbp-cmake-toolchain"
diff --git a/src/plugins/cmake/gbp-cmake-toolchain.h b/src/plugins/cmake/gbp-cmake-toolchain.h
index 19d6c4948..67f1d4795 100644
--- a/src/plugins/cmake/gbp-cmake-toolchain.h
+++ b/src/plugins/cmake/gbp-cmake-toolchain.h
@@ -16,11 +16,13 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/cmake/meson.build b/src/plugins/cmake/meson.build
index b13e99851..37abefb9b 100644
--- a/src/plugins/cmake/meson.build
+++ b/src/plugins/cmake/meson.build
@@ -1,28 +1,22 @@
-if get_option('with_cmake')
+if get_option('plugin_cmake')
 
-cmake_resources = gnome.compile_resources(
-  'gbp-cmake-resources',
-  'cmake.gresource.xml',
-  c_name: 'gbp_cmake',
-)
-
-cmake_sources = [
+plugins_sources += files([
   'cmake-plugin.c',
   'gbp-cmake-build-stage-cross-file.c',
-  'gbp-cmake-build-stage-cross-file.h',
   'gbp-cmake-build-system.c',
-  'gbp-cmake-build-system.h',
+  'gbp-cmake-build-system-discovery.c',
   'gbp-cmake-build-target.c',
-  'gbp-cmake-build-target.h',
   'gbp-cmake-pipeline-addin.c',
-  'gbp-cmake-pipeline-addin.h',
   'gbp-cmake-toolchain.c',
-  'gbp-cmake-toolchain.h',
   'gbp-cmake-toolchain-provider.c',
-  'gbp-cmake-toolchain-provider.h',
-]
+])
+
+plugin_cmake_resources = gnome.compile_resources(
+  'gbp-cmake-resources',
+  'cmake.gresource.xml',
+  c_name: 'gbp_cmake',
+)
 
-gnome_builder_plugins_sources += files(cmake_sources)
-gnome_builder_plugins_sources += cmake_resources[0]
+plugins_sources += plugin_cmake_resources[0]
 
 endif
diff --git a/src/plugins/code-index/code-index-plugin.c b/src/plugins/code-index/code-index-plugin.c
index 31726a796..65029523e 100644
--- a/src/plugins/code-index/code-index-plugin.c
+++ b/src/plugins/code-index/code-index-plugin.c
@@ -1,6 +1,7 @@
 /* code-index-plugin.c
  *
  * Copyright 2017 Anoop Chandu <anoopchandu96 gmail com>
+ * Copyright 2018-2019 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
@@ -14,27 +15,31 @@
  *
  * 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
  */
 
+#include "config.h"
+
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+#include <libide-search.h>
 
-#include "ide-code-index-service.h"
 #include "ide-code-index-search-provider.h"
 #include "ide-code-index-symbol-resolver.h"
+#include "gbp-code-index-workbench-addin.h"
 
-void
-ide_code_index_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_ide_code_index_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_SERVICE,
-                                              IDE_TYPE_CODE_INDEX_SERVICE);
-
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_SEARCH_PROVIDER,
                                               IDE_TYPE_CODE_INDEX_SEARCH_PROVIDER);
-
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_SYMBOL_RESOLVER,
                                               IDE_TYPE_CODE_INDEX_SYMBOL_RESOLVER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GBP_TYPE_CODE_INDEX_WORKBENCH_ADDIN);
 }
diff --git a/src/plugins/code-index/code-index.gresource.xml b/src/plugins/code-index/code-index.gresource.xml
index 12fc75e05..bb6304147 100644
--- a/src/plugins/code-index/code-index.gresource.xml
+++ b/src/plugins/code-index/code-index.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/code-index">
     <file>code-index.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/code-index/code-index.plugin b/src/plugins/code-index/code-index.plugin
index 33a7156cc..bffcc36ae 100644
--- a/src/plugins/code-index/code-index.plugin
+++ b/src/plugins/code-index/code-index.plugin
@@ -1,10 +1,10 @@
 [Plugin]
-Module=code-index-plugin
-Name=Code Index
-Description=Manages Code Index and does Global Search and Jump to Definition using Code Index.
 Authors=Anoop Chandu <anoopchandu96 gmail com>
-Copyright=Copyright © 2017 Anoop Chandu
 Builtin=true
-Embedded=ide_code_index_register_types
-X-Symbol-Resolver-Languages=c,chdr,cpp
+Copyright=Copyright © 2017 Anoop Chandu
+Description=Manages Code Index and does Global Search and Jump to Definition using Code Index.
+Embedded=_ide_code_index_register_types
+Module=code-index
+Name=Code Index
 X-Symbol-Resolver-Languages-Priority=200
+X-Symbol-Resolver-Languages=c,chdr,cpp
diff --git a/src/plugins/code-index/gbp-code-index-workbench-addin.c 
b/src/plugins/code-index/gbp-code-index-workbench-addin.c
new file mode 100644
index 000000000..5fdd3f646
--- /dev/null
+++ b/src/plugins/code-index/gbp-code-index-workbench-addin.c
@@ -0,0 +1,759 @@
+/* gbp-code-index-workbench-addin.c
+ *
+ * Copyright 2018-2019 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-code-index-workbench-addin"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <gtksourceview/gtksource.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+#include <libide-plugins.h>
+#include <libide-projects.h>
+#include <libide-vcs.h>
+
+#include "gbp-code-index-workbench-addin.h"
+#include "ide-code-index-builder.h"
+
+#define DEFAULT_INDEX_TIMEOUT_SECS 5
+#define MAX_TRIALS 3
+
+struct _GbpCodeIndexWorkbenchAddin
+{
+  IdeObject               parent_instance;
+
+  IdeWorkbench           *workbench;
+
+  /* The builder to build & update index */
+  IdeCodeIndexBuilder    *builder;
+
+  /* The Index which will store all declarations */
+  IdeCodeIndexIndex      *index;
+
+  /* Queue of directories which needs to be indexed */
+  GQueue                  build_queue;
+  GHashTable             *build_dirs;
+
+  GHashTable             *code_indexers;
+
+  IdeNotification        *notif;
+  GCancellable           *cancellable;
+
+  guint                   paused : 1;
+  guint                   delayed_build_reqeusted : 1;
+};
+
+typedef struct
+{
+  volatile gint               ref_count;
+  GbpCodeIndexWorkbenchAddin *self;
+  GFile                      *directory;
+  guint                       n_trial;
+  guint                       recursive : 1;
+} BuildData;
+
+static void gbp_code_index_workbench_addin_build (GbpCodeIndexWorkbenchAddin *self,
+                                                  GFile                      *directory,
+                                                  gboolean                    recursive,
+                                                  guint                       n_trial);
+
+static void
+remove_source (gpointer source_id)
+{
+  if (source_id != NULL)
+    g_source_remove (GPOINTER_TO_UINT (source_id));
+}
+
+static void
+build_data_unref (BuildData *data)
+{
+  g_assert (data != NULL);
+  g_assert (data->ref_count > 0);
+
+  if (g_atomic_int_dec_and_test (&data->ref_count))
+    {
+      g_clear_object (&data->self);
+      g_clear_object (&data->directory);
+      g_slice_free (BuildData, data);
+    }
+}
+
+static BuildData *
+build_data_ref (BuildData *data)
+{
+  g_assert (data != NULL);
+  g_assert (data->ref_count > 0);
+  g_atomic_int_inc (&data->ref_count);
+  return data;
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (BuildData, build_data_unref)
+
+static void
+register_notification (GbpCodeIndexWorkbenchAddin *self)
+{
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(GIcon) icon = NULL;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (self->notif == NULL);
+
+  icon = g_icon_new_for_string ("media-playback-pause-symbolic", NULL);
+
+  notif = ide_notification_new ();
+  ide_notification_set_id (notif, "org.gnome.builder.code-index");
+  ide_notification_set_title (notif, "Indexing Source Code");
+  ide_notification_set_body (notif, "Search, diagnostics, and autocompletion may be limited until 
complete.");
+  ide_notification_set_has_progress (notif, TRUE);
+  ide_notification_set_progress (notif, 0);
+  ide_notification_set_progress_is_imprecise (notif, TRUE);
+  ide_notification_add_button (notif, NULL, icon, "code-index.paused");
+
+  context = ide_workbench_get_context (self->workbench);
+  ide_notification_attach (notif, IDE_OBJECT (context));
+
+  self->notif = g_object_ref (notif);
+}
+
+static void
+unregister_notification (GbpCodeIndexWorkbenchAddin *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+
+  if (self->notif != NULL)
+    {
+      ide_notification_withdraw (self->notif);
+      g_clear_object (&self->notif);
+    }
+}
+
+static gboolean
+delay_until_build_completes (GbpCodeIndexWorkbenchAddin *self)
+{
+  IdeBuildPipeline *pipeline;
+  IdeBuildManager *build_manager;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+
+  if (self->delayed_build_reqeusted)
+    return TRUE;
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  g_assert (IDE_IS_CONTEXT (context));
+
+  build_manager = ide_build_manager_from_context (context);
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (pipeline == NULL || !ide_build_pipeline_has_configured (pipeline))
+    {
+      self->delayed_build_reqeusted = TRUE;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+gbp_code_index_workbench_addin_build_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  g_autoptr(GbpCodeIndexWorkbenchAddin) self = user_data;
+  IdeCodeIndexBuilder *builder = (IdeCodeIndexBuilder *)object;
+  g_autoptr(BuildData) bdata = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_CODE_INDEX_BUILDER (builder));
+
+  if (ide_code_index_builder_build_finish (builder, result, &error))
+    g_debug ("Finished building code index");
+  else if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    g_warning ("Failed to build code index: %s", error->message);
+
+  if (ide_object_in_destruction (IDE_OBJECT (self)))
+    return;
+
+  bdata = g_queue_pop_head (&self->build_queue);
+
+  /*
+   * If we're paused, push this item back on the queue to
+   * be processed when we unpause.
+   */
+  if (self->paused)
+    {
+      g_queue_push_head (&self->build_queue, g_steal_pointer (&bdata));
+      return;
+    }
+
+  if (error != NULL &&
+      !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    {
+      gbp_code_index_workbench_addin_build (self,
+                                            bdata->directory,
+                                            bdata->recursive,
+                                            bdata->n_trial + 1);
+    }
+
+  /* Index next directory */
+  if (!g_queue_is_empty (&self->build_queue))
+    {
+      BuildData *peek = g_queue_peek_head (&self->build_queue);
+
+      g_clear_object (&self->cancellable);
+      self->cancellable = g_cancellable_new ();
+
+      ide_code_index_builder_build_async (builder,
+                                          peek->directory,
+                                          peek->recursive,
+                                          self->cancellable,
+                                          gbp_code_index_workbench_addin_build_cb,
+                                          g_object_ref (self));
+    }
+  else
+    {
+      unregister_notification (self);
+    }
+}
+
+static gboolean
+ide_code_index_serivce_push (BuildData *bdata)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (bdata != NULL);
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (bdata->self));
+  g_assert (G_IS_FILE (bdata->directory));
+
+  if (g_queue_is_empty (&bdata->self->build_queue))
+    {
+      g_queue_push_tail (&bdata->self->build_queue, build_data_ref (bdata));
+
+      g_clear_object (&bdata->self->cancellable);
+      bdata->self->cancellable = g_cancellable_new ();
+
+      register_notification (bdata->self);
+
+      ide_code_index_builder_build_async (bdata->self->builder,
+                                          bdata->directory,
+                                          bdata->recursive,
+                                          bdata->self->cancellable,
+                                          gbp_code_index_workbench_addin_build_cb,
+                                          g_object_ref (bdata->self));
+    }
+  else
+    {
+      g_queue_push_tail (&bdata->self->build_queue, build_data_ref (bdata));
+    }
+
+  if (bdata->self->build_dirs != NULL)
+    g_hash_table_remove (bdata->self->build_dirs, bdata->directory);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_code_index_workbench_addin_build (GbpCodeIndexWorkbenchAddin *self,
+                                      GFile                      *directory,
+                                      gboolean                    recursive,
+                                      guint                       n_trial)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_FILE (directory));
+
+  if (n_trial > MAX_TRIALS)
+    return;
+
+  /*
+   * If the build system is currently failed, then don't try to
+   * do any indexing now. We'll wait for a successful build that
+   * at least reaches IDE_BUILD_PHASE_CONFIGURE and then trigger
+   * after that.
+   */
+  if (delay_until_build_completes (self))
+    return;
+
+  if (!g_hash_table_lookup (self->build_dirs, directory))
+    {
+      g_autoptr(BuildData) bdata = NULL;
+      guint source_id;
+
+      bdata = g_slice_new0 (BuildData);
+      bdata->ref_count = 1;
+      bdata->self = g_object_ref (self);
+      bdata->directory = g_object_ref (directory);
+      bdata->recursive = recursive;
+      bdata->n_trial = n_trial;
+
+      source_id = g_timeout_add_seconds_full (G_PRIORITY_LOW,
+                                              DEFAULT_INDEX_TIMEOUT_SECS,
+                                              (GSourceFunc) ide_code_index_serivce_push,
+                                              g_steal_pointer (&bdata),
+                                              (GDestroyNotify) build_data_unref);
+
+      g_hash_table_insert (self->build_dirs,
+                           g_object_ref (directory),
+                           GUINT_TO_POINTER (source_id));
+    }
+}
+
+static void
+gbp_code_index_workbench_addin_vcs_changed (GbpCodeIndexWorkbenchAddin *self,
+                                            IdeVcs                     *vcs)
+{
+  GFile *workdir;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_VCS (vcs));
+
+  workdir = ide_vcs_get_workdir (vcs);
+  gbp_code_index_workbench_addin_build (self, workdir, TRUE, 1);
+}
+
+static void
+gbp_code_index_workbench_addin_buffer_saved (GbpCodeIndexWorkbenchAddin *self,
+                                             IdeBuffer                  *buffer,
+                                             IdeBufferManager           *buffer_manager)
+{
+  g_autofree gchar *file_name = NULL;
+  GFile *file;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  file = ide_buffer_get_file (buffer);
+  file_name = g_file_get_uri (file);
+
+  if (!!gbp_code_index_workbench_addin_get_code_indexer (self, file_name))
+    {
+      g_autoptr(GFile) parent = NULL;
+
+      parent = g_file_get_parent (file);
+      gbp_code_index_workbench_addin_build (self, parent, FALSE, 1);
+    }
+}
+
+static void
+gbp_code_index_workbench_addin_file_trashed (GbpCodeIndexWorkbenchAddin *self,
+                                             GFile                      *file,
+                                             IdeProject                 *project)
+{
+  g_autofree gchar *file_name = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+
+  file_name = g_file_get_uri (file);
+
+  if (!!gbp_code_index_workbench_addin_get_code_indexer (self, file_name))
+    {
+      g_autoptr(GFile) parent = NULL;
+
+      parent = g_file_get_parent (file);
+      gbp_code_index_workbench_addin_build (self, parent, FALSE, 1);
+    }
+}
+
+static void
+gbp_code_index_workbench_addin_file_renamed (GbpCodeIndexWorkbenchAddin *self,
+                                             GFile                      *src_file,
+                                             GFile                      *dst_file,
+                                             IdeProject                 *project)
+{
+  g_autofree gchar *src_file_name = NULL;
+  g_autofree gchar *dst_file_name = NULL;
+  g_autoptr(GFile) src_parent = NULL;
+  g_autoptr(GFile) dst_parent = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_FILE (src_file));
+  g_assert (G_IS_FILE (dst_file));
+
+  src_file_name = g_file_get_uri (src_file);
+  dst_file_name = g_file_get_uri (dst_file);
+
+  src_parent = g_file_get_parent (src_file);
+  dst_parent = g_file_get_parent (dst_file);
+
+  if (g_file_equal (src_parent, dst_parent))
+    {
+      if (NULL != gbp_code_index_workbench_addin_get_code_indexer (self, src_file_name) ||
+          NULL != gbp_code_index_workbench_addin_get_code_indexer (self, dst_file_name))
+        gbp_code_index_workbench_addin_build (self, src_parent, FALSE, 1);
+    }
+  else
+    {
+      if (NULL != gbp_code_index_workbench_addin_get_code_indexer (self, src_file_name))
+        gbp_code_index_workbench_addin_build (self, src_parent, FALSE, 1);
+
+      if (NULL != gbp_code_index_workbench_addin_get_code_indexer (self, dst_file_name))
+        gbp_code_index_workbench_addin_build (self, dst_parent, FALSE, 1);
+    }
+}
+
+static void
+gbp_code_index_workbench_addin_build_finished (GbpCodeIndexWorkbenchAddin *self,
+                                               IdeBuildPipeline           *pipeline,
+                                               IdeBuildManager            *build_manager)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_BUILD_MANAGER (build_manager));
+
+  if (self->delayed_build_reqeusted &&
+      ide_build_pipeline_has_configured (pipeline))
+    {
+      g_autoptr(GFile) workdir = NULL;
+      IdeContext *context;
+
+      self->delayed_build_reqeusted = FALSE;
+
+      context = ide_object_get_context (IDE_OBJECT (self));
+      workdir = ide_context_ref_workdir (context);
+
+      gbp_code_index_workbench_addin_build (self, workdir, TRUE, 1);
+    }
+}
+
+static void
+gbp_code_index_workbench_addin_load (IdeWorkbenchAddin *addin,
+                                     IdeWorkbench      *workbench)
+{
+  GbpCodeIndexWorkbenchAddin *self = (GbpCodeIndexWorkbenchAddin *)addin;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+
+  self->workbench = workbench;
+
+  context = ide_workbench_get_context (workbench);
+  ide_object_append (IDE_OBJECT (context), IDE_OBJECT (self));
+}
+
+static void
+gbp_code_index_workbench_addin_unload (IdeWorkbenchAddin *addin,
+                                       IdeWorkbench      *workbench)
+{
+  GbpCodeIndexWorkbenchAddin *self = (GbpCodeIndexWorkbenchAddin *)addin;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+
+  g_clear_pointer (&self->build_dirs, g_hash_table_unref);
+  g_clear_pointer (&self->code_indexers, g_hash_table_unref);
+  ide_clear_and_destroy_object (&self->builder);
+  ide_clear_and_destroy_object (&self->index);
+
+  context = ide_workbench_get_context (workbench);
+  ide_object_remove (IDE_OBJECT (context), IDE_OBJECT (self));
+
+  self->workbench = NULL;
+}
+
+static void
+gbp_code_index_workbench_addin_project_loaded (IdeWorkbenchAddin *addin,
+                                               IdeProjectInfo    *project_info)
+{
+  GbpCodeIndexWorkbenchAddin *self = (GbpCodeIndexWorkbenchAddin *)addin;
+  IdeBufferManager *bufmgr;
+  IdeBuildManager *buildmgr;
+  IdeContext *context;
+  IdeProject *project;
+  IdeVcs *vcs;
+  GFile *workdir;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+
+  context = ide_workbench_get_context (self->workbench);
+  project = ide_project_from_context (context);
+  bufmgr = ide_buffer_manager_from_context (context);
+  buildmgr = ide_build_manager_from_context (context);
+  vcs = ide_vcs_from_context (context);
+  workdir = ide_vcs_get_workdir (vcs);
+
+  self->code_indexers = g_hash_table_new_full (NULL,
+                                               NULL,
+                                               NULL,
+                                               (GDestroyNotify)ide_object_unref_and_destroy);
+  self->index = ide_code_index_index_new (IDE_OBJECT (self));
+  self->builder = ide_code_index_builder_new (IDE_OBJECT (self), self->index);
+  self->build_dirs = g_hash_table_new_full (g_file_hash,
+                                            (GEqualFunc)g_file_equal,
+                                            g_object_unref,
+                                            remove_source);
+
+  g_signal_connect_object (vcs,
+                           "changed",
+                           G_CALLBACK (gbp_code_index_workbench_addin_vcs_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (bufmgr,
+                           "buffer-saved",
+                           G_CALLBACK (gbp_code_index_workbench_addin_buffer_saved),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (buildmgr,
+                           "build-finished",
+                           G_CALLBACK (gbp_code_index_workbench_addin_build_finished),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (project,
+                           "file-trashed",
+                           G_CALLBACK (gbp_code_index_workbench_addin_file_trashed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (project,
+                           "file-renamed",
+                           G_CALLBACK (gbp_code_index_workbench_addin_file_renamed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gbp_code_index_workbench_addin_build (self, workdir, TRUE, 1);
+}
+
+static void
+gbp_code_index_workbench_addin_workspace_added (IdeWorkbenchAddin *addin,
+                                                IdeWorkspace      *workspace)
+{
+  GbpCodeIndexWorkbenchAddin *self = (GbpCodeIndexWorkbenchAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace),
+                                  "code-index",
+                                  G_ACTION_GROUP (self));
+}
+
+static void
+gbp_code_index_workbench_addin_workspace_removed (IdeWorkbenchAddin *addin,
+                                                  IdeWorkspace      *workspace)
+{
+  GbpCodeIndexWorkbenchAddin *self = (GbpCodeIndexWorkbenchAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "code-index", NULL);
+}
+
+static void
+workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load = gbp_code_index_workbench_addin_load;
+  iface->unload = gbp_code_index_workbench_addin_unload;
+  iface->project_loaded = gbp_code_index_workbench_addin_project_loaded;
+  iface->workspace_added = gbp_code_index_workbench_addin_workspace_added;
+  iface->workspace_removed = gbp_code_index_workbench_addin_workspace_removed;
+}
+
+static void
+gbp_code_index_workbench_addin_paused (GbpCodeIndexWorkbenchAddin *self,
+                                       GVariant                   *state)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+
+  if (state == NULL || !g_variant_is_of_type (state, G_VARIANT_TYPE_BOOLEAN))
+    return;
+
+  if (g_variant_get_boolean (state))
+    gbp_code_index_workbench_addin_pause (self);
+  else
+    gbp_code_index_workbench_addin_unpause (self);
+}
+
+DZL_DEFINE_ACTION_GROUP (GbpCodeIndexWorkbenchAddin, gbp_code_index_workbench_addin, {
+  { "paused", NULL, NULL, "false", gbp_code_index_workbench_addin_paused },
+})
+
+G_DEFINE_TYPE_WITH_CODE (GbpCodeIndexWorkbenchAddin, gbp_code_index_workbench_addin, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP,
+                                                gbp_code_index_workbench_addin_init_action_group)
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKBENCH_ADDIN, workbench_addin_iface_init))
+
+static void
+gbp_code_index_workbench_addin_class_init (GbpCodeIndexWorkbenchAddinClass *klass)
+{
+}
+
+static void
+gbp_code_index_workbench_addin_init (GbpCodeIndexWorkbenchAddin *self)
+{
+}
+
+void
+gbp_code_index_workbench_addin_pause (GbpCodeIndexWorkbenchAddin *self)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+
+  if (ide_object_in_destruction (IDE_OBJECT (self)))
+    return;
+
+  if (self->paused)
+    return;
+
+  self->paused = TRUE;
+
+  gbp_code_index_workbench_addin_set_action_state (self,
+                                                   "paused",
+                                                   g_variant_new_boolean (TRUE));
+
+  /*
+   * To pause things, we need to cancel our current task. The completion of the
+   * async task will check for cancelled and leave the build task for another
+   * pass.
+   */
+
+  g_cancellable_cancel (self->cancellable);
+}
+
+void
+gbp_code_index_workbench_addin_unpause (GbpCodeIndexWorkbenchAddin *self)
+{
+  BuildData *peek;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self));
+
+  if (ide_object_in_destruction (IDE_OBJECT (self)))
+    return;
+
+  if (!self->paused)
+    return;
+
+  self->paused = FALSE;
+
+  gbp_code_index_workbench_addin_set_action_state (self,
+                                                   "paused",
+                                                   g_variant_new_boolean (FALSE));
+
+  peek = g_queue_peek_head (&self->build_queue);
+
+  if (peek != NULL)
+    {
+      GCancellable *cancellable;
+
+      g_clear_object (&self->cancellable);
+      self->cancellable = cancellable = g_cancellable_new ();
+
+      ide_code_index_builder_build_async (self->builder,
+                                          peek->directory,
+                                          peek->recursive,
+                                          cancellable,
+                                          gbp_code_index_workbench_addin_build_cb,
+                                          g_object_ref (self));
+    }
+}
+
+/**
+ * gbp_code_index_workbench_addin_get_code_indexer:
+ * @self: a #GbpCodeIndexWorkbenchAddin
+ * @file_name: the name of the file to index
+ *
+ * Gets an #IdeCodeIndexer suitable for @file_name.
+ *
+ * Returns: (transfer none) (nullable): an #IdeCodeIndexer or %NULL
+ */
+IdeCodeIndexer *
+gbp_code_index_workbench_addin_get_code_indexer (GbpCodeIndexWorkbenchAddin *self,
+                                                 const gchar                *file_name)
+{
+  GtkSourceLanguageManager *manager;
+  IdeExtensionAdapter *adapter;
+  GtkSourceLanguage *language;
+  const gchar *lang_id;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self), NULL);
+  g_return_val_if_fail (file_name != NULL, NULL);
+
+  if (self->code_indexers == NULL ||
+      !(manager = gtk_source_language_manager_get_default ()) ||
+      !(language = gtk_source_language_manager_guess_language (manager, file_name, NULL)) ||
+      !(lang_id = gtk_source_language_get_id (language)))
+    return NULL;
+
+  lang_id = g_intern_string (lang_id);
+  adapter = g_hash_table_lookup (self->code_indexers, lang_id);
+
+  g_assert (!adapter || IDE_IS_EXTENSION_ADAPTER (adapter));
+
+  if (adapter == NULL)
+    {
+      adapter = ide_extension_adapter_new (IDE_OBJECT (self),
+                                           peas_engine_get_default (),
+                                           IDE_TYPE_CODE_INDEXER,
+                                           "Code-Indexer-Languages",
+                                           lang_id);
+      g_hash_table_insert (self->code_indexers, (gchar *)lang_id, adapter);
+    }
+
+  g_assert (IDE_IS_EXTENSION_ADAPTER (adapter));
+
+  return ide_extension_adapter_get_extension (adapter);
+}
+
+GbpCodeIndexWorkbenchAddin *
+gbp_code_index_workbench_addin_from_context (IdeContext *context)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  return ide_context_peek_child_typed (context, GBP_TYPE_CODE_INDEX_WORKBENCH_ADDIN);
+}
+
+IdeCodeIndexIndex *
+gbp_code_index_workbench_addin_get_index (GbpCodeIndexWorkbenchAddin *self)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (self), NULL);
+
+  return self->index;
+}
diff --git a/src/plugins/code-index/gbp-code-index-workbench-addin.h 
b/src/plugins/code-index/gbp-code-index-workbench-addin.h
new file mode 100644
index 000000000..11ec81e3f
--- /dev/null
+++ b/src/plugins/code-index/gbp-code-index-workbench-addin.h
@@ -0,0 +1,40 @@
+/* gbp-code-index-workbench-addin.h
+ *
+ * Copyright 2018-2019 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-code.h>
+
+#include "ide-code-index-index.h"
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_CODE_INDEX_WORKBENCH_ADDIN (gbp_code_index_workbench_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCodeIndexWorkbenchAddin, gbp_code_index_workbench_addin, GBP, 
CODE_INDEX_WORKBENCH_ADDIN, IdeObject)
+
+GbpCodeIndexWorkbenchAddin *gbp_code_index_workbench_addin_from_context     (IdeContext                 
*context);
+void                        gbp_code_index_workbench_addin_pause            (GbpCodeIndexWorkbenchAddin 
*self);
+void                        gbp_code_index_workbench_addin_unpause          (GbpCodeIndexWorkbenchAddin 
*self);
+IdeCodeIndexer             *gbp_code_index_workbench_addin_get_code_indexer (GbpCodeIndexWorkbenchAddin 
*self,
+                                                                             const gchar                
*file_name);
+IdeCodeIndexIndex          *gbp_code_index_workbench_addin_get_index        (GbpCodeIndexWorkbenchAddin 
*self);
+
+G_END_DECLS
diff --git a/src/plugins/code-index/ide-code-index-builder.c b/src/plugins/code-index/ide-code-index-builder.c
index b553db2ae..43ee1b48d 100644
--- a/src/plugins/code-index/ide-code-index-builder.c
+++ b/src/plugins/code-index/ide-code-index-builder.c
@@ -1,7 +1,7 @@
 /* ide-code-index-builder.c
  *
  * Copyright 2017 Anoop Chandu <anoopchandu96 gmail com>
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -15,19 +15,23 @@
  *
  * 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 "ide-code-index-builder"
 
 #include <dazzle.h>
+#include <libpeas/peas.h>
+#include <libide-vcs.h>
 #include <string.h>
 
 #include "ide-code-index-builder.h"
+#include "gbp-code-index-workbench-addin.h"
 
 struct _IdeCodeIndexBuilder
 {
   IdeObject            parent;
-  IdeCodeIndexService *service;
   IdeCodeIndexIndex   *index;
 };
 
@@ -101,7 +105,6 @@ typedef struct
 enum {
   PROP_0,
   PROP_INDEX,
-  PROP_SERVICE,
   N_PROPS
 };
 
@@ -299,7 +302,6 @@ ide_code_index_builder_dispose (GObject *object)
   IdeCodeIndexBuilder *self = (IdeCodeIndexBuilder *)object;
 
   g_clear_object (&self->index);
-  g_clear_object (&self->service);
 
   G_OBJECT_CLASS (ide_code_index_builder_parent_class)->dispose (object);
 }
@@ -318,10 +320,6 @@ ide_code_index_builder_get_property (GObject    *object,
       g_value_set_object (value, self->index);
       break;
 
-    case PROP_SERVICE:
-      g_value_set_object (value, self->service);
-      break;
-
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -341,10 +339,6 @@ ide_code_index_builder_set_property (GObject      *object,
       self->index = g_value_dup_object (value);
       break;
 
-    case PROP_SERVICE:
-      self->service = g_value_dup_object (value);
-      break;
-
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -366,13 +360,6 @@ ide_code_index_builder_class_init (IdeCodeIndexBuilderClass *klass)
                          IDE_TYPE_CODE_INDEX_INDEX,
                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
 
-  properties [PROP_SERVICE] =
-    g_param_spec_object ("service",
-                         "Service",
-                         "The service to query for various build information",
-                         IDE_TYPE_CODE_INDEX_SERVICE,
-                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
-
   g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
@@ -382,18 +369,15 @@ ide_code_index_builder_init (IdeCodeIndexBuilder *self)
 }
 
 IdeCodeIndexBuilder *
-ide_code_index_builder_new (IdeContext          *context,
-                            IdeCodeIndexService *service,
-                            IdeCodeIndexIndex   *index)
+ide_code_index_builder_new (IdeObject         *parent,
+                            IdeCodeIndexIndex *index)
 {
-  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
-  g_return_val_if_fail (IDE_IS_CODE_INDEX_SERVICE (service), NULL);
+  g_return_val_if_fail (IDE_IS_OBJECT (parent), NULL);
   g_return_val_if_fail (IDE_IS_CODE_INDEX_INDEX (index), NULL);
 
   return g_object_new (IDE_TYPE_CODE_INDEX_BUILDER,
-                       "context", context,
-                       "service", service,
                        "index", index,
+                       "parent", parent,
                        NULL);
 }
 
@@ -778,7 +762,7 @@ get_changes_async (IdeCodeIndexBuilder *self,
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
+  vcs = ide_vcs_from_context (context);
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, get_changes_async);
@@ -1151,6 +1135,7 @@ index_directory_async (IdeCodeIndexBuilder *self,
                        GAsyncReadyCallback  callback,
                        gpointer             user_data)
 {
+  g_autoptr(GbpCodeIndexWorkbenchAddin) addin = NULL;
   g_autoptr(IdeTask) task = NULL;
   IndexDirectoryData *idd;
   GHashTableIter iter;
@@ -1182,24 +1167,24 @@ index_directory_async (IdeCodeIndexBuilder *self,
 
   idd->n_active++;
 
+  addin = GBP_CODE_INDEX_WORKBENCH_ADDIN (ide_object_ref_parent (IDE_OBJECT (self)));
+
   while (g_hash_table_iter_next (&iter, &k, &v))
     {
-      IdeFile *file = k;
+      GFile *file = k;
       const gchar * const *file_flags = v;
-      const gchar *path = ide_file_get_path (file);
-      GFile *gfile = ide_file_get_file (file);
+      const gchar *path = g_file_peek_path (file);
       IdeCodeIndexer *indexer;
 
-      g_assert (IDE_IS_FILE (file));
-      g_assert (G_IS_FILE (gfile));
+      g_assert (G_IS_FILE (file));
       g_assert (path != NULL);
-      g_assert (IDE_IS_CODE_INDEX_SERVICE (self->service));
+      g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (addin));
 
-      if ((indexer = ide_code_index_service_get_code_indexer (self->service, path)))
+      if ((indexer = gbp_code_index_workbench_addin_get_code_indexer (addin, path)))
         {
           idd->n_active++;
           ide_code_indexer_index_file_async (indexer,
-                                             gfile,
+                                             file,
                                              file_flags,
                                              cancellable,
                                              index_directory_index_file_cb,
@@ -1423,9 +1408,9 @@ ide_code_index_builder_build_async (IdeCodeIndexBuilder *self,
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_system = ide_context_get_build_system (context);
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  build_system = ide_build_system_from_context (context);
+  vcs = ide_vcs_from_context (context);
+  workdir = ide_vcs_get_workdir (vcs);
   relative = g_file_get_relative_path (workdir, directory);
   index_dir = ide_context_cache_file (context, "code-index", relative, NULL);
 
diff --git a/src/plugins/code-index/ide-code-index-builder.h b/src/plugins/code-index/ide-code-index-builder.h
index 29a571c05..ff54714a5 100644
--- a/src/plugins/code-index/ide-code-index-builder.h
+++ b/src/plugins/code-index/ide-code-index-builder.h
@@ -1,7 +1,7 @@
 /* ide-code-index-builder.h
  *
  * Copyright 2017 Anoop Chandu <anoopchandu96 gmail com>
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -15,14 +15,15 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 #include "ide-code-index-index.h"
-#include "ide-code-index-service.h"
 
 G_BEGIN_DECLS
 
@@ -30,8 +31,7 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeCodeIndexBuilder, ide_code_index_builder, IDE, CODE_INDEX_BUILDER, IdeObject)
 
-IdeCodeIndexBuilder *ide_code_index_builder_new          (IdeContext           *context,
-                                                          IdeCodeIndexService  *service,
+IdeCodeIndexBuilder *ide_code_index_builder_new          (IdeObject            *parent,
                                                           IdeCodeIndexIndex    *index);
 void                 ide_code_index_builder_drop_caches  (IdeCodeIndexBuilder  *self);
 void                 ide_code_index_builder_build_async  (IdeCodeIndexBuilder  *self,
diff --git a/src/plugins/code-index/ide-code-index-index.c b/src/plugins/code-index/ide-code-index-index.c
index 84238773e..d9a449747 100644
--- a/src/plugins/code-index/ide-code-index-index.c
+++ b/src/plugins/code-index/ide-code-index-index.c
@@ -14,6 +14,8 @@
  *
  * 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 "ide-code-index-index"
@@ -225,8 +227,8 @@ static IdeCodeIndexSearchResult *
 ide_code_index_index_create_search_result (IdeContext       *context,
                                            const FuzzyMatch *fuzzy_match)
 {
-  g_autoptr(IdeFile) file = NULL;
-  g_autoptr(IdeSourceLocation) location = NULL;
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(IdeLocation) location = NULL;
   g_autoptr(GString) subtitle = NULL;
   const gchar *key;
   const gchar *icon_name;
@@ -249,7 +251,7 @@ ide_code_index_index_create_search_result (IdeContext       *context,
   g_variant_get (value, "(uuuuu)", &file_id, &line, &line_offset, &flags, &kind);
 
   /* Ignore variables in global search */
-  if (kind == IDE_SYMBOL_VARIABLE)
+  if (kind == IDE_SYMBOL_KIND_VARIABLE)
     return NULL;
 
   key = dzl_fuzzy_index_match_get_key (fuzzy_match->match);
@@ -258,8 +260,8 @@ ide_code_index_index_create_search_result (IdeContext       *context,
 
   path = dzl_fuzzy_index_get_metadata_string (fuzzy_match->index, num);
 
-  file = ide_file_new_for_path (context, path);
-  location = ide_source_location_new (file, line - 1, line_offset - 1, 0);
+  file = g_file_new_for_path (path);
+  location = ide_location_new (file, line - 1, line_offset - 1);
 
   icon_name = ide_symbol_kind_get_icon_name (kind);
   score = dzl_fuzzy_index_match_get_score (fuzzy_match->match);
@@ -269,7 +271,7 @@ ide_code_index_index_create_search_result (IdeContext       *context,
   if (NULL != (shortname = strrchr (path, G_DIR_SEPARATOR)))
     g_string_append (subtitle, shortname + 1);
 
-  if ((kind == IDE_SYMBOL_FUNCTION) && !(flags & IDE_SYMBOL_FLAGS_IS_DEFINITION))
+  if ((kind == IDE_SYMBOL_KIND_FUNCTION) && !(flags & IDE_SYMBOL_FLAGS_IS_DEFINITION))
     {
       /* translators: "Declaration" is describing a function that is defined in a header
        *              file (.h) rather than a source file (.c).
@@ -487,16 +489,15 @@ IdeSymbol *
 ide_code_index_index_lookup_symbol (IdeCodeIndexIndex *self,
                                     const gchar       *key)
 {
-  g_autoptr(IdeSourceLocation) declaration = NULL;
-  g_autoptr(IdeSourceLocation) definition = NULL;
-  g_autoptr(IdeFile) file = NULL;
+  g_autoptr(IdeLocation) declaration = NULL;
+  g_autoptr(IdeLocation) definition = NULL;
+  g_autoptr(GFile) file = NULL;
   g_autoptr(GMutexLocker) locker = NULL;
   g_autofree gchar *name = NULL;
-  IdeSymbolKind kind = IDE_SYMBOL_NONE;
+  IdeSymbolKind kind = IDE_SYMBOL_KIND_NONE;
   IdeSymbolFlags flags = IDE_SYMBOL_FLAGS_NONE;
   DzlFuzzyIndex *symbol_names = NULL;
   const DirectoryIndex *dir_index = NULL;
-  IdeContext *context;
   const gchar *filename;
   guint32 file_id = 0;
   guint32 line = 0;
@@ -537,15 +538,14 @@ ide_code_index_index_lookup_symbol (IdeCodeIndexIndex *self,
   g_snprintf (num, sizeof(num), "%u", file_id);
 
   filename = dzl_fuzzy_index_get_metadata_string (symbol_names, num);
-  context = ide_object_get_context (IDE_OBJECT (self));
-  file = ide_file_new_for_path (context, filename);
+  file = g_file_new_for_path (filename);
 
   if (flags & IDE_SYMBOL_FLAGS_IS_DEFINITION)
-    definition = ide_source_location_new (file, line - 1, line_offset - 1, 0);
+    definition = ide_location_new (file, line - 1, line_offset - 1);
   else
-    declaration = ide_source_location_new (file, line - 1, line_offset - 1, 0);
+    declaration = ide_location_new (file, line - 1, line_offset - 1);
 
-  return ide_symbol_new (name, kind, flags, declaration, definition, NULL);
+  return ide_symbol_new (name, kind, flags, definition, declaration);
 }
 
 static void
@@ -579,9 +579,11 @@ ide_code_index_index_class_init (IdeCodeIndexIndexClass *klass)
 }
 
 IdeCodeIndexIndex *
-ide_code_index_index_new (IdeContext *context)
+ide_code_index_index_new (IdeObject *parent)
 {
+  g_return_val_if_fail (IDE_IS_OBJECT (parent), NULL);
+
   return g_object_new (IDE_TYPE_CODE_INDEX_INDEX,
-                       "context", context,
+                       "parent", parent,
                        NULL);
 }
diff --git a/src/plugins/code-index/ide-code-index-index.h b/src/plugins/code-index/ide-code-index-index.h
index 9231b459c..27681a3df 100644
--- a/src/plugins/code-index/ide-code-index-index.h
+++ b/src/plugins/code-index/ide-code-index-index.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
@@ -26,7 +28,7 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeCodeIndexIndex, ide_code_index_index, IDE, CODE_INDEX_INDEX, IdeObject)
 
-IdeCodeIndexIndex *ide_code_index_index_new             (IdeContext           *context);
+IdeCodeIndexIndex *ide_code_index_index_new             (IdeObject            *parent);
 gboolean           ide_code_index_index_load            (IdeCodeIndexIndex    *self,
                                                          GFile                *directory,
                                                          GFile                *source_directory,
diff --git a/src/plugins/code-index/ide-code-index-search-provider.c 
b/src/plugins/code-index/ide-code-index-search-provider.c
index 702bd98a5..2efc86e3e 100644
--- a/src/plugins/code-index/ide-code-index-search-provider.c
+++ b/src/plugins/code-index/ide-code-index-search-provider.c
@@ -14,13 +14,18 @@
  *
  * 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 "ide-code-index-search-provider"
 
+#include <libide-code.h>
+#include <libide-foundry.h>
+
 #include "ide-code-index-search-provider.h"
-#include "ide-code-index-service.h"
 #include "ide-code-index-index.h"
+#include "gbp-code-index-workbench-addin.h"
 
 static void
 populate_cb (GObject      *object,
@@ -55,8 +60,8 @@ ide_code_index_search_provider_search_async (IdeSearchProvider   *provider,
                                              gpointer             user_data)
 {
   IdeCodeIndexSearchProvider *self = (IdeCodeIndexSearchProvider *)provider;
+  GbpCodeIndexWorkbenchAddin *addin = NULL;
   g_autoptr(IdeTask) task = NULL;
-  IdeCodeIndexService *service;
   IdeCodeIndexIndex *index;
   IdeContext *context;
 
@@ -70,10 +75,17 @@ ide_code_index_search_provider_search_async (IdeSearchProvider   *provider,
   context = ide_object_get_context (IDE_OBJECT (self));
   g_assert (IDE_IS_CONTEXT (context));
 
-  service = ide_context_get_service_typed (context, IDE_TYPE_CODE_INDEX_SERVICE);
-  g_assert (IDE_IS_CODE_INDEX_SERVICE (service));
-
-  index = ide_code_index_service_get_index (service);
+  if (!ide_context_has_project (context) ||
+      !(addin = gbp_code_index_workbench_addin_from_context (context)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Code index requires a project");
+      IDE_EXIT;
+    }
+
+  index = gbp_code_index_workbench_addin_get_index (addin);
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_code_index_search_provider_search_async);
diff --git a/src/plugins/code-index/ide-code-index-search-provider.h 
b/src/plugins/code-index/ide-code-index-search-provider.h
index 325a599a4..b0b70d06a 100644
--- a/src/plugins/code-index/ide-code-index-search-provider.h
+++ b/src/plugins/code-index/ide-code-index-search-provider.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-search.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/code-index/ide-code-index-search-result.c 
b/src/plugins/code-index/ide-code-index-search-result.c
index a68c96c0e..8425fd09a 100644
--- a/src/plugins/code-index/ide-code-index-search-result.c
+++ b/src/plugins/code-index/ide-code-index-search-result.c
@@ -14,16 +14,23 @@
  *
  * 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 "ide-code-index-search-result"
 
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-editor.h>
+
 #include "ide-code-index-search-result.h"
 
 struct _IdeCodeIndexSearchResult
 {
-  IdeSearchResult    parent;
-  IdeSourceLocation *location;
+  IdeSearchResult  parent;
+  IdeLocation     *location;
 };
 
 G_DEFINE_TYPE (IdeCodeIndexSearchResult, ide_code_index_search_result, IDE_TYPE_SEARCH_RESULT)
@@ -36,14 +43,23 @@ enum {
 
 static GParamSpec *properties [N_PROPS];
 
-static IdeSourceLocation *
-ide_code_index_search_result_get_source_location (IdeSearchResult *result)
+static void
+ide_code_index_search_result_activate (IdeSearchResult *result,
+                                       GtkWidget       *last_focus)
 {
   IdeCodeIndexSearchResult *self = (IdeCodeIndexSearchResult *)result;
+  IdeWorkspace *workspace;
+  IdeSurface *editor;
+
+  g_assert (IDE_IS_CODE_INDEX_SEARCH_RESULT (self));
+  g_assert (GTK_IS_WIDGET (last_focus));
 
-  g_return_val_if_fail (IDE_IS_CODE_INDEX_SEARCH_RESULT (self), NULL);
+  if (!last_focus)
+    return;
 
-  return ide_source_location_ref (self->location);
+  if ((workspace = ide_widget_get_workspace (last_focus)) &&
+      (editor = ide_workspace_get_surface_by_name (workspace, "editor")))
+    ide_editor_surface_focus_location (IDE_EDITOR_SURFACE (editor), self->location);
 }
 
 static void
@@ -57,7 +73,7 @@ ide_code_index_search_result_get_property (GObject    *object,
   switch (prop_id)
     {
     case PROP_LOCATION:
-      g_value_set_boxed (value, self->location);
+      g_value_set_object (value, self->location);
       break;
 
     default:
@@ -76,7 +92,7 @@ ide_code_index_search_result_set_property (GObject      *object,
   switch (prop_id)
     {
     case PROP_LOCATION:
-      self->location = g_value_dup_boxed (value);
+      self->location = g_value_dup_object (value);
       break;
 
     default:
@@ -89,7 +105,7 @@ ide_code_index_search_result_finalize (GObject *object)
 {
   IdeCodeIndexSearchResult *self = (IdeCodeIndexSearchResult *)object;
 
-  g_clear_pointer (&self->location, ide_source_location_unref);
+  g_clear_object (&self->location);
 
   G_OBJECT_CLASS (ide_code_index_search_result_parent_class)->finalize (object);
 }
@@ -104,14 +120,14 @@ ide_code_index_search_result_class_init (IdeCodeIndexSearchResultClass *klass)
   object_class->set_property = ide_code_index_search_result_set_property;
   object_class->finalize = ide_code_index_search_result_finalize;
 
-  result_class->get_source_location = ide_code_index_search_result_get_source_location;
+  result_class->activate = ide_code_index_search_result_activate;
 
   properties [PROP_LOCATION] =
-    g_param_spec_boxed ("location",
-                        "location",
-                        "Location of symbol.",
-                        IDE_TYPE_SOURCE_LOCATION,
-                        (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+    g_param_spec_object ("location",
+                         "location",
+                         "Location of symbol.",
+                         IDE_TYPE_LOCATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 }
@@ -122,11 +138,11 @@ ide_code_index_search_result_init (IdeCodeIndexSearchResult *self)
 }
 
 IdeCodeIndexSearchResult *
-ide_code_index_search_result_new (const gchar       *title,
-                                  const gchar       *subtitle,
-                                  const gchar       *icon_name,
-                                  IdeSourceLocation *location,
-                                  gfloat             score)
+ide_code_index_search_result_new (const gchar *title,
+                                  const gchar *subtitle,
+                                  const gchar *icon_name,
+                                  IdeLocation *location,
+                                  gfloat       score)
 {
   return g_object_new (IDE_TYPE_CODE_INDEX_SEARCH_RESULT,
                        "title", title,
diff --git a/src/plugins/code-index/ide-code-index-search-result.h 
b/src/plugins/code-index/ide-code-index-search-result.h
index 943f06ec7..519221dc9 100644
--- a/src/plugins/code-index/ide-code-index-search-result.h
+++ b/src/plugins/code-index/ide-code-index-search-result.h
@@ -14,11 +14,14 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
+#include <libide-search.h>
 
 G_BEGIN_DECLS
 
@@ -26,10 +29,10 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeCodeIndexSearchResult, ide_code_index_search_result, IDE, CODE_INDEX_SEARCH_RESULT, 
IdeSearchResult)
 
-IdeCodeIndexSearchResult *ide_code_index_search_result_new (const gchar       *title,
-                                                            const gchar       *subtitle,
-                                                            const gchar       *icon_name,
-                                                            IdeSourceLocation *location,
-                                                            gfloat             score);
+IdeCodeIndexSearchResult *ide_code_index_search_result_new (const gchar *title,
+                                                            const gchar *subtitle,
+                                                            const gchar *icon_name,
+                                                            IdeLocation *location,
+                                                            gfloat       score);
 
 G_END_DECLS
diff --git a/src/plugins/code-index/ide-code-index-symbol-resolver.c 
b/src/plugins/code-index/ide-code-index-symbol-resolver.c
index ef871dc74..ee877ab39 100644
--- a/src/plugins/code-index/ide-code-index-symbol-resolver.c
+++ b/src/plugins/code-index/ide-code-index-symbol-resolver.c
@@ -14,11 +14,13 @@
  *
  * 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 "code-index-symbol-resolver"
 
-#include "ide-code-index-service.h"
+#include "gbp-code-index-workbench-addin.h"
 #include "ide-code-index-symbol-resolver.h"
 
 static void
@@ -27,12 +29,12 @@ ide_code_index_symbol_resolver_lookup_cb (GObject      *object,
                                           gpointer      user_data)
 {
   IdeCodeIndexer *code_indexer = (IdeCodeIndexer *)object;
+  GbpCodeIndexWorkbenchAddin *addin = NULL;
   g_autoptr(IdeTask) task = user_data;
   g_autoptr(IdeSymbol) symbol = NULL;
   g_autoptr(GError) error = NULL;
   g_autofree gchar *key = NULL;
   IdeCodeIndexSymbolResolver *self;
-  IdeCodeIndexService *service;
   IdeCodeIndexIndex *index;
   IdeContext *context;
 
@@ -53,10 +55,10 @@ ide_code_index_symbol_resolver_lookup_cb (GObject      *object,
   context = ide_object_get_context (IDE_OBJECT (self));
   g_assert (IDE_IS_CONTEXT (context));
 
-  service = ide_context_get_service_typed (context, IDE_TYPE_CODE_INDEX_SERVICE);
-  g_assert (IDE_IS_CODE_INDEX_SERVICE (service));
+  addin = gbp_code_index_workbench_addin_from_context (context);
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (addin));
 
-  index = ide_code_index_service_get_index (service);
+  index = gbp_code_index_workbench_addin_get_index (addin);
   g_assert (IDE_IS_CODE_INDEX_INDEX (index));
 
   symbol = ide_code_index_index_lookup_symbol (index, key);
@@ -64,7 +66,7 @@ ide_code_index_symbol_resolver_lookup_cb (GObject      *object,
   if (symbol != NULL)
     ide_task_return_pointer (task,
                              g_steal_pointer (&symbol),
-                             (GDestroyNotify)ide_symbol_unref);
+                             g_object_unref);
   else
     ide_task_return_new_error (task,
                                G_IO_ERROR,
@@ -75,7 +77,7 @@ ide_code_index_symbol_resolver_lookup_cb (GObject      *object,
 typedef struct
 {
   IdeCodeIndexer    *code_indexer;
-  IdeSourceLocation *location;
+  IdeLocation *location;
 } LookupSymbol;
 
 static void
@@ -84,7 +86,7 @@ lookup_symbol_free (gpointer data)
   LookupSymbol *state = data;
 
   g_clear_object (&state->code_indexer);
-  g_clear_pointer (&state->location, ide_source_location_unref);
+  g_clear_object (&state->location);
   g_slice_free (LookupSymbol, state);
 }
 
@@ -130,20 +132,20 @@ ide_code_index_symbol_resolver_lookup_flags_cb (GObject      *object,
 
 static void
 ide_code_index_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolver,
-                                                    IdeSourceLocation   *location,
+                                                    IdeLocation   *location,
                                                     GCancellable        *cancellable,
                                                     GAsyncReadyCallback  callback,
                                                     gpointer             user_data)
 {
   IdeCodeIndexSymbolResolver *self = (IdeCodeIndexSymbolResolver *)resolver;
+  GbpCodeIndexWorkbenchAddin *addin;
   g_autoptr(IdeTask) task = NULL;
-  IdeCodeIndexService *service;
   IdeCodeIndexer *code_indexer;
   IdeBuildSystem *build_system;
   const gchar *path;
   IdeContext *context;
-  IdeFile *file;
   LookupSymbol *lookup;
+  GFile *file;
 
   g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_CODE_INDEX_SYMBOL_RESOLVER (self));
@@ -157,14 +159,14 @@ ide_code_index_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolve
   context = ide_object_get_context (IDE_OBJECT (self));
   g_assert (IDE_IS_CONTEXT (context));
 
-  service = ide_context_get_service_typed (context, IDE_TYPE_CODE_INDEX_SERVICE);
-  g_assert (IDE_IS_CODE_INDEX_SERVICE (service));
+  addin = gbp_code_index_workbench_addin_from_context (context);
+  g_assert (GBP_IS_CODE_INDEX_WORKBENCH_ADDIN (addin));
 
-  file = ide_source_location_get_file (location);
-  path = ide_file_get_path (file);
+  file = ide_location_get_file (location);
+  path = g_file_peek_path (file);
   g_assert (path != NULL);
 
-  code_indexer = ide_code_index_service_get_code_indexer (service, path);
+  code_indexer = gbp_code_index_workbench_addin_get_code_indexer (addin, path);
   g_assert (!code_indexer || IDE_IS_CODE_INDEXER (code_indexer));
 
   if (code_indexer == NULL)
@@ -176,12 +178,12 @@ ide_code_index_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolve
       return;
     }
 
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
   g_assert (IDE_IS_BUILD_SYSTEM (build_system));
 
   lookup = g_slice_new0 (LookupSymbol);
   lookup->code_indexer = g_object_ref (code_indexer);
-  lookup->location = ide_source_location_ref (location);
+  lookup->location = g_object_ref (location);
 
   ide_task_set_task_data (task, lookup, lookup_symbol_free);
 
diff --git a/src/plugins/code-index/ide-code-index-symbol-resolver.h 
b/src/plugins/code-index/ide-code-index-symbol-resolver.h
index daab98096..a4cd69bd1 100644
--- a/src/plugins/code-index/ide-code-index-symbol-resolver.h
+++ b/src/plugins/code-index/ide-code-index-symbol-resolver.h
@@ -14,12 +14,14 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/code-index/meson.build b/src/plugins/code-index/meson.build
index 290d8cf77..e1faf9015 100644
--- a/src/plugins/code-index/meson.build
+++ b/src/plugins/code-index/meson.build
@@ -1,28 +1,21 @@
-if get_option('with_code_index')
+if get_option('plugin_code_index')
 
-code_index_resources = gnome.compile_resources(    
-  'ide-code-index-resources',                      
-  'code-index.gresource.xml',                      
-  c_name: 'ide_code_index',                        
-)                                           
-
-code_index_sources = [
+plugins_sources += files([
   'code-index-plugin.c',
   'ide-code-index-builder.c',
-  'ide-code-index-builder.h',
   'ide-code-index-index.c',
-  'ide-code-index-index.h',
   'ide-code-index-search-provider.c',
-  'ide-code-index-search-provider.h',
   'ide-code-index-search-result.c',
-  'ide-code-index-search-result.h',
-  'ide-code-index-service.c',
-  'ide-code-index-service.h',
+  'gbp-code-index-workbench-addin.c',
   'ide-code-index-symbol-resolver.c',
-  'ide-code-index-symbol-resolver.h',
-]
+])
+
+plugin_code_index_resources = gnome.compile_resources(
+  'code-index-resources',
+  'code-index.gresource.xml',
+  c_name: 'gbp_code_index',
+)
 
-gnome_builder_plugins_sources += files(code_index_sources)
-gnome_builder_plugins_sources += code_index_resources[0]
+plugins_sources += plugin_code_index_resources[0]
 
 endif
diff --git a/src/plugins/codeui/codeui-plugin.c b/src/plugins/codeui/codeui-plugin.c
new file mode 100644
index 000000000..099edef6f
--- /dev/null
+++ b/src/plugins/codeui/codeui-plugin.c
@@ -0,0 +1,35 @@
+/* codeui-plugin.c
+ *
+ * Copyright 2018-2019 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
+ */
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-code.h>
+#include <libide-foundry.h>
+
+#include "gbp-codeui-buffer-addin.h"
+
+_IDE_EXTERN void
+_gbp_codeui_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUFFER_ADDIN,
+                                              GBP_TYPE_CODEUI_BUFFER_ADDIN);
+}
diff --git a/src/plugins/codeui/codeui.gresource.xml b/src/plugins/codeui/codeui.gresource.xml
new file mode 100644
index 000000000..e0733b815
--- /dev/null
+++ b/src/plugins/codeui/codeui.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/codeui">
+    <file>codeui.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/codeui/codeui.plugin b/src/plugins/codeui/codeui.plugin
new file mode 100644
index 000000000..17bc396ea
--- /dev/null
+++ b/src/plugins/codeui/codeui.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2018 Christian Hergert
+Depends=editor;
+Description=Provides user interface components for code subsystem.
+Embedded=_gbp_codeui_register_types
+Hidden=true
+Module=codeui
+Name=Code UI integration plugin
diff --git a/src/plugins/codeui/gbp-codeui-buffer-addin.c b/src/plugins/codeui/gbp-codeui-buffer-addin.c
new file mode 100644
index 000000000..ca26551f1
--- /dev/null
+++ b/src/plugins/codeui/gbp-codeui-buffer-addin.c
@@ -0,0 +1,203 @@
+/* gbp-codeui-buffer-addin.c
+ *
+ * Copyright 2018-2019 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-codeui-buffer-addin"
+
+#include "config.h"
+
+#include <libide-code.h>
+
+#include "ide-diagnostics-manager-private.h"
+
+#include "gbp-codeui-buffer-addin.h"
+
+struct _GbpCodeuiBufferAddin
+{
+  GObject    parent_instance;
+  IdeBuffer *buffer;
+  GFile     *file;
+};
+
+static void
+gbp_codeui_buffer_addin_queue_diagnose (GbpCodeuiBufferAddin *self,
+                                        IdeBuffer            *buffer)
+{
+  g_autoptr(IdeContext) context = NULL;
+  IdeDiagnosticsManager *manager;
+  g_autoptr(GBytes) contents = NULL;
+  const gchar *lang_id;
+  GFile *file;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODEUI_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  context = ide_buffer_ref_context (buffer);
+  manager = ide_diagnostics_manager_from_context (context);
+  file = ide_buffer_get_file (buffer);
+  lang_id = ide_buffer_get_language_id (buffer);
+  contents = ide_buffer_dup_content (buffer);
+
+  _ide_diagnostics_manager_file_changed (manager, file, contents, lang_id);
+}
+
+static void
+gbp_codeui_buffer_addin_change_settled (IdeBufferAddin *addin,
+                                        IdeBuffer      *buffer)
+{
+  GbpCodeuiBufferAddin *self = (GbpCodeuiBufferAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODEUI_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  gbp_codeui_buffer_addin_queue_diagnose (self, buffer);
+}
+
+static void
+gbp_codeui_buffer_addin_file_loaded (IdeBufferAddin *addin,
+                                     IdeBuffer      *buffer,
+                                     GFile          *file)
+{
+  GbpCodeuiBufferAddin *self = (GbpCodeuiBufferAddin *)addin;
+  g_autoptr(IdeContext) context = NULL;
+  IdeDiagnosticsManager *manager;
+  const gchar *lang_id;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODEUI_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+
+  g_set_object (&self->file, file);
+
+  context = ide_buffer_ref_context (buffer);
+  manager = ide_diagnostics_manager_from_context (context);
+  lang_id = ide_buffer_get_language_id (buffer);
+
+  _ide_diagnostics_manager_file_opened (manager, file, lang_id);
+}
+
+static void
+gbp_codeui_buffer_addin_file_saved (IdeBufferAddin *addin,
+                                    IdeBuffer      *buffer,
+                                    GFile          *file)
+{
+  GbpCodeuiBufferAddin *self = (GbpCodeuiBufferAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODEUI_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+
+  g_set_object (&self->file, file);
+
+  gbp_codeui_buffer_addin_queue_diagnose (self, buffer);
+}
+
+static void
+gbp_codeui_buffer_addin_changed_cb (GbpCodeuiBufferAddin  *self,
+                                    IdeDiagnosticsManager *manager)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODEUI_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_DIAGNOSTICS_MANAGER (manager));
+
+  if (self->file != NULL)
+    {
+      g_autoptr(IdeDiagnostics) diagnostics = NULL;
+
+      diagnostics = ide_diagnostics_manager_get_diagnostics_for_file (manager, self->file);
+      ide_buffer_set_diagnostics (self->buffer, diagnostics);
+    }
+}
+
+static void
+gbp_codeui_buffer_addin_load (IdeBufferAddin *addin,
+                              IdeBuffer      *buffer)
+{
+  GbpCodeuiBufferAddin *self = (GbpCodeuiBufferAddin *)addin;
+  g_autoptr(IdeContext) context = NULL;
+  IdeDiagnosticsManager *manager;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODEUI_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  context = ide_buffer_ref_context (buffer);
+  manager = ide_diagnostics_manager_from_context (context);
+
+  self->buffer = g_object_ref (buffer);
+
+  g_signal_connect_object (manager,
+                           "changed",
+                           G_CALLBACK (gbp_codeui_buffer_addin_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+gbp_codeui_buffer_addin_unload (IdeBufferAddin *addin,
+                                IdeBuffer      *buffer)
+{
+  GbpCodeuiBufferAddin *self = (GbpCodeuiBufferAddin *)addin;
+  g_autoptr(IdeContext) context = NULL;
+  IdeDiagnosticsManager *manager;
+  GFile *file;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_CODEUI_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  context = ide_buffer_ref_context (buffer);
+  manager = ide_diagnostics_manager_from_context (context);
+  file = ide_buffer_get_file (buffer);
+
+  g_signal_handlers_disconnect_by_func (manager,
+                                        G_CALLBACK (gbp_codeui_buffer_addin_changed_cb),
+                                        self);
+
+  _ide_diagnostics_manager_file_closed (manager, file);
+
+  g_clear_object (&self->file);
+}
+
+static void
+buffer_addin_iface_init (IdeBufferAddinInterface *iface)
+{
+  iface->change_settled = gbp_codeui_buffer_addin_change_settled;
+  iface->file_saved = gbp_codeui_buffer_addin_file_saved;
+  iface->file_loaded = gbp_codeui_buffer_addin_file_loaded;
+  iface->load = gbp_codeui_buffer_addin_load;
+  iface->unload = gbp_codeui_buffer_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpCodeuiBufferAddin, gbp_codeui_buffer_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUFFER_ADDIN, buffer_addin_iface_init))
+
+static void
+gbp_codeui_buffer_addin_class_init (GbpCodeuiBufferAddinClass *klass)
+{
+}
+
+static void
+gbp_codeui_buffer_addin_init (GbpCodeuiBufferAddin *self)
+{
+}
diff --git a/src/plugins/codeui/gbp-codeui-buffer-addin.h b/src/plugins/codeui/gbp-codeui-buffer-addin.h
new file mode 100644
index 000000000..51aa494a4
--- /dev/null
+++ b/src/plugins/codeui/gbp-codeui-buffer-addin.h
@@ -0,0 +1,31 @@
+/* gbp-codeui-buffer-addin.h
+ *
+ * Copyright 2018-2019 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_CODEUI_BUFFER_ADDIN (gbp_codeui_buffer_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCodeuiBufferAddin, gbp_codeui_buffer_addin, GBP, CODEUI_BUFFER_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/codeui/meson.build b/src/plugins/codeui/meson.build
new file mode 100644
index 000000000..c374a40cb
--- /dev/null
+++ b/src/plugins/codeui/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'codeui-plugin.c',
+  'gbp-codeui-buffer-addin.c',
+])
+
+plugin_codeui_resources = gnome.compile_resources(
+  'codeui-resources',
+  'codeui.gresource.xml',
+  c_name: 'gbp_codeui',
+)
+
+plugins_sources += plugin_codeui_resources[0]
diff --git a/src/plugins/color-picker/color-picker.gresource.xml 
b/src/plugins/color-picker/color-picker.gresource.xml
new file mode 100644
index 000000000..477648936
--- /dev/null
+++ b/src/plugins/color-picker/color-picker.gresource.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/color-picker">
+    <file>color-picker.plugin</file>
+
+    <file>themes/Adwaita.css</file>
+    <file>themes/Adwaita-dark.css</file>
+    <file>themes/shared.css</file>
+
+    <file preprocess="xml-stripblanks">gtk/color-picker.ui</file>
+    <file preprocess="xml-stripblanks">gtk/color-picker-prefs.ui</file>
+    <file preprocess="xml-stripblanks">gtk/color-picker-preview.ui</file>
+    <file preprocess="xml-stripblanks">gtk/color-picker-palette-row.ui</file>
+    <file preprocess="xml-stripblanks">gtk/color-picker-palette-menu.ui</file>
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+
+    <file preprocess="xml-stripblanks">data/basic.gstyle.xml</file>
+    <file>data/svg.gpl</file>
+
+    <file compressed="true" 
alias="icons/scalable/actions/builder-colorpicker-load-palette.svg">icons/palette/load-palette.svg</file>
+    <file compressed="true" 
alias="icons/scalable/actions/builder-colorpicker-save-palette.svg">icons/palette/save-palette.svg</file>
+    <file compressed="true" 
alias="icons/scalable/actions/builder-colorpicker-palette-from-document.svg">icons/palette/palette-from-document.svg</file>
+    <file compressed="true" 
alias="icons/scalable/actions/builder-colorpicker-viewmode-list.svg">icons/viewmode/viewmode-list.svg</file>
+    <file compressed="true" 
alias="icons/scalable/actions/builder-colorpicker-viewmode-swatchs.svg">icons/viewmode/viewmode-swatchs.svg</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/color-picker/color-picker.plugin b/src/plugins/color-picker/color-picker.plugin
index 11fc62486..ba3500512 100644
--- a/src/plugins/color-picker/color-picker.plugin
+++ b/src/plugins/color-picker/color-picker.plugin
@@ -1,9 +1,9 @@
 [Plugin]
-Module=color-picker-plugin
-Name=Color Picker
-Description=Show a color picker to inspect or change text color codes
 Authors=Sébastien Lafargue <slafargue gnome org>
+Builtin=true
 Copyright=Copyright © 2016 Sébastien Lafargue
 Depends=editor
-Builtin=true
-Embedded=gb_color_picker_register_types
+Description=Show a color picker to inspect or change text color codes
+Embedded=_gb_color_picker_register_types
+Module=color-picker
+Name=Color Picker
diff --git a/src/plugins/color-picker/gb-color-picker-document-monitor.c 
b/src/plugins/color-picker/gb-color-picker-document-monitor.c
index f78ef31f1..f398a4fe8 100644
--- a/src/plugins/color-picker/gb-color-picker-document-monitor.c
+++ b/src/plugins/color-picker/gb-color-picker-document-monitor.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "gb-color-picker-helper.h"
diff --git a/src/plugins/color-picker/gb-color-picker-document-monitor.h 
b/src/plugins/color-picker/gb-color-picker-document-monitor.h
index 60b85fac9..06b5d2590 100644
--- a/src/plugins/color-picker/gb-color-picker-document-monitor.h
+++ b/src/plugins/color-picker/gb-color-picker-document-monitor.h
@@ -14,12 +14,14 @@
  *
  * 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>
-#include <ide.h>
+#include <libide-editor.h>
 
 #include "gstyle-color.h"
 
diff --git a/src/plugins/color-picker/gb-color-picker-editor-addin.c 
b/src/plugins/color-picker/gb-color-picker-editor-addin.c
index 8e40f378f..84b1fc3b2 100644
--- a/src/plugins/color-picker/gb-color-picker-editor-addin.c
+++ b/src/plugins/color-picker/gb-color-picker-editor-addin.c
@@ -1,6 +1,6 @@
 /* gb-color-picker-editor-addin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "gb-color-picker-editor-addin"
@@ -22,7 +24,7 @@
 #include <gstyle-color-panel.h>
 
 #include "gb-color-picker-editor-addin.h"
-#include "gb-color-picker-editor-view-addin.h"
+#include "gb-color-picker-editor-page-addin.h"
 #include "gb-color-picker-prefs.h"
 
 struct _GbColorPickerEditorAddin
@@ -33,7 +35,7 @@ struct _GbColorPickerEditorAddin
    * An unowned reference to the editor. This is set/unset when
    * load/unload vfuncs are called.
    */
-  IdeEditorPerspective *editor;
+  IdeEditorSurface *editor;
 
   /*
    * Out preferences to use in conjunction with the pane. This needs
@@ -44,7 +46,7 @@ struct _GbColorPickerEditorAddin
 
   /*
    * Our transient panel which we will slide into visibility when
-   * the current view is an IdeEditorView with the color-picker
+   * the current view is an IdeEditorPage with the color-picker
    * enabled.
    */
   GstyleColorPanel *panel;
@@ -56,10 +58,10 @@ struct _GbColorPickerEditorAddin
   DzlDockWidget *dock;
 
   /*
-   * If the current view in the perspective is an editor view, then
+   * If the current view in the surface is an editor view, then
    * this unowned reference will point to that view.
    */
-  IdeEditorView *view;
+  IdeEditorPage *view;
 
   /*
    * This signal group manages correctly binding/unbinding signals from
@@ -110,8 +112,8 @@ gb_color_picker_editor_addin_add_palette (GbColorPickerEditorAddin *self,
 }
 
 static const gchar * internal_palettes[] = {
-  "resource:///org/gnome/builder/plugins/color-picker-plugin/data/basic.gstyle.xml",
-  "resource:///org/gnome/builder/plugins/color-picker-plugin/data/svg.gpl",
+  "resource:///plugins/color-picker/data/basic.gstyle.xml",
+  "resource:///plugins/color-picker/data/svg.gpl",
 };
 
 static void
@@ -153,12 +155,12 @@ gb_color_picker_editor_addin_notify_rgba (GbColorPickerEditorAddin *self,
 
   if (self->view_addin_signals != NULL)
     {
-      GbColorPickerEditorViewAddin *view_addin;
+      GbColorPickerEditorPageAddin *view_addin;
 
       view_addin = dzl_signal_group_get_target (self->view_addin_signals);
 
-      if (GB_IS_COLOR_PICKER_EDITOR_VIEW_ADDIN (view_addin))
-        gb_color_picker_editor_view_addin_set_color (view_addin, color);
+      if (GB_IS_COLOR_PICKER_EDITOR_PAGE_ADDIN (view_addin))
+        gb_color_picker_editor_page_addin_set_color (view_addin, color);
     }
 }
 
@@ -195,16 +197,16 @@ gb_color_picker_editor_addin_show_panel (GbColorPickerEditorAddin *self)
 
   if (self->view != NULL)
     {
-      IdeLayoutTransientSidebar *sidebar;
-      IdeLayoutView *view = IDE_LAYOUT_VIEW (self->view);
+      IdeTransientSidebar *sidebar;
+      IdePage *view = IDE_PAGE (self->view);
 
       if (self->panel == NULL)
         gb_color_picker_editor_addin_set_panel (self);
 
-      sidebar = ide_editor_perspective_get_transient_sidebar (self->editor);
+      sidebar = ide_editor_surface_get_transient_sidebar (self->editor);
 
-      ide_layout_transient_sidebar_set_view (sidebar, view);
-      ide_layout_transient_sidebar_set_panel (sidebar, GTK_WIDGET (self->dock));
+      ide_transient_sidebar_set_page (sidebar, view);
+      ide_transient_sidebar_set_panel (sidebar, GTK_WIDGET (self->dock));
 
       g_object_set (self->editor, "right-visible", TRUE, NULL);
     }
@@ -225,17 +227,17 @@ gb_color_picker_editor_addin_hide_panel (GbColorPickerEditorAddin *self)
 static void
 gb_color_picker_editor_addin_notify_enabled (GbColorPickerEditorAddin     *self,
                                              GParamSpec                   *pspec,
-                                             GbColorPickerEditorViewAddin *view_addin)
+                                             GbColorPickerEditorPageAddin *view_addin)
 {
   g_assert (GB_IS_COLOR_PICKER_EDITOR_ADDIN (self));
-  g_assert (GB_IS_COLOR_PICKER_EDITOR_VIEW_ADDIN (view_addin));
+  g_assert (GB_IS_COLOR_PICKER_EDITOR_PAGE_ADDIN (view_addin));
 
   /* This function is called when the enabled state is toggled
    * for the specific view in question. We hide the panel if it
    * is current visible, otherwise we show it.
    */
 
-  if (gb_color_picker_editor_view_addin_get_enabled (view_addin))
+  if (gb_color_picker_editor_page_addin_get_enabled (view_addin))
     gb_color_picker_editor_addin_show_panel (self);
   else
     gb_color_picker_editor_addin_hide_panel (self);
@@ -244,13 +246,13 @@ gb_color_picker_editor_addin_notify_enabled (GbColorPickerEditorAddin     *self,
 static void
 gb_color_picker_editor_addin_color_found (GbColorPickerEditorAddin     *self,
                                           GstyleColor                  *color,
-                                          GbColorPickerEditorViewAddin *view_addin)
+                                          GbColorPickerEditorPageAddin *view_addin)
 {
   GdkRGBA rgba;
 
   g_assert (GB_IS_COLOR_PICKER_EDITOR_ADDIN (self));
   g_assert (GSTYLE_IS_COLOR (color));
-  g_assert (GB_IS_COLOR_PICKER_EDITOR_VIEW_ADDIN (view_addin));
+  g_assert (GB_IS_COLOR_PICKER_EDITOR_PAGE_ADDIN (view_addin));
 
   dzl_signal_group_block (self->view_addin_signals);
   gstyle_color_fill_rgba (color, &rgba);
@@ -264,16 +266,16 @@ gb_color_picker_editor_addin_color_found (GbColorPickerEditorAddin     *self,
 
 static void
 gb_color_picker_editor_addin_load (IdeEditorAddin       *addin,
-                                   IdeEditorPerspective *perspective)
+                                   IdeEditorSurface *surface)
 {
   GbColorPickerEditorAddin *self = (GbColorPickerEditorAddin *)addin;
-  IdeLayoutTransientSidebar *sidebar;
+  IdeTransientSidebar *sidebar;
 
   g_assert (GB_IS_COLOR_PICKER_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (perspective));
+  g_assert (IDE_IS_EDITOR_SURFACE (surface));
 
-  self->editor = perspective;
-  self->view_addin_signals = dzl_signal_group_new (GB_TYPE_COLOR_PICKER_EDITOR_VIEW_ADDIN);
+  self->editor = surface;
+  self->view_addin_signals = dzl_signal_group_new (GB_TYPE_COLOR_PICKER_EDITOR_PAGE_ADDIN);
   dzl_signal_group_connect_swapped (self->view_addin_signals,
                                     "color-found",
                                     G_CALLBACK (gb_color_picker_editor_addin_color_found),
@@ -295,19 +297,19 @@ gb_color_picker_editor_addin_load (IdeEditorAddin       *addin,
                     G_CALLBACK (gtk_widget_destroyed),
                     &self->dock);
 
-  sidebar = ide_editor_perspective_get_transient_sidebar (self->editor);
+  sidebar = ide_editor_surface_get_transient_sidebar (self->editor);
   gtk_container_add (GTK_CONTAINER (sidebar), GTK_WIDGET (self->dock));
 }
 
 
 static void
 gb_color_picker_editor_addin_unload (IdeEditorAddin       *addin,
-                                     IdeEditorPerspective *perspective)
+                                     IdeEditorSurface *surface)
 {
   GbColorPickerEditorAddin *self = (GbColorPickerEditorAddin *)addin;
 
   g_assert (GB_IS_COLOR_PICKER_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (perspective));
+  g_assert (IDE_IS_EDITOR_SURFACE (surface));
 
   g_clear_object (&self->view_addin_signals);
 
@@ -323,30 +325,30 @@ gb_color_picker_editor_addin_unload (IdeEditorAddin       *addin,
 }
 
 static void
-gb_color_picker_editor_addin_view_set (IdeEditorAddin *addin,
-                                       IdeLayoutView  *view)
+gb_color_picker_editor_addin_page_set (IdeEditorAddin *addin,
+                                       IdePage  *view)
 {
   GbColorPickerEditorAddin *self = (GbColorPickerEditorAddin *)addin;
 
   g_assert (GB_IS_COLOR_PICKER_EDITOR_ADDIN (self));
-  g_assert (!view || IDE_IS_LAYOUT_VIEW (view));
+  g_assert (!view || IDE_IS_PAGE (view));
 
-  if (IDE_IS_EDITOR_VIEW (view))
+  if (IDE_IS_EDITOR_PAGE (view))
     {
-      IdeEditorViewAddin *view_addin;
+      IdeEditorPageAddin *view_addin;
 
-      self->view = IDE_EDITOR_VIEW (view);
+      self->view = IDE_EDITOR_PAGE (view);
 
       /* The addin may not be available yet if things are just initializing.
        * We'll have to wait for a follow up view-set to make progress.
        */
-      view_addin = ide_editor_view_addin_find_by_module_name (self->view, "color-picker-plugin");
-      g_assert (!view_addin || GB_IS_COLOR_PICKER_EDITOR_VIEW_ADDIN (view_addin));
+      view_addin = ide_editor_page_addin_find_by_module_name (self->view, "color-picker");
+      g_assert (!view_addin || GB_IS_COLOR_PICKER_EDITOR_PAGE_ADDIN (view_addin));
 
       dzl_signal_group_set_target (self->view_addin_signals, view_addin);
 
       if (view_addin != NULL &&
-          gb_color_picker_editor_view_addin_get_enabled (GB_COLOR_PICKER_EDITOR_VIEW_ADDIN (view_addin)))
+          gb_color_picker_editor_page_addin_get_enabled (GB_COLOR_PICKER_EDITOR_PAGE_ADDIN (view_addin)))
         gb_color_picker_editor_addin_show_panel (self);
     }
   else
@@ -362,7 +364,7 @@ editor_addin_iface_init (IdeEditorAddinInterface *iface)
 {
   iface->load = gb_color_picker_editor_addin_load;
   iface->unload = gb_color_picker_editor_addin_unload;
-  iface->view_set = gb_color_picker_editor_addin_view_set;
+  iface->page_set = gb_color_picker_editor_addin_page_set;
 }
 
 G_DEFINE_TYPE_WITH_CODE (GbColorPickerEditorAddin,
@@ -400,7 +402,7 @@ gb_color_picker_editor_addin_create_palette (GbColorPickerEditorAddin *self)
 
   if (self->view != NULL)
     {
-      IdeBuffer *buffer = ide_editor_view_get_buffer (self->view);
+      IdeBuffer *buffer = ide_editor_page_get_buffer (self->view);
 
       return gstyle_palette_new_from_buffer (GTK_TEXT_BUFFER (buffer),
                                              NULL, NULL, NULL, NULL);
diff --git a/src/plugins/color-picker/gb-color-picker-editor-addin.h 
b/src/plugins/color-picker/gb-color-picker-editor-addin.h
index 988705f71..4d4a96643 100644
--- a/src/plugins/color-picker/gb-color-picker-editor-addin.h
+++ b/src/plugins/color-picker/gb-color-picker-editor-addin.h
@@ -1,6 +1,6 @@
 /* gb-color-picker-editor-addin.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-editor.h>
 
 #include <gstyle-palette.h>
 
diff --git a/src/plugins/color-picker/gb-color-picker-editor-page-addin.c 
b/src/plugins/color-picker/gb-color-picker-editor-page-addin.c
new file mode 100644
index 000000000..cdeb9e1ab
--- /dev/null
+++ b/src/plugins/color-picker/gb-color-picker-editor-page-addin.c
@@ -0,0 +1,237 @@
+/* gb-color-picker-editor-page-addin.c
+ *
+ * Copyright 2017-2019 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 "gb-color-picker-editor-page-addin"
+
+#include "gb-color-picker-document-monitor.h"
+#include "gb-color-picker-editor-page-addin.h"
+
+struct _GbColorPickerEditorPageAddin
+{
+  GObject parent_instance;
+
+  /* Unowned reference to the view */
+  IdeEditorPage *view;
+
+  /* Our document monitor, or NULL */
+  GbColorPickerDocumentMonitor *monitor;
+
+  /* If we've been enabled by the user */
+  guint enabled : 1;
+
+  /* Re-entrancy check for color-found */
+  guint in_color_found : 1;
+};
+
+enum {
+  COLOR_FOUND,
+  N_SIGNALS
+};
+
+enum {
+  PROP_0,
+  PROP_ENABLED,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static void
+monitor_color_found (GbColorPickerEditorPageAddin *self,
+                     GstyleColor                  *color,
+                     GbColorPickerDocumentMonitor *monitor)
+{
+  g_assert (GB_IS_COLOR_PICKER_EDITOR_PAGE_ADDIN (self));
+  g_assert (GSTYLE_IS_COLOR (color));
+  g_assert (GB_IS_COLOR_PICKER_DOCUMENT_MONITOR (monitor));
+
+  self->in_color_found = TRUE;
+  g_signal_emit (self, signals [COLOR_FOUND], 0, color);
+  self->in_color_found = FALSE;
+}
+
+void
+gb_color_picker_editor_page_addin_set_enabled (GbColorPickerEditorPageAddin *self,
+                                               gboolean                      enabled)
+{
+  g_return_if_fail (GB_IS_COLOR_PICKER_EDITOR_PAGE_ADDIN (self));
+
+  enabled = !!enabled;
+
+  if (enabled != self->enabled)
+    {
+      if (self->enabled)
+        {
+          self->enabled = FALSE;
+          gb_color_picker_document_monitor_queue_uncolorize (self->monitor, NULL, NULL);
+          gb_color_picker_document_monitor_set_buffer (self->monitor, NULL);
+          g_clear_object (&self->monitor);
+        }
+
+      if (enabled)
+        {
+          IdeBuffer *buffer = ide_editor_page_get_buffer (self->view);
+
+          self->enabled = TRUE;
+          self->monitor = gb_color_picker_document_monitor_new (buffer);
+          g_signal_connect_object (self->monitor,
+                                   "color-found",
+                                   G_CALLBACK (monitor_color_found),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+          gb_color_picker_document_monitor_queue_colorize (self->monitor, NULL, NULL);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ENABLED]);
+    }
+}
+
+gboolean
+gb_color_picker_editor_page_addin_get_enabled (GbColorPickerEditorPageAddin *self)
+{
+  g_return_val_if_fail (GB_IS_COLOR_PICKER_EDITOR_PAGE_ADDIN (self), FALSE);
+
+  return self->enabled;
+}
+
+static void
+gb_color_picker_editor_page_addin_load (IdeEditorPageAddin *addin,
+                                        IdeEditorPage      *view)
+{
+  GbColorPickerEditorPageAddin *self = (GbColorPickerEditorPageAddin *)addin;
+  g_autoptr(DzlPropertiesGroup) group = NULL;
+
+  g_assert (GB_IS_COLOR_PICKER_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  self->view = view;
+
+  group = dzl_properties_group_new (G_OBJECT (self));
+  dzl_properties_group_add_all_properties (group);
+  gtk_widget_insert_action_group (GTK_WIDGET (view), "color-picker", G_ACTION_GROUP (group));
+}
+
+static void
+gb_color_picker_editor_page_addin_unload (IdeEditorPageAddin *addin,
+                                          IdeEditorPage      *view)
+{
+  GbColorPickerEditorPageAddin *self = (GbColorPickerEditorPageAddin *)addin;
+
+  g_assert (GB_IS_COLOR_PICKER_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  if (self->monitor != NULL)
+    {
+      gb_color_picker_document_monitor_set_buffer (self->monitor, NULL);
+      g_clear_object (&self->monitor);
+    }
+
+  gtk_widget_insert_action_group (GTK_WIDGET (view), "color-picker", NULL);
+
+  self->view = NULL;
+}
+
+static void
+editor_page_addin_iface_init (IdeEditorPageAddinInterface *iface)
+{
+  iface->load = gb_color_picker_editor_page_addin_load;
+  iface->unload = gb_color_picker_editor_page_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbColorPickerEditorPageAddin, gb_color_picker_editor_page_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_PAGE_ADDIN, editor_page_addin_iface_init))
+
+static void
+gb_color_picker_editor_page_addin_get_property (GObject    *object,
+                                                guint       prop_id,
+                                                GValue     *value,
+                                                GParamSpec *pspec)
+{
+  GbColorPickerEditorPageAddin *self = GB_COLOR_PICKER_EDITOR_PAGE_ADDIN (object);
+
+  switch (prop_id)
+    {
+    case PROP_ENABLED:
+      g_value_set_boolean (value, gb_color_picker_editor_page_addin_get_enabled (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_color_picker_editor_page_addin_set_property (GObject      *object,
+                                                guint         prop_id,
+                                                const GValue *value,
+                                                GParamSpec   *pspec)
+{
+  GbColorPickerEditorPageAddin *self = GB_COLOR_PICKER_EDITOR_PAGE_ADDIN (object);
+
+  switch (prop_id)
+    {
+    case PROP_ENABLED:
+      gb_color_picker_editor_page_addin_set_enabled (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gb_color_picker_editor_page_addin_class_init (GbColorPickerEditorPageAddinClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = gb_color_picker_editor_page_addin_get_property;
+  object_class->set_property = gb_color_picker_editor_page_addin_set_property;
+
+  properties [PROP_ENABLED] =
+    g_param_spec_boolean ("enabled", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [COLOR_FOUND] =
+    g_signal_new ("color-found",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 1, GSTYLE_TYPE_COLOR);
+}
+
+static void
+gb_color_picker_editor_page_addin_init (GbColorPickerEditorPageAddin *self)
+{
+}
+
+void
+gb_color_picker_editor_page_addin_set_color (GbColorPickerEditorPageAddin *self,
+                                             GstyleColor                  *color)
+{
+  g_return_if_fail (GB_IS_COLOR_PICKER_EDITOR_PAGE_ADDIN (self));
+  g_return_if_fail (GSTYLE_IS_COLOR (color));
+
+  if (self->monitor != NULL && !self->in_color_found)
+    gb_color_picker_document_monitor_set_color_tag_at_cursor (self->monitor, color);
+}
diff --git a/src/plugins/color-picker/gb-color-picker-editor-page-addin.h 
b/src/plugins/color-picker/gb-color-picker-editor-page-addin.h
new file mode 100644
index 000000000..2cbc0494e
--- /dev/null
+++ b/src/plugins/color-picker/gb-color-picker-editor-page-addin.h
@@ -0,0 +1,37 @@
+/* gb-color-picker-editor-page-addin.h
+ *
+ * Copyright 2017-2019 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-editor.h>
+
+G_BEGIN_DECLS
+
+#define GB_TYPE_COLOR_PICKER_EDITOR_PAGE_ADDIN (gb_color_picker_editor_page_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbColorPickerEditorPageAddin, gb_color_picker_editor_page_addin, GB, 
COLOR_PICKER_EDITOR_PAGE_ADDIN, GObject)
+
+gboolean gb_color_picker_editor_page_addin_get_enabled (GbColorPickerEditorPageAddin *self);
+void     gb_color_picker_editor_page_addin_set_enabled (GbColorPickerEditorPageAddin *self,
+                                                        gboolean                      enabled);
+void     gb_color_picker_editor_page_addin_set_color   (GbColorPickerEditorPageAddin *self,
+                                                        GstyleColor                  *color);
+
+G_END_DECLS
diff --git a/src/plugins/color-picker/gb-color-picker-helper.c 
b/src/plugins/color-picker/gb-color-picker-helper.c
index 47e029694..9821fa0e5 100644
--- a/src/plugins/color-picker/gb-color-picker-helper.c
+++ b/src/plugins/color-picker/gb-color-picker-helper.c
@@ -14,13 +14,15 @@
  *
  * 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
  */
 
 #include <string.h>
 
 #include <libpeas/peas.h>
 #include "gb-color-picker-private.h"
-#include <ide.h>
+#include <libide-editor.h>
 
 #include "gb-color-picker-helper.h"
 
diff --git a/src/plugins/color-picker/gb-color-picker-helper.h 
b/src/plugins/color-picker/gb-color-picker-helper.h
index 91696083d..7b3eac5c0 100644
--- a/src/plugins/color-picker/gb-color-picker-helper.h
+++ b/src/plugins/color-picker/gb-color-picker-helper.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/color-picker/gb-color-picker-plugin.c 
b/src/plugins/color-picker/gb-color-picker-plugin.c
index dff1780d9..dc025a27d 100644
--- a/src/plugins/color-picker/gb-color-picker-plugin.c
+++ b/src/plugins/color-picker/gb-color-picker-plugin.c
@@ -14,21 +14,25 @@
  *
  * 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
  */
 
-#include <ide.h>
+#include "config.h"
+
+#include <libide-editor.h>
 #include <libpeas/peas.h>
 
 #include "gb-color-picker-editor-addin.h"
-#include "gb-color-picker-editor-view-addin.h"
+#include "gb-color-picker-editor-page-addin.h"
 
-void
-gb_color_picker_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_gb_color_picker_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_EDITOR_ADDIN,
                                               GB_TYPE_COLOR_PICKER_EDITOR_ADDIN);
   peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_EDITOR_VIEW_ADDIN,
-                                              GB_TYPE_COLOR_PICKER_EDITOR_VIEW_ADDIN);
+                                              IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                              GB_TYPE_COLOR_PICKER_EDITOR_PAGE_ADDIN);
 }
diff --git a/src/plugins/color-picker/gb-color-picker-prefs-list.c 
b/src/plugins/color-picker/gb-color-picker-prefs-list.c
index 98a18a06f..270c6cf67 100644
--- a/src/plugins/color-picker/gb-color-picker-prefs-list.c
+++ b/src/plugins/color-picker/gb-color-picker-prefs-list.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "gb-color-picker-prefs-list.h"
diff --git a/src/plugins/color-picker/gb-color-picker-prefs-list.h 
b/src/plugins/color-picker/gb-color-picker-prefs-list.h
index 42912f437..42dd0c612 100644
--- a/src/plugins/color-picker/gb-color-picker-prefs-list.h
+++ b/src/plugins/color-picker/gb-color-picker-prefs-list.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/color-picker/gb-color-picker-prefs-palette-list.c 
b/src/plugins/color-picker/gb-color-picker-prefs-palette-list.c
index af2464e3c..9c63824f4 100644
--- a/src/plugins/color-picker/gb-color-picker-prefs-palette-list.c
+++ b/src/plugins/color-picker/gb-color-picker-prefs-palette-list.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "gb-color-picker-prefs-palette-row.h"
diff --git a/src/plugins/color-picker/gb-color-picker-prefs-palette-list.h 
b/src/plugins/color-picker/gb-color-picker-prefs-palette-list.h
index d080dddcb..22d626e08 100644
--- a/src/plugins/color-picker/gb-color-picker-prefs-palette-list.h
+++ b/src/plugins/color-picker/gb-color-picker-prefs-palette-list.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/color-picker/gb-color-picker-prefs-palette-row.c 
b/src/plugins/color-picker/gb-color-picker-prefs-palette-row.c
index 5c8801750..d1326f042 100644
--- a/src/plugins/color-picker/gb-color-picker-prefs-palette-row.c
+++ b/src/plugins/color-picker/gb-color-picker-prefs-palette-row.c
@@ -14,11 +14,13 @@
  *
  * 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
  */
 
 #include <gdk/gdk.h>
 #include "glib/gi18n.h"
-#include <ide.h>
+#include <libide-editor.h>
 
 #include "gstyle-rename-popover.h"
 
@@ -504,7 +506,7 @@ gb_color_picker_prefs_palette_row_class_init (GbColorPickerPrefsPaletteRowClass
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/color-picker-plugin/gtk/color-picker-palette-row.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/color-picker/gtk/color-picker-palette-row.ui");
   gtk_widget_class_bind_template_child (widget_class, GbColorPickerPrefsPaletteRow, image);
   gtk_widget_class_bind_template_child (widget_class, GbColorPickerPrefsPaletteRow, event_box);
   gtk_widget_class_bind_template_child (widget_class, GbColorPickerPrefsPaletteRow, palette_name);
@@ -526,7 +528,7 @@ gb_color_picker_prefs_palette_row_init (GbColorPickerPrefsPaletteRow *self)
                             G_CALLBACK (event_box_button_pressed_cb),
                             self);
 
-  builder = gtk_builder_new_from_resource 
("/org/gnome/builder/plugins/color-picker-plugin/gtk/color-picker-palette-menu.ui");
+  builder = gtk_builder_new_from_resource ("/plugins/color-picker/gtk/color-picker-palette-menu.ui");
   self->popover_menu = GTK_WIDGET (g_object_ref_sink (gtk_builder_get_object (builder, "popover")));
   button_rename = GTK_WIDGET (gtk_builder_get_object (builder, "button_rename"));
   g_signal_connect_object (button_rename, "button-release-event",
diff --git a/src/plugins/color-picker/gb-color-picker-prefs-palette-row.h 
b/src/plugins/color-picker/gb-color-picker-prefs-palette-row.h
index fbec0fa4e..183ef0d7e 100644
--- a/src/plugins/color-picker/gb-color-picker-prefs-palette-row.h
+++ b/src/plugins/color-picker/gb-color-picker-prefs-palette-row.h
@@ -14,6 +14,8 @@
  *
  * 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
 
diff --git a/src/plugins/color-picker/gb-color-picker-prefs.c 
b/src/plugins/color-picker/gb-color-picker-prefs.c
index e441395c3..3e4d8e0bb 100644
--- a/src/plugins/color-picker/gb-color-picker-prefs.c
+++ b/src/plugins/color-picker/gb-color-picker-prefs.c
@@ -14,11 +14,13 @@
  *
  * 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
  */
 
 #include <glib/gi18n.h>
 
-#include <ide.h>
+#include <libide-editor.h>
 
 #include "gb-color-picker-editor-addin.h"
 #include "gb-color-picker-prefs.h"
@@ -367,8 +369,8 @@ generate_palette_button_clicked_cb (GbColorPickerPrefs *self,
   g_assert (GB_IS_COLOR_PICKER_PREFS (self));
   g_assert (GTK_IS_BUTTON (button));
 
-  editor = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_EDITOR_PERSPECTIVE);
-  addin = ide_editor_addin_find_by_module_name (IDE_EDITOR_PERSPECTIVE (editor), "color-picker-plugin");
+  editor = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_EDITOR_SURFACE);
+  addin = ide_editor_addin_find_by_module_name (IDE_EDITOR_SURFACE (editor), "color-picker");
   palette = gb_color_picker_editor_addin_create_palette (GB_COLOR_PICKER_EDITOR_ADDIN (addin));
 
   if (palette != NULL)
@@ -680,7 +682,7 @@ gb_color_picker_prefs_init (GbColorPickerPrefs *self)
   g_type_ensure (GB_TYPE_COLOR_PICKER_PREFS_LIST);
   g_type_ensure (GB_TYPE_COLOR_PICKER_PREFS_PALETTE_LIST);
 
-  builder = gtk_builder_new_from_resource 
("/org/gnome/builder/plugins/color-picker-plugin/gtk/color-picker-prefs.ui");
+  builder = gtk_builder_new_from_resource ("/plugins/color-picker/gtk/color-picker-prefs.ui");
 
   self->palettes_box = GB_COLOR_PICKER_PREFS_PALETTE_LIST (gtk_builder_get_object (builder, "palettes_box"));
   palettes_placeholder = GTK_WIDGET (gtk_builder_get_object (builder, "palettes_placeholder"));
@@ -728,7 +730,7 @@ gb_color_picker_prefs_init (GbColorPickerPrefs *self)
 
   g_object_unref (builder);
 
-  builder = gtk_builder_new_from_resource 
("/org/gnome/builder/plugins/color-picker-plugin/gtk/color-picker-preview.ui");
+  builder = gtk_builder_new_from_resource ("/plugins/color-picker/gtk/color-picker-preview.ui");
   self->preview = GTK_WIDGET (gtk_builder_get_object (builder, "preview"));
   g_object_ref_sink (self->preview);
   self->preview_palette_widget = GTK_WIDGET (gtk_builder_get_object (builder, "preview_palette_widget"));
diff --git a/src/plugins/color-picker/gb-color-picker-prefs.h 
b/src/plugins/color-picker/gb-color-picker-prefs.h
index 00c7ae812..89894ea59 100644
--- a/src/plugins/color-picker/gb-color-picker-prefs.h
+++ b/src/plugins/color-picker/gb-color-picker-prefs.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/color-picker/gb-color-picker-private.h 
b/src/plugins/color-picker/gb-color-picker-private.h
index d90c0e08a..675235bc1 100644
--- a/src/plugins/color-picker/gb-color-picker-private.h
+++ b/src/plugins/color-picker/gb-color-picker-private.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/color-picker/meson.build b/src/plugins/color-picker/meson.build
index 198b4b521..cbfca15eb 100644
--- a/src/plugins/color-picker/meson.build
+++ b/src/plugins/color-picker/meson.build
@@ -1,38 +1,27 @@
-if get_option('with_color_picker')
+if get_option('plugin_code_index')
 
-color_picker_resources = gnome.compile_resources(
-  'gb-color-picker-resources',
-  'gb-color-picker.gresource.xml',
-  c_name: 'gb_color_picker',
-)
+install_data('gsettings/org.gnome.builder.plugins.color_picker_plugin.gschema.xml', install_dir: schema_dir)
+
+plugins_deps += libgstyle_dep
 
-color_picker_sources = [
+plugins_sources += files([
+  'gb-color-picker-document-monitor.c',
   'gb-color-picker-editor-addin.c',
-  'gb-color-picker-editor-addin.h',
-  'gb-color-picker-editor-view-addin.c',
-  'gb-color-picker-editor-view-addin.h',
+  'gb-color-picker-editor-page-addin.c',
   'gb-color-picker-helper.c',
-  'gb-color-picker-helper.h',
   'gb-color-picker-plugin.c',
-  'gb-color-picker-document-monitor.c',
-  'gb-color-picker-document-monitor.h',
-  'gb-color-picker-prefs.c',
-  'gb-color-picker-prefs.h',
   'gb-color-picker-prefs-list.c',
-  'gb-color-picker-prefs-palette-list.h',
   'gb-color-picker-prefs-palette-list.c',
-  'gb-color-picker-prefs-list.h',
   'gb-color-picker-prefs-palette-row.c',
-  'gb-color-picker-prefs-palette-row.h',
-  'gb-color-picker-private.h',
-]
-
-gnome_builder_plugins_deps += [libgstyle_dep]
-gnome_builder_plugins_sources += files(color_picker_sources)
-gnome_builder_plugins_sources += color_picker_resources[0]
+  'gb-color-picker-prefs.c',
+])
 
-install_data('gsettings/org.gnome.builder.plugins.color_picker_plugin.gschema.xml',
-  install_dir: schema_dir,
+plugin_color_picker_resources = gnome.compile_resources(
+  'gbp-color-picker-resources',
+  'color-picker.gresource.xml',
+  c_name: 'gbp_color_picker',
 )
 
+plugins_sources += plugin_color_picker_resources[0]
+
 endif
diff --git a/src/plugins/color-picker/themes/Adwaita-dark.css 
b/src/plugins/color-picker/themes/Adwaita-dark.css
index efd6d6ee9..d59762ff7 100644
--- a/src/plugins/color-picker/themes/Adwaita-dark.css
+++ b/src/plugins/color-picker/themes/Adwaita-dark.css
@@ -1,4 +1,4 @@
-@import url("resource:///org/gnome/builder/plugins/color-picker-plugin/themes/shared.css");
+@import url("resource:///plugins/color-picker/themes/shared.css");
 
 /* palettew widget dnd indicator */
 gstylecolorpanel gstylepalettewidget.dnd {
diff --git a/src/plugins/color-picker/themes/Adwaita.css b/src/plugins/color-picker/themes/Adwaita.css
index aa154d0bd..bdcb10615 100644
--- a/src/plugins/color-picker/themes/Adwaita.css
+++ b/src/plugins/color-picker/themes/Adwaita.css
@@ -1,4 +1,4 @@
-@import url("resource:///org/gnome/builder/plugins/color-picker-plugin/themes/shared.css");
+@import url("resource:///plugins/color-picker/themes/shared.css");
 
 /*
  * FileChooserDialog preview
diff --git a/src/plugins/command-bar/command-bar-plugin.c b/src/plugins/command-bar/command-bar-plugin.c
new file mode 100644
index 000000000..748ccd2a0
--- /dev/null
+++ b/src/plugins/command-bar/command-bar-plugin.c
@@ -0,0 +1,40 @@
+/* command-bar-plugin.c
+ *
+ * Copyright 2015-2019 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 "command-bar-plugin"
+
+#include "config.h"
+
+#include <libide-gui.h>
+#include <libpeas/peas.h>
+
+#include "gbp-command-bar-command-provider.h"
+#include "gbp-command-bar-workspace-addin.h"
+
+void
+_gbp_command_bar_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_COMMAND_PROVIDER,
+                                              GBP_TYPE_COMMAND_BAR_COMMAND_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_COMMAND_BAR_WORKSPACE_ADDIN);
+}
diff --git a/src/plugins/command-bar/command-bar.gresource.xml 
b/src/plugins/command-bar/command-bar.gresource.xml
new file mode 100644
index 000000000..f20869f4c
--- /dev/null
+++ b/src/plugins/command-bar/command-bar.gresource.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/command-bar">
+    <file>command-bar.plugin</file>
+    <file>themes/shared.css</file>
+    <file preprocess="xml-stripblanks">gbp-command-bar.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/command-bar/command-bar.plugin b/src/plugins/command-bar/command-bar.plugin
index 48ea3d0ef..2a0b145a3 100644
--- a/src/plugins/command-bar/command-bar.plugin
+++ b/src/plugins/command-bar/command-bar.plugin
@@ -1,9 +1,11 @@
 [Plugin]
-Module=command-bar
-Name=Command Bar
-Description=Provides a command bar at the bottom of the workbench window.
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Depends=editor;terminal;
+Description=Builder's workspace command-bar
+Embedded=_gbp_command_bar_register_types
 Hidden=true
-Embedded=gb_command_bar_register_types
+Module=command-bar
+Name=Command Bar
+X-Workspace-Kind=primary;editor;terminal;
diff --git a/src/plugins/command-bar/gbp-command-bar-command-provider.c 
b/src/plugins/command-bar/gbp-command-bar-command-provider.c
new file mode 100644
index 000000000..3eca2a278
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar-command-provider.c
@@ -0,0 +1,198 @@
+/* gbp-command-bar-command-provider.c
+ *
+ * Copyright 2018-2019 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-command-bar-command-provider"
+
+#include <libide-gui.h>
+#include <libide-sourceview.h>
+#include <libide-threading.h>
+
+#include "gbp-command-bar-command-provider.h"
+#include "gbp-gaction-command.h"
+
+struct _GbpCommandBarCommandProvider
+{
+  GObject parent_instance;
+};
+
+static gint
+sort_actions_by_priority (gconstpointer a,
+                          gconstpointer b)
+{
+  GbpGactionCommand *cmd_a = *(GbpGactionCommand **)a;
+  GbpGactionCommand *cmd_b = *(GbpGactionCommand **)b;
+
+  return gbp_gaction_command_compare (cmd_a, cmd_b);
+}
+
+static void
+add_from_group (const gchar  *needle,
+                GPtrArray    *results,
+                const gchar  *prefix,
+                GtkWidget    *widget,
+                GActionGroup *group,
+                GHashTable   *seen)
+{
+  g_auto(GStrv) actions = NULL;
+
+  g_assert (needle != NULL);
+  g_assert (results != NULL);
+  g_assert (prefix != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (G_IS_ACTION_GROUP (group));
+
+  /* Skip source-view actions which bridge to signals */
+  if (g_str_equal (prefix, "source-view"))
+    return;
+
+  actions = g_action_group_list_actions (group);
+
+  for (guint j = 0; actions[j]; j++)
+    {
+      g_autofree gchar *title = NULL;
+      const GVariantType *type;
+      GbpGactionCommand *command;
+      guint priority = 0;
+
+      if (g_hash_table_contains (seen, actions[j]))
+        continue;
+
+      g_hash_table_insert (seen, g_strdup (actions[j]), NULL);
+
+      /* Skip actions with params */
+      if ((type = g_action_group_get_action_parameter_type (group, actions[j])))
+        continue;
+
+      if (!ide_completion_fuzzy_match (actions[j], needle, &priority))
+        continue;
+
+      title = ide_completion_fuzzy_highlight (actions[j], needle);
+      command = gbp_gaction_command_new (widget, prefix, actions[j], NULL, title, priority);
+      g_ptr_array_add (results, g_steal_pointer (&command));
+    }
+}
+
+static void
+populate_gactions_at_widget (const gchar *needle,
+                             GPtrArray   *results,
+                             GtkWidget   *widget,
+                             GHashTable  *seen)
+{
+  g_autofree const gchar **prefixes = NULL;
+  GtkWidget *parent;
+
+  g_assert (results != NULL);
+  g_assert (GTK_IS_WIDGET (widget));
+
+  if ((prefixes = gtk_widget_list_action_prefixes (widget)))
+    {
+      for (guint i = 0; prefixes[i]; i++)
+        {
+          GActionGroup *group;
+
+          if ((group = gtk_widget_get_action_group (widget, prefixes[i])))
+            add_from_group (needle, results, prefixes[i], widget, group, seen);
+        }
+    }
+
+  if ((parent = gtk_widget_get_parent (widget)))
+    populate_gactions_at_widget (needle, results, parent, seen);
+  else
+    add_from_group (needle, results, "app", widget, G_ACTION_GROUP (IDE_APPLICATION_DEFAULT), seen);
+}
+
+static void
+gbp_command_bar_command_provider_query_async (IdeCommandProvider  *provider,
+                                              IdeWorkspace        *workspace,
+                                              const gchar         *typed_text,
+                                              GCancellable        *cancellable,
+                                              GAsyncReadyCallback  callback,
+                                              gpointer             user_data)
+{
+  GbpCommandBarCommandProvider *self = (GbpCommandBarCommandProvider *)provider;
+  g_autoptr(GHashTable) seen = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) results = NULL;
+  g_autofree gchar *needle = NULL;
+  IdeSurface *surface;
+  IdePage *page;
+
+  g_assert (GBP_IS_COMMAND_BAR_COMMAND_PROVIDER (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_command_bar_command_provider_query_async);
+
+  seen = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+  needle = g_utf8_casefold (typed_text, -1);
+  results = g_ptr_array_new_with_free_func (g_object_unref);
+  surface = ide_workspace_get_visible_surface (workspace);
+
+  if ((page = ide_workspace_get_most_recent_page (workspace)))
+    populate_gactions_at_widget (needle, results, GTK_WIDGET (page), seen);
+  else
+    populate_gactions_at_widget (needle, results, GTK_WIDGET (surface), seen);
+
+  g_ptr_array_sort (results, sort_actions_by_priority);
+
+  ide_task_return_pointer (task,
+                           g_steal_pointer (&results),
+                           (GDestroyNotify)g_ptr_array_unref);
+}
+
+static GPtrArray *
+gbp_command_bar_command_provider_query_finish (IdeCommandProvider  *provider,
+                                               GAsyncResult        *result,
+                                               GError             **error)
+{
+  GbpCommandBarCommandProvider *self = (GbpCommandBarCommandProvider *)provider;
+  GPtrArray *ret;
+
+  g_assert (GBP_IS_COMMAND_BAR_COMMAND_PROVIDER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ret);
+}
+
+static void
+command_provider_iface_init (IdeCommandProviderInterface *iface)
+{
+  iface->query_async = gbp_command_bar_command_provider_query_async;
+  iface->query_finish = gbp_command_bar_command_provider_query_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpCommandBarCommandProvider,
+                         gbp_command_bar_command_provider,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_COMMAND_PROVIDER,
+                                                command_provider_iface_init))
+
+static void
+gbp_command_bar_command_provider_class_init (GbpCommandBarCommandProviderClass *klass)
+{
+}
+
+static void
+gbp_command_bar_command_provider_init (GbpCommandBarCommandProvider *self)
+{
+}
diff --git a/src/plugins/command-bar/gbp-command-bar-command-provider.h 
b/src/plugins/command-bar/gbp-command-bar-command-provider.h
new file mode 100644
index 000000000..6407b5ad7
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar-command-provider.h
@@ -0,0 +1,31 @@
+/* gbp-command-bar-command-provider.h
+ *
+ * Copyright 2018-2019 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_COMMAND_BAR_COMMAND_PROVIDER (gbp_command_bar_command_provider_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCommandBarCommandProvider, gbp_command_bar_command_provider, GBP, 
COMMAND_BAR_COMMAND_PROVIDER, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/command-bar/gbp-command-bar-model.c b/src/plugins/command-bar/gbp-command-bar-model.c
new file mode 100644
index 000000000..cd773fa05
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar-model.c
@@ -0,0 +1,236 @@
+/* gbp-command-bar-model.c
+ *
+ * Copyright 2018-2019 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-command-bar-model"
+
+#include "config.h"
+
+#include <libide-gui.h>
+#include <libide-threading.h>
+#include <libpeas/peas.h>
+#include <libpeas/peas-autocleanups.h>
+
+#include "gbp-command-bar-suggestion.h"
+#include "gbp-command-bar-model.h"
+
+struct _GbpCommandBarModel
+{
+  IdeObject  parent_instance;
+  GPtrArray *items;
+};
+
+typedef struct
+{
+  IdeWorkspace *workspace;
+  IdeTask      *task;
+  const gchar  *typed_text;
+  GPtrArray    *providers;
+} Complete;
+
+static GType
+gbp_command_bar_model_get_item_type (GListModel *model)
+{
+  return GBP_TYPE_COMMAND_BAR_SUGGESTION;
+}
+
+static guint
+gbp_command_bar_model_get_n_items (GListModel *model)
+{
+  return GBP_COMMAND_BAR_MODEL (model)->items->len;
+}
+
+static gpointer
+gbp_command_bar_model_get_item (GListModel *model,
+                                guint       position)
+{
+  GbpCommandBarModel *self = (GbpCommandBarModel *)model;
+
+  g_assert (GBP_IS_COMMAND_BAR_MODEL (self));
+
+  if (position < self->items->len)
+    return g_object_ref (g_ptr_array_index (self->items, position));
+
+  return NULL;
+}
+
+static void
+list_model_iface_init (GListModelInterface *iface)
+{
+  iface->get_item_type = gbp_command_bar_model_get_item_type;
+  iface->get_n_items = gbp_command_bar_model_get_n_items;
+  iface->get_item = gbp_command_bar_model_get_item;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpCommandBarModel, gbp_command_bar_model, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init))
+
+static void
+gbp_command_bar_model_dispose (GObject *object)
+{
+  GbpCommandBarModel *self = (GbpCommandBarModel *)object;
+
+  g_clear_pointer (&self->items, g_ptr_array_unref);
+
+  G_OBJECT_CLASS (gbp_command_bar_model_parent_class)->dispose (object);
+}
+
+static void
+gbp_command_bar_model_class_init (GbpCommandBarModelClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gbp_command_bar_model_dispose;
+}
+
+static void
+gbp_command_bar_model_init (GbpCommandBarModel *self)
+{
+  self->items = g_ptr_array_new_with_free_func (g_object_unref);
+}
+
+GbpCommandBarModel *
+gbp_command_bar_model_new (IdeContext *context)
+{
+  GbpCommandBarModel *self;
+
+  self = g_object_new (GBP_TYPE_COMMAND_BAR_MODEL, NULL);
+  ide_object_append (IDE_OBJECT (context), IDE_OBJECT (self));
+
+  return g_steal_pointer (&self);
+}
+
+static void
+gbp_command_bar_model_query_cb (GObject      *object,
+                                GAsyncResult *result,
+                                gpointer      user_data)
+{
+  IdeCommandProvider *provider = (IdeCommandProvider *)object;
+  g_autoptr(GPtrArray) items = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GbpCommandBarModel *self;
+  GPtrArray *providers;
+  guint position;
+
+  g_assert (IDE_IS_COMMAND_PROVIDER (provider));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  providers = ide_task_get_task_data (task);
+
+  g_assert (GBP_IS_COMMAND_BAR_MODEL (self));
+  g_assert (providers != NULL);
+
+  position = self->items->len;
+
+  if ((items = ide_command_provider_query_finish (provider, result, &error)))
+    {
+      for (guint i = 0; i < items->len; i++)
+        {
+          IdeCommand *command = g_ptr_array_index (items, i);
+
+          g_ptr_array_add (self->items, gbp_command_bar_suggestion_new (command));
+        }
+
+      g_list_model_items_changed (G_LIST_MODEL (self), position, 0, items->len);
+    }
+
+  IDE_PTR_ARRAY_SET_FREE_FUNC (items, g_object_unref);
+
+  g_ptr_array_remove (providers, provider);
+
+  if (providers->len == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_command_bar_query_foreach_cb (PeasExtensionSet *set,
+                                  PeasPluginInfo   *plugin_info,
+                                  PeasExtension    *exten,
+                                  gpointer          user_data)
+{
+  IdeCommandProvider *provider = (IdeCommandProvider *)exten;
+  Complete *complete = user_data;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (plugin_info != NULL);
+  g_assert (IDE_IS_COMMAND_PROVIDER (provider));
+  g_assert (complete != NULL);
+
+  g_ptr_array_add (complete->providers, g_object_ref (provider));
+
+  ide_command_provider_query_async (provider,
+                                    complete->workspace,
+                                    complete->typed_text,
+                                    ide_task_get_cancellable (complete->task),
+                                    gbp_command_bar_model_query_cb,
+                                    g_object_ref (complete->task));
+}
+
+void
+gbp_command_bar_model_complete_async (GbpCommandBarModel  *self,
+                                      IdeWorkspace        *workspace,
+                                      const gchar         *typed_text,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  g_autoptr(PeasExtensionSet) set = NULL;
+  g_autoptr(GPtrArray) providers = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  Complete complete = {0};
+
+  g_return_if_fail (GBP_IS_COMMAND_BAR_MODEL (self));
+  g_return_if_fail (IDE_IS_WORKSPACE (workspace));
+  g_return_if_fail (typed_text != NULL);
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  providers = g_ptr_array_new_with_free_func (g_object_unref);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_command_bar_model_complete_async);
+  ide_task_set_task_data (task, g_ptr_array_ref (providers), g_ptr_array_unref);
+
+  set = peas_extension_set_new (peas_engine_get_default (),
+                                IDE_TYPE_COMMAND_PROVIDER,
+                                NULL);
+
+  complete.workspace = workspace;
+  complete.providers = providers;
+  complete.typed_text = typed_text;
+  complete.task = task;
+
+  peas_extension_set_foreach (set, gbp_command_bar_query_foreach_cb, &complete);
+
+  if (providers->len == 0)
+    ide_task_return_boolean (task, TRUE);
+}
+
+gboolean
+gbp_command_bar_model_complete_finish (GbpCommandBarModel  *self,
+                                       GAsyncResult        *result,
+                                       GError             **error)
+{
+  g_return_val_if_fail (GBP_IS_COMMAND_BAR_MODEL (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
diff --git a/src/plugins/command-bar/gbp-command-bar-model.h b/src/plugins/command-bar/gbp-command-bar-model.h
new file mode 100644
index 000000000..d1ac820dc
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar-model.h
@@ -0,0 +1,42 @@
+/* gbp-command-bar-model.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_COMMAND_BAR_MODEL (gbp_command_bar_model_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCommandBarModel, gbp_command_bar_model, GBP, COMMAND_BAR_MODEL, IdeObject)
+
+GbpCommandBarModel *gbp_command_bar_model_new             (IdeContext           *context);
+void                gbp_command_bar_model_complete_async  (GbpCommandBarModel   *self,
+                                                           IdeWorkspace         *workspace,
+                                                           const gchar          *typed_text,
+                                                           GCancellable         *cancellable,
+                                                           GAsyncReadyCallback   callback,
+                                                           gpointer              user_data);
+gboolean            gbp_command_bar_model_complete_finish (GbpCommandBarModel   *self,
+                                                           GAsyncResult         *result,
+                                                           GError              **error);
+
+G_END_DECLS
diff --git a/src/plugins/command-bar/gbp-command-bar-private.h 
b/src/plugins/command-bar/gbp-command-bar-private.h
new file mode 100644
index 000000000..8becbab60
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar-private.h
@@ -0,0 +1,29 @@
+/* gbp-command-bar-private.h
+ *
+ * Copyright 2018-2019 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 "gbp-command-bar.h"
+
+G_BEGIN_DECLS
+
+void _gbp_command_bar_init_shortcuts (GbpCommandBar *self);
+
+G_END_DECLS
diff --git a/src/plugins/command-bar/gbp-command-bar-shortcuts.c 
b/src/plugins/command-bar/gbp-command-bar-shortcuts.c
new file mode 100644
index 000000000..6e9fb51e6
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar-shortcuts.c
@@ -0,0 +1,64 @@
+/* gbp-command-bar-shortcuts.c
+ *
+ * Copyright 2018-2019 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-command-bar-shortcuts"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <dazzle.h>
+
+#include "gbp-command-bar-private.h"
+
+#define I_(s) g_intern_static_string(s)
+
+static const DzlShortcutEntry command_bar_shortcuts[] = {
+  { "org.gnome.builder.command-bar.reveal",
+    DZL_SHORTCUT_PHASE_GLOBAL | DZL_SHORTCUT_PHASE_CAPTURE,
+    NULL,
+    NC_("shortcut window", "Workspace Shortcuts"),
+    NC_("shortcut window", "Command Bar"),
+    NC_("shortcut window", "Show the workspace command bar") },
+};
+
+void
+_gbp_command_bar_init_shortcuts (GbpCommandBar *self)
+{
+  DzlShortcutController *controller;
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             command_bar_shortcuts,
+                                             G_N_ELEMENTS (command_bar_shortcuts),
+                                             GETTEXT_PACKAGE);
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.command-bar.reveal"),
+                                              I_("<Primary>Return"),
+                                              DZL_SHORTCUT_PHASE_GLOBAL | DZL_SHORTCUT_PHASE_CAPTURE,
+                                              I_("win.reveal-command-bar"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.command-bar.dismiss"),
+                                              I_("Escape"),
+                                              DZL_SHORTCUT_PHASE_CAPTURE,
+                                              I_("win.dismiss-command-bar"));
+}
diff --git a/src/plugins/command-bar/gbp-command-bar-suggestion.c 
b/src/plugins/command-bar/gbp-command-bar-suggestion.c
new file mode 100644
index 000000000..71a528314
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar-suggestion.c
@@ -0,0 +1,156 @@
+/* gbp-command-bar-suggestion.c
+ *
+ * Copyright 2018-2019 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-command-bar-suggestion"
+
+#include "config.h"
+
+#include "gbp-command-bar-suggestion.h"
+
+struct _GbpCommandBarSuggestion
+{
+  DzlSuggestion  parent_instance;
+  IdeCommand    *command;
+} GbpCommandBarSuggestionPrivate;
+
+enum {
+  PROP_0,
+  PROP_COMMAND,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (GbpCommandBarSuggestion, gbp_command_bar_suggestion, DZL_TYPE_SUGGESTION)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_command_bar_suggestion_set_command (GbpCommandBarSuggestion *self,
+                                        IdeCommand              *command)
+{
+  g_return_if_fail (GBP_IS_COMMAND_BAR_SUGGESTION (self));
+  g_return_if_fail (IDE_IS_COMMAND (command));
+
+  if (g_set_object (&self->command, command))
+    {
+      g_autofree gchar *title = ide_command_get_title (command);
+      g_autofree gchar *subtitle = ide_command_get_subtitle (command);
+
+      dzl_suggestion_set_title (DZL_SUGGESTION (self), title);
+      dzl_suggestion_set_subtitle (DZL_SUGGESTION (self), subtitle);
+    }
+}
+
+static void
+gbp_command_bar_suggestion_dispose (GObject *object)
+{
+  GbpCommandBarSuggestion *self = (GbpCommandBarSuggestion *)object;
+
+  g_clear_object (&self->command);
+
+  G_OBJECT_CLASS (gbp_command_bar_suggestion_parent_class)->dispose (object);
+}
+
+static void
+gbp_command_bar_suggestion_get_property (GObject    *object,
+                                         guint       prop_id,
+                                         GValue     *value,
+                                         GParamSpec *pspec)
+{
+  GbpCommandBarSuggestion *self = GBP_COMMAND_BAR_SUGGESTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMMAND:
+      g_value_set_object (value, gbp_command_bar_suggestion_get_command (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_command_bar_suggestion_set_property (GObject      *object,
+                                         guint         prop_id,
+                                         const GValue *value,
+                                         GParamSpec   *pspec)
+{
+  GbpCommandBarSuggestion *self = GBP_COMMAND_BAR_SUGGESTION (object);
+
+  switch (prop_id)
+    {
+    case PROP_COMMAND:
+      gbp_command_bar_suggestion_set_command (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_command_bar_suggestion_class_init (GbpCommandBarSuggestionClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gbp_command_bar_suggestion_dispose;
+  object_class->get_property = gbp_command_bar_suggestion_get_property;
+  object_class->set_property = gbp_command_bar_suggestion_set_property;
+
+  properties [PROP_COMMAND] =
+    g_param_spec_object ("command",
+                         "Command",
+                         "The command for the suggestion",
+                         IDE_TYPE_COMMAND,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_command_bar_suggestion_init (GbpCommandBarSuggestion *self)
+{
+}
+
+/**
+ * gbp_command_bar_suggestion_get_command:
+ * @self: a #GbpCommandBarSuggestion
+ *
+ * Returns: (transfer none): an #IdeCommand
+ *
+ * Since: 3.32
+ */
+IdeCommand *
+gbp_command_bar_suggestion_get_command (GbpCommandBarSuggestion *self)
+{
+  g_return_val_if_fail (GBP_IS_COMMAND_BAR_SUGGESTION (self), NULL);
+
+  return self->command;
+}
+
+GbpCommandBarSuggestion *
+gbp_command_bar_suggestion_new (IdeCommand *command)
+{
+  g_return_val_if_fail (IDE_IS_COMMAND (command), NULL);
+
+  return g_object_new (GBP_TYPE_COMMAND_BAR_SUGGESTION,
+                       "command", command,
+                       NULL);
+}
diff --git a/src/plugins/command-bar/gbp-command-bar-suggestion.h 
b/src/plugins/command-bar/gbp-command-bar-suggestion.h
new file mode 100644
index 000000000..0d4f5bee4
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar-suggestion.h
@@ -0,0 +1,35 @@
+/* gbp-command-bar-suggestion.h
+ *
+ * Copyright 2018-2019 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 <dazzle.h>
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_COMMAND_BAR_SUGGESTION (gbp_command_bar_suggestion_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCommandBarSuggestion, gbp_command_bar_suggestion, GBP, COMMAND_BAR_SUGGESTION, 
DzlSuggestion)
+
+GbpCommandBarSuggestion *gbp_command_bar_suggestion_new         (IdeCommand              *command);
+IdeCommand              *gbp_command_bar_suggestion_get_command (GbpCommandBarSuggestion *self);
+
+G_END_DECLS
diff --git a/src/plugins/command-bar/gbp-command-bar-workspace-addin.c 
b/src/plugins/command-bar/gbp-command-bar-workspace-addin.c
new file mode 100644
index 000000000..fb9ca10f4
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar-workspace-addin.c
@@ -0,0 +1,165 @@
+/* gbp-command-bar-workspace-addin.c
+ *
+ * Copyright 2018-2019 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-command-bar-workspace-addin"
+
+#include "config.h"
+
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <libide-terminal.h>
+
+#include "gbp-command-bar.h"
+#include "gbp-command-bar-workspace-addin.h"
+
+struct _GbpCommandBarWorkspaceAddin
+{
+  GObject        parent_instance;
+  GbpCommandBar *command_bar;
+};
+
+static gboolean
+position_command_bar_cb (GbpCommandBarWorkspaceAddin *self,
+                         GtkWidget                   *child,
+                         GdkRectangle                *area,
+                         GtkOverlay                  *overlay)
+{
+  GtkRequisition min, nat;
+
+  g_assert (GBP_IS_COMMAND_BAR_WORKSPACE_ADDIN (self));
+  g_assert (GTK_IS_WIDGET (child));
+
+  if (!GBP_IS_COMMAND_BAR (child))
+    return FALSE;
+
+  gtk_widget_get_allocation (GTK_WIDGET (overlay), area);
+  gtk_widget_get_preferred_size (child, &min, &nat);
+
+  area->x = (area->width - nat.width) / 2;
+  area->y = 100;
+  area->width = nat.width;
+  area->height = nat.height;
+
+  return TRUE;
+}
+
+static void
+gbp_command_bar_workspace_addin_dismiss_command_bar (GSimpleAction *action,
+                                                     GVariant      *param,
+                                                     gpointer       user_data)
+{
+  GbpCommandBarWorkspaceAddin *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_COMMAND_BAR_WORKSPACE_ADDIN (self));
+
+  if (self->command_bar)
+    gbp_command_bar_dismiss (self->command_bar);
+}
+
+static void
+gbp_command_bar_workspace_addin_reveal_command_bar (GSimpleAction *action,
+                                                    GVariant      *param,
+                                                    gpointer       user_data)
+{
+  GbpCommandBarWorkspaceAddin *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_COMMAND_BAR_WORKSPACE_ADDIN (self));
+
+  if (self->command_bar)
+    gbp_command_bar_reveal (self->command_bar);
+}
+
+static const GActionEntry entries[] = {
+  { "dismiss-command-bar", gbp_command_bar_workspace_addin_dismiss_command_bar },
+  { "reveal-command-bar", gbp_command_bar_workspace_addin_reveal_command_bar },
+};
+
+static void
+gbp_command_bar_workspace_addin_load (IdeWorkspaceAddin *addin,
+                                      IdeWorkspace      *workspace)
+{
+  GbpCommandBarWorkspaceAddin *self = (GbpCommandBarWorkspaceAddin *)addin;
+  GtkOverlay *overlay;
+
+  g_assert (IDE_IS_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace) ||
+            IDE_IS_EDITOR_WORKSPACE (workspace) ||
+            IDE_IS_TERMINAL_WORKSPACE (workspace));
+
+  self->command_bar = g_object_new (GBP_TYPE_COMMAND_BAR,
+                                    "hexpand", TRUE,
+                                    "valign", GTK_ALIGN_END,
+                                    "visible", FALSE,
+                                    NULL);
+  overlay = ide_workspace_get_overlay (workspace);
+  g_signal_connect_object (overlay,
+                           "get-child-position",
+                           G_CALLBACK (position_command_bar_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_overlay_add_overlay (overlay, GTK_WIDGET (self->command_bar));
+
+  /* Add actions for shortcuts to activate */
+  g_action_map_add_action_entries (G_ACTION_MAP (workspace),
+                                   entries,
+                                   G_N_ELEMENTS (entries),
+                                   self);
+}
+
+static void
+gbp_command_bar_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                        IdeWorkspace      *workspace)
+{
+  GbpCommandBarWorkspaceAddin *self = (GbpCommandBarWorkspaceAddin *)addin;
+
+  g_assert (IDE_IS_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace) ||
+            IDE_IS_EDITOR_WORKSPACE (workspace) ||
+            IDE_IS_TERMINAL_WORKSPACE (workspace));
+
+  /* Remove all the actions we added */
+  for (guint i = 0; i < G_N_ELEMENTS (entries); i++)
+    g_action_map_remove_action (G_ACTION_MAP (workspace), entries[i].name);
+
+  if (self->command_bar != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->command_bar));
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_command_bar_workspace_addin_load;
+  iface->unload = gbp_command_bar_workspace_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpCommandBarWorkspaceAddin, gbp_command_bar_workspace_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
+
+static void
+gbp_command_bar_workspace_addin_class_init (GbpCommandBarWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_command_bar_workspace_addin_init (GbpCommandBarWorkspaceAddin *self)
+{
+}
diff --git a/src/plugins/command-bar/gbp-command-bar-workspace-addin.h 
b/src/plugins/command-bar/gbp-command-bar-workspace-addin.h
new file mode 100644
index 000000000..4a091259d
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-command-bar-workspace-addin.h
+ *
+ * Copyright 2018-2019 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_COMMAND_BAR_WORKSPACE_ADDIN (gbp_command_bar_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCommandBarWorkspaceAddin, gbp_command_bar_workspace_addin, GBP, 
COMMAND_BAR_WORKSPACE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/command-bar/gbp-command-bar.c b/src/plugins/command-bar/gbp-command-bar.c
new file mode 100644
index 000000000..a15d57cc7
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar.c
@@ -0,0 +1,294 @@
+/* gbp-command-bar.c
+ *
+ * Copyright 2018-2019 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-command-bar"
+
+#include "config.h"
+
+#include <libide-gui.h>
+
+#include "gbp-command-bar.h"
+#include "gbp-command-bar-model.h"
+#include "gbp-command-bar-private.h"
+#include "gbp-command-bar-suggestion.h"
+
+struct _GbpCommandBar
+{
+  DzlBin              parent_instance;
+  DzlSuggestionEntry *entry;
+  GtkRevealer        *revealer;
+};
+
+G_DEFINE_TYPE (GbpCommandBar, gbp_command_bar, DZL_TYPE_BIN)
+
+static void
+replace_model (GbpCommandBar      *self,
+               GbpCommandBarModel *model)
+{
+  GListModel *old_model;
+
+  g_assert (GBP_IS_COMMAND_BAR (self));
+  g_assert (!model || GBP_IS_COMMAND_BAR_MODEL (model));
+
+  old_model = dzl_suggestion_entry_get_model (self->entry);
+  dzl_suggestion_entry_set_model (self->entry, G_LIST_MODEL (model));
+  if (old_model != NULL)
+    ide_object_destroy (IDE_OBJECT (old_model));
+}
+
+static void
+gbp_command_bar_complete_cb (GObject      *object,
+                             GAsyncResult *result,
+                             gpointer      user_data)
+{
+  GbpCommandBarModel *model = (GbpCommandBarModel *)object;
+  g_autoptr(GbpCommandBar) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_COMMAND_BAR_MODEL (model));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GBP_IS_COMMAND_BAR (self));
+
+  if (gbp_command_bar_model_complete_finish (model, result, &error))
+    replace_model (self, model);
+}
+
+static void
+gbp_command_bar_changed_cb (GbpCommandBar      *self,
+                            DzlSuggestionEntry *entry)
+{
+  g_autoptr(GbpCommandBarModel) model = NULL;
+  IdeWorkspace *workspace;
+  const gchar *text;
+  IdeContext *context;
+
+  g_assert (GBP_IS_COMMAND_BAR (self));
+  g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
+
+  text = dzl_suggestion_entry_get_typed_text (entry);
+
+  if (!gtk_widget_has_focus (GTK_WIDGET (entry)) || ide_str_empty0 (text))
+    {
+      replace_model (self, NULL);
+      return;
+    }
+
+  g_debug ("Command Bar: %s", text);
+
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  model = gbp_command_bar_model_new (context);
+  workspace = ide_widget_get_workspace (GTK_WIDGET (self));
+
+  gbp_command_bar_model_complete_async (model,
+                                        workspace,
+                                        text,
+                                        NULL,
+                                        gbp_command_bar_complete_cb,
+                                        g_object_ref (self));
+}
+
+static gboolean
+gbp_command_bar_focus_out_event_cb (GbpCommandBar      *self,
+                                    GdkEventFocus      *focus,
+                                    DzlSuggestionEntry *entry)
+{
+  g_assert (GBP_IS_COMMAND_BAR (self));
+  g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
+
+  if (gtk_revealer_get_reveal_child (self->revealer))
+    {
+      gtk_revealer_set_reveal_child (self->revealer, FALSE);
+      gtk_entry_set_text (GTK_ENTRY (entry), "");
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+gbp_command_bar_child_revealed_cb (GbpCommandBar *self,
+                                   GParamSpec    *pspec,
+                                   GtkRevealer   *revealer)
+{
+  g_assert (GBP_IS_COMMAND_BAR (self));
+  g_assert (GTK_IS_REVEALER (revealer));
+
+  if (gtk_revealer_get_child_revealed (revealer))
+    {
+      if (!gtk_widget_has_focus (GTK_WIDGET (self->entry)))
+        gtk_widget_grab_focus (GTK_WIDGET (self->entry));
+    }
+  else
+    gtk_widget_hide (GTK_WIDGET (self));
+}
+
+static void
+gbp_command_bar_activate_suggestion_cb (GbpCommandBar      *self,
+                                        DzlSuggestionEntry *entry)
+{
+  DzlSuggestion *suggestion;
+
+  g_assert (GBP_IS_COMMAND_BAR (self));
+  g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
+
+  if ((suggestion = dzl_suggestion_entry_get_suggestion (entry)))
+    {
+      GbpCommandBarSuggestion *cbs = GBP_COMMAND_BAR_SUGGESTION (suggestion);
+      IdeCommand *command = gbp_command_bar_suggestion_get_command (cbs);
+
+      ide_command_run_async (command, NULL, NULL, NULL);
+    }
+}
+
+static void
+gbp_command_bar_hide_suggestions_cb (GbpCommandBar      *self,
+                                     DzlSuggestionEntry *entry)
+{
+  g_assert (GBP_IS_COMMAND_BAR (self));
+  g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
+
+  if (gtk_widget_has_focus (GTK_WIDGET (entry)))
+    gbp_command_bar_dismiss (self);
+}
+
+static void
+position_popover_cb (DzlSuggestionEntry *entry,
+                     GdkRectangle       *area,
+                     gboolean           *is_absolute,
+                     gpointer            user_data)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (DZL_IS_SUGGESTION_ENTRY (entry));
+  g_assert (area != NULL);
+  g_assert (is_absolute != NULL);
+
+  dzl_suggestion_entry_default_position_func (entry, area, is_absolute, NULL);
+
+  /* We want to slightly adjust the popover positioning so it looks like the
+   * popover disappears into the entry. It makes the revealer out a bit less
+   * jarring as we hide the entry/window.
+   */
+  area->x += 3;
+  area->width -= 6;
+  area->y += 3;
+}
+
+static void
+gbp_command_bar_class_init (GbpCommandBarClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_css_name (widget_class, "commandbar");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/command-bar/gbp-command-bar.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpCommandBar, entry);
+  gtk_widget_class_bind_template_child (widget_class, GbpCommandBar, revealer);
+}
+
+static void
+gbp_command_bar_init (GbpCommandBar *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_widget_set_can_focus (GTK_WIDGET (self), FALSE);
+
+  g_signal_connect_object (self->revealer,
+                           "notify::child-revealed",
+                           G_CALLBACK (gbp_command_bar_child_revealed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->entry,
+                           "activate-suggestion",
+                           G_CALLBACK (gbp_command_bar_activate_suggestion_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->entry,
+                           "hide-suggestions",
+                           G_CALLBACK (gbp_command_bar_hide_suggestions_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->entry,
+                           "focus-out-event",
+                           G_CALLBACK (gbp_command_bar_focus_out_event_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->entry,
+                           "changed",
+                           G_CALLBACK (gbp_command_bar_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  dzl_suggestion_entry_set_position_func (self->entry,
+                                          position_popover_cb,
+                                          NULL,
+                                          NULL);
+
+  _gbp_command_bar_init_shortcuts (self);
+}
+
+void
+gbp_command_bar_reveal (GbpCommandBar *self)
+{
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (GBP_IS_COMMAND_BAR (self));
+
+  if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+    {
+      /* First clear reveal child so that we will fade in properly
+       * when setting reveal child below.
+       */
+      gtk_revealer_set_reveal_child (self->revealer, FALSE);
+      gtk_widget_show (GTK_WIDGET (self));
+    }
+
+  gtk_revealer_set_reveal_child (self->revealer, TRUE);
+
+  /* We need to try to grab focus immediately (best effort) or there is
+   * potential for input events to be delivered to the previously focused
+   * widget. We can't do this until after setting reveal-child or we can
+   * get warnings about the widget not being ready for events.
+   */
+  gtk_widget_grab_focus (GTK_WIDGET (self->entry));
+}
+
+void
+gbp_command_bar_dismiss (GbpCommandBar *self)
+{
+  IdeWorkspace *workspace;
+  IdeSurface *surface;
+  IdePage *page;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (GBP_IS_COMMAND_BAR (self));
+
+  gtk_revealer_set_reveal_child (self->revealer, FALSE);
+  workspace = ide_widget_get_workspace (GTK_WIDGET (self));
+  surface = ide_workspace_get_visible_surface (workspace);
+  page = ide_workspace_get_most_recent_page (workspace);
+
+  if (page != NULL)
+    gtk_widget_grab_focus (GTK_WIDGET (page));
+  else
+    gtk_widget_child_focus (GTK_WIDGET (surface), GTK_DIR_TAB_FORWARD);
+
+  gtk_entry_set_text (GTK_ENTRY (self->entry), "");
+}
diff --git a/src/plugins/command-bar/gbp-command-bar.h b/src/plugins/command-bar/gbp-command-bar.h
new file mode 100644
index 000000000..cecff6166
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar.h
@@ -0,0 +1,35 @@
+/* gbp-command-bar.h
+ *
+ * Copyright 2018-2019 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 <dazzle.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_COMMAND_BAR (gbp_command_bar_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCommandBar, gbp_command_bar, GBP, COMMAND_BAR, DzlBin)
+
+void gbp_command_bar_dismiss (GbpCommandBar *self);
+void gbp_command_bar_reveal  (GbpCommandBar *self);
+
+G_END_DECLS
diff --git a/src/plugins/command-bar/gbp-command-bar.ui b/src/plugins/command-bar/gbp-command-bar.ui
new file mode 100644
index 000000000..49791b93f
--- /dev/null
+++ b/src/plugins/command-bar/gbp-command-bar.ui
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpCommandBar" parent="DzlBin">
+    <child>
+      <object class="GtkRevealer" id="revealer">
+        <property name="reveal-child">false</property>
+        <property name="transition-type">crossfade</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="DzlSuggestionEntry" id="entry">
+            <property name="width-chars">45</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/command-bar/gbp-gaction-command.c b/src/plugins/command-bar/gbp-gaction-command.c
new file mode 100644
index 000000000..9ffbf7dfa
--- /dev/null
+++ b/src/plugins/command-bar/gbp-gaction-command.c
@@ -0,0 +1,160 @@
+/* gbp-gaction-command.c
+ *
+ * Copyright 2018-2019 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-gaction-command"
+
+#include "config.h"
+
+#include "gbp-gaction-command.h"
+
+struct _GbpGactionCommand
+{
+  IdeObject  parent_instance;
+  GtkWidget *widget;
+  gchar     *group;
+  gchar     *name;
+  GVariant  *param;
+  gchar     *title;
+  guint      priority;
+};
+
+static void
+gbp_gaction_command_run_async (IdeCommand          *command,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  GbpGactionCommand *self = (GbpGactionCommand *)command;
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (GBP_IS_GACTION_COMMAND (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_gaction_command_run_async);
+
+  if (self->widget != NULL)
+    dzl_gtk_widget_action (self->widget, self->group, self->name, self->param);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_gaction_command_run_finish (IdeCommand    *command,
+                                GAsyncResult  *result,
+                                GError       **error)
+{
+  g_assert (GBP_IS_GACTION_COMMAND (command));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static gchar *
+gbp_gaction_command_get_title (IdeCommand *command)
+{
+  GbpGactionCommand *self = (GbpGactionCommand *)command;
+
+  g_assert (GBP_IS_GACTION_COMMAND (self));
+
+  return g_strdup (self->title);
+}
+
+static void
+command_iface_init (IdeCommandInterface *iface)
+{
+  iface->run_async = gbp_gaction_command_run_async;
+  iface->run_finish = gbp_gaction_command_run_finish;
+  iface->get_title = gbp_gaction_command_get_title;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGactionCommand, gbp_gaction_command, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_COMMAND, command_iface_init))
+
+static void
+gbp_gaction_command_finalize (GObject *object)
+{
+  GbpGactionCommand *self = (GbpGactionCommand *)object;
+
+  g_clear_pointer (&self->group, g_free);
+  g_clear_pointer (&self->name, g_free);
+  g_clear_pointer (&self->param, g_variant_unref);
+  g_clear_pointer (&self->title, g_free);
+
+  if (self->widget != NULL)
+    {
+      g_signal_handlers_disconnect_by_func (self->widget,
+                                            G_CALLBACK (gtk_widget_destroyed),
+                                            &self->widget);
+      self->widget = NULL;
+    }
+
+  G_OBJECT_CLASS (gbp_gaction_command_parent_class)->finalize (object);
+}
+
+static void
+gbp_gaction_command_class_init (GbpGactionCommandClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_gaction_command_finalize;
+}
+
+static void
+gbp_gaction_command_init (GbpGactionCommand *self)
+{
+}
+
+GbpGactionCommand *
+gbp_gaction_command_new (GtkWidget   *widget,
+                         const gchar *group,
+                         const gchar *name,
+                         GVariant    *param,
+                         const gchar *title,
+                         guint        priority)
+{
+  GbpGactionCommand *self;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+  g_return_val_if_fail (group != NULL, NULL);
+  g_return_val_if_fail (name != NULL, NULL);
+
+  self = g_object_new (GBP_TYPE_GACTION_COMMAND, NULL);
+  self->widget = widget;
+  self->group = g_strdup (group);
+  self->name = g_strdup (name);
+  self->param = param ? g_variant_ref_sink (param) : NULL;
+  self->title = g_strdup (title);
+  self->priority = priority;
+
+  g_signal_connect (self->widget,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->widget);
+
+  return g_steal_pointer (&self);
+}
+
+gint
+gbp_gaction_command_compare (GbpGactionCommand *a,
+                             GbpGactionCommand *b)
+{
+  return (gint)a->priority - (gint)b->priority;
+}
diff --git a/src/plugins/command-bar/gbp-gaction-command.h b/src/plugins/command-bar/gbp-gaction-command.h
new file mode 100644
index 000000000..13cf7a7e3
--- /dev/null
+++ b/src/plugins/command-bar/gbp-gaction-command.h
@@ -0,0 +1,40 @@
+/* gbp-gaction-command.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GACTION_COMMAND (gbp_gaction_command_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGactionCommand, gbp_gaction_command, GBP, GACTION_COMMAND, IdeObject)
+
+gint               gbp_gaction_command_compare (GbpGactionCommand *a,
+                                                GbpGactionCommand *b);
+GbpGactionCommand *gbp_gaction_command_new     (GtkWidget         *widget,
+                                                const gchar       *group,
+                                                const gchar       *name,
+                                                GVariant          *param,
+                                                const gchar       *title,
+                                                guint              priority);
+
+G_END_DECLS
diff --git a/src/plugins/command-bar/meson.build b/src/plugins/command-bar/meson.build
index 139844f76..76bde4928 100644
--- a/src/plugins/command-bar/meson.build
+++ b/src/plugins/command-bar/meson.build
@@ -1,35 +1,18 @@
-if get_option('with_command_bar')
+plugins_sources += files([
+  'command-bar-plugin.c',
+  'gbp-command-bar.c',
+  'gbp-command-bar-command-provider.c',
+  'gbp-command-bar-model.c',
+  'gbp-command-bar-shortcuts.c',
+  'gbp-command-bar-suggestion.c',
+  'gbp-command-bar-workspace-addin.c',
+  'gbp-gaction-command.c',
+])
 
-command_bar_resources = gnome.compile_resources(
-  'gb-command-bar-resources',
-  'gb-command-bar.gresource.xml',
-  c_name: 'gb_command_bar',
+plugin_command_bar_resources = gnome.compile_resources(
+  'gbp-command-bar-resources',
+  'command-bar.gresource.xml',
+  c_name: 'gbp_command_bar',
 )
 
-command_bar_sources = [
-  'gb-command-bar.c',
-  'gb-command-bar.h',
-  'gb-command-gaction-provider.c',
-  'gb-command-gaction-provider.h',
-  'gb-command-gaction.c',
-  'gb-command-gaction.h',
-  'gb-command-manager.c',
-  'gb-command-manager.h',
-  'gb-command-provider.c',
-  'gb-command-provider.h',
-  'gb-command-result.c',
-  'gb-command-result.h',
-  'gb-command-vim-provider.c',
-  'gb-command-vim-provider.h',
-  'gb-command-vim.c',
-  'gb-command-vim.h',
-  'gb-command.c',
-  'gb-command.h',
-  'gb-vim.c',
-  'gb-vim.h',
-]
-
-gnome_builder_plugins_sources += files(command_bar_sources)
-gnome_builder_plugins_sources += command_bar_resources[0]
-
-endif
+plugins_sources += plugin_command_bar_resources[0]
diff --git a/src/plugins/command-bar/themes/shared.css b/src/plugins/command-bar/themes/shared.css
index 22f302a4b..4e66a47de 100644
--- a/src/plugins/command-bar/themes/shared.css
+++ b/src/plugins/command-bar/themes/shared.css
@@ -1,32 +1,5 @@
-commandbar > box.vertical > box.horizontal {
-  border: none;
-  box-shadow: 0px 10px 5px -10px shade(@theme_selected_bg_color, 0.3) inset;
-  color: @theme_selected_fg_color;
-  background-color: @theme_selected_bg_color;
-  background-size: 8px 8px;
-  background-image: repeating-linear-gradient(0deg, alpha(@theme_selected_fg_color,0.05), 
alpha(@theme_selected_fg_color,0.05) 1px, transparent 1px, transparent 8px),
-                    repeating-linear-gradient(-90deg, alpha(@theme_selected_fg_color,0.05), 
alpha(@theme_selected_fg_color,0.05) 1px, transparent 1px, transparent 8px);
-}
 commandbar entry {
-  font-family: Monospace;
-  background-image: none;
-  background-color: transparent;
-  min-height: 0px;
-  color: @theme_selected_fg_color;
-  border: none;
-  padding: 6px;
-  caret-color: @theme_selected_fg_color;
-  box-shadow: none;
-}
-commandbar flowbox {
-  opacity: 0.9;
-  padding: 12px;
-  color: @theme_selected_fg_color;
-  background: transparent;
-}
-commandbar viewport {
-  background-color: @theme_selected_bg_color;
-  background-size: 8px 8px;
-  background-image: repeating-linear-gradient(0deg, alpha(@theme_selected_fg_color,0.05), 
alpha(@theme_selected_fg_color,0.05) 1px, transparent 1px, transparent 8px),
-                    repeating-linear-gradient(-90deg, alpha(@theme_selected_fg_color,0.05), 
alpha(@theme_selected_fg_color,0.05) 1px, transparent 1px, transparent 8px);
+  margin: 10px;
+  border-width: 3px;
+  box-shadow: 0 0 5px @wm_shadow;
 }
diff --git a/src/plugins/comment-code/comment-code-plugin.c b/src/plugins/comment-code/comment-code-plugin.c
new file mode 100644
index 000000000..dc46c4bc1
--- /dev/null
+++ b/src/plugins/comment-code/comment-code-plugin.c
@@ -0,0 +1,34 @@
+/* comment-code-plugin.c
+ *
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include <libide-editor.h>
+#include <libpeas/peas.h>
+
+#include "gbp-comment-code-editor-page-addin.h"
+
+_IDE_EXTERN void
+_gbp_comment_code_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                              GBP_TYPE_COMMENT_CODE_EDITOR_PAGE_ADDIN);
+}
diff --git a/src/plugins/comment-code/comment-code.gresource.xml 
b/src/plugins/comment-code/comment-code.gresource.xml
new file mode 100644
index 000000000..b1bef6cf1
--- /dev/null
+++ b/src/plugins/comment-code/comment-code.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/comment-code">
+    <file>comment-code.plugin</file>
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/comment-code/comment-code.plugin b/src/plugins/comment-code/comment-code.plugin
index 94a60f983..8d193eac8 100644
--- a/src/plugins/comment-code/comment-code.plugin
+++ b/src/plugins/comment-code/comment-code.plugin
@@ -1,11 +1,10 @@
 [Plugin]
-Module=comment-code-plugin
-Name=Comment Code
-Description=Comment code lines with Builder editor.
 Authors=Sebastien Lafargue <slafargue gnome org>
-Copyright=Copyright © 2016 Sebastien Lafargue
 Builtin=true
-Depends=editor
-Embedded=gbp_comment_code_register_types
-X-Tool-Name=comment-code
-X-Tool-Description=Comment code lines
+Copyright=Copyright © 2016 Sebastien Lafargue
+Depends=editor;
+Description=Comment code lines with Builder editor.
+Embedded=_gbp_comment_code_register_types
+Hidden=true
+Module=comment-code
+Name=Comment Code
diff --git a/src/plugins/comment-code/gbp-comment-code-editor-page-addin.c 
b/src/plugins/comment-code/gbp-comment-code-editor-page-addin.c
new file mode 100644
index 000000000..3b611861f
--- /dev/null
+++ b/src/plugins/comment-code/gbp-comment-code-editor-page-addin.c
@@ -0,0 +1,450 @@
+/* gbp-comment-code-editor-page-addin.c
+ *
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+#include <libide-editor.h>
+
+#include "gbp-comment-code-editor-page-addin.h"
+
+#define I_(s) g_intern_static_string(s)
+
+struct _GbpCommentCodeEditorPageAddin
+{
+  GObject        parent_instance;
+
+  IdeEditorPage *editor_view;
+};
+
+static void editor_view_addin_iface_init (IdeEditorPageAddinInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GbpCommentCodeEditorPageAddin, gbp_comment_code_editor_page_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_PAGE_ADDIN, editor_view_addin_iface_init))
+
+/* If there's only empty lines, G_MAXINT is returned */
+static gint
+get_buffer_range_min_indent (GtkTextBuffer *buffer,
+                             gint           start_line,
+                             gint           end_line)
+{
+  GtkTextIter iter;
+  gint current_indent;
+  gint min_indent = G_MAXINT;
+
+  for (gint line = start_line; line <= end_line; ++line)
+    {
+      current_indent = 0;
+      gtk_text_buffer_get_iter_at_line (buffer, &iter, line);
+      while (!gtk_text_iter_ends_line (&iter) && g_unichar_isspace (gtk_text_iter_get_char (&iter)))
+        {
+          gtk_text_iter_forward_char (&iter);
+          ++current_indent;
+        }
+
+      if (gtk_text_iter_ends_line (&iter))
+        continue;
+      else
+        min_indent = MIN (min_indent, current_indent);
+    }
+
+  return min_indent;
+}
+
+/* Empty lines, with only spaces and tabs or already commented from the start
+ * are returned as not commentables.
+ */
+static gboolean
+is_line_commentable (GtkTextBuffer *buffer,
+                     gint           line,
+                     const gchar   *start_tag)
+{
+  GtkTextIter iter;
+
+  gtk_text_buffer_get_iter_at_line (buffer, &iter, line);
+  if (gtk_text_iter_is_end (&iter))
+    return FALSE;
+
+  while (g_unichar_isspace (gtk_text_iter_get_char (&iter)))
+    {
+      if (gtk_text_iter_ends_line (&iter) ||
+          !gtk_text_iter_forward_char (&iter))
+        return FALSE;
+    }
+
+  if (ide_text_iter_find_chars_forward (&iter, NULL, NULL, start_tag, TRUE))
+    return FALSE;
+
+  return TRUE;
+}
+
+/* Empty lines, with only spaces and tabs or not commented from the start
+ * are returned as not uncommentables.
+ * If TRUE, the start_tag_begin and start_tag_end are updated respectively
+ * to the start_tag begin and end positions.
+ */
+static gboolean
+is_line_uncommentable (GtkTextBuffer *buffer,
+                       gint           line,
+                       const gchar   *start_tag,
+                       GtkTextIter   *start_tag_begin,
+                       GtkTextIter   *start_tag_end)
+{
+  GtkTextIter iter;
+
+  gtk_text_buffer_get_iter_at_line (buffer, &iter, line);
+  if (gtk_text_iter_is_end (&iter))
+    return FALSE;
+
+  while (g_unichar_isspace (gtk_text_iter_get_char (&iter)))
+    {
+      if (gtk_text_iter_ends_line (&iter) ||
+          !gtk_text_iter_forward_char (&iter))
+        return FALSE;
+    }
+
+  if (ide_text_iter_find_chars_forward (&iter, NULL, start_tag_end, start_tag, TRUE))
+    {
+      *start_tag_begin = iter;
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+/* start_offset, in chars, is where we insert the start_tag.
+ * Empty lines or containing only spaces or tabs are skipped.
+ */
+static void
+gbp_comment_code_editor_page_addin_comment_line (GtkTextBuffer *buffer,
+                                                 const gchar   *start_tag,
+                                                 const gchar   *end_tag,
+                                                 gint           line,
+                                                 gint           start_offset,
+                                                 gboolean       is_block_tag)
+{
+  g_autofree gchar *start_tag_str = NULL;
+  g_autofree gchar *end_tag_str = NULL;
+  GtkTextIter start;
+  GtkTextIter previous;
+  GtkTextIter end_of_line;
+  gboolean res;
+
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+  g_assert (!dzl_str_empty0 (start_tag));
+  g_assert ((is_block_tag && !dzl_str_empty0 (end_tag)) || !is_block_tag);
+  g_assert (line >= 0 && line < gtk_text_buffer_get_line_count(buffer));
+
+  if (!is_line_commentable (buffer, line, start_tag))
+    return;
+
+  gtk_text_buffer_get_iter_at_line_offset (buffer, &start, line, start_offset);
+  if (gtk_text_iter_ends_line (&start))
+    return;
+
+  start_tag_str = g_strconcat (start_tag, " ", NULL);
+  gtk_text_buffer_insert (buffer, &start, start_tag_str, -1);
+  if (!is_block_tag)
+    return;
+
+  end_of_line = start;
+  gtk_text_iter_forward_to_line_end (&end_of_line);
+
+  while ((res = ide_text_iter_find_chars_forward (&start, &end_of_line, NULL, start_tag, FALSE)))
+    {
+      previous = start;
+      gtk_text_iter_backward_char (&previous);
+      if (gtk_text_iter_get_char (&previous) != '\\')
+        break;
+
+      gtk_text_iter_forward_char (&start);
+    }
+
+  if (!res)
+    {
+      start = end_of_line;
+      end_tag_str = g_strconcat (" ", end_tag, NULL);
+    }
+  else
+    end_tag_str = g_strconcat (" ", end_tag, " ", NULL);
+
+  gtk_text_buffer_insert (buffer, &start, end_tag_str, -1);
+}
+
+static void
+gbp_comment_code_editor_page_addin_uncomment_line (GtkTextBuffer *buffer,
+                                                   const gchar   *start_tag,
+                                                   const gchar   *end_tag,
+                                                   gint           line,
+                                                   gboolean       is_block_tag)
+{
+  GtkTextIter end_of_line;
+  GtkTextIter tag_begin;
+  GtkTextIter tag_end;
+  GtkTextIter tmp_iter;
+  GtkTextIter previous;
+  gunichar ch;
+  gboolean res;
+
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+  g_assert (!dzl_str_empty0 (start_tag));
+  g_assert ((is_block_tag && !dzl_str_empty0 (end_tag)) || !is_block_tag);
+  g_assert (line >= 0 && line < gtk_text_buffer_get_line_count(buffer));
+
+  if (!is_line_uncommentable (buffer, line, start_tag, &tag_begin, &tag_end))
+    return;
+
+  gtk_text_buffer_delete (buffer, &tag_begin, &tag_end);
+  ch = gtk_text_iter_get_char (&tag_begin);
+  if (ch == ' ' || ch == '\t')
+    {
+      gtk_text_iter_forward_char (&tag_end);
+      gtk_text_buffer_delete (buffer, &tag_begin, &tag_end);
+    }
+
+  if (!is_block_tag)
+    return;
+
+  end_of_line = tag_begin;
+  gtk_text_iter_forward_to_line_end (&end_of_line);
+  while ((res = ide_text_iter_find_chars_forward (&tag_begin, &end_of_line, &tag_end, end_tag, FALSE)))
+    {
+      previous = tag_begin;
+      gtk_text_iter_backward_char (&previous);
+      if (gtk_text_iter_get_char (&previous) != '\\')
+        break;
+
+      gtk_text_iter_forward_char (&tag_begin);
+    }
+
+  if (res)
+    {
+      tmp_iter = tag_begin;
+      gtk_text_iter_backward_char (&tmp_iter);
+      ch = gtk_text_iter_get_char (&tmp_iter);
+      if (ch == ' ' || ch == '\t')
+        tag_begin = tmp_iter;
+
+      tmp_iter = tag_end;
+      if (!gtk_text_iter_ends_line (&tmp_iter))
+        {
+          gtk_text_iter_forward_char (&tmp_iter);
+          ch = gtk_text_iter_get_char (&tmp_iter);
+          if (ch == ' ' || ch == '\t')
+            {
+              tag_end = tmp_iter;
+              gtk_text_iter_forward_char (&tag_end);
+            }
+        }
+
+      gtk_text_buffer_delete (buffer, &tag_begin, &tag_end);
+    }
+}
+
+static void
+gbp_comment_code_editor_page_addin_comment_action (GSimpleAction *action,
+                                                   GVariant      *variant,
+                                                   gpointer       user_data)
+{
+  GbpCommentCodeEditorPageAddin *self = GBP_COMMENT_CODE_EDITOR_PAGE_ADDIN (user_data);
+  IdeEditorPage *editor_view = self->editor_view;
+  IdeSourceView *source_view;
+  GtkTextBuffer *buffer;
+  const gchar *param;
+  IdeCompletion *completion;
+  GtkSourceLanguage *lang;
+  const gchar *start_tag;
+  const gchar *end_tag = NULL;
+  gint start_line;
+  gint end_line;
+  gint indent;
+  GtkTextIter begin;
+  GtkTextIter end;
+  gboolean editable;
+  gboolean block_comment = TRUE;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING));
+
+  buffer = GTK_TEXT_BUFFER (ide_editor_page_get_buffer (editor_view));
+  source_view = ide_editor_page_get_view (editor_view);
+  if (source_view == NULL || !GTK_SOURCE_IS_VIEW (source_view))
+    return;
+
+  editable = gtk_text_view_get_editable (GTK_TEXT_VIEW (source_view));
+  completion = ide_source_view_get_completion (IDE_SOURCE_VIEW (source_view));
+  lang = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer));
+  if (!editable || lang == NULL)
+    return;
+
+  if (dzl_str_equal0 (gtk_source_language_get_id(lang), "c"))
+    {
+      start_tag = gtk_source_language_get_metadata (lang, "block-comment-start");
+      end_tag = gtk_source_language_get_metadata (lang, "block-comment-end");
+      if (start_tag == NULL || end_tag == NULL)
+        {
+          block_comment = FALSE;
+          start_tag = gtk_source_language_get_metadata (lang, "line-comment-start");
+          if (start_tag == NULL)
+            return;
+        }
+    }
+  else
+    {
+      start_tag = gtk_source_language_get_metadata (lang, "line-comment-start");
+      if (start_tag == NULL)
+        {
+          start_tag = gtk_source_language_get_metadata (lang, "block-comment-start");
+          end_tag = gtk_source_language_get_metadata (lang, "block-comment-end");
+          if (start_tag == NULL || end_tag == NULL)
+            return;
+        }
+      else
+        block_comment = FALSE;
+    }
+
+  gtk_text_buffer_get_selection_bounds (buffer, &begin, &end);
+  gtk_text_iter_order (&begin, &end);
+
+  if (!gtk_text_iter_equal (&begin, &end) &&
+      gtk_text_iter_starts_line (&end))
+    gtk_text_iter_backward_char (&end);
+
+  start_line = gtk_text_iter_get_line (&begin);
+  end_line = gtk_text_iter_get_line (&end);
+
+  param = g_variant_get_string (variant, NULL);
+
+  if (*param == '0')
+    {
+      indent = get_buffer_range_min_indent (buffer, start_line, end_line);
+     if (indent == G_MAXINT)
+       return;
+
+      ide_completion_block_interactive (completion);
+      gtk_text_buffer_begin_user_action (buffer);
+
+      for (gint line = start_line; line <= end_line; ++line)
+        gbp_comment_code_editor_page_addin_comment_line (buffer, start_tag, end_tag, line, indent, 
block_comment);
+
+      gtk_text_buffer_end_user_action (buffer);
+      ide_completion_unblock_interactive (completion);
+    }
+  else if (*param == '1')
+    {
+      ide_completion_block_interactive (completion);
+      gtk_text_buffer_begin_user_action (buffer);
+
+      for (gint line = start_line; line <= end_line; ++line)
+        gbp_comment_code_editor_page_addin_uncomment_line (buffer, start_tag, end_tag, line, block_comment);
+
+      gtk_text_buffer_end_user_action (buffer);
+      ide_completion_unblock_interactive (completion);
+    }
+  else
+    g_assert_not_reached ();
+}
+
+static const DzlShortcutEntry comment_code_shortcut_entries[] = {
+  { "org.gnome.builder.editor-view.comment-code",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Editing"),
+    NC_("shortcut window", "Comment the code") },
+
+  { "org.gnome.builder.editor-view.uncomment-code",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Editing"),
+    NC_("shortcut window", "Uncomment the code") },
+};
+
+static const GActionEntry actions[] = {
+  { "comment-code", gbp_comment_code_editor_page_addin_comment_action, "s" },
+};
+
+static void
+gbp_comment_code_editor_page_addin_load (IdeEditorPageAddin *addin,
+                                         IdeEditorPage      *view)
+{
+  GbpCommentCodeEditorPageAddin *self;
+  g_autoptr(GSimpleActionGroup) group = NULL;
+  DzlShortcutController *controller;
+
+  g_assert (GBP_IS_COMMENT_CODE_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  self = GBP_COMMENT_CODE_EDITOR_PAGE_ADDIN (addin);
+  self->editor_view = view;
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (view), "comment-code", G_ACTION_GROUP (group));
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (view));
+  dzl_shortcut_controller_add_command_action (controller,
+                                              "org.gnome.builder.editor-view.comment-code",
+                                              I_("<primary>m"),
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              "view.comment-code::0");
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              "org.gnome.builder.editor-view.uncomment-code",
+                                              I_("<primary><shift>m"),
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              "view.comment-code::1");
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             comment_code_shortcut_entries,
+                                             G_N_ELEMENTS (comment_code_shortcut_entries),
+                                             GETTEXT_PACKAGE);
+}
+
+static void
+gbp_comment_code_editor_page_addin_unload (IdeEditorPageAddin *addin,
+                                           IdeEditorPage      *view)
+{
+  g_assert (GBP_IS_COMMENT_CODE_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (view), "comment-code", NULL);
+}
+
+static void
+gbp_comment_code_editor_page_addin_class_init (GbpCommentCodeEditorPageAddinClass *klass)
+{
+}
+
+static void
+gbp_comment_code_editor_page_addin_init (GbpCommentCodeEditorPageAddin *self)
+{
+}
+
+static void
+editor_view_addin_iface_init (IdeEditorPageAddinInterface *iface)
+{
+  iface->load = gbp_comment_code_editor_page_addin_load;
+  iface->unload = gbp_comment_code_editor_page_addin_unload;
+}
diff --git a/src/plugins/comment-code/gbp-comment-code-editor-page-addin.h 
b/src/plugins/comment-code/gbp-comment-code-editor-page-addin.h
new file mode 100644
index 000000000..ba70ea8cf
--- /dev/null
+++ b/src/plugins/comment-code/gbp-comment-code-editor-page-addin.h
@@ -0,0 +1,31 @@
+/* gbp-comment-code-editor-page-addin.h
+ *
+ * Copyright 2016 sebastien lafargue <slafargue gnome org>
+ *
+ * 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
+
+G_BEGIN_DECLS
+
+#include <glib-object.h>
+
+#define GBP_TYPE_COMMENT_CODE_EDITOR_PAGE_ADDIN (gbp_comment_code_editor_page_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCommentCodeEditorPageAddin, gbp_comment_code_editor_page_addin, GBP, 
COMMENT_CODE_EDITOR_PAGE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/comment-code/gtk/menus.ui b/src/plugins/comment-code/gtk/menus.ui
index 8945dd16e..7069386b8 100644
--- a/src/plugins/comment-code/gtk/menus.ui
+++ b/src/plugins/comment-code/gtk/menus.ui
@@ -7,12 +7,12 @@
           <attribute name="after">ide-source-view-popup-menu-line-section</attribute>
           <item>
             <attribute name="label" translatable="yes">Comment code</attribute>
-            <attribute name="action">view.comment-code</attribute>
+            <attribute name="action">comment-code.comment-code</attribute>
             <attribute name="target" type="s">'0'</attribute>
           </item>
           <item>
             <attribute name="label" translatable="yes">Uncomment code</attribute>
-            <attribute name="action">view.comment-code</attribute>
+            <attribute name="action">comment-code.comment-code</attribute>
             <attribute name="target" type="s">'1'</attribute>
           </item>
         </section>
diff --git a/src/plugins/comment-code/meson.build b/src/plugins/comment-code/meson.build
index 3c27211ce..703df526c 100644
--- a/src/plugins/comment-code/meson.build
+++ b/src/plugins/comment-code/meson.build
@@ -1,18 +1,12 @@
-if get_option('with_comment_code')
+plugins_sources += files([
+  'comment-code-plugin.c',
+  'gbp-comment-code-editor-page-addin.c',
+])
 
-comment_code_resources = gnome.compile_resources(
+plugin_comment_code_resources = gnome.compile_resources(
   'gbp-comment-code-resources',
-  'gbp-comment-code.gresource.xml',
+  'comment-code.gresource.xml',
   c_name: 'gbp_comment_code',
 )
 
-comment_code_sources = [
-  'gbp-comment-code-plugin.c',
-  'gbp-comment-code-view-addin.c',
-  'gbp-comment-code-view-addin.h',
-]
-
-gnome_builder_plugins_sources += files(comment_code_sources)
-gnome_builder_plugins_sources += comment_code_resources[0]
-
-endif
+plugins_sources += plugin_comment_code_resources[0]
diff --git a/src/plugins/create-project/create-project-plugin.c 
b/src/plugins/create-project/create-project-plugin.c
new file mode 100644
index 000000000..d0005b787
--- /dev/null
+++ b/src/plugins/create-project/create-project-plugin.c
@@ -0,0 +1,40 @@
+/* create-project-plugin.c
+ *
+ * Copyright 2015-2019 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 "create-project-plugin"
+
+#include "config.h"
+
+#include <libide-greeter.h>
+#include <libpeas/peas.h>
+
+#include "gbp-create-project-application-addin.h"
+#include "gbp-create-project-workspace-addin.h"
+
+void
+_gbp_create_project_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_APPLICATION_ADDIN,
+                                              GBP_TYPE_CREATE_PROJECT_APPLICATION_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_CREATE_PROJECT_WORKSPACE_ADDIN);
+}
diff --git a/src/plugins/create-project/create-project.gresource.xml 
b/src/plugins/create-project/create-project.gresource.xml
new file mode 100644
index 000000000..0174828ee
--- /dev/null
+++ b/src/plugins/create-project/create-project.gresource.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/create-project">
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+    <file preprocess="xml-stripblanks">gbp-create-project-surface.ui</file>
+    <file preprocess="xml-stripblanks">gbp-create-project-template-icon.ui</file>
+    <file>create-project.plugin</file>
+  </gresource>
+
+  <gresource prefix="/plugins/create-project/license/full">
+    <file compressed="true" alias="agpl_3">resources/agpl_3_full</file>
+    <file compressed="true" alias="apache_2">resources/apache_2_full</file>
+    <file compressed="true" alias="gpl_2">resources/gpl_2_full</file>
+    <file compressed="true" alias="gpl_3">resources/gpl_3_full</file>
+    <file compressed="true" alias="lgpl_2_1">resources/lgpl_2_1_full</file>
+    <file compressed="true" alias="lgpl_3">resources/lgpl_3_full</file>
+    <file compressed="true" alias="mit_x11">resources/mit_x11_full</file>
+  </gresource>
+
+  <gresource prefix="/plugins/create-project/license/short">
+    <file compressed="true" alias="agpl_3">resources/agpl_3_short</file>
+    <file compressed="true" alias="apache_2">resources/apache_2_short</file>
+    <file compressed="true" alias="gpl_2">resources/gpl_2_short</file>
+    <file compressed="true" alias="gpl_3">resources/gpl_3_short</file>
+    <file compressed="true" alias="lgpl_2_1">resources/lgpl_2_1_short</file>
+    <file compressed="true" alias="lgpl_3">resources/lgpl_3_short</file>
+    <file compressed="true" alias="mit_x11">resources/mit_x11_short</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/create-project/create-project.plugin 
b/src/plugins/create-project/create-project.plugin
index 4bbb2762f..93dd3e5d5 100644
--- a/src/plugins/create-project/create-project.plugin
+++ b/src/plugins/create-project/create-project.plugin
@@ -1,12 +1,11 @@
 [Plugin]
-Module=create-project-plugin
-Name=Create Project
-Description=Create projects with Builder
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Description=Builder's project creation wizard
+Embedded=_gbp_create_project_register_types
 Hidden=true
-Embedded=gbp_create_project_register_types
-Depends=git-plugin
-X-Tool-Name=create-project
-X-Tool-Description=Create a new project
+Module=create-project
+Name=Project Creation
+X-At-Startup=true
+X-Workspace-Kind=greeter;
diff --git a/src/plugins/create-project/gbp-create-project-application-addin.c 
b/src/plugins/create-project/gbp-create-project-application-addin.c
new file mode 100644
index 000000000..79727afa1
--- /dev/null
+++ b/src/plugins/create-project/gbp-create-project-application-addin.c
@@ -0,0 +1,107 @@
+/* gbp-create-project-application-addin.c
+ *
+ * Copyright 2018-2019 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-create-project-application-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-greeter.h>
+#include <libide-gui.h>
+
+#include "gbp-create-project-application-addin.h"
+
+struct _GbpCreateProjectApplicationAddin
+{
+  GObject parent_instance;
+};
+
+static void
+gbp_create_project_application_addin_add_option_entries (IdeApplicationAddin *addin,
+                                                         IdeApplication      *app)
+{
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (G_IS_APPLICATION (app));
+
+  g_application_add_main_option (G_APPLICATION (app),
+                                 "create-project",
+                                 0,
+                                 G_OPTION_FLAG_IN_MAIN,
+                                 G_OPTION_ARG_NONE,
+                                 _("Display the project creation guide"),
+                                 NULL);
+}
+
+static void
+gbp_create_project_application_addin_handle_command_line (IdeApplicationAddin     *addin,
+                                                          IdeApplication          *application,
+                                                          GApplicationCommandLine *cmdline)
+{
+  GVariantDict *dict;
+
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (application));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  dict = g_application_command_line_get_options_dict (cmdline);
+
+  /*
+   * If we are processing the arguments for the startup of the primary
+   * instance, then we want to show the greeter if no arguments are
+   * provided. (That means argc == 1, the programe executable).
+   *
+   * Also, if they provided --greeter or -g we'll show a new greeter.
+   */
+  if (g_variant_dict_contains (dict, "create-project"))
+    {
+      g_autoptr(IdeWorkbench) workbench = NULL;
+      IdeGreeterWorkspace *workspace;
+      IdeApplication *app = IDE_APPLICATION (application);
+
+      workbench = ide_workbench_new ();
+      ide_application_add_workbench (app, workbench);
+
+      workspace = ide_greeter_workspace_new (app);
+      ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+
+      ide_workspace_set_visible_surface_name (IDE_WORKSPACE (workspace), "create-project");
+      ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+    }
+}
+
+static void
+cmdline_addin_iface_init (IdeApplicationAddinInterface *iface)
+{
+  iface->add_option_entries = gbp_create_project_application_addin_add_option_entries;
+  iface->handle_command_line = gbp_create_project_application_addin_handle_command_line;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpCreateProjectApplicationAddin, gbp_create_project_application_addin, 
G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_APPLICATION_ADDIN, cmdline_addin_iface_init))
+
+static void
+gbp_create_project_application_addin_class_init (GbpCreateProjectApplicationAddinClass *klass)
+{
+}
+
+static void
+gbp_create_project_application_addin_init (GbpCreateProjectApplicationAddin *self)
+{
+}
diff --git a/src/plugins/create-project/gbp-create-project-application-addin.h 
b/src/plugins/create-project/gbp-create-project-application-addin.h
new file mode 100644
index 000000000..a8176404e
--- /dev/null
+++ b/src/plugins/create-project/gbp-create-project-application-addin.h
@@ -0,0 +1,31 @@
+/* gbp-create-project-application-addin.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_CREATE_PROJECT_APPLICATION_ADDIN (gbp_create_project_application_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCreateProjectApplicationAddin, gbp_create_project_application_addin, GBP, 
CREATE_PROJECT_APPLICATION_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/create-project/gbp-create-project-surface.c 
b/src/plugins/create-project/gbp-create-project-surface.c
new file mode 100644
index 000000000..a412c9353
--- /dev/null
+++ b/src/plugins/create-project/gbp-create-project-surface.c
@@ -0,0 +1,880 @@
+/* gbp-create-project-surface.c
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-create-project-surface"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-greeter.h>
+#include <libide-projects.h>
+#include <libide-vcs.h>
+#include <libpeas/peas.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "ide-greeter-private.h"
+
+#include "gbp-create-project-template-icon.h"
+#include "gbp-create-project-surface.h"
+
+struct _GbpCreateProjectSurface
+{
+  IdeSurface            parent;
+
+  GtkEntry             *app_id_entry;
+  GtkEntry             *project_name_entry;
+  DzlFileChooserEntry  *project_location_entry;
+  DzlRadioBox          *project_language_chooser;
+  GtkFlowBox           *project_template_chooser;
+  GtkSwitch            *versioning_switch;
+  DzlRadioBox          *license_chooser;
+  GtkLabel             *destination_label;
+  GtkButton            *create_button;
+
+  guint                 invalid_directory : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_IS_READY,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+G_DEFINE_TYPE (GbpCreateProjectSurface, gbp_create_project_surface, IDE_TYPE_SURFACE)
+
+static gboolean
+is_preferred (const gchar *name)
+{
+  return 0 == strcasecmp (name, "c") ||
+         0 == strcasecmp (name, "vala") ||
+         0 == strcasecmp (name, "javascript") ||
+         0 == strcasecmp (name, "python");
+}
+
+static int
+sort_by_name (gconstpointer a,
+              gconstpointer b)
+{
+  const gchar * const *astr = a;
+  const gchar * const *bstr = b;
+  gboolean apref = is_preferred (*astr);
+  gboolean bpref = is_preferred (*bstr);
+
+  if (apref && !bpref)
+    return -1;
+  else if (!apref && bpref)
+    return 1;
+
+  return g_utf8_collate (*astr, *bstr);
+}
+
+static void
+gbp_create_project_surface_add_languages (GbpCreateProjectSurface *self,
+                                         const GList            *templates)
+{
+  g_autoptr(GHashTable) languages = NULL;
+  g_autofree const gchar **keys = NULL;
+  const GList *iter;
+  guint len;
+
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+
+  languages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+  for (iter = templates; iter != NULL; iter = iter->next)
+    {
+      IdeProjectTemplate *template = iter->data;
+      g_auto(GStrv) template_languages = NULL;
+
+      g_assert (IDE_IS_PROJECT_TEMPLATE (template));
+
+      template_languages = ide_project_template_get_languages (template);
+
+      for (guint i = 0; template_languages [i]; i++)
+        g_hash_table_add (languages, g_strdup (template_languages [i]));
+    }
+
+  keys = (const gchar **)g_hash_table_get_keys_as_array (languages, &len);
+  qsort (keys, len, sizeof (gchar *), sort_by_name);
+  for (guint i = 0; keys[i]; i++)
+    dzl_radio_box_add_item (self->project_language_chooser, keys[i], keys[i]);
+}
+
+static gboolean
+validate_name (const gchar *name)
+{
+  if (name == NULL)
+    return FALSE;
+
+  if (g_unichar_isdigit (g_utf8_get_char (name)))
+    return FALSE;
+
+  for (; *name; name = g_utf8_next_char (name))
+    {
+      gunichar ch = g_utf8_get_char (name);
+
+      if (g_unichar_isspace (ch))
+        return FALSE;
+
+      if (ch == '/')
+        return FALSE;
+    }
+
+  return TRUE;
+}
+
+static gboolean
+directory_exists (GbpCreateProjectSurface *self,
+                  const gchar            *name)
+{
+  g_autoptr(GFile) directory = NULL;
+  g_autoptr(GFile) child = NULL;
+
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+  g_assert (name != NULL);
+
+  directory = dzl_file_chooser_entry_get_file (self->project_location_entry);
+  child = g_file_get_child (directory, name);
+
+  self->invalid_directory = g_file_query_exists (child, NULL);
+
+  return self->invalid_directory;
+}
+
+static void
+gbp_create_project_surface_create_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  GbpCreateProjectSurface *self = (GbpCreateProjectSurface *)object;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (user_data == NULL);
+
+  if (!gbp_create_project_surface_create_finish (self, result, &error))
+    {
+      g_warning ("Failed to create project: %s", error->message);
+    }
+}
+
+static void
+gbp_create_project_surface_create_clicked (GbpCreateProjectSurface *self,
+                                           GtkButton               *button)
+{
+  GCancellable *cancellable;
+  GtkWidget *workspace;
+
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_WORKSPACE);
+  cancellable = ide_workspace_get_cancellable (IDE_WORKSPACE (workspace));
+
+  gbp_create_project_surface_create_async (self,
+                                           cancellable,
+                                           gbp_create_project_surface_create_cb,
+                                           NULL);
+}
+
+static void
+gbp_create_project_surface_name_changed (GbpCreateProjectSurface *self,
+                                        GtkEntry               *entry)
+{
+  g_autofree gchar *project_name = NULL;
+  const gchar *text;
+
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  text = gtk_entry_get_text (entry);
+  project_name = g_strstrip (g_strdup (text));
+
+  if (ide_str_empty0 (project_name) || !validate_name (project_name))
+    {
+      g_object_set (self->project_name_entry,
+                    "secondary-icon-name", "dialog-warning-symbolic",
+                    "tooltip-text", _("Characters were used which might cause technical issues as a project 
name"),
+                    NULL);
+      gtk_label_set_label (self->destination_label,
+                           _("Your project will be created within a new child directory."));
+    }
+  else if (directory_exists (self, project_name))
+    {
+      g_object_set (self->project_name_entry,
+                    "secondary-icon-name", "dialog-warning-symbolic",
+                    "tooltip-text", _("Directory already exists with that name"),
+                    NULL);
+      gtk_label_set_label (self->destination_label, NULL);
+    }
+  else
+    {
+      g_autofree gchar *formatted = NULL;
+      g_autoptr(GFile) file = dzl_file_chooser_entry_get_file (self->project_location_entry);
+      g_autoptr(GFile) child = g_file_get_child (file, project_name);
+      g_autofree gchar *path = g_file_get_path (child);
+      g_autofree gchar *collapsed = ide_path_collapse (path);
+
+      g_object_set (self->project_name_entry,
+                    "secondary-icon-name", NULL,
+                    "tooltip-text", NULL,
+                    NULL);
+
+      /* translators: %s is replaced with a short-form file-system path to the project */
+      formatted = g_strdup_printf (_("Your project will be created within %s."), collapsed);
+      gtk_label_set_label (self->destination_label, formatted);
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_READY]);
+}
+
+static void
+gbp_create_project_surface_location_changed (GbpCreateProjectSurface *self,
+                                            GParamSpec             *pspec,
+                                            DzlFileChooserEntry    *chooser)
+{
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+  g_assert (DZL_IS_FILE_CHOOSER_ENTRY (chooser));
+
+  /* Piggyback on the name changed signal to update things */
+  gbp_create_project_surface_name_changed (self, self->project_name_entry);
+}
+
+static void
+update_language_sensitivity (GtkWidget *widget,
+                             gpointer   data)
+{
+  GbpCreateProjectSurface *self = data;
+  GbpCreateProjectTemplateIcon *template_icon;
+  IdeProjectTemplate *template;
+  g_auto(GStrv) template_languages = NULL;
+  const gchar *language;
+  gboolean sensitive = FALSE;
+  gint i;
+
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+  g_assert (GTK_IS_FLOW_BOX_CHILD (widget));
+
+  language = dzl_radio_box_get_active_id (self->project_language_chooser);
+
+  if (ide_str_empty0 (language))
+    goto apply;
+
+  template_icon = GBP_CREATE_PROJECT_TEMPLATE_ICON (gtk_bin_get_child (GTK_BIN (widget)));
+  g_object_get (template_icon,
+                "template", &template,
+                NULL);
+  template_languages = ide_project_template_get_languages (template);
+
+  for (i = 0; template_languages [i]; i++)
+    {
+      if (g_str_equal (language, template_languages [i]))
+        {
+          sensitive = TRUE;
+          goto apply;
+        }
+    }
+
+apply:
+  gtk_widget_set_sensitive (widget, sensitive);
+}
+
+static void
+gbp_create_project_surface_refilter (GbpCreateProjectSurface *self)
+{
+  gtk_container_foreach (GTK_CONTAINER (self->project_template_chooser),
+                         update_language_sensitivity,
+                         self);
+}
+
+static void
+gbp_create_project_surface_language_changed (GbpCreateProjectSurface *self,
+                                            DzlRadioBox            *language_chooser)
+{
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+  g_assert (DZL_IS_RADIO_BOX (language_chooser));
+
+  gbp_create_project_surface_refilter (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_READY]);
+}
+
+static void
+gbp_create_project_surface_template_selected (GbpCreateProjectSurface *self,
+                                             GtkFlowBox             *box,
+                                             GtkFlowBoxChild        *child)
+{
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_READY]);
+}
+
+static gint
+project_template_sort_func (GtkFlowBoxChild *child1,
+                            GtkFlowBoxChild *child2,
+                            gpointer         user_data)
+{
+  GbpCreateProjectTemplateIcon *icon1;
+  GbpCreateProjectTemplateIcon *icon2;
+  IdeProjectTemplate *tmpl1;
+  IdeProjectTemplate *tmpl2;
+
+  icon1 = GBP_CREATE_PROJECT_TEMPLATE_ICON (gtk_bin_get_child (GTK_BIN (child1)));
+  icon2 = GBP_CREATE_PROJECT_TEMPLATE_ICON (gtk_bin_get_child (GTK_BIN (child2)));
+
+  tmpl1 = gbp_create_project_template_icon_get_template (icon1);
+  tmpl2 = gbp_create_project_template_icon_get_template (icon2);
+
+  return ide_project_template_compare (tmpl1, tmpl2);
+}
+
+static void
+gbp_create_project_surface_add_template_buttons (GbpCreateProjectSurface *self,
+                                                GList                  *templates)
+{
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+
+  for (const GList *iter = templates; iter; iter = iter->next)
+    {
+      IdeProjectTemplate *template = iter->data;
+      GbpCreateProjectTemplateIcon *template_icon;
+      GtkFlowBoxChild *template_container;
+
+      g_assert (IDE_IS_PROJECT_TEMPLATE (template));
+
+      template_icon = g_object_new (GBP_TYPE_CREATE_PROJECT_TEMPLATE_ICON,
+                                    "visible", TRUE,
+                                    "template", template,
+                                    NULL);
+
+      template_container = g_object_new (GTK_TYPE_FLOW_BOX_CHILD,
+                                         "visible", TRUE,
+                                         NULL);
+      gtk_container_add (GTK_CONTAINER (template_container), GTK_WIDGET (template_icon));
+      gtk_flow_box_insert (self->project_template_chooser, GTK_WIDGET (template_container), -1);
+    }
+}
+
+static void
+template_providers_foreach_cb (PeasExtensionSet *set,
+                               PeasPluginInfo   *plugin_info,
+                               PeasExtension    *exten,
+                               gpointer          user_data)
+{
+  GbpCreateProjectSurface *self = user_data;
+  IdeTemplateProvider *provider = (IdeTemplateProvider *)exten;
+  GList *templates;
+
+  g_assert (PEAS_IS_EXTENSION_SET (set));
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+  g_assert (IDE_IS_TEMPLATE_PROVIDER (provider));
+
+  templates = ide_template_provider_get_project_templates (provider);
+
+  gbp_create_project_surface_add_template_buttons (self, templates);
+  gbp_create_project_surface_add_languages (self, templates);
+
+  gtk_flow_box_invalidate_sort (self->project_template_chooser);
+  gbp_create_project_surface_refilter (self);
+
+  g_list_free_full (templates, g_object_unref);
+}
+
+static GFile *
+gbp_create_project_surface_get_directory (GbpCreateProjectSurface *self)
+{
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+
+  return dzl_file_chooser_entry_get_file (self->project_location_entry);
+}
+
+static void
+gbp_create_project_surface_set_directory (GbpCreateProjectSurface *self,
+                                         GFile                  *directory)
+{
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+  g_assert (G_IS_FILE (directory));
+
+  dzl_file_chooser_entry_set_file (self->project_location_entry, directory);
+}
+
+static void
+gbp_create_project_surface_constructed (GObject *object)
+{
+  GbpCreateProjectSurface *self = GBP_CREATE_PROJECT_SURFACE (object);
+  PeasExtensionSet *extensions;
+  GtkFlowBoxChild *child;
+  PeasEngine *engine;
+
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+
+  engine = peas_engine_get_default ();
+
+  /* Load templates */
+  extensions = peas_extension_set_new (engine, IDE_TYPE_TEMPLATE_PROVIDER, NULL);
+  peas_extension_set_foreach (extensions, template_providers_foreach_cb, self);
+  g_clear_object (&extensions);
+
+  G_OBJECT_CLASS (gbp_create_project_surface_parent_class)->constructed (object);
+
+  /* Default to C, always. We might investigate setting this to the
+   * previously selected item in the future.
+   */
+  dzl_radio_box_set_active_id (self->project_language_chooser, "C");
+
+  /* Select the first template that is visible so we have a selection
+   * initially without the user having to select. We might also try to
+   * re-select a previous item in the future.
+   */
+  if ((child = gtk_flow_box_get_child_at_index (self->project_template_chooser, 0)))
+    gtk_flow_box_select_child (self->project_template_chooser, child);
+}
+
+static void
+gbp_create_project_surface_finalize (GObject *object)
+{
+  G_OBJECT_CLASS (gbp_create_project_surface_parent_class)->finalize (object);
+}
+
+static gboolean
+gbp_create_project_surface_is_ready (GbpCreateProjectSurface *self)
+{
+  const gchar *text;
+  g_autofree gchar *project_name = NULL;
+  const gchar *language = NULL;
+  GList *selected_template = NULL;
+  gboolean ret = FALSE;
+
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+
+  if (self->invalid_directory)
+    return FALSE;
+
+  text = gtk_entry_get_text (self->project_name_entry);
+  project_name = g_strstrip (g_strdup (text));
+
+  if (ide_str_empty0 (project_name) || !validate_name (project_name))
+    return FALSE;
+
+  language = dzl_radio_box_get_active_id (self->project_language_chooser);
+
+  if (ide_str_empty0 (language))
+    return FALSE;
+
+  selected_template = gtk_flow_box_get_selected_children (self->project_template_chooser);
+
+  if (selected_template == NULL)
+    return FALSE;
+
+  ret = gtk_widget_get_sensitive (selected_template->data);
+
+  g_list_free (selected_template);
+
+  return ret;
+}
+
+static void
+gbp_create_project_surface_grab_focus (GtkWidget *widget)
+{
+  gtk_widget_grab_focus (GTK_WIDGET (GBP_CREATE_PROJECT_SURFACE (widget)->project_name_entry));
+}
+
+static void
+gbp_create_project_surface_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  GbpCreateProjectSurface *self = GBP_CREATE_PROJECT_SURFACE(object);
+
+  switch (prop_id)
+    {
+    case PROP_IS_READY:
+      g_value_set_boolean (value, gbp_create_project_surface_is_ready (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_create_project_surface_class_init (GbpCreateProjectSurfaceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->constructed = gbp_create_project_surface_constructed;
+  object_class->finalize = gbp_create_project_surface_finalize;
+  object_class->get_property = gbp_create_project_surface_get_property;
+
+  widget_class->grab_focus = gbp_create_project_surface_grab_focus;
+
+  properties [PROP_IS_READY] =
+    g_param_spec_boolean ("is-ready",
+                          "Is Ready",
+                          "Is Ready",
+                          FALSE,
+                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "createprojectsurface");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/create-project/gbp-create-project-surface.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, app_id_entry);
+  gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, create_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, destination_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, license_chooser);
+  gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, project_language_chooser);
+  gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, project_location_entry);
+  gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, project_name_entry);
+  gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, project_template_chooser);
+  gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectSurface, versioning_switch);
+}
+
+static void
+gbp_create_project_surface_init (GbpCreateProjectSurface *self)
+{
+  g_autoptr(GFile) projects_dir = NULL;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_widget_set_name (GTK_WIDGET (self), "create-project");
+  ide_surface_set_title (IDE_SURFACE (self), _("New Project"));
+
+  projects_dir = g_file_new_for_path (ide_get_projects_dir ());
+  gbp_create_project_surface_set_directory (self, projects_dir);
+
+  g_signal_connect_object (self->project_name_entry,
+                           "changed",
+                           G_CALLBACK (gbp_create_project_surface_name_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->project_location_entry,
+                           "notify::file",
+                           G_CALLBACK (gbp_create_project_surface_location_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->project_language_chooser,
+                           "changed",
+                           G_CALLBACK (gbp_create_project_surface_language_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->project_template_chooser,
+                           "child-activated",
+                           G_CALLBACK (gbp_create_project_surface_template_selected),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->create_button,
+                           "clicked",
+                           G_CALLBACK (gbp_create_project_surface_create_clicked),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_flow_box_set_sort_func (self->project_template_chooser,
+                              project_template_sort_func,
+                              NULL, NULL);
+
+  g_object_bind_property (self, "is-ready", self->create_button, "sensitive",
+                          G_BINDING_SYNC_CREATE);
+}
+
+static void
+init_vcs_cb (GObject      *object,
+             GAsyncResult *result,
+             gpointer      user_data)
+{
+  IdeVcsInitializer *vcs = (IdeVcsInitializer *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeProjectInfo) project_info = NULL;
+  g_autoptr(GError) error = NULL;
+  GbpCreateProjectSurface *self;
+  GtkWidget *workspace;
+  GFile *project_file;
+
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_vcs_initializer_initialize_finish (vcs, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self = ide_task_get_source_object (task);
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+
+  project_file = ide_task_get_task_data (task);
+
+  project_info = ide_project_info_new ();
+  ide_project_info_set_file (project_info, project_file);
+
+  workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GREETER_WORKSPACE);
+  ide_greeter_workspace_open_project (IDE_GREETER_WORKSPACE (workspace), project_info);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+extract_cb (GObject      *object,
+            GAsyncResult *result,
+            gpointer      user_data)
+{
+  IdeProjectTemplate *template = (IdeProjectTemplate *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(IdeVcsInitializer) vcs = NULL;
+  g_autoptr(GError) error = NULL;
+  GbpCreateProjectSurface *self;
+  PeasPluginInfo *plugin_info;
+  PeasEngine *engine;
+  GFile *project_file;
+
+  /* To keep the UI simple, we only support git from
+   * the creation today. However, at the time of writing
+   * that is our only supported VCS anyway. If you'd like to
+   * add support for an additional VCS, we need to redesign
+   * this part of the UI.
+   */
+  const gchar *vcs_id = "git";
+
+  g_assert (IDE_IS_PROJECT_TEMPLATE (template));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_project_template_expand_finish (template, result, &error))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self = ide_task_get_source_object (task);
+  g_assert (GBP_IS_CREATE_PROJECT_SURFACE (self));
+
+  project_file = ide_task_get_task_data (task);
+  g_assert (G_IS_FILE (project_file));
+
+  if (!gtk_switch_get_active (self->versioning_switch))
+    {
+      g_autoptr(IdeProjectInfo) project_info = NULL;
+      GtkWidget *workspace;
+
+      project_info = ide_project_info_new ();
+      ide_project_info_set_file (project_info, project_file);
+
+      workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GREETER_WORKSPACE);
+      ide_greeter_workspace_open_project (IDE_GREETER_WORKSPACE (workspace), project_info);
+
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  engine = peas_engine_get_default ();
+  plugin_info = peas_engine_get_plugin_info (engine, vcs_id);
+  if (plugin_info == NULL)
+    IDE_GOTO (failure);
+
+  vcs = (IdeVcsInitializer *)peas_engine_create_extension (engine, plugin_info,
+                                                           IDE_TYPE_VCS_INITIALIZER,
+                                                           NULL);
+  if (vcs == NULL)
+    IDE_GOTO (failure);
+
+  ide_vcs_initializer_initialize_async (vcs,
+                                        project_file,
+                                        ide_task_get_cancellable (task),
+                                        init_vcs_cb,
+                                        g_object_ref (task));
+
+  return;
+
+failure:
+  ide_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_FAILED,
+                             _("A failure occurred while initializing version control"));
+}
+
+void
+gbp_create_project_surface_create_async (GbpCreateProjectSurface *self,
+                                         GCancellable            *cancellable,
+                                         GAsyncReadyCallback      callback,
+                                         gpointer                 user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GHashTable) params = NULL;
+  g_autoptr(IdeProjectTemplate) template = NULL;
+  g_autoptr(IdeVcsConfig) vcs_conf = NULL;
+  GValue str = G_VALUE_INIT;
+  g_autofree gchar *name = NULL;
+  g_autofree gchar *path = NULL;
+  g_autoptr(GFile) location = NULL;
+  g_autoptr(GFile) child = NULL;
+  const gchar *language = NULL;
+  const gchar *license_id = NULL;
+  GtkFlowBoxChild *template_container;
+  GbpCreateProjectTemplateIcon *template_icon;
+  PeasEngine *engine;
+  PeasPluginInfo *plugin_info;
+  const gchar *text;
+  const gchar *app_id;
+  const gchar *vcs_id = "git";
+  const gchar *author_name;
+  GList *selected_box_child;
+
+  g_return_if_fail (GBP_CREATE_PROJECT_SURFACE (self));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->create_button), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->license_chooser), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->project_language_chooser), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->project_location_entry), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->project_name_entry), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->project_template_chooser), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->versioning_switch), FALSE);
+
+  selected_box_child = gtk_flow_box_get_selected_children (self->project_template_chooser);
+  template_container = selected_box_child->data;
+  template_icon = GBP_CREATE_PROJECT_TEMPLATE_ICON (gtk_bin_get_child (GTK_BIN (template_container)));
+  g_object_get (template_icon,
+                "template", &template,
+                NULL);
+  g_list_free (selected_box_child);
+
+  params = g_hash_table_new_full (g_str_hash,
+                                  g_str_equal,
+                                  g_free,
+                                  (GDestroyNotify)g_variant_unref);
+
+  text = gtk_entry_get_text (self->project_name_entry);
+  name = g_strstrip (g_strdup (text));
+  g_hash_table_insert (params,
+                       g_strdup ("name"),
+                       g_variant_ref_sink (g_variant_new_string (g_strdelimit (name, " ", '-'))));
+
+  location = gbp_create_project_surface_get_directory (self);
+  child = g_file_get_child (location, name);
+  path = g_file_get_path (child);
+
+  g_hash_table_insert (params,
+                       g_strdup ("path"),
+                       g_variant_ref_sink (g_variant_new_string (path)));
+
+  language = dzl_radio_box_get_active_id (self->project_language_chooser);
+  g_hash_table_insert (params,
+                       g_strdup ("language"),
+                       g_variant_ref_sink (g_variant_new_string (language)));
+
+  license_id = dzl_radio_box_get_active_id (DZL_RADIO_BOX (self->license_chooser));
+
+  if (!g_str_equal (license_id, "none"))
+    {
+      g_autofree gchar *license_full_path = NULL;
+      g_autofree gchar *license_short_path = NULL;
+
+      license_full_path = g_strjoin (NULL, "resource://", "/plugins/create-project/license/full/", 
license_id, NULL);
+      license_short_path = g_strjoin (NULL, "resource://", "/plugins/create-project/license/short/", 
license_id, NULL);
+
+      g_hash_table_insert (params,
+                           g_strdup ("license_full"),
+                           g_variant_ref_sink (g_variant_new_string (license_full_path)));
+
+      g_hash_table_insert (params,
+                           g_strdup ("license_short"),
+                           g_variant_ref_sink (g_variant_new_string (license_short_path)));
+    }
+
+  if (gtk_switch_get_active (self->versioning_switch))
+    {
+      g_hash_table_insert (params,
+                           g_strdup ("versioning"),
+                           g_variant_ref_sink (g_variant_new_string ("git")));
+
+      engine = peas_engine_get_default ();
+      plugin_info = peas_engine_get_plugin_info (engine, vcs_id);
+
+      if (plugin_info != NULL)
+        {
+          vcs_conf = (IdeVcsConfig *)peas_engine_create_extension (engine, plugin_info,
+                                                                   IDE_TYPE_VCS_CONFIG,
+                                                                   NULL);
+
+          if (vcs_conf != NULL)
+            {
+              g_value_init (&str, G_TYPE_STRING);
+              ide_vcs_config_get_config (vcs_conf, IDE_VCS_CONFIG_FULL_NAME, &str);
+            }
+        }
+    }
+
+  if (G_VALUE_HOLDS_STRING (&str) && !ide_str_empty0 (g_value_get_string (&str)))
+    author_name = g_value_get_string (&str);
+  else
+    author_name = g_get_real_name ();
+
+  app_id = gtk_entry_get_text (self->app_id_entry);
+
+  if (ide_str_empty0 (app_id))
+    app_id = "org.example.App";
+
+  g_hash_table_insert (params,
+                       g_strdup ("author"),
+                       g_variant_take_ref (g_variant_new_string (author_name)));
+
+  g_hash_table_insert (params,
+                       g_strdup ("app-id"),
+                       g_variant_take_ref (g_variant_new_string (app_id)));
+
+  g_value_unset (&str);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_task_data (task, g_file_new_for_path (path), g_object_unref);
+
+  ide_project_template_expand_async (template,
+                                     params,
+                                     NULL,
+                                     extract_cb,
+                                     g_object_ref (task));
+}
+
+gboolean
+gbp_create_project_surface_create_finish (GbpCreateProjectSurface  *self,
+                                          GAsyncResult             *result,
+                                          GError                  **error)
+{
+  g_return_val_if_fail (GBP_IS_CREATE_PROJECT_SURFACE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->create_button), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->license_chooser), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->project_language_chooser), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->project_location_entry), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->project_name_entry), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->project_template_chooser), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->versioning_switch), TRUE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
diff --git a/src/plugins/create-project/gbp-create-project-surface.h 
b/src/plugins/create-project/gbp-create-project-surface.h
new file mode 100644
index 000000000..3475f9ba3
--- /dev/null
+++ b/src/plugins/create-project/gbp-create-project-surface.h
@@ -0,0 +1,39 @@
+/* gbp-create-project-surface.h
+ *
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-projects.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_CREATE_PROJECT_SURFACE (gbp_create_project_surface_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCreateProjectSurface, gbp_create_project_surface, GBP, CREATE_PROJECT_SURFACE, 
IdeSurface)
+
+void     gbp_create_project_surface_create_async  (GbpCreateProjectSurface  *self,
+                                                   GCancellable             *cancellable,
+                                                   GAsyncReadyCallback       callback,
+                                                   gpointer                  user_data);
+gboolean gbp_create_project_surface_create_finish (GbpCreateProjectSurface  *self,
+                                                   GAsyncResult             *result,
+                                                   GError                  **error);
+
+G_END_DECLS
diff --git a/src/plugins/create-project/gbp-create-project-surface.ui 
b/src/plugins/create-project/gbp-create-project-surface.ui
new file mode 100644
index 000000000..5ed65f58d
--- /dev/null
+++ b/src/plugins/create-project/gbp-create-project-surface.ui
@@ -0,0 +1,396 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpCreateProjectSurface" parent="IdeSurface">
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="visible">true</property>
+        <property name="propagate-natural-height">true</property>
+        <property name="propagate-natural-width">true</property>
+        <property name="hscrollbar-policy">never</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <property name="expand">true</property>
+            <property name="margin-top">72</property>
+            <property name="margin-start">64</property>
+            <property name="margin-end">64</property>
+            <property name="margin-bottom">64</property>
+            <property name="valign">start</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="DzlThreeGrid" id="three_grid">
+                <property name="column-spacing">12</property>
+                <property name="row-spacing">24</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel" id="project_name_label">
+                    <property name="halign">end</property>
+                    <property name="label" translatable="yes">Project Name</property>
+                    <property name="valign">start</property>
+                    <property name="visible">true</property>
+                  </object>
+                  <packing>
+                    <property name="column">left</property>
+                    <property name="row">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="spacing">6</property>
+                    <property name="orientation">vertical</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkEntry" id="project_name_entry">
+                        <property name="width-chars">50</property>
+                        <property name="expand">true</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="xalign">0.0</property>
+                        <property name="wrap">true</property>
+                        <property name="visible">true</property>
+                        <property name="max-width-chars">60</property>
+                        <property name="label" translatable="yes">Unique name that is used for your 
project’s folder and other technical resources. Should be in lower case without spaces and may not start with 
a number.</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                        <attributes>
+                          <attribute name="scale" value="0.833333"/>
+                        </attributes>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="column">center</property>
+                    <property name="row">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="app_id_label">
+                    <property name="halign">end</property>
+                    <property name="label" translatable="yes">Application ID</property>
+                    <property name="valign">start</property>
+                    <property name="visible">true</property>
+                  </object>
+                  <packing>
+                    <property name="column">left</property>
+                    <property name="row">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="spacing">6</property>
+                    <property name="orientation">vertical</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkEntry" id="app_id_entry">
+                        <property name="width-chars">50</property>
+                        <property name="expand">true</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="xalign">0.0</property>
+                        <property name="wrap">true</property>
+                        <property name="visible">true</property>
+                        <property name="max-width-chars">60</property>
+                        <property name="label" translatable="yes">The Application ID is a reverse 
domain-name identifier used to uniquely identify your application such as “org.gnome.Builder”.</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                        <attributes>
+                          <attribute name="scale" value="0.833333"/>
+                        </attributes>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="column">center</property>
+                    <property name="row">1</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="project_location_label">
+                    <property name="halign">end</property>
+                    <property name="label" translatable="yes">Project Location</property>
+                    <property name="valign">start</property>
+                    <property name="visible">true</property>
+                  </object>
+                  <packing>
+                    <property name="column">left</property>
+                    <property name="row">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="spacing">6</property>
+                    <property name="orientation">vertical</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="DzlFileChooserEntry" id="project_location_entry">
+                        <property name="action">select-folder</property>
+                        <property name="title" translatable="yes">Select Project Directory</property>
+                        <property name="hexpand">true</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel" id="destination_label">
+                        <property name="xalign">0.0</property>
+                        <property name="wrap">true</property>
+                        <property name="visible">true</property>
+                        <property name="max-width-chars">60</property>
+                        <property name="label" translatable="yes">Your project will be created within a new 
child directory.</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                        <attributes>
+                          <attribute name="scale" value="0.833333"/>
+                        </attributes>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="column">center</property>
+                    <property name="row">2</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkLabel" id="language_label">
+                    <property name="halign">end</property>
+                    <property name="label" translatable="yes">Language</property>
+                    <property name="valign">start</property>
+                    <property name="visible">true</property>
+                  </object>
+                  <packing>
+                    <property name="column">left</property>
+                    <property name="row">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="DzlRadioBox" id="project_language_chooser">
+                    <property name="expand">true</property>
+                    <property name="visible">true</property>
+                  </object>
+                  <packing>
+                    <property name="column">center</property>
+                    <property name="row">3</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkLabel" id="license_label">
+                    <property name="halign">end</property>
+                    <property name="label" translatable="yes">License</property>
+                    <property name="valign">start</property>
+                    <property name="visible">true</property>
+                  </object>
+                  <packing>
+                    <property name="column">left</property>
+                    <property name="row">4</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="DzlRadioBox" id="license_chooser">
+                    <property name="active-id">gpl_3</property>
+                    <property name="expand">true</property>
+                    <property name="visible">true</property>
+                    <items>
+                      <item id="gpl_3" translatable="yes">GPLv3+</item>
+                      <item id="lgpl_3" translatable="yes">LGPLv3+</item>
+                      <item id="agpl_3" translatable="yes">AGPLv3+</item>
+                      <item id="mit_x11" translatable="yes">MIT/X11</item>
+                      <item id="apache_2" translatable="yes">Apache 2.0</item>
+                      <item id="gpl_2" translatable="yes">GPLv2+</item>
+                      <item id="lgpl_2_1" translatable="yes">LGPLv2.1+</item>
+                      <item id="none" translatable="yes">No license</item>
+                    </items>
+                  </object>
+                  <packing>
+                    <property name="column">center</property>
+                    <property name="row">4</property>
+                  </packing>
+                </child>
+
+                <child>
+                  <object class="GtkLabel">
+                    <property name="halign">end</property>
+                    <property name="label" translatable="yes">Version Control</property>
+                    <property name="visible">true</property>
+                  </object>
+                  <packing>
+                    <property name="column">left</property>
+                    <property name="row">5</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox" id="versioning_box">
+                    <property name="orientation">horizontal</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkSwitch" id="versioning_switch">
+                        <property name="active">true</property>
+                        <property name="halign">start</property>
+                        <property name="visible">true</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkLabel">
+                        <property name="label" translatable="yes">Uses the Git version control 
system</property>
+                        <property name="visible">true</property>
+                        <style>
+                          <class name="dim-label"/>
+                        </style>
+                        <attributes>
+                          <attribute name="scale" value="0.833333"/>
+                        </attributes>
+                      </object>
+                      <packing>
+                        <property name="pack-type">end</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="column">center</property>
+                    <property name="row">5</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkToggleButton" id="license_more">
+                        <property name="active" bind-source="license_chooser" bind-property="show-more" 
bind-flags="bidirectional"/>
+                        <property name="sensitive" bind-source="license_chooser" bind-property="has-more"/>
+                        <property name="valign">start</property>
+                        <property name="vexpand">false</property>
+                        <property name="visible">true</property>
+                        <property name="focus-on-click">false</property>
+                        <style>
+                          <class name="flat"/>
+                          <class name="image-button"/>
+                        </style>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="icon-name">view-more-symbolic</property>
+                            <property name="visible">true</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="column">right</property>
+                    <property name="row">4</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkToggleButton" id="language_more">
+                        <property name="active" bind-source="project_language_chooser" 
bind-property="show-more" bind-flags="bidirectional"/>
+                        <property name="sensitive" bind-source="project_language_chooser" 
bind-property="has-more"/>
+                        <property name="valign">start</property>
+                        <property name="vexpand">false</property>
+                        <property name="visible">true</property>
+                        <property name="focus-on-click">false</property>
+                        <style>
+                          <class name="flat"/>
+                          <class name="image-button"/>
+                        </style>
+                        <child>
+                          <object class="GtkImage">
+                            <property name="icon-name">view-more-symbolic</property>
+                            <property name="visible">true</property>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="column">right</property>
+                    <property name="row">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkBox">
+                    <property name="orientation">horizontal</property>
+                    <property name="margin-top">12</property>
+                    <property name="hexpand">true</property>
+                    <property name="visible">true</property>
+                    <child>
+                      <object class="GtkButton" id="create_button">
+                        <property name="label" translatable="yes">Create Project</property>
+                        <property name="halign">end</property>
+                        <property name="visible">true</property>
+                        <style>
+                          <class name="suggested-action"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="pack-type">end</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="column">1</property>
+                    <property name="row">6</property>
+                  </packing>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <property name="expand">true</property>
+                <property name="valign">start</property>
+                <property name="spacing">12</property>
+                <property name="margin-top">24</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkLabel">
+                    <property name="label" translatable="yes">Select a Template</property>
+                    <property name="visible">true</property>
+                    <attributes>
+                      <attribute name="weight" value="bold"/>
+                    </attributes>
+                    <style>
+                      <class name="dim-label"/>
+                    </style>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkFlowBox" id="project_template_chooser">
+                    <property name="column-spacing">12</property>
+                    <property name="row-spacing">12</property>
+                    <property name="max-children-per-line">4</property>
+                    <property name="min-children-per-line">4</property>
+                    <property name="halign">center</property>
+                    <property name="valign">start</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkSizeGroup">
+    <property name="mode">vertical</property>
+    <widgets>
+      <widget name="project_name_label"/>
+      <widget name="app_id_label"/>
+      <widget name="project_name_entry"/>
+      <widget name="project_location_label"/>
+      <widget name="license_label"/>
+      <widget name="language_label"/>
+    </widgets>
+  </object>
+</interface>
diff --git a/src/plugins/create-project/gbp-create-project-template-icon.c 
b/src/plugins/create-project/gbp-create-project-template-icon.c
index bdb729fc8..738d9a376 100644
--- a/src/plugins/create-project/gbp-create-project-template-icon.c
+++ b/src/plugins/create-project/gbp-create-project-template-icon.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "gbp-create-project-template-icon.h"
@@ -81,7 +83,7 @@ gbp_create_project_template_icon_set_property (GObject      *object,
                     "icon-name", icon_name,
                     NULL);
       gtk_label_set_text (self->template_name, name);
-      if (!dzl_str_empty0 (description))
+      if (!ide_str_empty0 (description))
         gtk_widget_set_tooltip_text (GTK_WIDGET (self), description);
       break;
 
@@ -121,7 +123,7 @@ gbp_create_project_template_icon_class_init (GbpCreateProjectTemplateIconClass *
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
   gtk_widget_class_set_template_from_resource (widget_class,
-                                               
"/org/gnome/builder/plugins/create-project-plugin/gbp-create-project-template-icon.ui");
+                                               
"/plugins/create-project/gbp-create-project-template-icon.ui");
   gtk_widget_class_set_css_name (widget_class, "createprojecttemplateicon");
   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectTemplateIcon, template_icon);
   gtk_widget_class_bind_template_child (widget_class, GbpCreateProjectTemplateIcon, template_name);
@@ -140,6 +142,8 @@ gbp_create_project_template_icon_init (GbpCreateProjectTemplateIcon *self)
  * Gets the template for the item.
  *
  * Returns: (transfer none): an #IdeProjectTemplate
+ *
+ * Since: 3.32
  */
 IdeProjectTemplate *
 gbp_create_project_template_icon_get_template (GbpCreateProjectTemplateIcon *self)
diff --git a/src/plugins/create-project/gbp-create-project-template-icon.h 
b/src/plugins/create-project/gbp-create-project-template-icon.h
index 47532f2f4..d20fde05a 100644
--- a/src/plugins/create-project/gbp-create-project-template-icon.h
+++ b/src/plugins/create-project/gbp-create-project-template-icon.h
@@ -14,12 +14,13 @@
  *
  * 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 <gtk/gtk.h>
-#include <ide.h>
+#include <libide-projects.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/create-project/gbp-create-project-workspace-addin.c 
b/src/plugins/create-project/gbp-create-project-workspace-addin.c
new file mode 100644
index 000000000..057f4c4fe
--- /dev/null
+++ b/src/plugins/create-project/gbp-create-project-workspace-addin.c
@@ -0,0 +1,92 @@
+/* gbp-create-project-workspace-addin.c
+ *
+ * Copyright 2018-2019 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-create-project-workspace-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-greeter.h>
+
+#include "gbp-create-project-surface.h"
+#include "gbp-create-project-workspace-addin.h"
+
+struct _GbpCreateProjectWorkspaceAddin
+{
+  GObject     parent_instance;
+  IdeSurface *surface;
+};
+
+static void
+gbp_create_project_workspace_addin_load (IdeWorkspaceAddin *addin,
+                                         IdeWorkspace      *workspace)
+{
+  GbpCreateProjectWorkspaceAddin *self = (GbpCreateProjectWorkspaceAddin *)addin;
+
+  g_assert (GBP_IS_CREATE_PROJECT_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_GREETER_WORKSPACE (workspace));
+
+  ide_greeter_workspace_add_button (IDE_GREETER_WORKSPACE (workspace),
+                                    g_object_new (GTK_TYPE_BUTTON,
+                                                  "action-name", "win.surface",
+                                                  "action-target", g_variant_new_string ("create-project"),
+                                                  "label", _("_New…"),
+                                                  "use-underline", TRUE,
+                                                  "visible", TRUE,
+                                                  NULL),
+                                    -10);
+
+  self->surface = g_object_new (GBP_TYPE_CREATE_PROJECT_SURFACE,
+                                "visible", TRUE,
+                                NULL);
+  ide_workspace_add_surface (workspace, self->surface);
+}
+
+static void
+gbp_create_project_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                           IdeWorkspace      *workspace)
+{
+  GbpCreateProjectWorkspaceAddin *self = (GbpCreateProjectWorkspaceAddin *)addin;
+
+  g_assert (GBP_IS_CREATE_PROJECT_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_GREETER_WORKSPACE (workspace));
+
+  gtk_widget_destroy (GTK_WIDGET (self->surface));
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_create_project_workspace_addin_load;
+  iface->unload = gbp_create_project_workspace_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpCreateProjectWorkspaceAddin, gbp_create_project_workspace_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
+
+static void
+gbp_create_project_workspace_addin_class_init (GbpCreateProjectWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_create_project_workspace_addin_init (GbpCreateProjectWorkspaceAddin *self)
+{
+}
diff --git a/src/plugins/create-project/gbp-create-project-workspace-addin.h 
b/src/plugins/create-project/gbp-create-project-workspace-addin.h
new file mode 100644
index 000000000..244911d04
--- /dev/null
+++ b/src/plugins/create-project/gbp-create-project-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-create-project-workspace-addin.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_CREATE_PROJECT_WORKSPACE_ADDIN (gbp_create_project_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCreateProjectWorkspaceAddin, gbp_create_project_workspace_addin, GBP, 
CREATE_PROJECT_WORKSPACE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/create-project/gtk/menus.ui b/src/plugins/create-project/gtk/menus.ui
new file mode 100644
index 000000000..39b891e3f
--- /dev/null
+++ b/src/plugins/create-project/gtk/menus.ui
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-greeter-workspace-menu">
+    <section id="ide-greeter-workspace-menu-projects">
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-new</attribute>
+        <attribute name="label" translatable="yes">_New Project</attribute>
+        <attribute name="action">win.surface</attribute>
+        <attribute name="target" type="s">'create-project'</attribute>
+        <attribute name="before">ide-greeter-workspace-menu-open</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-primary-workspace-menu">
+    <section id="ide-primary-workspace-menu-projects-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-new</attribute>
+        <attribute name="label" translatable="yes">_New Project</attribute>
+        <attribute name="action">app.present-greeter-with-surface</attribute>
+        <attribute name="target" type="s">'create-project'</attribute>
+        <attribute name="before">ide-primary-workspace-menu-open</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-editor-workspace-menu">
+    <section id="ide-editor-workspace-menu-projects-section">
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-new</attribute>
+        <attribute name="label" translatable="yes">_New Project</attribute>
+        <attribute name="action">app.present-greeter-with-surface</attribute>
+        <attribute name="target" type="s">'create-project'</attribute>
+        <attribute name="before">ide-editor-workspace-menu-open</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/plugins/create-project/meson.build b/src/plugins/create-project/meson.build
index bfd6371ef..e99c1fc80 100644
--- a/src/plugins/create-project/meson.build
+++ b/src/plugins/create-project/meson.build
@@ -1,24 +1,15 @@
-if get_option('with_create_project')
+plugins_sources += files([
+  'create-project-plugin.c',
+  'gbp-create-project-application-addin.c',
+  'gbp-create-project-template-icon.c',
+  'gbp-create-project-surface.c',
+  'gbp-create-project-workspace-addin.c',
+])
 
-create_project_resources = gnome.compile_resources(
+plugin_create_project_resources = gnome.compile_resources(
   'gbp-create-project-resources',
-  'gbp-create-project.gresource.xml',
+  'create-project.gresource.xml',
   c_name: 'gbp_create_project',
 )
 
-create_project_sources = [
-  'gbp-create-project-genesis-addin.c',
-  'gbp-create-project-genesis-addin.h',
-  'gbp-create-project-plugin.c',
-  'gbp-create-project-template-icon.c',
-  'gbp-create-project-template-icon.h',
-  'gbp-create-project-tool.c',
-  'gbp-create-project-tool.h',
-  'gbp-create-project-widget.c',
-  'gbp-create-project-widget.h',
-]
-
-gnome_builder_plugins_sources += files(create_project_sources)
-gnome_builder_plugins_sources += create_project_resources[0]
-
-endif
+plugins_sources += plugin_create_project_resources[0]
diff --git a/src/plugins/ctags/ctags-plugin.c b/src/plugins/ctags/ctags-plugin.c
index d06b9edbc..af6b794fb 100644
--- a/src/plugins/ctags/ctags-plugin.c
+++ b/src/plugins/ctags/ctags-plugin.c
@@ -1,6 +1,6 @@
 /* ctags-plugin.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,28 +14,46 @@
  *
  * 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
  */
 
+#include "config.h"
+
 #include <libpeas/peas.h>
 #include <gtksourceview/gtksource.h>
+#include <libide-code.h>
+#include <libide-gui.h>
+#include <libide-sourceview.h>
+#include <libide-io.h>
 
+#include "gbp-ctags-workbench-addin.h"
 #include "ide-ctags-builder.h"
 #include "ide-ctags-completion-item.h"
 #include "ide-ctags-completion-provider.h"
 #include "ide-ctags-highlighter.h"
 #include "ide-ctags-index.h"
 #include "ide-ctags-preferences-addin.h"
-#include "ide-ctags-service.h"
 #include "ide-ctags-symbol-resolver.h"
 
-void
-ide_ctags_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_ide_ctags_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module, IDE_TYPE_COMPLETION_PROVIDER, 
IDE_TYPE_CTAGS_COMPLETION_PROVIDER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_HIGHLIGHTER, IDE_TYPE_CTAGS_HIGHLIGHTER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_SERVICE, IDE_TYPE_CTAGS_SERVICE);
-  peas_object_module_register_extension_type (module, IDE_TYPE_PREFERENCES_ADDIN, 
IDE_TYPE_CTAGS_PREFERENCES_ADDIN);
-  peas_object_module_register_extension_type (module, IDE_TYPE_SYMBOL_RESOLVER, 
IDE_TYPE_CTAGS_SYMBOL_RESOLVER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_COMPLETION_PROVIDER,
+                                              IDE_TYPE_CTAGS_COMPLETION_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_HIGHLIGHTER,
+                                              IDE_TYPE_CTAGS_HIGHLIGHTER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              IDE_TYPE_CTAGS_PREFERENCES_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_SYMBOL_RESOLVER,
+                                              IDE_TYPE_CTAGS_SYMBOL_RESOLVER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GBP_TYPE_CTAGS_WORKBENCH_ADDIN);
 
-  ide_vcs_register_ignored ("tags.??????");
+  ide_g_file_add_ignored_pattern ("tags.??????");
 }
diff --git a/src/plugins/ctags/ctags.gresource.xml b/src/plugins/ctags/ctags.gresource.xml
index 6f6c91d16..fa181ff16 100644
--- a/src/plugins/ctags/ctags.gresource.xml
+++ b/src/plugins/ctags/ctags.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/ctags">
     <file>ctags.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/ctags/ctags.plugin b/src/plugins/ctags/ctags.plugin
index 48543afba..1a29b388f 100644
--- a/src/plugins/ctags/ctags.plugin
+++ b/src/plugins/ctags/ctags.plugin
@@ -1,11 +1,12 @@
 [Plugin]
-Module=ctags-plugin
-Name=Ctags Auto-Completion
-Description=Provides integration with Ctags for auto-completion and symbol resolving
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
-Embedded=ide_ctags_register_types
+Copyright=Copyright © 2015-2018 Christian Hergert
+Depends=editor;
+Description=Provides integration with Ctags for auto-completion and symbol resolving
+Embedded=_ide_ctags_register_types
+Module=ctags
+Name=Ctags Auto-Completion
 X-Completion-Provider-Languages=c,cpp,chdr,cpphdr,python,python3,js,ruby
-X-Highlighter-Languages=c,cpp,chdr,python,python3,js,ruby
-X-Symbol-Resolver-Languages=c,cpp,chdr,python,python3,js,css,html,ruby
+X-Highlighter-Languages=c,cpp,chdr,cpphdr,python,python3,js,ruby
+X-Symbol-Resolver-Languages=c,cpp,chdr,cpphdr,python,python3,js,css,html,ruby
diff --git a/src/plugins/ctags/gbp-ctags-workbench-addin.c b/src/plugins/ctags/gbp-ctags-workbench-addin.c
new file mode 100644
index 000000000..7ab17d797
--- /dev/null
+++ b/src/plugins/ctags/gbp-ctags-workbench-addin.c
@@ -0,0 +1,183 @@
+/* gbp-ctags-workbench-addin.c
+ *
+ * Copyright 2018-2019 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-ctags-workbench-addin"
+
+#include "config.h"
+
+#include <libide-gui.h>
+
+#include "gbp-ctags-workbench-addin.h"
+#include "ide-ctags-service.h"
+
+struct _GbpCtagsWorkbenchAddin
+{
+  GObject       parent_instance;
+  IdeWorkbench *workbench;
+};
+
+static void
+gbp_ctags_workbench_addin_load_project_async (IdeWorkbenchAddin   *addin,
+                                              IdeProjectInfo      *project_info,
+                                              GCancellable        *cancellable,
+                                              GAsyncReadyCallback  callback,
+                                              gpointer             user_data)
+{
+  GbpCtagsWorkbenchAddin *self = (GbpCtagsWorkbenchAddin *)addin;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(IdeCtagsService) service = NULL;
+  IdeContext *context;
+
+  g_assert (GBP_IS_CTAGS_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (addin, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_ctags_workbench_addin_load_project_async);
+
+  /* We don't load the ctags service until a project is loaded so that
+   * we have a stable workdir to use.
+   */
+  context = ide_workbench_get_context (self->workbench);
+  service = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_CTAGS_SERVICE);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_ctags_workbench_addin_load_project_finish (IdeWorkbenchAddin  *addin,
+                                               GAsyncResult       *result,
+                                               GError            **error)
+{
+  g_assert (GBP_IS_CTAGS_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+gbp_ctags_workbench_addin_load (IdeWorkbenchAddin *addin,
+                                IdeWorkbench      *workbench)
+{
+  GbpCtagsWorkbenchAddin *self = (GbpCtagsWorkbenchAddin *)addin;
+
+  g_assert (GBP_IS_CTAGS_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+
+  self->workbench = workbench;
+}
+
+static void
+gbp_ctags_workbench_addin_unload (IdeWorkbenchAddin *addin,
+                                  IdeWorkbench      *workbench)
+{
+  GbpCtagsWorkbenchAddin *self = (GbpCtagsWorkbenchAddin *)addin;
+
+  g_assert (GBP_IS_CTAGS_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+
+  self->workbench = NULL;
+}
+
+static void
+pause_ctags_cb (GSimpleAction *action,
+                GVariant      *param,
+                gpointer       user_data)
+{
+  GbpCtagsWorkbenchAddin *self = user_data;
+  IdeContext *context;
+  IdeCtagsService *service;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_CTAGS_WORKBENCH_ADDIN (self));
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_BOOLEAN));
+
+  if ((context = ide_workbench_get_context (self->workbench)) &&
+      (service = ide_context_peek_child_typed (context, IDE_TYPE_CTAGS_SERVICE)))
+    {
+      gboolean val;
+
+      if (g_variant_get_boolean (param))
+        ide_ctags_service_pause (service);
+      else
+        ide_ctags_service_unpause (service);
+
+      /* Re-fetch the value incase we were out-of-sync */
+      g_object_get (service, "paused", &val, NULL);
+      g_simple_action_set_state (action, g_variant_new_boolean (val));
+    }
+}
+
+static const GActionEntry actions[] = {
+  { "pause-ctags", NULL, NULL, "false", pause_ctags_cb },
+};
+
+static void
+gbp_ctags_workbench_addin_workspace_added (IdeWorkbenchAddin *addin,
+                                           IdeWorkspace      *workspace)
+{
+  GbpCtagsWorkbenchAddin *self = (GbpCtagsWorkbenchAddin *)addin;
+
+  g_assert (GBP_IS_CTAGS_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (workspace),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+}
+
+static void
+gbp_ctags_workbench_addin_workspace_removed (IdeWorkbenchAddin *addin,
+                                             IdeWorkspace      *workspace)
+{
+  GbpCtagsWorkbenchAddin *self = (GbpCtagsWorkbenchAddin *)addin;
+
+  g_assert (GBP_IS_CTAGS_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
+    g_action_map_remove_action (G_ACTION_MAP (workspace), actions[i].name);
+}
+
+static void
+workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load = gbp_ctags_workbench_addin_load;
+  iface->unload = gbp_ctags_workbench_addin_unload;
+  iface->load_project_async = gbp_ctags_workbench_addin_load_project_async;
+  iface->load_project_finish = gbp_ctags_workbench_addin_load_project_finish;
+  iface->workspace_added = gbp_ctags_workbench_addin_workspace_added;
+  iface->workspace_removed = gbp_ctags_workbench_addin_workspace_removed;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpCtagsWorkbenchAddin, gbp_ctags_workbench_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKBENCH_ADDIN,
+                                                workbench_addin_iface_init))
+
+static void
+gbp_ctags_workbench_addin_class_init (GbpCtagsWorkbenchAddinClass *klass)
+{
+}
+
+static void
+gbp_ctags_workbench_addin_init (GbpCtagsWorkbenchAddin *self)
+{
+}
diff --git a/src/plugins/ctags/gbp-ctags-workbench-addin.h b/src/plugins/ctags/gbp-ctags-workbench-addin.h
new file mode 100644
index 000000000..f8918c6f9
--- /dev/null
+++ b/src/plugins/ctags/gbp-ctags-workbench-addin.h
@@ -0,0 +1,31 @@
+/* gbp-ctags-workbench-addin.h
+ *
+ * Copyright 2018-2019 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_CTAGS_WORKBENCH_ADDIN (gbp_ctags_workbench_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpCtagsWorkbenchAddin, gbp_ctags_workbench_addin, GBP, CTAGS_WORKBENCH_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/ctags/ide-ctags-builder.c b/src/plugins/ctags/ide-ctags-builder.c
index 35e8db713..63ae07f58 100644
--- a/src/plugins/ctags/ide-ctags-builder.c
+++ b/src/plugins/ctags/ide-ctags-builder.c
@@ -1,6 +1,6 @@
 /* ide-ctags-builder.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,10 +14,14 @@
  *
  * 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 "ide-ctags-builder"
 
+#include <libide-vcs.h>
+
 #include "ide-ctags-builder.h"
 
 struct _IdeCtagsBuilder
@@ -77,13 +81,9 @@ ide_ctags_builder_init (IdeCtagsBuilder *self)
 }
 
 IdeTagsBuilder *
-ide_ctags_builder_new (IdeContext *context)
+ide_ctags_builder_new (void)
 {
-  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
-
-  return g_object_new (IDE_TYPE_CTAGS_BUILDER,
-                       "context", context,
-                       NULL);
+  return g_object_new (IDE_TYPE_CTAGS_BUILDER, NULL);
 }
 
 static gboolean
@@ -106,9 +106,9 @@ ide_ctags_builder_build (IdeCtagsBuilder *self,
   g_autofree gchar *options_path = NULL;
   g_autofree gchar *tags_path = NULL;
   g_autoptr(GString) filenames = NULL;
+  g_autoptr(IdeVcs) vcs = NULL;
   GOutputStream *stdin_stream;
   IdeContext *context;
-  IdeVcs *vcs;
   gpointer infoptr;
 
   g_assert (IDE_IS_CTAGS_BUILDER (self));
@@ -116,7 +116,7 @@ ide_ctags_builder_build (IdeCtagsBuilder *self,
   g_assert (G_IS_FILE (destination));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
+  vcs = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_VCS);
 
   dest_dir = g_file_get_path (destination);
   if (0 != g_mkdir_with_parents (dest_dir, 0750))
@@ -303,15 +303,28 @@ ide_ctags_builder_build_async (IdeTagsBuilder      *builder,
   g_autoptr(GSettings) settings = NULL;
   g_autofree gchar *destination_path = NULL;
   g_autofree gchar *relative_path = NULL;
+  g_autoptr(GFile) workdir = NULL;
   BuildTaskData *task_data;
   IdeContext *context;
-  GFile *workdir;
 
   IDE_ENTRY;
 
   g_assert (IDE_IS_CTAGS_BUILDER (self));
   g_assert (G_IS_FILE (directory_or_file));
 
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_ctags_builder_build_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW + 200);
+
+  if (ide_object_in_destruction (IDE_OBJECT (self)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_CANCELLED,
+                                 "The operation was cancelled");
+      IDE_EXIT;
+    }
+
   settings = g_settings_new ("org.gnome.builder.code-insight");
 
   task_data = g_slice_new0 (BuildTaskData);
@@ -327,14 +340,11 @@ ide_ctags_builder_build_async (IdeTagsBuilder      *builder,
    * putting things in the source tree.
    */
   context = ide_object_get_context (IDE_OBJECT (self));
-  workdir = ide_vcs_get_working_directory (ide_context_get_vcs (context));
+  workdir = ide_context_ref_workdir (context);
   relative_path = g_file_get_relative_path (workdir, directory_or_file);
   destination_path = ide_context_cache_filename (context, "ctags", relative_path, NULL);
   task_data->destination = g_file_new_for_path (destination_path);
 
-  task = ide_task_new (self, cancellable, callback, user_data);
-  ide_task_set_source_tag (task, ide_ctags_builder_build_async);
-  ide_task_set_priority (task, G_PRIORITY_LOW + 200);
   ide_task_set_task_data (task, task_data, build_task_data_free);
   ide_task_set_kind (task, IDE_TASK_KIND_INDEXER);
   ide_task_run_in_thread (task, ide_ctags_builder_build_worker);
diff --git a/src/plugins/ctags/ide-ctags-builder.h b/src/plugins/ctags/ide-ctags-builder.h
index 9fefdd452..40390847d 100644
--- a/src/plugins/ctags/ide-ctags-builder.h
+++ b/src/plugins/ctags/ide-ctags-builder.h
@@ -1,6 +1,6 @@
 /* ide-ctags-builder.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,15 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
+
+#include "ide-tags-builder.h"
 
 G_BEGIN_DECLS
 
@@ -26,6 +30,6 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeCtagsBuilder, ide_ctags_builder, IDE, CTAGS_BUILDER, IdeObject)
 
-IdeTagsBuilder *ide_ctags_builder_new (IdeContext *context);
+IdeTagsBuilder *ide_ctags_builder_new (void);
 
 G_END_DECLS
diff --git a/src/plugins/ctags/ide-ctags-completion-item.c b/src/plugins/ctags/ide-ctags-completion-item.c
index 142464b24..20cd61e0c 100644
--- a/src/plugins/ctags/ide-ctags-completion-item.c
+++ b/src/plugins/ctags/ide-ctags-completion-item.c
@@ -1,6 +1,6 @@
 /* ide-ctags-completion-item.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-ctags-completion-item"
diff --git a/src/plugins/ctags/ide-ctags-completion-item.h b/src/plugins/ctags/ide-ctags-completion-item.h
index bbaa736cb..1894be5fc 100644
--- a/src/plugins/ctags/ide-ctags-completion-item.h
+++ b/src/plugins/ctags/ide-ctags-completion-item.h
@@ -1,6 +1,6 @@
 /* ide-ctags-completion-item.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,14 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
+#include <libide-sourceview.h>
 
 #include "ide-ctags-index.h"
 #include "ide-ctags-results.h"
diff --git a/src/plugins/ctags/ide-ctags-completion-provider-private.h 
b/src/plugins/ctags/ide-ctags-completion-provider-private.h
index 896048e42..a62e746b7 100644
--- a/src/plugins/ctags/ide-ctags-completion-provider-private.h
+++ b/src/plugins/ctags/ide-ctags-completion-provider-private.h
@@ -1,6 +1,6 @@
 /* ide-ctags-completion-provider-private.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,10 +14,14 @@
  *
  * 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-core.h>
+
 #include "ide-ctags-completion-provider.h"
 
 G_BEGIN_DECLS
diff --git a/src/plugins/ctags/ide-ctags-completion-provider.c 
b/src/plugins/ctags/ide-ctags-completion-provider.c
index 11de06eef..afc9a427a 100644
--- a/src/plugins/ctags/ide-ctags-completion-provider.c
+++ b/src/plugins/ctags/ide-ctags-completion-provider.c
@@ -1,6 +1,6 @@
 /* ide-ctags-completion-provider.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,13 +14,14 @@
  *
  * 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 "ide-ctags-completion-provider"
 
 #include <glib/gi18n.h>
-
-#include "sourceview/ide-source-iter.h"
+#include <libide-code.h>
 
 #include "ide-ctags-completion-item.h"
 #include "ide-ctags-completion-provider.h"
@@ -139,24 +140,24 @@ ide_ctags_completion_provider_load (IdeCompletionProvider *provider,
                                     IdeContext            *context)
 {
   IdeCtagsCompletionProvider *self = (IdeCtagsCompletionProvider *)provider;
-  IdeCtagsService *service;
+  g_autoptr(IdeCtagsService) service = NULL;
 
   g_assert (IDE_IS_CTAGS_COMPLETION_PROVIDER (self));
   g_assert (IDE_IS_CONTEXT (context));
 
-  service = ide_context_get_service_typed (context, IDE_TYPE_CTAGS_SERVICE);
-  ide_ctags_service_register_completion (service, self);
+  if ((service = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_CTAGS_SERVICE)))
+    ide_ctags_service_register_completion (service, self);
 }
 
 static void
 ide_ctags_completion_provider_dispose (GObject *object)
 {
   IdeCtagsCompletionProvider *self = (IdeCtagsCompletionProvider *)object;
+  g_autoptr(IdeCtagsService) service = NULL;
   IdeContext *context;
-  IdeCtagsService *service;
 
   if ((context = ide_object_get_context (IDE_OBJECT (self))) &&
-      (service = ide_context_get_service_typed (context, IDE_TYPE_CTAGS_SERVICE)))
+      (service = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_CTAGS_SERVICE)))
     ide_ctags_service_unregister_completion (service, self);
 
   G_OBJECT_CLASS (ide_ctags_completion_provider_parent_class)->dispose (object);
@@ -328,12 +329,11 @@ ide_ctags_completion_provider_activate_proposal (IdeCompletionProvider *provider
   IdeCtagsCompletionItem *item = (IdeCtagsCompletionItem *)proposal;
   g_autofree gchar *slice = NULL;
   g_autoptr(IdeSnippet) snippet = NULL;
-  IdeFileSettings *file_settings = NULL;
+  IdeFileSettings *file_settings;
   GtkTextBuffer *buffer;
   GtkTextView *view;
   GtkTextIter begin;
   GtkTextIter end;
-  IdeFile *file;
 
   g_assert (IDE_IS_CTAGS_COMPLETION_PROVIDER (provider));
   g_assert (IDE_IS_CTAGS_COMPLETION_ITEM (item));
@@ -347,10 +347,7 @@ ide_ctags_completion_provider_activate_proposal (IdeCompletionProvider *provider
   buffer = ide_completion_context_get_buffer (context);
   g_assert (IDE_IS_BUFFER (buffer));
 
-  file = ide_buffer_get_file (IDE_BUFFER (buffer));
-  g_assert (IDE_IS_FILE (file));
-
-  file_settings = ide_file_peek_settings (file);
+  file_settings = ide_buffer_get_file_settings (IDE_BUFFER (buffer));
   g_assert (!file_settings || IDE_IS_FILE_SETTINGS (file_settings));
 
   slice = gtk_text_iter_get_slice (&begin, &end);
diff --git a/src/plugins/ctags/ide-ctags-completion-provider.h 
b/src/plugins/ctags/ide-ctags-completion-provider.h
index e6ab830b1..47fd9d77e 100644
--- a/src/plugins/ctags/ide-ctags-completion-provider.h
+++ b/src/plugins/ctags/ide-ctags-completion-provider.h
@@ -1,6 +1,6 @@
 /* ide-ctags-completion-provider.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
 
 #include "ide-ctags-index.h"
 
diff --git a/src/plugins/ctags/ide-ctags-highlighter.c b/src/plugins/ctags/ide-ctags-highlighter.c
index acc21471d..1f63c97e4 100644
--- a/src/plugins/ctags/ide-ctags-highlighter.c
+++ b/src/plugins/ctags/ide-ctags-highlighter.c
@@ -14,12 +14,14 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-ctags-highlighter"
 
+#include "config.h"
+
 #include <glib/gi18n.h>
 
 #include "ide-ctags-highlighter.h"
@@ -105,25 +107,26 @@ get_tag_from_kind (IdeCtagsIndexEntryKind kind)
 
 static const gchar *
 get_tag (IdeCtagsHighlighter *self,
-         IdeFile             *file,
+         GFile               *file,
          const gchar         *word)
 {
-  const gchar *file_path = ide_file_get_path (file);
+  const gchar *file_path = g_file_peek_path (file);
   const IdeCtagsIndexEntry *entries;
   gsize n_entries;
-  gsize i;
-  gsize j;
 
-  for (i = 0; i < self->indexes->len; i++)
+  for (guint i = 0; i < self->indexes->len; i++)
     {
       IdeCtagsIndex *item = g_ptr_array_index (self->indexes, i);
+
       entries = ide_ctags_index_lookup_prefix (item, word, &n_entries);
       if ((entries == NULL) || (n_entries == 0))
         continue;
 
-      for (j = 0; j < n_entries; j++)
-        if (dzl_str_equal0 (entries[j].path, file_path))
-          return get_tag_from_kind (entries[j].kind);
+      for (guint j = 0; j < n_entries; j++)
+        {
+          if (ide_str_equal0 (entries[j].path, file_path))
+            return get_tag_from_kind (entries[j].kind);
+        }
 
       return get_tag_from_kind (entries[0].kind);
     }
@@ -141,7 +144,7 @@ ide_ctags_highlighter_real_update (IdeHighlighter       *highlighter,
   GtkTextBuffer *text_buffer;
   GtkSourceBuffer *source_buffer;
   IdeBuffer *buffer;
-  IdeFile *file;
+  GFile *file;
   GtkTextIter begin;
   GtkTextIter end;
 
@@ -241,20 +244,20 @@ ide_ctags_highlighter_real_set_engine (IdeHighlighter      *highlighter,
                                        IdeHighlightEngine  *engine)
 {
   IdeCtagsHighlighter *self = (IdeCtagsHighlighter *)highlighter;
+  g_autoptr(IdeCtagsService) service = NULL;
   IdeContext *context;
-  IdeCtagsService *service;
 
   g_return_if_fail (IDE_IS_CTAGS_HIGHLIGHTER (self));
   g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (engine));
 
   self->engine = engine;
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  service = ide_context_get_service_typed (context, IDE_TYPE_CTAGS_SERVICE);
-
-  g_set_weak_pointer (&self->service, service);
-
-  ide_ctags_service_register_highlighter (service, self);
+  if ((context = ide_object_get_context (IDE_OBJECT (self))) &&
+      (service = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_CTAGS_SERVICE)))
+    {
+      g_set_weak_pointer (&self->service, service);
+      ide_ctags_service_register_highlighter (service, self);
+    }
 }
 
 static void
diff --git a/src/plugins/ctags/ide-ctags-highlighter.h b/src/plugins/ctags/ide-ctags-highlighter.h
index 35e1ccb08..600051cd8 100644
--- a/src/plugins/ctags/ide-ctags-highlighter.h
+++ b/src/plugins/ctags/ide-ctags-highlighter.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
 
 #include "ide-ctags-index.h"
 
diff --git a/src/plugins/ctags/ide-ctags-index.c b/src/plugins/ctags/ide-ctags-index.c
index b7060abfb..da1bc7826 100644
--- a/src/plugins/ctags/ide-ctags-index.c
+++ b/src/plugins/ctags/ide-ctags-index.c
@@ -1,6 +1,6 @@
 /* ide-ctags-index.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,13 +14,14 @@
  *
  * 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 "ide-ctags-index"
 
 #include <dazzle.h>
 #include <glib/gi18n.h>
-#include <ide.h>
 #include <stdlib.h>
 #include <string.h>
 
@@ -293,7 +294,7 @@ ide_ctags_index_set_path_root (IdeCtagsIndex *self,
 {
   g_return_if_fail (IDE_IS_CTAGS_INDEX (self));
 
-  if (!dzl_str_equal0 (self->path_root, path_root))
+  if (!ide_str_equal0 (self->path_root, path_root))
     {
       g_free (self->path_root);
       self->path_root = g_strdup (path_root);
@@ -644,6 +645,8 @@ ide_ctags_index_get_mtime (IdeCtagsIndex *self)
  *
  * Returns: (transfer container) (element-type Ide.CtagsIndexEntry): An array
  *   of items matching the relative path.
+ *
+ * Since: 3.32
  */
 GPtrArray *
 ide_ctags_index_find_with_path (IdeCtagsIndex *self,
diff --git a/src/plugins/ctags/ide-ctags-index.h b/src/plugins/ctags/ide-ctags-index.h
index beabbd893..aabc79692 100644
--- a/src/plugins/ctags/ide-ctags-index.h
+++ b/src/plugins/ctags/ide-ctags-index.h
@@ -1,6 +1,6 @@
 /* ide-ctags-index.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,12 +14,14 @@
  *
  * 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>
-#include <ide.h>
+#include <libide-core.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
@@ -99,37 +101,37 @@ ide_ctags_index_entry_kind_to_symbol_kind (IdeCtagsIndexEntryKind kind)
     case IDE_CTAGS_INDEX_ENTRY_PROTOTYPE:
       /* bit of an impedenece mismatch */
     case IDE_CTAGS_INDEX_ENTRY_CLASS_NAME:
-      return IDE_SYMBOL_CLASS;
+      return IDE_SYMBOL_KIND_CLASS;
 
     case IDE_CTAGS_INDEX_ENTRY_ENUMERATOR:
-      return IDE_SYMBOL_ENUM;
+      return IDE_SYMBOL_KIND_ENUM;
 
     case IDE_CTAGS_INDEX_ENTRY_ENUMERATION_NAME:
-      return IDE_SYMBOL_ENUM_VALUE;
+      return IDE_SYMBOL_KIND_ENUM_VALUE;
 
     case IDE_CTAGS_INDEX_ENTRY_FUNCTION:
-      return IDE_SYMBOL_FUNCTION;
+      return IDE_SYMBOL_KIND_FUNCTION;
 
     case IDE_CTAGS_INDEX_ENTRY_MEMBER:
-      return IDE_SYMBOL_FIELD;
+      return IDE_SYMBOL_KIND_FIELD;
 
     case IDE_CTAGS_INDEX_ENTRY_STRUCTURE:
-      return IDE_SYMBOL_STRUCT;
+      return IDE_SYMBOL_KIND_STRUCT;
 
     case IDE_CTAGS_INDEX_ENTRY_UNION:
-      return IDE_SYMBOL_UNION;
+      return IDE_SYMBOL_KIND_UNION;
 
     case IDE_CTAGS_INDEX_ENTRY_VARIABLE:
-      return IDE_SYMBOL_VARIABLE;
+      return IDE_SYMBOL_KIND_VARIABLE;
 
     case IDE_CTAGS_INDEX_ENTRY_IMPORT:
-      return IDE_SYMBOL_PACKAGE;
+      return IDE_SYMBOL_KIND_PACKAGE;
 
     case IDE_CTAGS_INDEX_ENTRY_ANCHOR:
     case IDE_CTAGS_INDEX_ENTRY_DEFINE:
     case IDE_CTAGS_INDEX_ENTRY_FILE_NAME:
     default:
-      return IDE_SYMBOL_NONE;
+      return IDE_SYMBOL_KIND_NONE;
     }
 }
 
diff --git a/src/plugins/ctags/ide-ctags-preferences-addin.c b/src/plugins/ctags/ide-ctags-preferences-addin.c
index a28cecfd6..586aa55e8 100644
--- a/src/plugins/ctags/ide-ctags-preferences-addin.c
+++ b/src/plugins/ctags/ide-ctags-preferences-addin.c
@@ -1,6 +1,6 @@
 /* ide-ctags-preferences-addin.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,14 +14,16 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-ctags-preferences-addin"
 
+#include "config.h"
+
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-gui.h>
 
 #include "ide-ctags-preferences-addin.h"
 
diff --git a/src/plugins/ctags/ide-ctags-preferences-addin.h b/src/plugins/ctags/ide-ctags-preferences-addin.h
index 74c901c70..5516580bd 100644
--- a/src/plugins/ctags/ide-ctags-preferences-addin.h
+++ b/src/plugins/ctags/ide-ctags-preferences-addin.h
@@ -1,6 +1,6 @@
 /* ide-ctags-preferences-addin.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/ctags/ide-ctags-results.c b/src/plugins/ctags/ide-ctags-results.c
index b826ac12a..4900210ba 100644
--- a/src/plugins/ctags/ide-ctags-results.c
+++ b/src/plugins/ctags/ide-ctags-results.c
@@ -1,6 +1,6 @@
 /* ide-ctags-results.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-ctags-results"
diff --git a/src/plugins/ctags/ide-ctags-results.h b/src/plugins/ctags/ide-ctags-results.h
index 8fe044173..5336974d5 100644
--- a/src/plugins/ctags/ide-ctags-results.h
+++ b/src/plugins/ctags/ide-ctags-results.h
@@ -1,6 +1,6 @@
 /* ide-ctags-results.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-sourceview.h>
 
 #include "ide-ctags-index.h"
 
diff --git a/src/plugins/ctags/ide-ctags-service.c b/src/plugins/ctags/ide-ctags-service.c
index eda7d2cb8..dd080775e 100644
--- a/src/plugins/ctags/ide-ctags-service.c
+++ b/src/plugins/ctags/ide-ctags-service.c
@@ -1,6 +1,6 @@
 /* ide-ctags-service.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-ctags-service"
@@ -21,12 +23,15 @@
 #include <dazzle.h>
 #include <glib/gi18n.h>
 #include <gtksourceview/gtksource.h>
+#include <libide-code.h>
+#include <libide-vcs.h>
 
 #include "ide-ctags-builder.h"
 #include "ide-ctags-completion-provider.h"
 #include "ide-ctags-highlighter.h"
 #include "ide-ctags-index.h"
 #include "ide-ctags-service.h"
+#include "ide-tags-builder.h"
 
 #define QUEUED_BUILD_TIMEOUT_SECS 5
 
@@ -40,8 +45,12 @@ struct _IdeCtagsService
   GPtrArray        *completions;
   GHashTable       *build_timeout_by_dir;
 
+  IdeNotification  *notif;
+  gint              n_active;
+
   guint             queued_miner_handler;
   guint             miner_active : 1;
+  guint             paused : 1;
 };
 
 typedef struct
@@ -57,10 +66,15 @@ typedef struct
   gboolean         recursive;
 } QueuedRequest;
 
-static void service_iface_init (IdeServiceInterface *iface);
+G_DEFINE_TYPE (IdeCtagsService, ide_ctags_service, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_PAUSED,
+  N_PROPS
+};
 
-G_DEFINE_TYPE_WITH_CODE (IdeCtagsService, ide_ctags_service, IDE_TYPE_OBJECT,
-                         G_IMPLEMENT_INTERFACE (IDE_TYPE_SERVICE, service_iface_init))
+static GParamSpec *properties [N_PROPS];
 
 static void
 queued_request_free (gpointer data)
@@ -72,6 +86,75 @@ queued_request_free (gpointer data)
   g_slice_free (QueuedRequest, qr);
 }
 
+static gboolean
+is_supported_language (const gchar *lang_id)
+{
+  /* Some languages we expect ctags to actually parse when saved.
+   * Keep in sync with our .plugin file.
+   */
+  static const gchar *supported[] = {
+    "c", "cpp", "chdr", "cpphdr", "python", "python3",
+    "js", "css", "html", "ruby",
+    NULL
+  };
+
+  if (lang_id == NULL)
+    return FALSE;
+
+  return g_strv_contains (supported, lang_id);
+}
+
+static void
+show_notification (IdeCtagsService *self)
+{
+  g_autoptr(IdeObject) root = NULL;
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(IdeNotifications) notifs = NULL;
+  g_autoptr(GIcon) icon = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CTAGS_SERVICE (self));
+  g_assert (self->n_active >= 0);
+
+  self->n_active++;
+
+  if (self->n_active > 1)
+    return;
+
+  g_assert (self->notif == NULL);
+
+  root = ide_object_ref_root (IDE_OBJECT (self));
+  notifs = ide_object_get_child_typed (root, IDE_TYPE_NOTIFICATIONS);
+
+  notif = ide_notification_new ();
+  icon = g_icon_new_for_string ("media-playback-pause-symbolic", NULL);
+  ide_notification_set_title (notif, _("Indexing Source Code"));
+  ide_notification_set_body (notif, _("Search, autocompletion, and symbol information may be limited until 
Ctags indexing is complete."));
+  ide_notification_set_has_progress (notif, TRUE);
+  ide_notification_set_progress_is_imprecise (notif, TRUE);
+  ide_notification_add_button (notif, NULL, icon, "win.pause-ctags");
+  ide_notifications_add_notification (notifs, notif);
+
+  self->notif = g_steal_pointer (&notif);
+}
+
+static void
+hide_notification (IdeCtagsService *self)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CTAGS_SERVICE (self));
+  g_assert (self->notif != NULL);
+  g_assert (self->n_active > 0);
+
+  self->n_active--;
+
+  if (self->n_active == 0)
+    {
+      ide_notification_withdraw_in_seconds (self->notif, 3);
+      g_clear_object (&self->notif);
+    }
+}
+
 static void
 ide_ctags_service_build_index_init_cb (GObject      *object,
                                        GAsyncResult *result,
@@ -117,13 +200,11 @@ resolve_path_root (IdeCtagsService *self,
 {
   g_autoptr(GFile) parent = NULL;
   g_autoptr(GFile) cache_file = NULL;
+  g_autoptr(GFile) workdir = NULL;
   IdeContext *context;
-  IdeVcs *vcs;
-  GFile *workdir;
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  workdir = ide_context_ref_workdir (context);
   parent = g_file_get_parent (file);
 
   /*
@@ -381,8 +462,8 @@ ide_ctags_service_miner (GTask        *task,
                          GCancellable *cancellable)
 {
   IdeCtagsService *self = source_object;
+  g_autoptr(IdeVcs) vcs = NULL;
   IdeContext *context;
-  IdeVcs *vcs;
   GArray *mine_info = task_data;
 
   IDE_ENTRY;
@@ -392,7 +473,7 @@ ide_ctags_service_miner (GTask        *task,
   g_assert (mine_info != NULL);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
+  vcs = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_VCS);
 
   for (guint i = 0; i < mine_info->len; i++)
     {
@@ -421,9 +502,9 @@ ide_ctags_service_do_mine (gpointer data)
   IdeCtagsService *self = data;
   g_autoptr(GTask) task = NULL;
   g_autoptr(GArray) mine_info = NULL;
+  g_autoptr(GFile) workdir = NULL;
   IdeContext *context;
   MineInfo info;
-  GFile *workdir;
 
   IDE_ENTRY;
 
@@ -433,7 +514,7 @@ ide_ctags_service_do_mine (gpointer data)
   self->miner_active = TRUE;
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  workdir = ide_vcs_get_working_directory (ide_context_get_vcs (context));
+  workdir = ide_context_ref_workdir (context);
 
   mine_info = g_array_new (FALSE, FALSE, sizeof (MineInfo));
   g_array_set_clear_func (mine_info, clear_mine_info);
@@ -487,20 +568,21 @@ build_system_tags_cb (GObject      *object,
 {
   g_autoptr(IdeCtagsService) self = user_data;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_TAGS_BUILDER (object));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_CTAGS_SERVICE (self));
 
+  ide_object_destroy (IDE_OBJECT (object));
   ide_ctags_service_queue_mine (self);
+  hide_notification (self);
 }
 
 static gboolean
 restart_miner (gpointer user_data)
 {
-  QueuedRequest *qr = user_data;
   g_autoptr(IdeTagsBuilder) tags_builder = NULL;
-  IdeBuildSystem *build_system;
-  IdeContext *context;
+  QueuedRequest *qr = user_data;
 
   IDE_ENTRY;
 
@@ -508,20 +590,21 @@ restart_miner (gpointer user_data)
   g_assert (IDE_IS_CTAGS_SERVICE (qr->self));
   g_assert (G_IS_FILE (qr->directory));
 
+  /* Just skip for now if we got here somehow and paused */
+  if (qr->self->paused)
+    IDE_RETURN (G_SOURCE_CONTINUE);
+
   g_hash_table_remove (qr->self->build_timeout_by_dir, qr->directory);
 
-  context = ide_object_get_context (IDE_OBJECT (qr->self));
-  build_system = ide_context_get_build_system (context);
+  tags_builder = ide_ctags_builder_new ();
+  ide_object_append (IDE_OBJECT (qr->self), IDE_OBJECT (tags_builder));
 
-  if (IDE_IS_TAGS_BUILDER (build_system))
-    tags_builder = g_object_ref (IDE_TAGS_BUILDER (build_system));
-  else
-    tags_builder = ide_ctags_builder_new (context);
+  show_notification (qr->self);
 
   ide_tags_builder_build_async (tags_builder,
                                 qr->directory,
                                 qr->recursive,
-                                NULL,
+                                qr->self->cancellable,
                                 build_system_tags_cb,
                                 g_object_ref (qr->self));
 
@@ -536,13 +619,14 @@ ide_ctags_service_queue_build_for_directory (IdeCtagsService *self,
   g_assert (IDE_IS_CTAGS_SERVICE (self));
   g_assert (G_IS_FILE (directory));
 
-  if (ide_object_is_unloading (IDE_OBJECT (self)))
+  if (ide_object_in_destruction (IDE_OBJECT (self)))
     return;
 
   if (!g_hash_table_lookup (self->build_timeout_by_dir, directory))
     {
       QueuedRequest *qr;
-      guint source_id;
+      GSource *source;
+      guint source_id = 0;
 
       qr = g_slice_new (QueuedRequest);
       qr->self = g_object_ref (self);
@@ -555,6 +639,10 @@ ide_ctags_service_queue_build_for_directory (IdeCtagsService *self,
                                               g_steal_pointer (&qr),
                                               queued_request_free);
 
+      if (self->paused &&
+          (source = g_main_context_find_source_by_id (NULL, source_id)))
+        g_source_set_ready_time (source, -1);
+
       g_hash_table_insert (self->build_timeout_by_dir,
                            g_object_ref (directory),
                            GUINT_TO_POINTER (source_id));
@@ -567,10 +655,9 @@ ide_ctags_service_buffer_saved (IdeCtagsService  *self,
                                 IdeBufferManager *buffer_manager)
 {
   g_autoptr(GFile) parent = NULL;
+  g_autoptr(GFile) workdir = NULL;
   IdeContext *context;
-  IdeVcs *vcs;
   GFile *file;
-  GFile *workdir;
 
   IDE_ENTRY;
 
@@ -579,35 +666,38 @@ ide_ctags_service_buffer_saved (IdeCtagsService  *self,
   g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  workdir = ide_context_ref_workdir (context);
 
-  file = ide_file_get_file (ide_buffer_get_file (buffer));
+  file = ide_buffer_get_file (buffer);
   parent = g_file_get_parent (file);
 
-  if (g_file_has_prefix (file, workdir))
+  if (g_file_has_prefix (file, workdir) &&
+      is_supported_language (ide_buffer_get_language_id (buffer)))
     ide_ctags_service_queue_build_for_directory (self, parent, FALSE);
 
   IDE_EXIT;
 }
 
 static void
-ide_ctags_service_context_loaded (IdeService *service)
+ide_ctags_service_parent_set (IdeObject *object,
+                              IdeObject *parent)
 {
-  IdeBufferManager *buffer_manager;
-  IdeCtagsService *self = (IdeCtagsService *)service;
-  IdeContext *context;
-  GFile *workdir;
+  IdeCtagsService *self = (IdeCtagsService *)object;
+  g_autoptr(GFile) workdir = NULL;
+  IdeBufferManager *bufmgr;
 
   IDE_ENTRY;
 
   g_assert (IDE_IS_CTAGS_SERVICE (self));
+  g_assert (!parent || IDE_IS_CONTEXT (parent));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  buffer_manager = ide_context_get_buffer_manager (context);
-  workdir = ide_vcs_get_working_directory (ide_context_get_vcs (context));
+  if (parent == NULL)
+    IDE_EXIT;
 
-  g_signal_connect_object (buffer_manager,
+  bufmgr = ide_buffer_manager_from_context (IDE_CONTEXT (parent));
+  workdir = ide_context_ref_workdir (IDE_CONTEXT (parent));
+
+  g_signal_connect_object (bufmgr,
                            "buffer-saved",
                            G_CALLBACK (ide_ctags_service_buffer_saved),
                            self,
@@ -623,21 +713,16 @@ ide_ctags_service_context_loaded (IdeService *service)
 }
 
 static void
-ide_ctags_service_start (IdeService *service)
+ide_ctags_service_destroy (IdeObject *object)
 {
-}
-
-static void
-ide_ctags_service_stop (IdeService *service)
-{
-  IdeCtagsService *self = (IdeCtagsService *)service;
-
-  g_return_if_fail (IDE_IS_CTAGS_SERVICE (self));
+  IdeCtagsService *self = (IdeCtagsService *)object;
 
-  if (self->cancellable && !g_cancellable_is_cancelled (self->cancellable))
-    g_cancellable_cancel (self->cancellable);
+  g_assert (IDE_IS_CTAGS_SERVICE (self));
 
+  g_cancellable_cancel (self->cancellable);
   g_clear_object (&self->cancellable);
+
+  IDE_OBJECT_CLASS (ide_ctags_service_parent_class)->destroy (object);
 }
 
 static void
@@ -659,19 +744,71 @@ ide_ctags_service_finalize (GObject *object)
 }
 
 static void
-ide_ctags_service_class_init (IdeCtagsServiceClass *klass)
+ide_ctags_service_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeCtagsService *self = IDE_CTAGS_SERVICE (object);
 
-  object_class->finalize = ide_ctags_service_finalize;
+  switch (prop_id)
+    {
+    case PROP_PAUSED:
+      g_value_set_boolean (value, self->paused);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
 }
 
 static void
-service_iface_init (IdeServiceInterface *iface)
+ide_ctags_service_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
 {
-  iface->context_loaded = ide_ctags_service_context_loaded;
-  iface->start = ide_ctags_service_start;
-  iface->stop = ide_ctags_service_stop;
+  IdeCtagsService *self = IDE_CTAGS_SERVICE (object);
+
+  switch (prop_id)
+    {
+    case PROP_PAUSED:
+      if (g_value_get_boolean (value) != self->paused)
+        {
+          if (self->paused)
+            ide_ctags_service_unpause (self);
+          else
+            ide_ctags_service_pause (self);
+        }
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_ctags_service_class_init (IdeCtagsServiceClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->get_property = ide_ctags_service_get_property;
+  object_class->set_property = ide_ctags_service_set_property;
+
+  i_object_class->parent_set = ide_ctags_service_parent_set;
+  i_object_class->destroy = ide_ctags_service_destroy;
+
+  object_class->finalize = ide_ctags_service_finalize;
+
+  properties [PROP_PAUSED] =
+    g_param_spec_boolean ("paused",
+                          "Paused",
+                          "If the service is paused",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
 static void
@@ -706,6 +843,8 @@ ide_ctags_service_init (IdeCtagsService *self)
  * Note: this does not sort the indexes by importance.
  *
  * Returns: (transfer container) (element-type Ide.CtagsIndex): An array of indexes.
+ *
+ * Since: 3.32
  */
 GPtrArray *
 ide_ctags_service_get_indexes (IdeCtagsService *self)
@@ -776,3 +915,67 @@ ide_ctags_service_unregister_completion (IdeCtagsService            *self,
 
   g_ptr_array_remove (self->completions, completion);
 }
+
+void
+ide_ctags_service_pause (IdeCtagsService *self)
+{
+  GHashTableIter iter;
+  gpointer key;
+  gpointer value;
+
+  g_return_if_fail (IDE_IS_CTAGS_SERVICE (self));
+
+  if (self->paused)
+    return;
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+  self->cancellable = g_cancellable_new ();
+
+  self->paused = TRUE;
+
+  /* Make sure we show the pause state so the user can unpause */
+  show_notification (self);
+  ide_notification_set_title (self->notif, _("Indexing Source Code (Paused)"));
+
+  g_hash_table_iter_init (&iter, self->build_timeout_by_dir);
+
+  while (g_hash_table_iter_next (&iter, &key, &value))
+    {
+      GSource *source;
+
+      /* Make the source innactive until we are unpaused */
+      if ((source = g_main_context_find_source_by_id (NULL, GPOINTER_TO_UINT (value))))
+        g_source_set_ready_time (source, -1);
+    }
+}
+
+void
+ide_ctags_service_unpause (IdeCtagsService *self)
+{
+  GHashTableIter iter;
+  gpointer key;
+  gpointer value;
+
+  g_return_if_fail (IDE_IS_CTAGS_SERVICE (self));
+
+  if (!self->paused)
+    return;
+
+  self->paused = FALSE;
+
+  g_hash_table_iter_init (&iter, self->build_timeout_by_dir);
+
+  while (g_hash_table_iter_next (&iter, &key, &value))
+    {
+      GSource *source;
+
+      /* Make the source innactive until we are unpaused */
+      if ((source = g_main_context_find_source_by_id (NULL, GPOINTER_TO_UINT (value))))
+        g_source_set_ready_time (source, 0);
+    }
+
+  /* Now we can drop our paused state */
+  ide_notification_set_title (self->notif, _("Indexing Source Code"));
+  hide_notification (self);
+}
diff --git a/src/plugins/ctags/ide-ctags-service.h b/src/plugins/ctags/ide-ctags-service.h
index 6cfd2fbfb..cf273f442 100644
--- a/src/plugins/ctags/ide-ctags-service.h
+++ b/src/plugins/ctags/ide-ctags-service.h
@@ -1,6 +1,6 @@
 /* ide-ctags-service.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,12 +14,13 @@
  *
  * 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 <gtksourceview/gtksource.h>
-#include <ide.h>
+#include <libide-sourceview.h>
 
 #include "ide-ctags-completion-provider.h"
 #include "ide-ctags-highlighter.h"
@@ -30,15 +31,16 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeCtagsService, ide_ctags_service, IDE, CTAGS_SERVICE, IdeObject)
 
-void ide_ctags_service_register_highlighter   (IdeCtagsService            *self,
-                                               IdeCtagsHighlighter        *highlighter);
-void ide_ctags_service_unregister_highlighter (IdeCtagsService            *self,
-                                               IdeCtagsHighlighter        *highlighter);
-void ide_ctags_service_register_completion    (IdeCtagsService            *self,
-                                               IdeCtagsCompletionProvider *completion);
-void ide_ctags_service_unregister_completion  (IdeCtagsService            *self,
-                                               IdeCtagsCompletionProvider *completion);
-
-GPtrArray *ide_ctags_service_get_indexes (IdeCtagsService *self);
+void       ide_ctags_service_register_highlighter   (IdeCtagsService            *self,
+                                                     IdeCtagsHighlighter        *highlighter);
+void       ide_ctags_service_unregister_highlighter (IdeCtagsService            *self,
+                                                     IdeCtagsHighlighter        *highlighter);
+void       ide_ctags_service_register_completion    (IdeCtagsService            *self,
+                                                     IdeCtagsCompletionProvider *completion);
+void       ide_ctags_service_unregister_completion  (IdeCtagsService            *self,
+                                                     IdeCtagsCompletionProvider *completion);
+void       ide_ctags_service_pause                  (IdeCtagsService            *self);
+void       ide_ctags_service_unpause                (IdeCtagsService            *self);
+GPtrArray *ide_ctags_service_get_indexes            (IdeCtagsService            *self);
 
 G_END_DECLS
diff --git a/src/plugins/ctags/ide-ctags-symbol-node.c b/src/plugins/ctags/ide-ctags-symbol-node.c
index 6cb40a8fd..94a96f1f6 100644
--- a/src/plugins/ctags/ide-ctags-symbol-node.c
+++ b/src/plugins/ctags/ide-ctags-symbol-node.c
@@ -1,6 +1,6 @@
 /* ide-ctags-symbol-node.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-ctags-symbol-node"
@@ -37,7 +39,7 @@ ide_ctags_symbol_node_get_location_cb (GObject      *object,
                                        gpointer      user_data)
 {
   IdeCtagsSymbolResolver *resolver = (IdeCtagsSymbolResolver *)object;
-  g_autoptr(IdeSourceLocation) location = NULL;
+  g_autoptr(IdeLocation) location = NULL;
   g_autoptr(IdeTask) task = user_data;
   g_autoptr(GError) error = NULL;
 
@@ -52,7 +54,7 @@ ide_ctags_symbol_node_get_location_cb (GObject      *object,
   else
     ide_task_return_pointer (task,
                              g_steal_pointer (&location),
-                             (GDestroyNotify)ide_source_location_unref);
+                             (GDestroyNotify)g_object_unref);
 }
 
 static void
@@ -78,7 +80,7 @@ ide_ctags_symbol_node_get_location_async (IdeSymbolNode       *node,
                                                 g_steal_pointer (&task));
 }
 
-static IdeSourceLocation *
+static IdeLocation *
 ide_ctags_symbol_node_get_location_finish (IdeSymbolNode  *node,
                                            GAsyncResult   *result,
                                            GError        **error)
diff --git a/src/plugins/ctags/ide-ctags-symbol-node.h b/src/plugins/ctags/ide-ctags-symbol-node.h
index aaa91d838..daea34a55 100644
--- a/src/plugins/ctags/ide-ctags-symbol-node.h
+++ b/src/plugins/ctags/ide-ctags-symbol-node.h
@@ -1,6 +1,6 @@
 /* ide-ctags-symbol-node.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 #include "ide-ctags-index.h"
 #include "ide-ctags-symbol-resolver.h"
diff --git a/src/plugins/ctags/ide-ctags-symbol-resolver.c b/src/plugins/ctags/ide-ctags-symbol-resolver.c
index 3a415d814..17d53c3c4 100644
--- a/src/plugins/ctags/ide-ctags-symbol-resolver.c
+++ b/src/plugins/ctags/ide-ctags-symbol-resolver.c
@@ -1,6 +1,6 @@
 /* ide-ctags-symbol-resolver.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,13 +14,15 @@
  *
  * 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 "ide-ctags-symbol-resolver"
 
 #include <errno.h>
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-code.h>
 
 #include "ide-ctags-service.h"
 #include "ide-ctags-symbol-node.h"
@@ -72,18 +74,15 @@ create_symbol (IdeCtagsSymbolResolver   *self,
                gint                      line_offset,
                gint                      offset)
 {
-  g_autoptr(IdeSourceLocation) loc = NULL;
+  g_autoptr(IdeLocation) loc = NULL;
   g_autoptr(GFile) gfile = NULL;
-  g_autoptr(IdeFile) file = NULL;
-  IdeContext *context;
 
-  context = ide_object_get_context (IDE_OBJECT (self));
   gfile = g_file_new_for_path (entry->path);
-  file = ide_file_new (context, gfile);
-  loc = ide_source_location_new (file, line, line_offset, offset);
-
-  return ide_symbol_new (entry->name, ide_ctags_index_entry_kind_to_symbol_kind (entry->kind), 0, loc, loc, 
loc);
+  loc = ide_location_new (gfile, line, line_offset);
 
+  return ide_symbol_new (entry->name,
+                         ide_ctags_index_entry_kind_to_symbol_kind (entry->kind),
+                         0, loc, loc);
 }
 
 static gboolean
@@ -243,7 +242,7 @@ regex_worker (IdeTask      *task,
           symbol = create_symbol (self, lookup->entry, line, line_offset, begin);
           ide_task_return_pointer (task,
                                    g_steal_pointer (&symbol),
-                                   (GDestroyNotify)ide_symbol_unref);
+                                   (GDestroyNotify)g_object_unref);
 
           return;
         }
@@ -270,19 +269,18 @@ is_linenum (const gchar *pattern)
 
 static void
 ide_ctags_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolver,
-                                               IdeSourceLocation   *location,
+                                               IdeLocation   *location,
                                                GCancellable        *cancellable,
                                                GAsyncReadyCallback  callback,
                                                gpointer             user_data)
 {
   IdeCtagsSymbolResolver *self = (IdeCtagsSymbolResolver *)resolver;
+  g_autoptr(IdeCtagsService) service = NULL;
   IdeContext *context;
   IdeBufferManager *bufmgr;
-  IdeCtagsService *service;
   g_autofree gchar *keyword = NULL;
   g_autoptr(IdeTask) task = NULL;
   g_autoptr(GPtrArray) indexes = NULL;
-  IdeFile *ifile;
   const gchar * const *allowed;
   const gchar *lang_id = NULL;
   GtkSourceLanguage *language;
@@ -299,16 +297,23 @@ ide_ctags_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolver,
 
   task = ide_task_new (self, cancellable, callback, user_data);
 
-  ifile = ide_source_location_get_file (location);
-  file = ide_file_get_file (ifile);
-  line = ide_source_location_get_line (location);
-  line_offset = ide_source_location_get_line_offset (location);
+  file = ide_location_get_file (location);
+  line = ide_location_get_line (location);
+  line_offset = ide_location_get_line_offset (location);
+
+  if (!(context = ide_object_get_context (IDE_OBJECT (self))) ||
+      !(service = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_CTAGS_SERVICE)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Service is not loaded. Likely no project was loaded");
+      return;
+    }
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  service = ide_context_get_service_typed (context, IDE_TYPE_CTAGS_SERVICE);
   indexes = ide_ctags_service_get_indexes (service);
 
-  bufmgr = ide_context_get_buffer_manager (context);
+  bufmgr = ide_buffer_manager_from_context (context);
   buffer = ide_buffer_manager_find_buffer (bufmgr, file);
 
   if (!buffer)
@@ -397,7 +402,7 @@ ide_ctags_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolver,
               symbol = create_symbol (self, entry, parsed, 0, 0);
               ide_task_return_pointer (task,
                                        g_steal_pointer (&symbol),
-                                       (GDestroyNotify)ide_symbol_unref);
+                                       (GDestroyNotify)g_object_unref);
               return;
             }
         }
@@ -639,7 +644,7 @@ ide_ctags_symbol_resolver_get_symbol_tree_worker (IdeTask      *task,
 static void
 ide_ctags_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *resolver,
                                                  GFile               *file,
-                                                 IdeBuffer           *buffer,
+                                                 GBytes              *contents,
                                                  GCancellable        *cancellable,
                                                  GAsyncReadyCallback  callback,
                                                  gpointer             user_data)
@@ -648,7 +653,7 @@ ide_ctags_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *resolver,
   TreeResolverState *state;
   g_autoptr(IdeTask) task = NULL;
   g_autoptr(GPtrArray) indexes = NULL;
-  IdeCtagsService *service;
+  g_autoptr(IdeCtagsService) service = NULL;
   IdeContext *context;
 
   IDE_ENTRY;
@@ -660,8 +665,16 @@ ide_ctags_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *resolver,
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_ctags_symbol_resolver_get_symbol_tree_async);
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  service = ide_context_get_service_typed (context, IDE_TYPE_CTAGS_SERVICE);
+  if (!(context = ide_object_get_context (IDE_OBJECT (self))) ||
+      !(service = ide_object_get_child_typed (IDE_OBJECT (context), IDE_TYPE_CTAGS_SERVICE)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "No ctags service is loaded, likely no project was loaded");
+      return;
+    }
+
   indexes = ide_ctags_service_get_indexes (service);
 
   if (indexes == NULL || indexes->len == 0)
@@ -754,7 +767,7 @@ ide_ctags_symbol_resolver_get_location_async (IdeCtagsSymbolResolver   *self,
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  bufmgr = ide_context_get_buffer_manager (context);
+  bufmgr = ide_buffer_manager_from_context (context);
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_ctags_symbol_resolver_get_location_async);
@@ -772,7 +785,7 @@ ide_ctags_symbol_resolver_get_location_async (IdeCtagsSymbolResolver   *self,
       symbol = create_symbol (self, entry, parsed, 0, 0);
       ide_task_return_pointer (task,
                                g_steal_pointer (&symbol),
-                               (GDestroyNotify)ide_symbol_unref);
+                               (GDestroyNotify)g_object_unref);
 
       IDE_EXIT;
     }
@@ -816,13 +829,13 @@ not_a_number:
   IDE_EXIT;
 }
 
-IdeSourceLocation *
+IdeLocation *
 ide_ctags_symbol_resolver_get_location_finish (IdeCtagsSymbolResolver  *self,
                                                GAsyncResult            *result,
                                                GError                 **error)
 {
   g_autoptr(IdeSymbol) symbol = NULL;
-  IdeSourceLocation *ret = NULL;
+  IdeLocation *ret = NULL;
 
   g_return_val_if_fail (IDE_IS_CTAGS_SYMBOL_RESOLVER (self), NULL);
   g_return_val_if_fail (IDE_IS_TASK (result), NULL);
@@ -831,8 +844,8 @@ ide_ctags_symbol_resolver_get_location_finish (IdeCtagsSymbolResolver  *self,
 
   if (symbol != NULL)
     {
-      if ((ret = ide_symbol_get_declaration_location (symbol)))
-        ide_source_location_ref (ret);
+      if ((ret = ide_symbol_get_location (symbol)))
+        g_object_ref (ret);
       else
         g_set_error (error,
                      G_IO_ERROR,
diff --git a/src/plugins/ctags/ide-ctags-symbol-resolver.h b/src/plugins/ctags/ide-ctags-symbol-resolver.h
index 1d494d094..897a0f40a 100644
--- a/src/plugins/ctags/ide-ctags-symbol-resolver.h
+++ b/src/plugins/ctags/ide-ctags-symbol-resolver.h
@@ -1,6 +1,6 @@
 /* ide-ctags-symbol-resolver.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,14 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
@@ -32,7 +35,7 @@ void               ide_ctags_symbol_resolver_get_location_async  (IdeCtagsSymbol
                                                                   GCancellable             *cancellable,
                                                                   GAsyncReadyCallback       callback,
                                                                   gpointer                  user_data);
-IdeSourceLocation *ide_ctags_symbol_resolver_get_location_finish (IdeCtagsSymbolResolver   *self,
+IdeLocation *ide_ctags_symbol_resolver_get_location_finish (IdeCtagsSymbolResolver   *self,
                                                                   GAsyncResult             *result,
                                                                   GError                  **error);
 
diff --git a/src/plugins/ctags/ide-ctags-symbol-tree.c b/src/plugins/ctags/ide-ctags-symbol-tree.c
index e50bacaac..4c6aae7d1 100644
--- a/src/plugins/ctags/ide-ctags-symbol-tree.c
+++ b/src/plugins/ctags/ide-ctags-symbol-tree.c
@@ -1,6 +1,6 @@
 /* ide-ctags-symbol-tree.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-ctags-symbol-tree"
@@ -69,8 +71,8 @@ symbol_tree_iface_init (IdeSymbolTreeInterface *iface)
   iface->get_nth_child = ide_ctags_symbol_tree_get_nth_child;
 }
 
-G_DEFINE_TYPE_EXTENDED (IdeCtagsSymbolTree, ide_ctags_symbol_tree, G_TYPE_OBJECT, 0,
-                        G_IMPLEMENT_INTERFACE (IDE_TYPE_SYMBOL_TREE, symbol_tree_iface_init))
+G_DEFINE_TYPE_WITH_CODE (IdeCtagsSymbolTree, ide_ctags_symbol_tree, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_SYMBOL_TREE, symbol_tree_iface_init))
 
 /**
  * ide_ctags_symbol_tree_new:
@@ -78,6 +80,8 @@ G_DEFINE_TYPE_EXTENDED (IdeCtagsSymbolTree, ide_ctags_symbol_tree, G_TYPE_OBJECT
  *
  * This function takes ownership of @ar.
  *
+ *
+ * Since: 3.32
  */
 IdeCtagsSymbolTree *
 ide_ctags_symbol_tree_new (GPtrArray *ar)
diff --git a/src/plugins/ctags/ide-ctags-symbol-tree.h b/src/plugins/ctags/ide-ctags-symbol-tree.h
index a0fe4dfe3..7d0944d51 100644
--- a/src/plugins/ctags/ide-ctags-symbol-tree.h
+++ b/src/plugins/ctags/ide-ctags-symbol-tree.h
@@ -1,6 +1,6 @@
 /* ide-ctags-symbol-tree.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/ctags/ide-ctags-util.c b/src/plugins/ctags/ide-ctags-util.c
index 6987ac41e..1c74a9195 100644
--- a/src/plugins/ctags/ide-ctags-util.c
+++ b/src/plugins/ctags/ide-ctags-util.c
@@ -1,6 +1,6 @@
 /* ide-ctags-util.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <string.h>
@@ -33,17 +35,17 @@ ide_ctags_get_allowed_suffixes (const gchar *lang_id)
   if (lang_id == NULL)
     return NULL;
 
-  if (dzl_str_equal0 (lang_id, "c") || dzl_str_equal0 (lang_id, "chdr") || dzl_str_equal0 (lang_id, "cpp"))
+  if (ide_str_equal0 (lang_id, "c") || ide_str_equal0 (lang_id, "chdr") || ide_str_equal0 (lang_id, "cpp"))
     return c_languages;
-  else if (dzl_str_equal0 (lang_id, "vala"))
+  else if (ide_str_equal0 (lang_id, "vala"))
     return vala_languages;
-  else if (dzl_str_equal0 (lang_id, "python"))
+  else if (ide_str_equal0 (lang_id, "python"))
     return python_languages;
-  else if (dzl_str_equal0 (lang_id, "js"))
+  else if (ide_str_equal0 (lang_id, "js"))
     return js_languages;
-  else if (dzl_str_equal0 (lang_id, "html"))
+  else if (ide_str_equal0 (lang_id, "html"))
     return html_languages;
-  else if (dzl_str_equal0 (lang_id, "ruby"))
+  else if (ide_str_equal0 (lang_id, "ruby"))
     return ruby_languages;
   else
     return NULL;
@@ -59,7 +61,7 @@ ide_ctags_is_allowed (const IdeCtagsIndexEntry *entry,
       gsize i;
 
       for (i = 0; allowed [i]; i++)
-        if (dzl_str_equal0 (dotptr, allowed [i]))
+        if (ide_str_equal0 (dotptr, allowed [i]))
           return TRUE;
     }
 
diff --git a/src/plugins/ctags/ide-ctags-util.h b/src/plugins/ctags/ide-ctags-util.h
index 5ef2d85bf..0b30b484b 100644
--- a/src/plugins/ctags/ide-ctags-util.h
+++ b/src/plugins/ctags/ide-ctags-util.h
@@ -1,6 +1,6 @@
 /* ide-ctags-util.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/ctags/ide-tags-builder.c b/src/plugins/ctags/ide-tags-builder.c
new file mode 100644
index 000000000..e16b8b881
--- /dev/null
+++ b/src/plugins/ctags/ide-tags-builder.c
@@ -0,0 +1,58 @@
+/* ide-tags-builder.c
+ *
+ * Copyright 2015-2019 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 "ide-tags-builder"
+
+#include "config.h"
+
+#include "ide-tags-builder.h"
+
+G_DEFINE_INTERFACE (IdeTagsBuilder, ide_tags_builder, G_TYPE_OBJECT)
+
+
+void
+ide_tags_builder_build_async (IdeTagsBuilder      *self,
+                              GFile               *directory_or_file,
+                              gboolean             recursive,
+                              GCancellable        *cancellable,
+                              GAsyncReadyCallback  callback,
+                              gpointer             user_data)
+{
+  g_return_if_fail (IDE_IS_TAGS_BUILDER (self));
+  g_return_if_fail (!directory_or_file || G_IS_FILE (directory_or_file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  IDE_TAGS_BUILDER_GET_IFACE (self)->build_async (self, directory_or_file, recursive, cancellable, callback, 
user_data);
+}
+
+gboolean
+ide_tags_builder_build_finish (IdeTagsBuilder  *self,
+                               GAsyncResult    *result,
+                               GError         **error)
+{
+  g_return_val_if_fail (IDE_IS_TAGS_BUILDER (self), FALSE);
+
+  return IDE_TAGS_BUILDER_GET_IFACE (self)->build_finish (self, result, error);
+}
+
+static void
+ide_tags_builder_default_init (IdeTagsBuilderInterface *iface)
+{
+}
diff --git a/src/plugins/ctags/ide-tags-builder.h b/src/plugins/ctags/ide-tags-builder.h
new file mode 100644
index 000000000..ffe390fec
--- /dev/null
+++ b/src/plugins/ctags/ide-tags-builder.h
@@ -0,0 +1,56 @@
+/* ide-tags-builder.h
+ *
+ * Copyright 2015-2019 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 IDE_TYPE_TAGS_BUILDER (ide_tags_builder_get_type ())
+
+G_DECLARE_INTERFACE (IdeTagsBuilder, ide_tags_builder, IDE, TAGS_BUILDER, GObject)
+
+struct _IdeTagsBuilderInterface
+{
+  GTypeInterface parent;
+
+  void     (*build_async)  (IdeTagsBuilder       *self,
+                            GFile                *directory_or_file,
+                            gboolean              recursive,
+                            GCancellable         *cancellable,
+                            GAsyncReadyCallback   callback,
+                            gpointer              user_data);
+  gboolean (*build_finish) (IdeTagsBuilder       *self,
+                            GAsyncResult         *result,
+                            GError              **error);
+};
+
+void      ide_tags_builder_build_async  (IdeTagsBuilder       *self,
+                                         GFile                *directory_or_file,
+                                         gboolean              recursive,
+                                         GCancellable         *cancellable,
+                                         GAsyncReadyCallback   callback,
+                                         gpointer              user_data);
+gboolean  ide_tags_builder_build_finish (IdeTagsBuilder       *self,
+                                         GAsyncResult         *result,
+                                         GError              **error);
+
+G_END_DECLS
diff --git a/src/plugins/ctags/meson.build b/src/plugins/ctags/meson.build
index 91c5db33e..acd7fa79a 100644
--- a/src/plugins/ctags/meson.build
+++ b/src/plugins/ctags/meson.build
@@ -1,28 +1,36 @@
-if get_option('with_ctags')
+if get_option('plugin_ctags')
 
-ctags_resources = gnome.compile_resources(
-  'ctags-resources',
-  'ctags.gresource.xml',
-  c_name: 'ide_ctags',
-)
-
-ctags_sources = [
+plugins_sources += files([
+  'ctags-plugin.c',
   'ide-ctags-builder.c',
   'ide-ctags-completion-item.c',
   'ide-ctags-completion-provider.c',
   'ide-ctags-highlighter.c',
   'ide-ctags-index.c',
   'ide-ctags-preferences-addin.c',
-  'ide-ctags-service.c',
   'ide-ctags-results.c',
+  'ide-ctags-service.c',
   'ide-ctags-symbol-node.c',
   'ide-ctags-symbol-resolver.c',
   'ide-ctags-symbol-tree.c',
   'ide-ctags-util.c',
-  'ctags-plugin.c',
-]
+  'ide-tags-builder.c',
+  'gbp-ctags-workbench-addin.c',
+])
+
+plugin_ctags_resources = gnome.compile_resources(
+  'gbp-ctags-resources',
+  'ctags.gresource.xml',
+  c_name: 'gbp_ctags',
+)
+
+plugins_sources += plugin_ctags_resources[0]
 
-gnome_builder_plugins_sources += files(ctags_sources)
-gnome_builder_plugins_sources += ctags_resources[0]
+test_ctags = executable('test-ctags',
+  'test-ctags.c', 'ide-ctags-index.c',
+        c_args: test_cflags,
+  dependencies: [ libide_projects_dep ],
+)
+test('test-ctags', test_ctags, env: test_env)
 
 endif
diff --git a/src/plugins/ctags/test-ctags.c b/src/plugins/ctags/test-ctags.c
new file mode 100644
index 000000000..e3673e790
--- /dev/null
+++ b/src/plugins/ctags/test-ctags.c
@@ -0,0 +1,110 @@
+/* test-ctags.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+ */
+
+#include <gio/gio.h>
+
+#include "ide-ctags-index.h"
+
+static GMainLoop *main_loop;
+
+static void
+init_cb (GObject      *object,
+         GAsyncResult *result,
+         gpointer      user_data)
+{
+  GAsyncInitable *initable = (GAsyncInitable *)object;
+  IdeCtagsIndex *index = (IdeCtagsIndex *)object;
+  const IdeCtagsIndexEntry *entries;
+  gsize n_entries = 0xFFFFFFFF;
+  GError *error = NULL;
+  gboolean ret;
+  gsize i;
+
+  g_assert (G_IS_ASYNC_INITABLE (initable));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  ret = g_async_initable_init_finish (initable, result, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+  g_assert (index != NULL);
+  g_assert (IDE_IS_CTAGS_INDEX (index));
+
+  g_assert_cmpint (28, ==, ide_ctags_index_get_size (index));
+
+  entries = ide_ctags_index_lookup (index, "__NOTHING_SHOULD_MATCH_THIS__", &n_entries);
+  g_assert_cmpint (n_entries, ==, 0);
+  g_assert (entries == NULL);
+
+  entries = ide_ctags_index_lookup (index, "G_LOG_DOMAIN", &n_entries);
+  g_assert_cmpint (n_entries, ==, 1);
+  g_assert (entries != NULL);
+  for (i = 0; i < 1; i++)
+    g_assert_cmpstr (entries [i].name, ==, "G_LOG_DOMAIN");
+
+  entries = ide_ctags_index_lookup (index, "bug_buddy_init", &n_entries);
+  g_assert_cmpint (n_entries, ==, 2);
+  g_assert (entries != NULL);
+  for (i = 0; i < 1; i++)
+    g_assert_cmpstr (entries [i].name, ==, "bug_buddy_init");
+
+  entries = ide_ctags_index_lookup_prefix (index, "G_DEFINE_", &n_entries);
+  g_assert_cmpint (n_entries, ==, 16);
+  g_assert (entries != NULL);
+  for (i = 0; i < 16; i++)
+    g_assert (g_str_has_prefix (entries [i].name, "G_DEFINE_"));
+
+  g_main_loop_quit (main_loop);
+}
+
+static void
+test_ctags_basic (void)
+{
+  IdeCtagsIndex *index;
+  GFile *test_file;
+  gchar *path;
+
+  main_loop = g_main_loop_new (NULL, FALSE);
+
+  path = g_build_filename (TEST_DATA_DIR, "../../plugins/ctags", "test-tags", NULL);
+  test_file = g_file_new_for_path (path);
+
+  index = ide_ctags_index_new (test_file, NULL, 0);
+
+  g_async_initable_init_async (G_ASYNC_INITABLE (index),
+                               G_PRIORITY_DEFAULT,
+                               NULL,
+                               init_cb,
+                               NULL);
+
+  g_main_loop_run (main_loop);
+
+  g_object_unref (index);
+  g_free (path);
+  g_object_unref (test_file);
+}
+
+gint
+main (gint   argc,
+      gchar *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  g_test_add_func ("/Ide/CTags/basic", test_ctags_basic);
+  return g_test_run ();
+}
diff --git a/src/plugins/ctags/test-tags b/src/plugins/ctags/test-tags
new file mode 100644
index 000000000..9dd63f71e
--- /dev/null
+++ b/src/plugins/ctags/test-tags
@@ -0,0 +1,28 @@
+G_DEFINE_CONSTRUCTOR   gconstructor.h  25;"    d
+G_DEFINE_CONSTRUCTOR   gconstructor.h  33;"    d
+G_DEFINE_CONSTRUCTOR   gconstructor.h  55;"    d
+G_DEFINE_CONSTRUCTOR   gconstructor.h  80;"    d
+G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA      gconstructor.h  50;"    d
+G_DEFINE_CONSTRUCTOR_NEEDS_PRAGMA      gconstructor.h  75;"    d
+G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS       gconstructor.h  53;"    d
+G_DEFINE_CONSTRUCTOR_PRAGMA_ARGS       gconstructor.h  78;"    d
+G_DEFINE_DESTRUCTOR    gconstructor.h  26;"    d
+G_DEFINE_DESTRUCTOR    gconstructor.h  39;"    d
+G_DEFINE_DESTRUCTOR    gconstructor.h  62;"    d
+G_DEFINE_DESTRUCTOR    gconstructor.h  85;"    d
+G_DEFINE_DESTRUCTOR_NEEDS_PRAGMA       gconstructor.h  51;"    d
+G_DEFINE_DESTRUCTOR_NEEDS_PRAGMA       gconstructor.h  76;"    d
+G_DEFINE_DESTRUCTOR_PRAGMA_ARGS        gconstructor.h  60;"    d
+G_DEFINE_DESTRUCTOR_PRAGMA_ARGS        gconstructor.h  83;"    d
+G_HAS_CONSTRUCTORS     gconstructor.h  23;"    d
+G_HAS_CONSTRUCTORS     gconstructor.h  31;"    d
+G_HAS_CONSTRUCTORS     gconstructor.h  47;"    d
+G_HAS_CONSTRUCTORS     gconstructor.h  73;"    d
+G_LOG_DOMAIN   main.c  21;"    d       file:
+bug_buddy_init bug-buddy.c     /^bug_buddy_init (void)$/;"     f
+bug_buddy_init bug-buddy.h     /^void bug_buddy_init (void);$/;"       p
+bug_buddy_sigsegv_handler      bug-buddy.c     /^bug_buddy_sigsegv_handler (int signum)$/;"    f       file:
+early_params_check     main.c  /^early_params_check (gint       *argc,$/;"     f       file:
+gdb_argv       bug-buddy.c     /^static gchar **gdb_argv = NULL;$/;"   v       file:
+main   main.c  /^main (gint   argc,$/;"        f
+verbose_cb     main.c  /^verbose_cb (const gchar  *option_name,$/;"    f       file:
diff --git a/src/plugins/debuggerui/debuggerui-plugin.c b/src/plugins/debuggerui/debuggerui-plugin.c
new file mode 100644
index 000000000..e240b614c
--- /dev/null
+++ b/src/plugins/debuggerui/debuggerui-plugin.c
@@ -0,0 +1,42 @@
+/* debuggerui-plugin.c
+ *
+ * Copyright 2015-2019 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 "debuggerui-plugin"
+
+#include "config.h"
+
+#include <libide-debugger.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <libpeas/peas.h>
+
+#include "ide-debugger-editor-addin.h"
+#include "ide-debugger-hover-provider.h"
+
+void
+_gbp_debuggerui_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_ADDIN,
+                                              IDE_TYPE_DEBUGGER_EDITOR_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_HOVER_PROVIDER,
+                                              IDE_TYPE_DEBUGGER_HOVER_PROVIDER);
+}
diff --git a/src/plugins/debuggerui/debuggerui.gresource.xml b/src/plugins/debuggerui/debuggerui.gresource.xml
new file mode 100644
index 000000000..609951f2c
--- /dev/null
+++ b/src/plugins/debuggerui/debuggerui.gresource.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/debuggerui">
+    <file>debuggerui.plugin</file>
+    <file>gtk/menus.ui</file>
+    <file preprocess="xml-stripblanks">ide-debugger-breakpoints-view.ui</file>
+    <file preprocess="xml-stripblanks">ide-debugger-controls.ui</file>
+    <file preprocess="xml-stripblanks">ide-debugger-disassembly-view.ui</file>
+    <file preprocess="xml-stripblanks">ide-debugger-hover-controls.ui</file>
+    <file preprocess="xml-stripblanks">ide-debugger-libraries-view.ui</file>
+    <file preprocess="xml-stripblanks">ide-debugger-locals-view.ui</file>
+    <file preprocess="xml-stripblanks">ide-debugger-registers-view.ui</file>
+    <file preprocess="xml-stripblanks">ide-debugger-threads-view.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/debuggerui/debuggerui.plugin b/src/plugins/debuggerui/debuggerui.plugin
new file mode 100644
index 000000000..1e51808ad
--- /dev/null
+++ b/src/plugins/debuggerui/debuggerui.plugin
@@ -0,0 +1,11 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Depends=editor;buildui;
+Description=Builder's visual debugger
+Embedded=_gbp_debuggerui_register_types
+Hidden=true
+Module=debuggerui
+Name=Debugger
+X-Workspace-Kind=primary;
diff --git a/src/plugins/debuggerui/gtk/menus.ui b/src/plugins/debuggerui/gtk/menus.ui
new file mode 100644
index 000000000..bde153012
--- /dev/null
+++ b/src/plugins/debuggerui/gtk/menus.ui
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<interface>
+  <menu id="run-menu">
+    <section id="run-menu-section">
+      <item>
+        <attribute name="id">debugger-run-handler</attribute>
+        <attribute name="after">default-run-handler</attribute>
+        <attribute name="action">run-manager.run-with-handler</attribute>
+        <attribute name="target">debugger</attribute>
+        <attribute name="label" translatable="yes">Run with Debugger</attribute>
+        <attribute name="verb-icon-name">builder-debugger-symbolic</attribute>
+        <attribute name="accel">F5</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="project-tree-run-with-submenu">
+    <section id="project-tree-menu-run-with-section">
+      <item>
+        <attribute name="id">project-tree-menu-debug</attribute>
+        <attribute name="label" translatable="yes">Run with Debugger</attribute>
+        <attribute name="action">buildui.run-with-handler</attribute>
+        <attribute name="target" type="s">'debugger'</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/plugins/debuggerui/ide-debugger-breakpoints-view.c 
b/src/plugins/debuggerui/ide-debugger-breakpoints-view.c
new file mode 100644
index 000000000..2a8da522e
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-breakpoints-view.c
@@ -0,0 +1,608 @@
+/* ide-debugger-breakpoints-view.c
+ *
+ * Copyright 2017-2019 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 "ide-debugger-breakpoints-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-debugger-breakpoints-view.h"
+
+struct _IdeDebuggerBreakpointsView
+{
+  GtkBin                 parent_instance;
+
+  /* Owned references */
+  DzlSignalGroup        *debugger_signals;
+
+  /* Template references */
+  GtkCellRendererText   *address_cell;
+  GtkCellRendererText   *file_cell;
+  GtkCellRendererText   *function_cell;
+  GtkCellRendererText   *hits_cell;
+  GtkCellRendererText   *id_cell;
+  GtkCellRendererText   *line_cell;
+  GtkCellRendererText   *spec_cell;
+  GtkCellRendererText   *type_cell;
+  GtkCellRendererToggle *enabled_cell;
+  GtkListStore          *list_store;
+  GtkTreeView           *tree_view;
+  GtkTreeViewColumn     *address_column;
+  GtkTreeViewColumn     *enabled_column;
+  GtkTreeViewColumn     *file_column;
+  GtkTreeViewColumn     *function_column;
+  GtkTreeViewColumn     *hits_column;
+  GtkTreeViewColumn     *id_column;
+  GtkTreeViewColumn     *line_column;
+  GtkTreeViewColumn     *spec_column;
+  GtkTreeViewColumn     *type_column;
+};
+
+enum {
+  PROP_0,
+  PROP_DEBUGGER,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeDebuggerBreakpointsView, ide_debugger_breakpoints_view, GTK_TYPE_BIN)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_debugger_breakpoints_view_bind (IdeDebuggerBreakpointsView *self,
+                                    IdeDebugger                *debugger,
+                                    DzlSignalGroup             *debugger_signals)
+{
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINTS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+  g_assert (DZL_IS_SIGNAL_GROUP (debugger_signals));
+
+  gtk_list_store_clear (self->list_store);
+}
+
+static void
+ide_debugger_breakpoints_view_running (IdeDebuggerBreakpointsView *self,
+                                       IdeDebugger                *debugger)
+{
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINTS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), FALSE);
+}
+
+static void
+ide_debugger_breakpoints_view_stopped (IdeDebuggerBreakpointsView *self,
+                                       IdeDebuggerStopReason       stop_reason,
+                                       IdeDebuggerBreakpoint      *breakpoint,
+                                       IdeDebugger                *debugger)
+{
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINTS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_STOP_REASON (stop_reason));
+  g_assert (!breakpoint || IDE_IS_DEBUGGER_BREAKPOINT (breakpoint));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), TRUE);
+}
+
+static void
+ide_debugger_breakpoints_view_breakpoint_added (IdeDebuggerBreakpointsView *self,
+                                                IdeDebuggerBreakpoint      *breakpoint,
+                                                IdeDebugger                *debugger)
+{
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINTS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINT (breakpoint));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  dzl_gtk_list_store_insert_sorted (self->list_store, &iter, breakpoint, 0,
+                                    (GCompareDataFunc)ide_debugger_breakpoint_compare,
+                                    NULL);
+
+  gtk_list_store_set (self->list_store, &iter, 0, breakpoint, -1);
+}
+
+static void
+ide_debugger_breakpoints_view_breakpoint_removed (IdeDebuggerBreakpointsView *self,
+                                                  IdeDebuggerBreakpoint      *breakpoint,
+                                                  IdeDebugger                *debugger)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINTS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINT (breakpoint));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  model = GTK_TREE_MODEL (self->list_store);
+
+  if (gtk_tree_model_get_iter_first (model, &iter))
+    {
+      do
+        {
+          g_autoptr(IdeDebuggerBreakpoint) row = NULL;
+
+          gtk_tree_model_get (model, &iter, 0, &row, -1);
+
+          if (ide_debugger_breakpoint_compare (row, breakpoint) == 0)
+            {
+              gtk_list_store_remove (self->list_store, &iter);
+              break;
+            }
+        }
+      while (gtk_tree_model_iter_next (model, &iter));
+    }
+}
+
+static void
+ide_debugger_breakpoints_view_breakpoint_modified (IdeDebuggerBreakpointsView *self,
+                                                   IdeDebuggerBreakpoint      *breakpoint,
+                                                   IdeDebugger                *debugger)
+{
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINTS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINT (breakpoint));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  /* We can optimize this into a single replace, but should be fine for now */
+  ide_debugger_breakpoints_view_breakpoint_removed (self, breakpoint, debugger);
+  ide_debugger_breakpoints_view_breakpoint_added (self, breakpoint, debugger);
+}
+
+static void
+ide_debugger_breakpoints_view_enabled_toggled (IdeDebuggerBreakpointsView *self,
+                                               const gchar                *path_str,
+                                               GtkCellRendererToggle      *cell)
+{
+  IdeDebugger *debugger;
+  GtkTreeModel *model;
+  GtkTreePath *path;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINTS_VIEW (self));
+  g_assert (path_str != NULL);
+  g_assert (GTK_IS_CELL_RENDERER_TOGGLE (cell));
+
+  debugger = ide_debugger_breakpoints_view_get_debugger (self);
+  if (debugger == NULL)
+    return;
+
+  model = GTK_TREE_MODEL (self->list_store);
+  path = gtk_tree_path_new_from_string (path_str);
+
+  if (gtk_tree_model_get_iter (model, &iter, path))
+    {
+      g_autoptr(IdeDebuggerBreakpoint) breakpoint = NULL;
+
+      gtk_tree_model_get (model, &iter, 0, &breakpoint, -1);
+
+      ide_debugger_breakpoint_set_enabled (breakpoint,
+                                           !ide_debugger_breakpoint_get_enabled (breakpoint));
+      ide_debugger_modify_breakpoint_async (debugger,
+                                            IDE_DEBUGGER_BREAKPOINT_CHANGE_ENABLED,
+                                            breakpoint,
+                                            NULL, NULL, NULL);
+    }
+
+  gtk_tree_path_free (path);
+}
+
+static void
+address_cell_data_func (GtkCellLayout   *cell_layout,
+                        GtkCellRenderer *cell,
+                        GtkTreeModel    *model,
+                        GtkTreeIter     *iter,
+                        gpointer         user_data)
+{
+  g_autoptr(IdeDebuggerBreakpoint) breakpoint = NULL;
+  IdeDebuggerAddress addr = IDE_DEBUGGER_ADDRESS_INVALID;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+
+  gtk_tree_model_get (model, iter, 0, &breakpoint, -1);
+
+  if (breakpoint != NULL)
+    addr = ide_debugger_breakpoint_get_address (breakpoint);
+
+  if (addr == IDE_DEBUGGER_ADDRESS_INVALID)
+    {
+      g_object_set (cell, "text", NULL, NULL);
+    }
+  else
+    {
+      g_autofree gchar *str = NULL;
+
+      str = g_strdup_printf ("0x%"G_GINT64_MODIFIER"x", addr);
+      g_object_set (cell, "text", str, NULL);
+    }
+}
+
+static void
+string_property_cell_data_func (GtkCellLayout   *cell_layout,
+                                GtkCellRenderer *cell,
+                                GtkTreeModel    *model,
+                                GtkTreeIter     *iter,
+                                gpointer         user_data)
+{
+  const gchar *property = user_data;
+  g_autoptr(GObject) object = NULL;
+  g_auto(GValue) value = G_VALUE_INIT;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (property != NULL);
+
+  g_value_init (&value, G_TYPE_STRING);
+  gtk_tree_model_get (model, iter, 0, &object, -1);
+
+  if (object != NULL)
+    g_object_get_property (object, property, &value);
+
+  g_object_set_property (G_OBJECT (cell), "text", &value);
+}
+
+static void
+int_property_cell_data_func (GtkCellLayout   *cell_layout,
+                             GtkCellRenderer *cell,
+                             GtkTreeModel    *model,
+                             GtkTreeIter     *iter,
+                             gpointer         user_data)
+{
+  const gchar *property = user_data;
+  g_autoptr(GObject) object = NULL;
+  g_auto(GValue) value = G_VALUE_INIT;
+  g_autofree gchar *str = NULL;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (property != NULL);
+
+  g_value_init (&value, G_TYPE_INT64);
+  gtk_tree_model_get (model, iter, 0, &object, -1);
+
+  if (object != NULL)
+    g_object_get_property (object, property, &value);
+
+  str = g_strdup_printf ("%"G_GINT64_FORMAT, g_value_get_int64 (&value));
+  g_object_set (cell, "text", str, NULL);
+}
+
+static void
+enum_property_cell_data_func (GtkCellLayout   *cell_layout,
+                              GtkCellRenderer *cell,
+                              GtkTreeModel    *model,
+                              GtkTreeIter     *iter,
+                              gpointer         user_data)
+{
+  GParamSpec *pspec = user_data;
+  g_autoptr(GObject) object = NULL;
+  g_auto(GValue) value = G_VALUE_INIT;
+  const gchar *str = NULL;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (pspec != NULL);
+
+  g_value_init (&value, pspec->value_type);
+  gtk_tree_model_get (model, iter, 0, &object, -1);
+
+  if (object != NULL)
+    {
+      GEnumValue *ev = g_enum_get_value (g_type_class_peek (pspec->value_type),
+                                         g_value_get_enum (&value));
+
+      if (ev != NULL)
+        str = ev->value_nick;
+    }
+
+  g_object_set (cell, "text", str, NULL);
+}
+
+static void
+bool_property_cell_data_func (GtkCellLayout   *cell_layout,
+                              GtkCellRenderer *cell,
+                              GtkTreeModel    *model,
+                              GtkTreeIter     *iter,
+                              gpointer         user_data)
+{
+  g_autoptr(GObject) object = NULL;
+  g_auto(GValue) value = G_VALUE_INIT;
+  const gchar *property = user_data;
+
+  g_value_init (&value, G_TYPE_BOOLEAN);
+  gtk_tree_model_get (model, iter, 0, &object, -1);
+  if (object != NULL)
+    g_object_get_property (object, property, &value);
+  g_object_set_property (G_OBJECT (cell), "active", &value);
+}
+
+static void
+ide_debugger_breakpoints_view_delete_breakpoint (GtkTreeView                *tree_view,
+                                                 IdeDebuggerBreakpointsView *self)
+{
+  GtkTreeSelection *selection;
+  GtkTreeModel *model = NULL;
+  IdeDebugger *debugger;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINTS_VIEW (self));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  debugger = ide_debugger_breakpoints_view_get_debugger (self);
+
+  if (debugger == NULL)
+    return;
+
+  selection = gtk_tree_view_get_selection (tree_view);
+
+  if (gtk_tree_selection_get_selected (selection, &model, &iter))
+    {
+      g_autoptr(IdeDebuggerBreakpoint) breakpoint = NULL;
+
+      gtk_tree_model_get (model, &iter, 0, &breakpoint, -1);
+
+      if (breakpoint != NULL)
+        ide_debugger_remove_breakpoint_async (debugger, breakpoint, NULL, NULL, NULL);
+    }
+}
+
+static void
+ide_debugger_breakpoints_view_destroy (GtkWidget *widget)
+{
+  IdeDebuggerBreakpointsView *self = (IdeDebuggerBreakpointsView *)widget;
+
+  g_clear_object (&self->debugger_signals);
+
+  GTK_WIDGET_CLASS (ide_debugger_breakpoints_view_parent_class)->destroy (widget);
+}
+
+static void
+ide_debugger_breakpoints_view_get_property (GObject    *object,
+                                            guint       prop_id,
+                                            GValue     *value,
+                                            GParamSpec *pspec)
+{
+  IdeDebuggerBreakpointsView *self = IDE_DEBUGGER_BREAKPOINTS_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEBUGGER:
+      g_value_set_object (value, ide_debugger_breakpoints_view_get_debugger (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_debugger_breakpoints_view_set_property (GObject      *object,
+                                            guint         prop_id,
+                                            const GValue *value,
+                                            GParamSpec   *pspec)
+{
+  IdeDebuggerBreakpointsView *self = IDE_DEBUGGER_BREAKPOINTS_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEBUGGER:
+      ide_debugger_breakpoints_view_set_debugger (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_debugger_breakpoints_view_class_init (IdeDebuggerBreakpointsViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_debugger_breakpoints_view_get_property;
+  object_class->set_property = ide_debugger_breakpoints_view_set_property;
+
+  widget_class->destroy = ide_debugger_breakpoints_view_destroy;
+
+  properties [PROP_DEBUGGER] =
+    g_param_spec_object ("debugger",
+                         "Debugger",
+                         "The debugger being observed",
+                         IDE_TYPE_DEBUGGER,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/debuggerui/ide-debugger-breakpoints-view.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, address_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, address_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, hits_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, hits_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, file_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, file_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, function_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, function_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, id_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, id_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, line_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, line_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, list_store);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, tree_view);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, spec_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, spec_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, type_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, type_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, enabled_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerBreakpointsView, enabled_column);
+
+  g_type_ensure (IDE_TYPE_DEBUGGER_BREAKPOINT);
+}
+
+static void
+ide_debugger_breakpoints_view_init (IdeDebuggerBreakpointsView *self)
+{
+  DzlShortcutController *controller;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->debugger_signals = dzl_signal_group_new (IDE_TYPE_DEBUGGER);
+
+  g_signal_connect_swapped (self->debugger_signals,
+                            "bind",
+                            G_CALLBACK (ide_debugger_breakpoints_view_bind),
+                            self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "running",
+                                    G_CALLBACK (ide_debugger_breakpoints_view_running),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "stopped",
+                                    G_CALLBACK (ide_debugger_breakpoints_view_stopped),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "breakpoint-added",
+                                    G_CALLBACK (ide_debugger_breakpoints_view_breakpoint_added),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "breakpoint-removed",
+                                    G_CALLBACK (ide_debugger_breakpoints_view_breakpoint_removed),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "breakpoint-modified",
+                                    G_CALLBACK (ide_debugger_breakpoints_view_breakpoint_modified),
+                                    self);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->id_column),
+                                      GTK_CELL_RENDERER (self->id_cell),
+                                      string_property_cell_data_func, (gchar *)"id", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->file_column),
+                                      GTK_CELL_RENDERER (self->file_cell),
+                                      string_property_cell_data_func, (gchar *)"file", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->line_column),
+                                      GTK_CELL_RENDERER (self->line_cell),
+                                      int_property_cell_data_func, (gchar *)"line", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->function_column),
+                                      GTK_CELL_RENDERER (self->function_cell),
+                                      string_property_cell_data_func, (gchar *)"function", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->address_column),
+                                      GTK_CELL_RENDERER (self->address_cell),
+                                      address_cell_data_func, NULL, NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->hits_column),
+                                      GTK_CELL_RENDERER (self->hits_cell),
+                                      int_property_cell_data_func, (gchar *)"count", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->type_column),
+                                      GTK_CELL_RENDERER (self->type_cell),
+                                      enum_property_cell_data_func,
+                                      g_object_class_find_property (g_type_class_peek 
(IDE_TYPE_DEBUGGER_BREAKPOINT), "mode"),
+                                      NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->spec_column),
+                                      GTK_CELL_RENDERER (self->spec_cell),
+                                      string_property_cell_data_func, (gchar *)"spec", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->enabled_column),
+                                      GTK_CELL_RENDERER (self->enabled_cell),
+                                      bool_property_cell_data_func, (gchar *)"enabled", NULL);
+
+  g_signal_connect_swapped (self->enabled_cell,
+                            "toggled",
+                            G_CALLBACK (ide_debugger_breakpoints_view_enabled_toggled),
+                            self);
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (self->tree_view));
+
+  dzl_shortcut_controller_add_command_callback (controller,
+                                                "org.gnome.builder.debugger.delete-breakpoint",
+                                                "Delete",
+                                                DZL_SHORTCUT_PHASE_BUBBLE,
+                                                (GtkCallback) 
ide_debugger_breakpoints_view_delete_breakpoint,
+                                                self, NULL);
+}
+
+GtkWidget *
+ide_debugger_breakpoints_view_new (void)
+{
+  return g_object_new (IDE_TYPE_DEBUGGER_BREAKPOINTS_VIEW, NULL);
+}
+
+/**
+ * ide_debugger_breakpoints_view_get_debugger:
+ * @self: a #IdeDebuggerBreakpointsView
+ *
+ * Gets the debugger that is being observed by the view.
+ *
+ * Returns: (nullable) (transfer none): An #IdeDebugger or %NULL
+ *
+ * Since: 3.32
+ */
+IdeDebugger *
+ide_debugger_breakpoints_view_get_debugger (IdeDebuggerBreakpointsView *self)
+{
+  g_return_val_if_fail (IDE_IS_DEBUGGER_BREAKPOINTS_VIEW (self), NULL);
+
+  if (self->debugger_signals != NULL)
+    return dzl_signal_group_get_target (self->debugger_signals);
+  else
+    return NULL;
+}
+
+/**
+ * ide_debugger_breakpoints_view_set_debugger:
+ * @self: a #IdeDebuggerBreakpointsView
+ * @debugger: (nullable): An #IdeDebugger or %NULL
+ *
+ * Sets the debugger that is being viewed.
+ *
+ * Since: 3.32
+ */
+void
+ide_debugger_breakpoints_view_set_debugger (IdeDebuggerBreakpointsView *self,
+                                            IdeDebugger                *debugger)
+{
+  g_return_if_fail (IDE_IS_DEBUGGER_BREAKPOINTS_VIEW (self));
+  g_return_if_fail (!debugger || IDE_IS_DEBUGGER (debugger));
+
+  if (self->debugger_signals != NULL)
+    {
+      dzl_signal_group_set_target (self->debugger_signals, debugger);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DEBUGGER]);
+    }
+}
diff --git a/src/plugins/debuggerui/ide-debugger-breakpoints-view.h 
b/src/plugins/debuggerui/ide-debugger-breakpoints-view.h
new file mode 100644
index 000000000..883475b79
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-breakpoints-view.h
@@ -0,0 +1,38 @@
+/* ide-debugger-breakpoints-view.h
+ *
+ * Copyright 2017-2019 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 <gtk/gtk.h>
+
+#include "ide-debugger.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_BREAKPOINTS_VIEW (ide_debugger_breakpoints_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerBreakpointsView, ide_debugger_breakpoints_view, IDE, 
DEBUGGER_BREAKPOINTS_VIEW, GtkBin)
+
+GtkWidget   *ide_debugger_breakpoints_view_new          (void);
+IdeDebugger *ide_debugger_breakpoints_view_get_debugger (IdeDebuggerBreakpointsView *self);
+void         ide_debugger_breakpoints_view_set_debugger (IdeDebuggerBreakpointsView *self,
+                                                         IdeDebugger                *debugger);
+
+G_END_DECLS
diff --git a/src/libide/debugger/ide-debugger-breakpoints-view.ui 
b/src/plugins/debuggerui/ide-debugger-breakpoints-view.ui
similarity index 100%
rename from src/libide/debugger/ide-debugger-breakpoints-view.ui
rename to src/plugins/debuggerui/ide-debugger-breakpoints-view.ui
diff --git a/src/plugins/debuggerui/ide-debugger-controls.c b/src/plugins/debuggerui/ide-debugger-controls.c
new file mode 100644
index 000000000..331f37536
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-controls.c
@@ -0,0 +1,43 @@
+/* ide-debugger-controls.c
+ *
+ * Copyright 2017-2019 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
+ */
+
+#include "ide-debugger-controls.h"
+
+struct _IdeDebuggerControls
+{
+  GtkBin parent_instance;
+};
+
+G_DEFINE_TYPE (IdeDebuggerControls, ide_debugger_controls, GTK_TYPE_REVEALER)
+
+static void
+ide_debugger_controls_class_init (IdeDebuggerControlsClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/debuggerui/ide-debugger-controls.ui");
+  gtk_widget_class_set_css_name (widget_class, "idedebuggercontrols");
+}
+
+static void
+ide_debugger_controls_init (IdeDebuggerControls *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/src/plugins/debuggerui/ide-debugger-controls.h b/src/plugins/debuggerui/ide-debugger-controls.h
new file mode 100644
index 000000000..eaff84473
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-controls.h
@@ -0,0 +1,37 @@
+/* ide-debugger-controls.h
+ *
+ * Copyright 2017-2019 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 <gtk/gtk.h>
+
+#include "ide-debugger.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_CONTROLS (ide_debugger_controls_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerControls, ide_debugger_controls, IDE, DEBUGGER_CONTROLS, GtkRevealer)
+
+IdeDebugger *ide_debugger_controls_get_debugger (IdeDebuggerControls *self);
+void         ide_debugger_controls_set_debugger (IdeDebuggerControls *self,
+                                                 IdeDebugger         *debugger);
+
+G_END_DECLS
diff --git a/src/libide/debugger/ide-debugger-controls.ui b/src/plugins/debuggerui/ide-debugger-controls.ui
similarity index 100%
rename from src/libide/debugger/ide-debugger-controls.ui
rename to src/plugins/debuggerui/ide-debugger-controls.ui
diff --git a/src/plugins/debuggerui/ide-debugger-disassembly-view.c 
b/src/plugins/debuggerui/ide-debugger-disassembly-view.c
new file mode 100644
index 000000000..433b2ba63
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-disassembly-view.c
@@ -0,0 +1,138 @@
+/* ide-debugger-disassembly-view.c
+ *
+ * Copyright 2017-2019 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 "ide-debugger-disassembly-view"
+
+#include "config.h"
+
+#include <libide-sourceview.h>
+
+#include "ide-debugger-disassembly-view.h"
+#include "ide-debugger-instruction.h"
+
+struct _IdeDebuggerDisassemblyView
+{
+  IdePage             parent_instance;
+
+  /* Owned references */
+  GPtrArray          *instructions;
+
+  /* Template references */
+  GtkSourceView      *source_view;
+  GtkSourceBuffer    *source_buffer;
+
+  IdeDebuggerAddress  current_address;
+};
+
+G_DEFINE_TYPE (IdeDebuggerDisassemblyView, ide_debugger_disassembly_view, IDE_TYPE_PAGE)
+
+static void
+ide_debugger_disassembly_view_destroy (GtkWidget *widget)
+{
+  IdeDebuggerDisassemblyView *self = (IdeDebuggerDisassemblyView *)widget;
+
+  g_clear_pointer (&self->instructions, g_ptr_array_unref);
+
+  GTK_WIDGET_CLASS (ide_debugger_disassembly_view_parent_class)->destroy (widget);
+}
+
+static void
+ide_debugger_disassembly_view_class_init (IdeDebuggerDisassemblyViewClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->destroy = ide_debugger_disassembly_view_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/debuggerui/ide-debugger-disassembly-view.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerDisassemblyView, source_buffer);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerDisassemblyView, source_view);
+}
+
+static void
+ide_debugger_disassembly_view_init (IdeDebuggerDisassemblyView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+void
+ide_debugger_disassembly_view_set_current_address (IdeDebuggerDisassemblyView *self,
+                                                   IdeDebuggerAddress          current_address)
+{
+  g_return_if_fail (IDE_IS_DEBUGGER_DISASSEMBLY_VIEW (self));
+
+  self->current_address = current_address;
+
+  /* Update gutter/etc */
+}
+
+/**
+ * ide_debugger_disassembly_view_set_instructions:
+ * @self: a #IdeDebuggerDisassemblyView
+ * @instructions: (nullable) (element-type Ide.DebuggerInstruction): An array of
+ *   instructions or %NULL.
+ *
+ * Sets the instructions to display in the disassembly view.
+ *
+ * This will take a reference to @instructions if non-%NULL so it is
+ * important that you do not modify @instructions after calling this.
+ *
+ * Since: 3.32
+ */
+void
+ide_debugger_disassembly_view_set_instructions (IdeDebuggerDisassemblyView *self,
+                                                GPtrArray                  *instructions)
+{
+  g_return_if_fail (IDE_IS_DEBUGGER_DISASSEMBLY_VIEW (self));
+
+  if (self->instructions == instructions)
+    return;
+
+  g_clear_pointer (&self->instructions, g_ptr_array_unref);
+  if (instructions != NULL)
+    self->instructions = g_ptr_array_ref (instructions);
+
+  gtk_text_buffer_set_text (GTK_TEXT_BUFFER (self->source_buffer), "", 0);
+
+  if (self->instructions != NULL && self->instructions->len > 0)
+    {
+      IdeDebuggerAddress first;
+      GtkTextIter iter;
+      GtkTextIter trim;
+
+      first = ide_debugger_instruction_get_address (g_ptr_array_index (self->instructions, 0));
+
+      gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (self->source_buffer), &iter);
+
+      for (guint i = 0; i < self->instructions->len; i++)
+        {
+          IdeDebuggerInstruction *inst = g_ptr_array_index (self->instructions, i);
+          g_autofree gchar *str = g_strdup_printf ("0x%"G_GINT64_MODIFIER"x <+%03"G_GINT64_MODIFIER"u>:  
%s\n",
+                                                   ide_debugger_instruction_get_address (inst),
+                                                   ide_debugger_instruction_get_address (inst) - first,
+                                                   ide_debugger_instruction_get_display (inst));
+          gtk_text_buffer_insert (GTK_TEXT_BUFFER (self->source_buffer), &iter, str, -1);
+        }
+
+      /* Trim the trailing \n */
+      trim = iter;
+      gtk_text_iter_backward_char (&iter);
+      gtk_text_buffer_delete (GTK_TEXT_BUFFER (self->source_buffer), &iter, &trim);
+    }
+}
diff --git a/src/plugins/debuggerui/ide-debugger-disassembly-view.h 
b/src/plugins/debuggerui/ide-debugger-disassembly-view.h
new file mode 100644
index 000000000..ed67230f3
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-disassembly-view.h
@@ -0,0 +1,38 @@
+/* ide-debugger-disassembly-view.h
+ *
+ * Copyright 2017-2019 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-gui.h>
+
+#include "ide-debugger-types.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_DISASSEMBLY_VIEW (ide_debugger_disassembly_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerDisassemblyView, ide_debugger_disassembly_view, IDE, 
DEBUGGER_DISASSEMBLY_VIEW, IdePage)
+
+void ide_debugger_disassembly_view_set_current_address (IdeDebuggerDisassemblyView *self,
+                                                        IdeDebuggerAddress          address);
+void ide_debugger_disassembly_view_set_instructions    (IdeDebuggerDisassemblyView *self,
+                                                        GPtrArray                  *instructions);
+
+G_END_DECLS
diff --git a/src/plugins/debuggerui/ide-debugger-disassembly-view.ui 
b/src/plugins/debuggerui/ide-debugger-disassembly-view.ui
new file mode 100644
index 000000000..2cd579636
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-disassembly-view.ui
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="IdeDebuggerDisassemblyView" parent="IdePage">
+    <property name="icon-name">application-x-executable-symbolic</property>
+    <property name="title" translatable="yes">Disassembly</property>
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="expand">true</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkSourceView" id="source_view">
+            <property name="show-line-numbers">true</property>
+            <property name="editable">false</property>
+            <property name="monospace">true</property>
+            <property name="buffer">source_buffer</property>
+            <property name="visible">true</property>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="GtkSourceBuffer" id="source_buffer">
+  </object>
+</interface>
diff --git a/src/plugins/debuggerui/ide-debugger-editor-addin.c 
b/src/plugins/debuggerui/ide-debugger-editor-addin.c
new file mode 100644
index 000000000..ecedc93ae
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-editor-addin.c
@@ -0,0 +1,691 @@
+/* ide-debugger-editor-addin.c
+ *
+ * Copyright 2017-2019 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 "ide-debugger-editor-addin"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-core.h>
+#include <libide-debugger.h>
+#include <libide-editor.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+#include <libide-io.h>
+#include <libide-terminal.h>
+#include <glib/gi18n.h>
+
+#include "ide-debugger-breakpoints-view.h"
+#include "ide-debugger-controls.h"
+#include "ide-debugger-disassembly-view.h"
+#include "ide-debugger-editor-addin.h"
+#include "ide-debugger-libraries-view.h"
+#include "ide-debugger-locals-view.h"
+#include "ide-debugger-registers-view.h"
+#include "ide-debugger-threads-view.h"
+
+/**
+ * SECTION:ide-debugger-editor-addin
+ * @title: IdeDebuggerEditorAddin
+ * @short_description: Debugger hooks for the editor perspective
+ *
+ * This class allows the debugger widgetry to hook into the editor. We add
+ * various panels to the editor perpective and ensure they are only visible
+ * when the process is being debugged.
+ *
+ * Since: 3.32
+ */
+
+struct _IdeDebuggerEditorAddin
+{
+  GObject                     parent_instance;
+
+  DzlSignalGroup             *debug_manager_signals;
+  DzlSignalGroup             *debugger_signals;
+
+  IdeEditorSurface           *editor;
+  IdeWorkbench               *workbench;
+
+  IdeDebuggerDisassemblyView *disassembly_view;
+  IdeDebuggerControls        *controls;
+  IdeDebuggerBreakpointsView *breakpoints_view;
+  IdeDebuggerLibrariesView   *libraries_view;
+  IdeDebuggerLocalsView      *locals_view;
+  DzlDockWidget              *panel;
+  IdeDebuggerRegistersView   *registers_view;
+  IdeDebuggerThreadsView     *threads_view;
+  IdeTerminal                *log_view;
+  GtkScrollbar               *log_view_scroller;
+};
+
+static void
+debugger_log (IdeDebuggerEditorAddin *self,
+              IdeDebuggerStream       stream,
+              GBytes                 *content,
+              IdeDebugger            *debugger)
+{
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_DEBUGGER_STREAM (stream));
+  g_assert (content != NULL);
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  if (stream == IDE_DEBUGGER_CONSOLE)
+    {
+      IdeLineReader reader;
+      const gchar *str;
+      gchar *line;
+      gsize len;
+      gsize line_len;
+
+      str = g_bytes_get_data (content, &len);
+
+      /*
+       * Ingnore \n so we can add \r\n. Otherwise we get problematic
+       * output in the terminal.
+       */
+      ide_line_reader_init (&reader, (gchar *)str, len);
+      while (NULL != (line = ide_line_reader_next (&reader, &line_len)))
+        {
+          vte_terminal_feed (VTE_TERMINAL (self->log_view), line, line_len);
+
+          if ((line + line_len) < (str + len))
+            {
+              if (line[line_len] == '\r' || line[line_len] == '\n')
+                vte_terminal_feed (VTE_TERMINAL (self->log_view), "\r\n", 2);
+            }
+        }
+    }
+}
+
+static void
+debugger_stopped (IdeDebuggerEditorAddin *self,
+                  IdeDebuggerStopReason   reason,
+                  IdeDebuggerBreakpoint  *breakpoint,
+                  IdeDebugger            *debugger)
+{
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_DEBUGGER_STOP_REASON (reason));
+  g_assert (!breakpoint || IDE_IS_DEBUGGER_BREAKPOINT (breakpoint));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  if (breakpoint != NULL)
+    ide_debugger_editor_addin_navigate_to_breakpoint (self, breakpoint);
+
+  IDE_EXIT;
+}
+
+static void
+send_notification (IdeDebuggerEditorAddin *self,
+                   const gchar            *title,
+                   const gchar            *body,
+                   const gchar            *icon_name,
+                   gboolean                urgent)
+{
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(GIcon) icon = NULL;
+  IdeContext *context;
+
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+
+  context = ide_workbench_get_context (self->workbench);
+
+  if (icon_name)
+    icon = g_themed_icon_new (icon_name);
+
+  notif = g_object_new (IDE_TYPE_NOTIFICATION,
+                        "has-progress", FALSE,
+                        "icon", icon,
+                        "title", title,
+                        "body", body,
+                        "urgent", TRUE,
+                        NULL);
+  ide_notification_attach (notif, IDE_OBJECT (context));
+  ide_notification_withdraw_in_seconds (notif, 30);
+}
+
+static void
+debugger_run_handler (IdeRunManager *run_manager,
+                      IdeRunner     *runner,
+                      gpointer       user_data)
+{
+  IdeDebuggerEditorAddin *self = user_data;
+  IdeDebugManager *debug_manager;
+  IdeContext *context;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_RUN_MANAGER (run_manager));
+  g_assert (IDE_IS_RUNNER (runner));
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+
+  /*
+   * Get the currently configured debugger and attach it to our runner.
+   * It might need to prepend arguments like `gdb', `pdb', `mdb', etc.
+   */
+  context = ide_object_get_context (IDE_OBJECT (run_manager));
+  debug_manager = ide_debug_manager_from_context (context);
+
+  if (!ide_debug_manager_start (debug_manager, runner, &error))
+    send_notification (self,
+                       _("Failed to start the debugger"),
+                       error->message,
+                       "computer-fail-symbolic",
+                       TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+debug_manager_notify_debugger (IdeDebuggerEditorAddin *self,
+                               GParamSpec             *pspec,
+                               IdeDebugManager        *debug_manager)
+{
+  IdeDebugger *debugger;
+  IdeWorkspace *workspace;
+
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_DEBUG_MANAGER (debug_manager));
+
+  if (!gtk_widget_get_visible (GTK_WIDGET (self->panel)))
+    {
+      GtkWidget *stack = gtk_widget_get_parent (GTK_WIDGET (self->panel));
+
+      gtk_widget_show (GTK_WIDGET (self->panel));
+
+      if (GTK_IS_STACK (stack))
+        gtk_stack_set_visible_child (GTK_STACK (stack), GTK_WIDGET (self->panel));
+    }
+
+  debugger = ide_debug_manager_get_debugger (debug_manager);
+
+  if ((workspace = ide_widget_get_workspace (GTK_WIDGET (self->editor))))
+    gtk_widget_insert_action_group (GTK_WIDGET (workspace),
+                                    "debugger",
+                                    G_ACTION_GROUP (debugger));
+
+  ide_debugger_breakpoints_view_set_debugger (self->breakpoints_view, debugger);
+  ide_debugger_locals_view_set_debugger (self->locals_view, debugger);
+  ide_debugger_libraries_view_set_debugger (self->libraries_view, debugger);
+  ide_debugger_registers_view_set_debugger (self->registers_view, debugger);
+  ide_debugger_threads_view_set_debugger (self->threads_view, debugger);
+
+  dzl_signal_group_set_target (self->debugger_signals, debugger);
+}
+
+static void
+debug_manager_notify_active (IdeDebuggerEditorAddin *self,
+                             GParamSpec             *pspec,
+                             IdeDebugManager        *debug_manager)
+{
+  gboolean reveal_child = FALSE;
+
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_DEBUG_MANAGER (debug_manager));
+
+  /*
+   * Instead of using a property binding, we use this signal callback so
+   * that we can adjust the reveal-child and visible. Otherwise the widgets
+   * will take up space+padding when reveal-child is FALSE.
+   */
+
+  if (ide_debug_manager_get_active (debug_manager))
+    {
+      gtk_widget_show (GTK_WIDGET (self->controls));
+      reveal_child = TRUE;
+    }
+
+  gtk_revealer_set_reveal_child (GTK_REVEALER (self->controls), reveal_child);
+}
+
+static void
+on_frame_activated (IdeDebuggerEditorAddin *self,
+                    IdeDebuggerThread      *thread,
+                    IdeDebuggerFrame       *frame,
+                    IdeDebuggerThreadsView *threads_view)
+{
+  IdeDebuggerAddress addr;
+  const gchar *path;
+  guint line;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_DEBUGGER_THREAD (thread));
+  g_assert (IDE_IS_DEBUGGER_FRAME (frame));
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (threads_view));
+
+  ide_debugger_locals_view_load_async (self->locals_view, thread, frame, NULL, NULL, NULL);
+
+  path = ide_debugger_frame_get_file (frame);
+  line = ide_debugger_frame_get_line (frame);
+
+  if (line > 0)
+    line--;
+
+  if (path != NULL)
+    {
+      IdeContext *context = ide_widget_get_context (GTK_WIDGET (threads_view));
+      g_autoptr(IdeLocation) location = NULL;
+      g_autofree gchar *project_path = ide_context_build_filename (context, path, NULL);
+      g_autoptr(GFile) file = g_file_new_for_path (project_path);
+
+      location = ide_location_new (file, line, -1);
+      ide_editor_surface_focus_location (self->editor, location);
+
+      IDE_EXIT;
+    }
+
+  addr = ide_debugger_frame_get_address (frame);
+
+  if (addr != IDE_DEBUGGER_ADDRESS_INVALID)
+    {
+      ide_debugger_editor_addin_navigate_to_address (self, addr);
+      IDE_EXIT;
+    }
+
+  g_warning ("Failed to locate source or memory address for frame");
+
+  IDE_EXIT;
+}
+
+static void
+ide_debugger_editor_addin_add_ui (IdeDebuggerEditorAddin *self)
+{
+  GtkWidget *scroll_box;
+  GtkWidget *box;
+  GtkWidget *hpaned;
+  GtkWidget *utilities;
+  GtkWidget *overlay;
+
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_SURFACE (self->editor));
+
+#define OBSERVE_DESTROY(ptr) \
+  g_signal_connect ((ptr), "destroy", G_CALLBACK (gtk_widget_destroyed), &(ptr))
+
+  overlay = ide_editor_surface_get_overlay (self->editor);
+
+  self->controls = g_object_new (IDE_TYPE_DEBUGGER_CONTROLS,
+                                 "transition-duration", 500,
+                                 "transition-type", GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP,
+                                 "reveal-child", FALSE,
+                                 "visible", TRUE,
+                                 "halign", GTK_ALIGN_CENTER,
+                                 "valign", GTK_ALIGN_END,
+                                 NULL);
+  OBSERVE_DESTROY (self->controls);
+  gtk_overlay_add_overlay (GTK_OVERLAY (overlay), GTK_WIDGET (self->controls));
+
+  self->panel = g_object_new (DZL_TYPE_DOCK_WIDGET,
+                              "title", _("Debugger"),
+                              "icon-name", "builder-debugger-symbolic",
+                              "visible", FALSE,
+                              NULL);
+  OBSERVE_DESTROY (self->panel);
+
+  box = g_object_new (GTK_TYPE_NOTEBOOK,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (self->panel), box);
+
+  hpaned = g_object_new (DZL_TYPE_MULTI_PANED,
+                         "orientation", GTK_ORIENTATION_HORIZONTAL,
+                         "visible", TRUE,
+                         NULL);
+  gtk_container_add_with_properties (GTK_CONTAINER (box), GTK_WIDGET (hpaned),
+                                     "tab-label", _("Threads"),
+                                     NULL);
+
+  self->threads_view = g_object_new (IDE_TYPE_DEBUGGER_THREADS_VIEW,
+                                     "hexpand", TRUE,
+                                     "visible", TRUE,
+                                     NULL);
+  OBSERVE_DESTROY (self->threads_view);
+  g_signal_connect_swapped (self->threads_view,
+                            "frame-activated",
+                            G_CALLBACK (on_frame_activated),
+                            self);
+  gtk_container_add (GTK_CONTAINER (hpaned), GTK_WIDGET (self->threads_view));
+
+  self->locals_view = g_object_new (IDE_TYPE_DEBUGGER_LOCALS_VIEW,
+                                    "width-request", 250,
+                                    "visible", TRUE,
+                                    NULL);
+  OBSERVE_DESTROY (self->locals_view);
+  gtk_container_add (GTK_CONTAINER (hpaned), GTK_WIDGET (self->locals_view));
+
+  self->breakpoints_view = g_object_new (IDE_TYPE_DEBUGGER_BREAKPOINTS_VIEW,
+                                         "visible", TRUE,
+                                         NULL);
+  OBSERVE_DESTROY (self->breakpoints_view);
+  gtk_container_add_with_properties (GTK_CONTAINER (box), GTK_WIDGET (self->breakpoints_view),
+                                     "tab-label", _("Breakpoints"),
+                                     NULL);
+
+  self->libraries_view = g_object_new (IDE_TYPE_DEBUGGER_LIBRARIES_VIEW,
+                                       "visible", TRUE,
+                                       NULL);
+  OBSERVE_DESTROY (self->libraries_view);
+  gtk_container_add_with_properties (GTK_CONTAINER (box), GTK_WIDGET (self->libraries_view),
+                                     "tab-label", _("Libraries"),
+                                     NULL);
+
+  self->registers_view = g_object_new (IDE_TYPE_DEBUGGER_REGISTERS_VIEW,
+                                       "visible", TRUE,
+                                       NULL);
+  OBSERVE_DESTROY (self->registers_view);
+  gtk_container_add_with_properties (GTK_CONTAINER (box), GTK_WIDGET (self->registers_view),
+                                     "tab-label", _("Registers"),
+                                     NULL);
+
+  scroll_box = g_object_new (GTK_TYPE_BOX,
+                             "orientation", GTK_ORIENTATION_HORIZONTAL,
+                             "visible", TRUE,
+                             NULL);
+  gtk_container_add_with_properties (GTK_CONTAINER (box), GTK_WIDGET (scroll_box),
+                                     "tab-label", _("Log"),
+                                     NULL);
+
+  self->log_view = g_object_new (IDE_TYPE_TERMINAL,
+                                 "hexpand", TRUE,
+                                 "visible", TRUE,
+                                 NULL);
+  OBSERVE_DESTROY (self->log_view);
+  gtk_container_add (GTK_CONTAINER (scroll_box), GTK_WIDGET (self->log_view));
+
+  self->log_view_scroller = g_object_new (GTK_TYPE_SCROLLBAR,
+                                          "adjustment", gtk_scrollable_get_vadjustment (GTK_SCROLLABLE 
(self->log_view)),
+                                          "orientation", GTK_ORIENTATION_VERTICAL,
+                                          "visible", TRUE,
+                                          NULL);
+  gtk_container_add (GTK_CONTAINER (scroll_box), GTK_WIDGET (self->log_view_scroller));
+
+  utilities = ide_editor_surface_get_utilities (self->editor);
+  gtk_container_add (GTK_CONTAINER (utilities), GTK_WIDGET (self->panel));
+
+#undef OBSERVE_DESTROY
+}
+
+static void
+ide_debugger_editor_addin_load (IdeEditorAddin   *addin,
+                                IdeEditorSurface *editor)
+{
+  IdeDebuggerEditorAddin *self = (IdeDebuggerEditorAddin *)addin;
+  IdeContext *context;
+  IdeRunManager *run_manager;
+  IdeDebugManager *debug_manager;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
+
+  self->editor = editor;
+  self->workbench = ide_widget_get_workbench (GTK_WIDGET (editor));
+
+  if (!ide_workbench_has_project (self->workbench))
+    return;
+
+  context = ide_widget_get_context (GTK_WIDGET (editor));
+  run_manager = ide_run_manager_from_context (context);
+  debug_manager = ide_debug_manager_from_context (context);
+
+  ide_debugger_editor_addin_add_ui (self);
+
+  ide_run_manager_add_handler (run_manager,
+                               "debugger",
+                               _("Run with Debugger"),
+                               "builder-debugger-symbolic",
+                               "F5",
+                               debugger_run_handler,
+                               g_object_ref (self),
+                               g_object_unref);
+
+  self->debugger_signals = dzl_signal_group_new (IDE_TYPE_DEBUGGER);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "log",
+                                    G_CALLBACK (debugger_log),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "stopped",
+                                    G_CALLBACK (debugger_stopped),
+                                    self);
+
+  self->debug_manager_signals = dzl_signal_group_new (IDE_TYPE_DEBUG_MANAGER);
+
+  dzl_signal_group_connect_swapped (self->debug_manager_signals,
+                                    "notify::active",
+                                    G_CALLBACK (debug_manager_notify_active),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debug_manager_signals,
+                                    "notify::debugger",
+                                    G_CALLBACK (debug_manager_notify_debugger),
+                                    self);
+
+  dzl_signal_group_set_target (self->debug_manager_signals, debug_manager);
+
+  IDE_EXIT;
+}
+
+static void
+ide_debugger_editor_addin_unload (IdeEditorAddin   *addin,
+                                  IdeEditorSurface *editor)
+{
+  IdeDebuggerEditorAddin *self = (IdeDebuggerEditorAddin *)addin;
+  IdeRunManager *run_manager;
+  IdeWorkspace *workspace;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
+
+  if (!ide_workbench_has_project (self->workbench))
+    return;
+
+  context = ide_workbench_get_context (self->workbench);
+  run_manager = ide_run_manager_from_context (context);
+
+  if ((workspace = ide_widget_get_workspace (GTK_WIDGET (editor))))
+    gtk_widget_insert_action_group (GTK_WIDGET (workspace), "debugger", NULL);
+
+  /* Remove the handler to initiate the debugger */
+  ide_run_manager_remove_handler (run_manager, "debugger");
+
+  g_clear_object (&self->debugger_signals);
+  g_clear_object (&self->debug_manager_signals);
+
+  if (self->panel != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->panel));
+  if (self->controls != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->controls));
+  if (self->disassembly_view != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->disassembly_view));
+
+  self->editor = NULL;
+  self->workbench = NULL;
+
+  IDE_EXIT;
+}
+
+static void
+editor_addin_iface_init (IdeEditorAddinInterface *iface)
+{
+  iface->load = ide_debugger_editor_addin_load;
+  iface->unload = ide_debugger_editor_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (IdeDebuggerEditorAddin, ide_debugger_editor_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_ADDIN, editor_addin_iface_init))
+
+static void
+ide_debugger_editor_addin_class_init (IdeDebuggerEditorAddinClass *klass)
+{
+}
+
+static void
+ide_debugger_editor_addin_init (IdeDebuggerEditorAddin *self)
+{
+}
+
+void
+ide_debugger_editor_addin_navigate_to_file (IdeDebuggerEditorAddin *self,
+                                            GFile                  *file,
+                                            guint                   line)
+{
+  g_autoptr(IdeLocation) location = NULL;
+
+  g_return_if_fail (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  location = ide_location_new (file, line, -1);
+
+  ide_editor_surface_focus_location (self->editor, location);
+}
+
+static void
+ide_debugger_editor_addin_disassemble_cb (GObject      *object,
+                                          GAsyncResult *result,
+                                          gpointer      user_data)
+{
+  IdeDebugger *debugger = (IdeDebugger *)object;
+  g_autoptr(IdeDebuggerEditorAddin) self = user_data;
+  g_autoptr(GPtrArray) instructions = NULL;
+  g_autoptr(GError) error = NULL;
+  GtkWidget *stack;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_DEBUGGER (debugger));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+
+  instructions = ide_debugger_disassemble_finish (debugger, result, &error);
+
+  if (instructions == NULL)
+    {
+      g_warning ("%s", error->message);
+      IDE_EXIT;
+    }
+
+  if (self->editor == NULL)
+    IDE_EXIT;
+
+  if (self->disassembly_view == NULL)
+    {
+      IdeGrid *grid = ide_editor_surface_get_grid (self->editor);
+
+      self->disassembly_view = g_object_new (IDE_TYPE_DEBUGGER_DISASSEMBLY_VIEW,
+                                             "visible", TRUE,
+                                             NULL);
+      g_signal_connect (self->disassembly_view,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        &self->disassembly_view);
+      gtk_container_add (GTK_CONTAINER (grid), GTK_WIDGET (self->disassembly_view));
+    }
+
+  ide_debugger_disassembly_view_set_instructions (self->disassembly_view, instructions);
+
+  /* TODO: Set current instruction */
+
+  /* FIXME: It would be nice if we had a nicer API for this */
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (self->disassembly_view), IDE_TYPE_FRAME);
+  if (stack != NULL)
+    ide_frame_set_visible_child (IDE_FRAME (stack),
+                                        IDE_PAGE (self->disassembly_view));
+
+  IDE_EXIT;
+}
+
+void
+ide_debugger_editor_addin_navigate_to_address (IdeDebuggerEditorAddin *self,
+                                               IdeDebuggerAddress      address)
+{
+  IdeDebugger *debugger;
+  IdeDebuggerAddressRange range;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_return_if_fail (address != IDE_DEBUGGER_ADDRESS_INVALID);
+
+  if (NULL == (debugger = dzl_signal_group_get_target (self->debugger_signals)))
+    IDE_EXIT;
+
+  if (address < 0x10)
+    range.from = 0;
+  else
+    range.from = address - 0x10;
+
+  if (G_MAXUINT64 - 0x20 < address)
+    range.to = G_MAXUINT64;
+  else
+    range.to = address + 0x20;
+
+  ide_debugger_disassemble_async (debugger,
+                                  &range,
+                                  NULL,
+                                  ide_debugger_editor_addin_disassemble_cb,
+                                  g_object_ref (self));
+
+  IDE_EXIT;
+
+}
+
+void
+ide_debugger_editor_addin_navigate_to_breakpoint (IdeDebuggerEditorAddin *self,
+                                                  IdeDebuggerBreakpoint  *breakpoint)
+{
+  IdeDebuggerAddress address;
+  const gchar *path;
+  guint line;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (IDE_IS_DEBUGGER_EDITOR_ADDIN (self));
+  g_return_if_fail (IDE_IS_DEBUGGER_BREAKPOINT (breakpoint));
+
+  address = ide_debugger_breakpoint_get_address (breakpoint);
+  path = ide_debugger_breakpoint_get_file (breakpoint);
+  line = ide_debugger_breakpoint_get_line (breakpoint);
+
+  if (line > 0)
+    line--;
+
+  if (path != NULL)
+    {
+      g_autoptr(GFile) file = g_file_new_for_path (path);
+      ide_debugger_editor_addin_navigate_to_file (self, file, line);
+    }
+  else if (address != IDE_DEBUGGER_ADDRESS_INVALID)
+    {
+      ide_debugger_editor_addin_navigate_to_address (self, address);
+    }
+
+  IDE_EXIT;
+}
diff --git a/src/plugins/debuggerui/ide-debugger-editor-addin.h 
b/src/plugins/debuggerui/ide-debugger-editor-addin.h
new file mode 100644
index 000000000..b9fe3455f
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-editor-addin.h
@@ -0,0 +1,39 @@
+/* ide-debugger-editor-addin.h
+ *
+ * Copyright 2017-2019 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-debugger.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_EDITOR_ADDIN (ide_debugger_editor_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerEditorAddin, ide_debugger_editor_addin, IDE, DEBUGGER_EDITOR_ADDIN, GObject)
+
+void ide_debugger_editor_addin_navigate_to_address    (IdeDebuggerEditorAddin *self,
+                                                       IdeDebuggerAddress      address);
+void ide_debugger_editor_addin_navigate_to_breakpoint (IdeDebuggerEditorAddin *self,
+                                                       IdeDebuggerBreakpoint  *breakpoint);
+void ide_debugger_editor_addin_navigate_to_file       (IdeDebuggerEditorAddin *self,
+                                                       GFile                  *file,
+                                                       guint                   line);
+
+G_END_DECLS
diff --git a/src/plugins/debuggerui/ide-debugger-hover-controls.c 
b/src/plugins/debuggerui/ide-debugger-hover-controls.c
new file mode 100644
index 000000000..2a0c8bfe3
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-hover-controls.c
@@ -0,0 +1,201 @@
+/* ide-debugger-hover-controls.c
+ *
+ * Copyright 2018-2019 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 "ide-debugger-hover-controls"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-debugger.h>
+#include <libide-sourceview.h>
+
+#include "ide-debugger-hover-controls.h"
+#include "ide-debugger-breakpoints.h"
+#include "ide-debugger-private.h"
+
+struct _IdeDebuggerHoverControls
+{
+  GtkBin parent_instance;
+
+  IdeDebugManager *debug_manager;
+  GFile *file;
+  guint line;
+
+  GtkToggleButton *nobreak;
+  GtkToggleButton *breakpoint;
+  GtkToggleButton *countpoint;
+};
+
+G_DEFINE_TYPE (IdeDebuggerHoverControls, ide_debugger_hover_controls, GTK_TYPE_BIN)
+
+static void
+ide_debugger_hover_controls_destroy (GtkWidget *widget)
+{
+  IdeDebuggerHoverControls *self = (IdeDebuggerHoverControls *)widget;
+
+  g_clear_object (&self->debug_manager);
+  g_clear_object (&self->file);
+
+  GTK_WIDGET_CLASS (ide_debugger_hover_controls_parent_class)->destroy (widget);
+}
+
+static void
+ide_debugger_hover_controls_class_init (IdeDebuggerHoverControlsClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->destroy = ide_debugger_hover_controls_destroy;
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/debuggerui/ide-debugger-hover-controls.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerHoverControls, nobreak);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerHoverControls, breakpoint);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerHoverControls, countpoint);
+}
+
+static void
+ide_debugger_hover_controls_init (IdeDebuggerHoverControls *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+on_toggle_cb (GtkToggleButton          *button,
+              IdeDebuggerHoverControls *self)
+{
+  g_autoptr(IdeDebuggerBreakpoints) breakpoints = NULL;
+  IdeDebuggerBreakMode break_type = IDE_DEBUGGER_BREAK_NONE;
+  IdeDebuggerBreakpoint *breakpoint;
+  GtkWidget *view;
+
+  g_assert (GTK_IS_TOGGLE_BUTTON (button));
+  g_assert (IDE_IS_DEBUGGER_HOVER_CONTROLS (self));
+
+  g_signal_handlers_block_by_func (self->nobreak, G_CALLBACK (on_toggle_cb), self);
+  g_signal_handlers_block_by_func (self->breakpoint, G_CALLBACK (on_toggle_cb), self);
+  g_signal_handlers_block_by_func (self->countpoint, G_CALLBACK (on_toggle_cb), self);
+
+  breakpoints = ide_debug_manager_get_breakpoints_for_file (self->debug_manager, self->file);
+  breakpoint = ide_debugger_breakpoints_get_line (breakpoints, self->line);
+
+  if (button == self->nobreak)
+    break_type = IDE_DEBUGGER_BREAK_NONE;
+  else if (button == self->breakpoint)
+    break_type = IDE_DEBUGGER_BREAK_BREAKPOINT;
+  else if (button == self->countpoint)
+    break_type = IDE_DEBUGGER_BREAK_COUNTPOINT;
+
+  if (breakpoint != NULL)
+    {
+      _ide_debug_manager_remove_breakpoint (self->debug_manager, breakpoint);
+      breakpoint = NULL;
+    }
+
+  switch (break_type)
+    {
+    default:
+    case IDE_DEBUGGER_BREAK_NONE:
+      gtk_toggle_button_set_active (self->nobreak, TRUE);
+      gtk_toggle_button_set_active (self->breakpoint, FALSE);
+      gtk_toggle_button_set_active (self->countpoint, FALSE);
+      break;
+
+    case IDE_DEBUGGER_BREAK_BREAKPOINT:
+    case IDE_DEBUGGER_BREAK_COUNTPOINT:
+      {
+        g_autoptr(IdeDebuggerBreakpoint) to_insert = NULL;
+        g_autofree gchar *path = g_file_get_path (self->file);
+
+        to_insert = ide_debugger_breakpoint_new (NULL);
+
+        ide_debugger_breakpoint_set_line (to_insert, self->line);
+        ide_debugger_breakpoint_set_file (to_insert, path);
+        ide_debugger_breakpoint_set_mode (to_insert, break_type);
+        ide_debugger_breakpoint_set_enabled (to_insert, TRUE);
+
+        _ide_debug_manager_add_breakpoint (self->debug_manager, to_insert);
+
+        gtk_toggle_button_set_active (self->nobreak, FALSE);
+        gtk_toggle_button_set_active (self->breakpoint, break_type == IDE_DEBUGGER_BREAK_BREAKPOINT);
+        gtk_toggle_button_set_active (self->countpoint, break_type == IDE_DEBUGGER_BREAK_COUNTPOINT);
+      }
+      break;
+
+    case IDE_DEBUGGER_BREAK_WATCHPOINT:
+      /* TODO: watchpoint not yet supported */
+      gtk_toggle_button_set_active (self->nobreak, FALSE);
+      gtk_toggle_button_set_active (self->breakpoint, FALSE);
+      gtk_toggle_button_set_active (self->countpoint, FALSE);
+      break;
+    }
+
+  view = dzl_gtk_widget_get_relative (GTK_WIDGET (self), IDE_TYPE_SOURCE_VIEW);
+  gtk_widget_queue_draw (view);
+
+  g_signal_handlers_unblock_by_func (self->nobreak, G_CALLBACK (on_toggle_cb), self);
+  g_signal_handlers_unblock_by_func (self->breakpoint, G_CALLBACK (on_toggle_cb), self);
+  g_signal_handlers_unblock_by_func (self->countpoint, G_CALLBACK (on_toggle_cb), self);
+}
+
+GtkWidget *
+ide_debugger_hover_controls_new (IdeDebugManager *debug_manager,
+                                 GFile           *file,
+                                 guint            line)
+{
+  g_autoptr(IdeDebuggerBreakpoints) breakpoints = NULL;
+  IdeDebuggerHoverControls *self;
+
+  self = g_object_new (IDE_TYPE_DEBUGGER_HOVER_CONTROLS, NULL);
+  self->debug_manager = g_object_ref (debug_manager);
+  self->file = g_object_ref (file);
+  self->line = line;
+
+  if ((breakpoints = ide_debug_manager_get_breakpoints_for_file (debug_manager, file)))
+    {
+      IdeDebuggerBreakMode mode;
+
+      mode = ide_debugger_breakpoints_get_line_mode (breakpoints, line);
+
+      switch (mode)
+        {
+        default:
+        case IDE_DEBUGGER_BREAK_NONE:
+          gtk_toggle_button_set_active (self->nobreak, TRUE);
+          break;
+
+        case IDE_DEBUGGER_BREAK_BREAKPOINT:
+          gtk_toggle_button_set_active (self->breakpoint, TRUE);
+          break;
+
+        case IDE_DEBUGGER_BREAK_COUNTPOINT:
+          gtk_toggle_button_set_active (self->countpoint, TRUE);
+          break;
+
+        case IDE_DEBUGGER_BREAK_WATCHPOINT:
+          /* TODO: not currently supported */
+          break;
+        }
+    }
+
+  g_signal_connect (self->nobreak, "toggled", G_CALLBACK (on_toggle_cb), self);
+  g_signal_connect (self->breakpoint, "toggled", G_CALLBACK (on_toggle_cb), self);
+  g_signal_connect (self->countpoint, "toggled", G_CALLBACK (on_toggle_cb), self);
+
+  return GTK_WIDGET (self);
+}
diff --git a/src/plugins/debuggerui/ide-debugger-hover-controls.h 
b/src/plugins/debuggerui/ide-debugger-hover-controls.h
new file mode 100644
index 000000000..c73df02e1
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-hover-controls.h
@@ -0,0 +1,37 @@
+/* ide-debugger-hover-controls.h
+ *
+ * Copyright 2018-2019 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 <gtk/gtk.h>
+
+#include "ide-debug-manager.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_HOVER_CONTROLS (ide_debugger_hover_controls_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerHoverControls, ide_debugger_hover_controls, IDE, DEBUGGER_HOVER_CONTROLS, 
GtkBin)
+
+GtkWidget *ide_debugger_hover_controls_new (IdeDebugManager *debug_manager,
+                                            GFile           *file,
+                                            guint            line);
+
+G_END_DECLS
diff --git a/src/libide/debugger/ide-debugger-hover-controls.ui 
b/src/plugins/debuggerui/ide-debugger-hover-controls.ui
similarity index 100%
rename from src/libide/debugger/ide-debugger-hover-controls.ui
rename to src/plugins/debuggerui/ide-debugger-hover-controls.ui
diff --git a/src/plugins/debuggerui/ide-debugger-hover-provider.c 
b/src/plugins/debuggerui/ide-debugger-hover-provider.c
new file mode 100644
index 000000000..45bbe19f2
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-hover-provider.c
@@ -0,0 +1,121 @@
+/* ide-debugger-hover-provider.c
+ *
+ * Copyright 2018-2019 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 "ide-debugger-hover-provider"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-core.h>
+#include <libide-debugger.h>
+#include <libide-sourceview.h>
+#include <libide-threading.h>
+#include <glib/gi18n.h>
+
+#include "ide-debugger-hover-controls.h"
+#include "ide-debugger-hover-provider.h"
+
+#define DEBUGGER_HOVER_PRIORITY 1000
+
+struct _IdeDebuggerHoverProvider
+{
+  GObject parent_instance;
+};
+
+static void
+ide_debugger_hover_provider_hover_async (IdeHoverProvider    *provider,
+                                         IdeHoverContext     *context,
+                                         const GtkTextIter   *iter,
+                                         GCancellable        *cancellable,
+                                         GAsyncReadyCallback  callback,
+                                         gpointer             user_data)
+{
+  IdeDebuggerHoverProvider *self = (IdeDebuggerHoverProvider *)provider;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(IdeContext) icontext = NULL;
+  IdeDebugManager *dbgmgr;
+  const gchar *lang_id;
+  IdeBuffer *buffer;
+  GFile *file;
+  guint line;
+
+  g_assert (IDE_IS_DEBUGGER_HOVER_PROVIDER (provider));
+  g_assert (IDE_IS_HOVER_CONTEXT (context));
+  g_assert (iter != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, ide_debugger_hover_provider_hover_async);
+
+  buffer = IDE_BUFFER (gtk_text_iter_get_buffer (iter));
+
+  if (gtk_source_buffer_iter_has_context_class (GTK_SOURCE_BUFFER (buffer), iter, "comment"))
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  lang_id = ide_buffer_get_language_id (buffer);
+  icontext = ide_buffer_ref_context (buffer);
+  dbgmgr = ide_debug_manager_from_context (icontext);
+  file = ide_buffer_get_file (buffer);
+  line = gtk_text_iter_get_line (iter);
+
+  if (ide_debug_manager_supports_language (dbgmgr, lang_id))
+    {
+      GtkWidget *controls;
+
+      controls = ide_debugger_hover_controls_new (dbgmgr, file, line + 1);
+      ide_hover_context_add_widget (context, DEBUGGER_HOVER_PRIORITY, _("Debugger"), controls);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+ide_debugger_hover_provider_hover_finish (IdeHoverProvider  *provider,
+                                          GAsyncResult      *result,
+                                          GError           **error)
+{
+  g_assert (IDE_IS_DEBUGGER_HOVER_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+hover_provider_iface_init (IdeHoverProviderInterface *iface)
+{
+  iface->hover_async = ide_debugger_hover_provider_hover_async;
+  iface->hover_finish = ide_debugger_hover_provider_hover_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (IdeDebuggerHoverProvider, ide_debugger_hover_provider, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_HOVER_PROVIDER, hover_provider_iface_init))
+
+static void
+ide_debugger_hover_provider_class_init (IdeDebuggerHoverProviderClass *klass)
+{
+}
+
+static void
+ide_debugger_hover_provider_init (IdeDebuggerHoverProvider *self)
+{
+}
diff --git a/src/plugins/debuggerui/ide-debugger-hover-provider.h 
b/src/plugins/debuggerui/ide-debugger-hover-provider.h
new file mode 100644
index 000000000..7e0ca3e55
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-hover-provider.h
@@ -0,0 +1,31 @@
+/* ide-debugger-hover-provider.h
+ *
+ * Copyright 2018-2019 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-sourceview.h>
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_HOVER_PROVIDER (ide_debugger_hover_provider_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerHoverProvider, ide_debugger_hover_provider, IDE, DEBUGGER_HOVER_PROVIDER, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/debuggerui/ide-debugger-libraries-view.c 
b/src/plugins/debuggerui/ide-debugger-libraries-view.c
new file mode 100644
index 000000000..b0fb552f7
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-libraries-view.c
@@ -0,0 +1,369 @@
+/* ide-debugger-libraries-view.c
+ *
+ * Copyright 2017-2019 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 "ide-debugger-libraries-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include "ide-debugger-libraries-view.h"
+
+struct _IdeDebuggerLibrariesView
+{
+  GtkBin parent_instance;
+
+  /* Template widgets */
+  GtkTreeView         *tree_view;
+  GtkListStore        *list_store;
+  GtkCellRendererText *range_cell;
+  GtkTreeViewColumn   *range_column;
+  GtkCellRendererText *target_cell;
+  GtkTreeViewColumn   *target_column;
+
+  /* Onwed refnerences */
+  DzlSignalGroup *debugger_signals;
+};
+
+enum {
+  PROP_0,
+  PROP_DEBUGGER,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeDebuggerLibrariesView, ide_debugger_libraries_view, GTK_TYPE_BIN)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_debugger_libraries_view_bind (IdeDebuggerLibrariesView *self,
+                                  IdeDebugger              *debugger,
+                                  DzlSignalGroup           *signals)
+{
+  g_assert (IDE_IS_DEBUGGER_LIBRARIES_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view),
+                            !ide_debugger_get_is_running (debugger));
+}
+
+static void
+ide_debugger_libraries_view_unbind (IdeDebuggerLibrariesView *self,
+                                    DzlSignalGroup           *signals)
+{
+  g_assert (IDE_IS_DEBUGGER_LIBRARIES_VIEW (self));
+  g_assert (DZL_IS_SIGNAL_GROUP (signals));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), FALSE);
+}
+
+static void
+ide_debugger_libraries_view_running (IdeDebuggerLibrariesView *self,
+                                     IdeDebugger              *debugger)
+{
+  g_assert (IDE_IS_DEBUGGER_LIBRARIES_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), FALSE);
+}
+
+static void
+ide_debugger_libraries_view_stopped (IdeDebuggerLibrariesView *self,
+                                     IdeDebuggerStopReason     stop_reason,
+                                     IdeDebuggerBreakpoint    *breakpoint,
+                                     IdeDebugger              *debugger)
+{
+  g_assert (IDE_IS_DEBUGGER_LIBRARIES_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), TRUE);
+}
+
+static void
+ide_debugger_libraries_view_library_loaded (IdeDebuggerLibrariesView *self,
+                                            IdeDebuggerLibrary       *library,
+                                            IdeDebugger              *debugger)
+{
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_LIBRARIES_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_LIBRARY (library));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  dzl_gtk_list_store_insert_sorted (self->list_store,
+                                    &iter, library, 0,
+                                    (GCompareDataFunc)ide_debugger_library_compare,
+                                    NULL);
+
+  gtk_list_store_set (self->list_store, &iter, 0, library, -1);
+}
+
+static void
+ide_debugger_libraries_view_library_unloaded (IdeDebuggerLibrariesView *self,
+                                              IdeDebuggerLibrary       *library,
+                                              IdeDebugger              *debugger)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_LIBRARIES_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_LIBRARY (library));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  model = GTK_TREE_MODEL (self->list_store);
+
+  if (gtk_tree_model_get_iter_first (model, &iter))
+    {
+      do
+        {
+          g_autoptr(IdeDebuggerLibrary) element = NULL;
+
+          gtk_tree_model_get (model, &iter, 0, &element, -1);
+
+          if (ide_debugger_library_compare (library, element) == 0)
+            {
+              gtk_list_store_remove (self->list_store, &iter);
+              break;
+            }
+        }
+      while (gtk_tree_model_iter_next (model, &iter));
+    }
+}
+
+static void
+range_cell_data_func (GtkCellLayout   *cell_layout,
+                      GtkCellRenderer *cell,
+                      GtkTreeModel    *model,
+                      GtkTreeIter     *iter,
+                      gpointer         user_data)
+{
+  g_autoptr(IdeDebuggerLibrary) library = NULL;
+  g_autofree gchar *str = NULL;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+
+  gtk_tree_model_get (model, iter, 0, &library, -1);
+
+  if (library != NULL)
+    {
+      GPtrArray *ranges = ide_debugger_library_get_ranges (library);
+
+      if (ranges != NULL && ranges->len > 0)
+        {
+          IdeDebuggerAddressRange *range = g_ptr_array_index (ranges, 0);
+
+          str = g_strdup_printf ("0x%"G_GINT64_MODIFIER"x - 0x%"G_GINT64_MODIFIER"x",
+                                 range->from, range->to);
+        }
+    }
+
+  g_object_set (cell, "text", str, NULL);
+}
+
+static void
+string_property_cell_data_func (GtkCellLayout   *cell_layout,
+                                GtkCellRenderer *cell,
+                                GtkTreeModel    *model,
+                                GtkTreeIter     *iter,
+                                gpointer         user_data)
+{
+  const gchar *property = user_data;
+  g_autoptr(GObject) object = NULL;
+  g_auto(GValue) value = G_VALUE_INIT;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (property != NULL);
+
+  g_value_init (&value, G_TYPE_STRING);
+  gtk_tree_model_get (model, iter, 0, &object, -1);
+
+  if (object != NULL)
+    g_object_get_property (object, property, &value);
+
+  g_object_set_property (G_OBJECT (cell), "text", &value);
+}
+
+static void
+ide_debugger_libraries_view_destroy (GtkWidget *widget)
+{
+  IdeDebuggerLibrariesView *self = (IdeDebuggerLibrariesView *)widget;
+
+  g_clear_object (&self->debugger_signals);
+
+  GTK_WIDGET_CLASS (ide_debugger_libraries_view_parent_class)->destroy (widget);
+}
+
+static void
+ide_debugger_libraries_view_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  IdeDebuggerLibrariesView *self = IDE_DEBUGGER_LIBRARIES_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEBUGGER:
+      g_value_set_object (value, ide_debugger_libraries_view_get_debugger (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_debugger_libraries_view_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  IdeDebuggerLibrariesView *self = IDE_DEBUGGER_LIBRARIES_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEBUGGER:
+      ide_debugger_libraries_view_set_debugger (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_debugger_libraries_view_class_init (IdeDebuggerLibrariesViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_debugger_libraries_view_get_property;
+  object_class->set_property = ide_debugger_libraries_view_set_property;
+
+  widget_class->destroy = ide_debugger_libraries_view_destroy;
+
+  properties [PROP_DEBUGGER] =
+    g_param_spec_object ("debugger",
+                         "Debugger",
+                         "The debugger instance",
+                         IDE_TYPE_DEBUGGER,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/debuggerui/ide-debugger-libraries-view.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLibrariesView, tree_view);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLibrariesView, list_store);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLibrariesView, target_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLibrariesView, target_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLibrariesView, range_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLibrariesView, range_column);
+
+  g_type_ensure (IDE_TYPE_DEBUGGER_LIBRARY);
+}
+
+static void
+ide_debugger_libraries_view_init (IdeDebuggerLibrariesView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->debugger_signals = dzl_signal_group_new (IDE_TYPE_DEBUGGER);
+
+  g_signal_connect_swapped (self->debugger_signals,
+                            "bind",
+                            G_CALLBACK (ide_debugger_libraries_view_bind),
+                            self);
+
+  g_signal_connect_swapped (self->debugger_signals,
+                            "unbind",
+                            G_CALLBACK (ide_debugger_libraries_view_unbind),
+                            self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "running",
+                                    G_CALLBACK (ide_debugger_libraries_view_running),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "stopped",
+                                    G_CALLBACK (ide_debugger_libraries_view_stopped),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "library-loaded",
+                                    G_CALLBACK (ide_debugger_libraries_view_library_loaded),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "library-unloaded",
+                                    G_CALLBACK (ide_debugger_libraries_view_library_unloaded),
+                                    self);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->target_column),
+                                      GTK_CELL_RENDERER (self->target_cell),
+                                      string_property_cell_data_func, (gchar *)"target-name", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->range_column),
+                                      GTK_CELL_RENDERER (self->range_cell),
+                                      range_cell_data_func, NULL, NULL);
+}
+
+GtkWidget *
+ide_debugger_libraries_view_new (void)
+{
+  return g_object_new (IDE_TYPE_DEBUGGER_LIBRARIES_VIEW, NULL);
+}
+
+/**
+ * ide_debugger_libraries_view_get_debugger:
+ * @self: a #IdeDebuggerLibrariesView
+ *
+ * Gets the debugger property.
+ *
+ * Returns: (transfer none): An #IdeDebugger or %NULL.
+ *
+ * Since: 3.32
+ */
+IdeDebugger *
+ide_debugger_libraries_view_get_debugger (IdeDebuggerLibrariesView *self)
+{
+  g_return_val_if_fail (IDE_IS_DEBUGGER_LIBRARIES_VIEW (self), NULL);
+
+  if (self->debugger_signals != NULL)
+    return dzl_signal_group_get_target (self->debugger_signals);
+  return NULL;
+}
+
+void
+ide_debugger_libraries_view_set_debugger (IdeDebuggerLibrariesView *self,
+                                          IdeDebugger              *debugger)
+{
+  g_return_if_fail (IDE_IS_DEBUGGER_LIBRARIES_VIEW (self));
+  g_return_if_fail (!debugger || IDE_IS_DEBUGGER (debugger));
+
+  dzl_signal_group_set_target (self->debugger_signals, debugger);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DEBUGGER]);
+}
diff --git a/src/plugins/debuggerui/ide-debugger-libraries-view.h 
b/src/plugins/debuggerui/ide-debugger-libraries-view.h
new file mode 100644
index 000000000..797b3a521
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-libraries-view.h
@@ -0,0 +1,38 @@
+/* ide-debugger-libraries-view.h
+ *
+ * Copyright 2017-2019 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 <gtk/gtk.h>
+
+#include "ide-debugger.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_LIBRARIES_VIEW (ide_debugger_libraries_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerLibrariesView, ide_debugger_libraries_view, IDE, DEBUGGER_LIBRARIES_VIEW, 
GtkBin)
+
+GtkWidget   *ide_debugger_libraries_view_new          (void);
+IdeDebugger *ide_debugger_libraries_view_get_debugger (IdeDebuggerLibrariesView *self);
+void         ide_debugger_libraries_view_set_debugger (IdeDebuggerLibrariesView *self,
+                                                       IdeDebugger              *debugger);
+
+G_END_DECLS
diff --git a/src/libide/debugger/ide-debugger-libraries-view.ui 
b/src/plugins/debuggerui/ide-debugger-libraries-view.ui
similarity index 100%
rename from src/libide/debugger/ide-debugger-libraries-view.ui
rename to src/plugins/debuggerui/ide-debugger-libraries-view.ui
diff --git a/src/plugins/debuggerui/ide-debugger-locals-view.c 
b/src/plugins/debuggerui/ide-debugger-locals-view.c
new file mode 100644
index 000000000..4981d1e4e
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-locals-view.c
@@ -0,0 +1,445 @@
+/* ide-debugger-locals-view.c
+ *
+ * Copyright 2017-2019 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 "ide-debugger-locals-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-core.h>
+#include <libide-threading.h>
+#include <glib/gi18n.h>
+
+#include "ide-debugger-locals-view.h"
+
+struct _IdeDebuggerLocalsView
+{
+  GtkBin          parent_instance;
+
+  /* Owned references */
+  DzlSignalGroup *debugger_signals;
+
+  /* Template references */
+  GtkTreeStore        *tree_store;
+  GtkTreeView         *tree_view;
+  GtkTreeViewColumn   *type_column;
+  GtkCellRendererText *type_cell;
+  GtkTreeViewColumn   *variable_column;
+  GtkCellRendererText *variable_cell;
+  GtkTreeViewColumn   *value_column;
+  GtkCellRendererText *value_cell;
+};
+
+enum {
+  PROP_0,
+  PROP_DEBUGGER,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeDebuggerLocalsView, ide_debugger_locals_view, GTK_TYPE_BIN)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_debugger_locals_view_running (IdeDebuggerLocalsView *self,
+                                  IdeDebugger           *debugger)
+{
+  g_assert (IDE_IS_DEBUGGER_LOCALS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), FALSE);
+  gtk_tree_store_clear (self->tree_store);
+}
+
+static void
+ide_debugger_locals_view_stopped (IdeDebuggerLocalsView *self,
+                                  IdeDebuggerStopReason  stop_reason,
+                                  IdeDebuggerBreakpoint *breakpoint,
+                                  IdeDebugger           *debugger)
+{
+  g_assert (IDE_IS_DEBUGGER_LOCALS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_STOP_REASON (stop_reason));
+  g_assert (!breakpoint || IDE_IS_DEBUGGER_BREAKPOINT (breakpoint));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), TRUE);
+}
+
+static void
+name_cell_data_func (GtkCellLayout   *cell_layout,
+                     GtkCellRenderer *cell,
+                     GtkTreeModel    *model,
+                     GtkTreeIter     *iter,
+                     gpointer         user_data)
+{
+  g_autoptr(IdeDebuggerVariable) var = NULL;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+
+  gtk_tree_model_get (model, iter, 0, &var, -1);
+
+  if (var != NULL)
+    {
+      g_object_set (cell,
+                    "text", ide_debugger_variable_get_name (var),
+                    NULL);
+    }
+  else
+    {
+      g_autofree gchar *str = NULL;
+
+      gtk_tree_model_get (model, iter, 1, &str, -1);
+      g_object_set (cell, "text", str, NULL);
+    }
+}
+
+static void
+string_property_cell_data_func (GtkCellLayout   *cell_layout,
+                                GtkCellRenderer *cell,
+                                GtkTreeModel    *model,
+                                GtkTreeIter     *iter,
+                                gpointer         user_data)
+{
+  const gchar *property = user_data;
+  g_autoptr(GObject) object = NULL;
+  g_auto(GValue) value = G_VALUE_INIT;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (property != NULL);
+
+  g_value_init (&value, G_TYPE_STRING);
+  gtk_tree_model_get (model, iter, 0, &object, -1);
+
+  if (object != NULL)
+    g_object_get_property (object, property, &value);
+
+  g_object_set_property (G_OBJECT (cell), "text", &value);
+}
+
+static void
+ide_debugger_locals_view_finalize (GObject *object)
+{
+  IdeDebuggerLocalsView *self = (IdeDebuggerLocalsView *)object;
+
+  g_clear_object (&self->debugger_signals);
+
+  G_OBJECT_CLASS (ide_debugger_locals_view_parent_class)->finalize (object);
+}
+
+static void
+ide_debugger_locals_view_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  IdeDebuggerLocalsView *self = IDE_DEBUGGER_LOCALS_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEBUGGER:
+      g_value_set_object (value, ide_debugger_locals_view_get_debugger (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_debugger_locals_view_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  IdeDebuggerLocalsView *self = IDE_DEBUGGER_LOCALS_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEBUGGER:
+      ide_debugger_locals_view_set_debugger (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_debugger_locals_view_class_init (IdeDebuggerLocalsViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = ide_debugger_locals_view_finalize;
+  object_class->get_property = ide_debugger_locals_view_get_property;
+  object_class->set_property = ide_debugger_locals_view_set_property;
+
+  properties [PROP_DEBUGGER] =
+    g_param_spec_object ("debugger",
+                         "Debugger",
+                         "The debugger instance",
+                         IDE_TYPE_DEBUGGER,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/debuggerui/ide-debugger-locals-view.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLocalsView, tree_store);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLocalsView, tree_view);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLocalsView, type_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLocalsView, type_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLocalsView, value_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLocalsView, value_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLocalsView, variable_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerLocalsView, variable_column);
+}
+
+static void
+ide_debugger_locals_view_init (IdeDebuggerLocalsView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->debugger_signals = dzl_signal_group_new (IDE_TYPE_DEBUGGER);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "running",
+                                    G_CALLBACK (ide_debugger_locals_view_running),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "stopped",
+                                    G_CALLBACK (ide_debugger_locals_view_stopped),
+                                    self);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->variable_column),
+                                      GTK_CELL_RENDERER (self->variable_cell),
+                                      name_cell_data_func, NULL, NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->type_column),
+                                      GTK_CELL_RENDERER (self->type_cell),
+                                      string_property_cell_data_func, (gchar *)"type-name", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->value_column),
+                                      GTK_CELL_RENDERER (self->value_cell),
+                                      string_property_cell_data_func, (gchar *)"value", NULL);
+}
+
+GtkWidget *
+ide_debugger_locals_view_new (void)
+{
+  return g_object_new (IDE_TYPE_DEBUGGER_LOCALS_VIEW, NULL);
+}
+
+/**
+ * ide_debugger_locals_view_get_debugger:
+ * @self: a #IdeDebuggerLocalsView
+ *
+ * Gets the debugger instance.
+ *
+ * Returns: (transfer none): An #IdeDebugger
+ *
+ * Since: 3.32
+ */
+IdeDebugger *
+ide_debugger_locals_view_get_debugger (IdeDebuggerLocalsView *self)
+{
+  g_return_val_if_fail (IDE_IS_DEBUGGER_LOCALS_VIEW (self), NULL);
+
+  return dzl_signal_group_get_target (self->debugger_signals);
+}
+
+void
+ide_debugger_locals_view_set_debugger (IdeDebuggerLocalsView *self,
+                                       IdeDebugger           *debugger)
+{
+  g_return_if_fail (IDE_IS_DEBUGGER_LOCALS_VIEW (self));
+  g_return_if_fail (!debugger || IDE_IS_DEBUGGER (debugger));
+
+  dzl_signal_group_set_target (self->debugger_signals, debugger);
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DEBUGGER]);
+}
+
+static void
+ide_debugger_locals_view_load_locals_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  IdeDebuggerLocalsView *self;
+  IdeDebugger *debugger = (IdeDebugger *)object;
+  g_autoptr(GPtrArray) locals = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  GtkTreeIter parent;
+
+  g_assert (IDE_IS_DEBUGGER (debugger));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  locals = ide_debugger_list_locals_finish (debugger, result, &error);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (locals, g_object_unref);
+
+  if (locals == NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  self = ide_task_get_source_object (task);
+  g_assert (IDE_IS_DEBUGGER_LOCALS_VIEW (self));
+
+  gtk_tree_store_append (self->tree_store, &parent, NULL);
+  gtk_tree_store_set (self->tree_store, &parent, 1, _("Locals"), -1);
+
+  for (guint i = 0; i < locals->len; i++)
+    {
+      IdeDebuggerVariable *var = g_ptr_array_index (locals, i);
+      GtkTreeIter iter;
+
+      gtk_tree_store_append (self->tree_store, &iter, &parent);
+      gtk_tree_store_set (self->tree_store, &iter, 0, var, -1);
+
+      /* Add a deummy row that we can backfill when the user requests
+       * that the variable is expanded.
+       */
+      if (ide_debugger_variable_get_has_children (var))
+        {
+          GtkTreeIter dummy;
+
+          gtk_tree_store_append (self->tree_store, &dummy, &iter);
+        }
+    }
+
+  gtk_tree_view_expand_all (self->tree_view);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+ide_debugger_locals_view_load_params_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  g_autoptr(IdeDebuggerLocalsView) self = user_data;
+  IdeDebugger *debugger = (IdeDebugger *)object;
+  g_autoptr(GPtrArray) params = NULL;
+  g_autoptr(GError) error = NULL;
+  GtkTreeIter parent;
+
+  g_assert (IDE_IS_DEBUGGER (debugger));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_DEBUGGER_LOCALS_VIEW (self));
+
+  params = ide_debugger_list_params_finish (debugger, result, &error);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (params, g_object_unref);
+
+  if (params == NULL)
+    {
+      g_warning ("%s", error->message);
+      return;
+    }
+
+  /* Disposal check */
+  if (self->tree_store == NULL)
+    return;
+
+  gtk_tree_store_append (self->tree_store, &parent, NULL);
+  gtk_tree_store_set (self->tree_store, &parent, 1, _("Parameters"), -1);
+
+  for (guint i = 0; i < params->len; i++)
+    {
+      IdeDebuggerVariable *var = g_ptr_array_index (params, i);
+      GtkTreeIter iter;
+
+      gtk_tree_store_append (self->tree_store, &iter, &parent);
+      gtk_tree_store_set (self->tree_store, &iter, 0, var, -1);
+
+      /* Add a deummy row that we can backfill when the user requests
+       * that the variable is expanded.
+       */
+      if (ide_debugger_variable_get_has_children (var))
+        {
+          GtkTreeIter dummy;
+
+          gtk_tree_store_append (self->tree_store, &dummy, &iter);
+        }
+    }
+}
+
+void
+ide_debugger_locals_view_load_async (IdeDebuggerLocalsView *self,
+                                     IdeDebuggerThread     *thread,
+                                     IdeDebuggerFrame      *frame,
+                                     GCancellable          *cancellable,
+                                     GAsyncReadyCallback    callback,
+                                     gpointer               user_data)
+{
+  IdeDebugger *debugger;
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_DEBUGGER_LOCALS_VIEW (self));
+  g_return_if_fail (IDE_IS_DEBUGGER_THREAD (thread));
+  g_return_if_fail (IDE_IS_DEBUGGER_FRAME (frame));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  gtk_tree_store_clear (self->tree_store);
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_source_tag (task, ide_debugger_locals_view_load_async);
+
+  debugger = ide_debugger_locals_view_get_debugger (self);
+
+  if (debugger == NULL)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  ide_debugger_list_params_async (debugger,
+                                  thread,
+                                  frame,
+                                  cancellable,
+                                  ide_debugger_locals_view_load_params_cb,
+                                  g_object_ref (self));
+
+  ide_debugger_list_locals_async (debugger,
+                                  thread,
+                                  frame,
+                                  cancellable,
+                                  ide_debugger_locals_view_load_locals_cb,
+                                  g_steal_pointer (&task));
+}
+
+gboolean
+ide_debugger_locals_view_load_finish (IdeDebuggerLocalsView  *self,
+                                      GAsyncResult           *result,
+                                      GError                **error)
+{
+  g_return_val_if_fail (IDE_IS_DEBUGGER_LOCALS_VIEW (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
diff --git a/src/plugins/debuggerui/ide-debugger-locals-view.h 
b/src/plugins/debuggerui/ide-debugger-locals-view.h
new file mode 100644
index 000000000..c29887f6e
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-locals-view.h
@@ -0,0 +1,47 @@
+/* ide-debugger-locals-view.h
+ *
+ * Copyright 2017-2019 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 <gtk/gtk.h>
+
+#include "ide-debugger.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_LOCALS_VIEW (ide_debugger_locals_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerLocalsView, ide_debugger_locals_view, IDE, DEBUGGER_LOCALS_VIEW, GtkBin)
+
+GtkWidget   *ide_debugger_locals_view_new          (void);
+IdeDebugger *ide_debugger_locals_view_get_debugger (IdeDebuggerLocalsView  *self);
+void         ide_debugger_locals_view_set_debugger (IdeDebuggerLocalsView  *self,
+                                                    IdeDebugger            *debugger);
+void         ide_debugger_locals_view_load_async   (IdeDebuggerLocalsView  *self,
+                                                    IdeDebuggerThread      *thread,
+                                                    IdeDebuggerFrame       *frame,
+                                                    GCancellable           *cancellable,
+                                                    GAsyncReadyCallback     callback,
+                                                    gpointer                user_data);
+gboolean     ide_debugger_locals_view_load_finish  (IdeDebuggerLocalsView  *self,
+                                                    GAsyncResult           *result,
+                                                    GError                **error);
+
+G_END_DECLS
diff --git a/src/libide/debugger/ide-debugger-locals-view.ui 
b/src/plugins/debuggerui/ide-debugger-locals-view.ui
similarity index 100%
rename from src/libide/debugger/ide-debugger-locals-view.ui
rename to src/plugins/debuggerui/ide-debugger-locals-view.ui
diff --git a/src/plugins/debuggerui/ide-debugger-registers-view.c 
b/src/plugins/debuggerui/ide-debugger-registers-view.c
new file mode 100644
index 000000000..9e5fce660
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-registers-view.c
@@ -0,0 +1,334 @@
+/* ide-debugger-registers-view.c
+ *
+ * Copyright 2017-2019 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 "ide-debugger-registers-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-core.h>
+
+#include "ide-debugger-registers-view.h"
+
+struct _IdeDebuggerRegistersView
+{
+  GtkBin          parent_instance;
+
+  /* Owned references */
+  DzlSignalGroup      *debugger_signals;
+
+  /* Template references */
+  GtkTreeView         *tree_view;
+  GtkListStore        *list_store;
+  GtkCellRendererText *id_cell;
+  GtkCellRendererText *name_cell;
+  GtkCellRendererText *value_cell;
+  GtkTreeViewColumn   *id_column;
+  GtkTreeViewColumn   *name_column;
+  GtkTreeViewColumn   *value_column;
+};
+
+enum {
+  PROP_0,
+  PROP_DEBUGGER,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (IdeDebuggerRegistersView, ide_debugger_registers_view, GTK_TYPE_BIN)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+ide_debugger_registers_view_bind (IdeDebuggerRegistersView *self,
+                                  IdeDebugger              *debugger,
+                                  DzlSignalGroup           *signals)
+{
+  g_assert (IDE_IS_DEBUGGER_REGISTERS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+  g_assert (DZL_IS_SIGNAL_GROUP (signals));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view),
+                            !ide_debugger_get_is_running (debugger));
+}
+
+static void
+ide_debugger_registers_view_unbind (IdeDebuggerRegistersView *self,
+                                    DzlSignalGroup           *signals)
+{
+  g_assert (IDE_IS_DEBUGGER_REGISTERS_VIEW (self));
+  g_assert (DZL_IS_SIGNAL_GROUP (signals));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), FALSE);
+}
+
+static void
+ide_debugger_registers_view_running (IdeDebuggerRegistersView *self,
+                                     IdeDebugger              *debugger)
+{
+  g_assert (IDE_IS_DEBUGGER_REGISTERS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), FALSE);
+}
+
+static void
+ide_debugger_registers_view_list_registers_cb (GObject      *object,
+                                               GAsyncResult *result,
+                                               gpointer      user_data)
+{
+  IdeDebugger *debugger = (IdeDebugger *)object;
+  g_autoptr(IdeDebuggerRegistersView) self = user_data;
+  g_autoptr(GPtrArray) registers = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_DEBUGGER (debugger));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_DEBUGGER_REGISTERS_VIEW (self));
+
+  gtk_list_store_clear (self->list_store);
+
+  registers = ide_debugger_list_registers_finish (debugger, result, &error);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (registers, g_object_unref);
+
+  if (error != NULL)
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+        g_warning ("%s", error->message);
+      return;
+    }
+
+  if (registers != NULL)
+    {
+      for (guint i = 0; i < registers->len; i++)
+        {
+          IdeDebuggerRegister *reg = g_ptr_array_index (registers, i);
+          GtkTreeIter iter;
+
+          dzl_gtk_list_store_insert_sorted (self->list_store, &iter, reg, 0,
+                                            (GCompareDataFunc)ide_debugger_register_compare,
+                                            NULL);
+          gtk_list_store_set (self->list_store, &iter, 0, reg, -1);
+        }
+    }
+}
+
+static void
+ide_debugger_registers_view_stopped (IdeDebuggerRegistersView *self,
+                                     IdeDebuggerStopReason     stop_reason,
+                                     IdeDebuggerBreakpoint    *breakpoint,
+                                     IdeDebugger              *debugger)
+{
+  g_assert (IDE_IS_DEBUGGER_REGISTERS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_STOP_REASON (stop_reason));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  ide_debugger_list_registers_async (debugger,
+                                     NULL,
+                                     ide_debugger_registers_view_list_registers_cb,
+                                     g_object_ref (self));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->tree_view), TRUE);
+}
+
+static void
+string_property_cell_data_func (GtkCellLayout   *cell_layout,
+                                GtkCellRenderer *cell,
+                                GtkTreeModel    *model,
+                                GtkTreeIter     *iter,
+                                gpointer         user_data)
+{
+  const gchar *property = user_data;
+  g_autoptr(GObject) object = NULL;
+  g_auto(GValue) value = G_VALUE_INIT;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (property != NULL);
+
+  g_value_init (&value, G_TYPE_STRING);
+  gtk_tree_model_get (model, iter, 0, &object, -1);
+  if (object != NULL)
+    g_object_get_property (object, property, &value);
+  g_object_set_property (G_OBJECT (cell), "text", &value);
+}
+
+static void
+ide_debugger_registers_view_destroy (GtkWidget *widget)
+{
+  IdeDebuggerRegistersView *self = (IdeDebuggerRegistersView *)widget;
+
+  g_clear_object (&self->debugger_signals);
+
+  GTK_WIDGET_CLASS (ide_debugger_registers_view_parent_class)->destroy (widget);
+}
+
+static void
+ide_debugger_registers_view_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  IdeDebuggerRegistersView *self = IDE_DEBUGGER_REGISTERS_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEBUGGER:
+      g_value_set_object (value, ide_debugger_registers_view_get_debugger (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_debugger_registers_view_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  IdeDebuggerRegistersView *self = IDE_DEBUGGER_REGISTERS_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEBUGGER:
+      ide_debugger_registers_view_set_debugger (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_debugger_registers_view_class_init (IdeDebuggerRegistersViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->get_property = ide_debugger_registers_view_get_property;
+  object_class->set_property = ide_debugger_registers_view_set_property;
+
+  widget_class->destroy = ide_debugger_registers_view_destroy;
+
+  properties [PROP_DEBUGGER] =
+    g_param_spec_object ("debugger",
+                         "Debugger",
+                         "The debugger instance",
+                         IDE_TYPE_DEBUGGER,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/debuggerui/ide-debugger-registers-view.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerRegistersView, id_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerRegistersView, id_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerRegistersView, list_store);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerRegistersView, name_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerRegistersView, name_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerRegistersView, tree_view);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerRegistersView, value_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerRegistersView, value_column);
+
+  g_type_ensure (IDE_TYPE_DEBUGGER_REGISTER);
+}
+
+static void
+ide_debugger_registers_view_init (IdeDebuggerRegistersView *self)
+{
+  self->debugger_signals = dzl_signal_group_new (IDE_TYPE_DEBUGGER);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_swapped (self->debugger_signals,
+                            "bind",
+                            G_CALLBACK (ide_debugger_registers_view_bind),
+                            self);
+
+  g_signal_connect_swapped (self->debugger_signals,
+                            "unbind",
+                            G_CALLBACK (ide_debugger_registers_view_unbind),
+                            self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "running",
+                                    G_CALLBACK (ide_debugger_registers_view_running),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "stopped",
+                                    G_CALLBACK (ide_debugger_registers_view_stopped),
+                                    self);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->id_column),
+                                      GTK_CELL_RENDERER (self->id_cell),
+                                      string_property_cell_data_func, (gchar *)"id", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->name_column),
+                                      GTK_CELL_RENDERER (self->name_cell),
+                                      string_property_cell_data_func, (gchar *)"name", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->value_column),
+                                      GTK_CELL_RENDERER (self->value_cell),
+                                      string_property_cell_data_func, (gchar *)"value", NULL);
+}
+
+GtkWidget *
+ide_debugger_registers_view_new (void)
+{
+  return g_object_new (IDE_TYPE_DEBUGGER_REGISTERS_VIEW, NULL);
+}
+
+/**
+ * ide_debugger_registers_view_get_debugger:
+ * @self: a #IdeDebuggerRegistersView
+ *
+ *
+ *
+ * Returns: (transfer none) (nullable): An #IdeDebugger or %NULL
+ *
+ * Since: 3.32
+ */
+IdeDebugger *
+ide_debugger_registers_view_get_debugger (IdeDebuggerRegistersView *self)
+{
+  g_return_val_if_fail (IDE_IS_DEBUGGER_REGISTERS_VIEW (self), NULL);
+
+  if (self->debugger_signals != NULL)
+    return dzl_signal_group_get_target (self->debugger_signals);
+
+  return NULL;
+}
+
+void
+ide_debugger_registers_view_set_debugger (IdeDebuggerRegistersView *self,
+                                          IdeDebugger              *debugger)
+{
+  g_return_if_fail (IDE_IS_DEBUGGER_REGISTERS_VIEW (self));
+  g_return_if_fail (!debugger || IDE_IS_DEBUGGER (debugger));
+
+  if (self->debugger_signals != NULL)
+    {
+      dzl_signal_group_set_target (self->debugger_signals, debugger);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DEBUGGER]);
+    }
+}
diff --git a/src/plugins/debuggerui/ide-debugger-registers-view.h 
b/src/plugins/debuggerui/ide-debugger-registers-view.h
new file mode 100644
index 000000000..45b989503
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-registers-view.h
@@ -0,0 +1,38 @@
+/* ide-debugger-registers-view.h
+ *
+ * Copyright 2017-2019 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 <gtk/gtk.h>
+
+#include "ide-debugger.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_REGISTERS_VIEW (ide_debugger_registers_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerRegistersView, ide_debugger_registers_view, IDE, DEBUGGER_REGISTERS_VIEW, 
GtkBin)
+
+GtkWidget   *ide_debugger_registers_view_new          (void);
+IdeDebugger *ide_debugger_registers_view_get_debugger (IdeDebuggerRegistersView *self);
+void         ide_debugger_registers_view_set_debugger (IdeDebuggerRegistersView *self,
+                                                       IdeDebugger              *debugger);
+
+G_END_DECLS
diff --git a/src/libide/debugger/ide-debugger-registers-view.ui 
b/src/plugins/debuggerui/ide-debugger-registers-view.ui
similarity index 100%
rename from src/libide/debugger/ide-debugger-registers-view.ui
rename to src/plugins/debuggerui/ide-debugger-registers-view.ui
diff --git a/src/plugins/debuggerui/ide-debugger-threads-view.c 
b/src/plugins/debuggerui/ide-debugger-threads-view.c
new file mode 100644
index 000000000..acdf24af9
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-threads-view.c
@@ -0,0 +1,831 @@
+/* ide-debugger-threads-view.c
+ *
+ * Copyright 2017-2019 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 "ide-debugger-threads-view"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <libide-core.h>
+#include <libide-gui.h>
+#include <glib/gi18n.h>
+
+#include "ide-debugger-threads-view.h"
+
+struct _IdeDebuggerThreadsView
+{
+  GtkBin               parent_instance;
+
+  /* Owned references */
+  DzlSignalGroup      *debugger_signals;
+
+  /* Template References */
+  GtkTreeView         *frames_tree_view;
+  GtkTreeView         *thread_groups_tree_view;
+  GtkTreeView         *threads_tree_view;
+  GtkListStore        *frames_store;
+  GtkListStore        *thread_groups_store;
+  GtkListStore        *threads_store;
+  GtkTreeViewColumn   *args_column;
+  GtkTreeViewColumn   *binary_column;
+  GtkTreeViewColumn   *depth_column;
+  GtkTreeViewColumn   *group_column;
+  GtkTreeViewColumn   *location_column;
+  GtkTreeViewColumn   *thread_column;
+  GtkTreeViewColumn   *function_column;
+  GtkCellRendererText *args_cell;
+  GtkCellRendererText *binary_cell;
+  GtkCellRendererText *depth_cell;
+  GtkCellRendererText *group_cell;
+  GtkCellRendererText *location_cell;
+  GtkCellRendererText *thread_cell;
+  GtkCellRendererText *function_cell;
+};
+
+enum {
+  PROP_0,
+  PROP_DEBUGGER,
+  N_PROPS
+};
+
+enum {
+  FRAME_ACTIVATED,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE (IdeDebuggerThreadsView, ide_debugger_threads_view, GTK_TYPE_BIN)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+static IdeDebuggerThread *
+ide_debugger_threads_view_get_current_thread (IdeDebuggerThreadsView *self)
+{
+  g_autoptr(IdeDebuggerThread) thread = NULL;
+  GtkTreeSelection *selection;
+  GtkTreeModel *model = NULL;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+
+  selection = gtk_tree_view_get_selection (self->threads_tree_view);
+  if (gtk_tree_selection_get_selected (selection, &model, &iter))
+    gtk_tree_model_get (model, &iter, 0, &thread, -1);
+
+  return g_steal_pointer (&thread);
+}
+
+static void
+ide_debugger_threads_view_running (IdeDebuggerThreadsView *self,
+                                   IdeDebugger            *debugger)
+{
+  GtkTreeSelection *selection;
+
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  gtk_list_store_clear (self->frames_store);
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->frames_tree_view), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->thread_groups_tree_view), FALSE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->threads_tree_view), FALSE);
+
+  selection = gtk_tree_view_get_selection (self->threads_tree_view);
+  gtk_tree_selection_unselect_all (selection);
+}
+
+static void
+ide_debugger_threads_view_stopped (IdeDebuggerThreadsView *self,
+                                   IdeDebuggerStopReason   stop_reason,
+                                   IdeDebuggerBreakpoint  *breakpoint,
+                                   IdeDebugger            *debugger)
+{
+  IdeDebuggerThread *selected;
+  GtkTreeSelection *selection;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_STOP_REASON (stop_reason));
+  g_assert (!breakpoint || IDE_IS_DEBUGGER_BREAKPOINT (breakpoint));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->frames_tree_view), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->thread_groups_tree_view), TRUE);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->threads_tree_view), TRUE);
+
+  selected = ide_debugger_get_selected_thread (debugger);
+
+  if (selected != NULL)
+    {
+      model = GTK_TREE_MODEL (self->threads_store);
+
+      if (gtk_tree_model_get_iter_first (model, &iter))
+        {
+          do
+            {
+              g_autoptr(IdeDebuggerThread) thread = NULL;
+
+              gtk_tree_model_get (model, &iter, 0, &thread, -1);
+
+              if (ide_debugger_thread_compare (thread, selected) == 0)
+                {
+                  GtkTreePath *path;
+
+                  selection = gtk_tree_view_get_selection (self->threads_tree_view);
+                  gtk_tree_selection_select_iter (selection, &iter);
+
+                  path = gtk_tree_model_get_path (model, &iter);
+                  gtk_tree_view_row_activated (self->threads_tree_view, path, self->thread_column);
+                  gtk_tree_path_free (path);
+
+                  break;
+                }
+            }
+          while (gtk_tree_model_iter_next (model, &iter));
+        }
+    }
+}
+
+static void
+ide_debugger_threads_view_thread_group_added (IdeDebuggerThreadsView *self,
+                                              IdeDebuggerThreadGroup *group,
+                                              IdeDebugger            *debugger)
+{
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_THREAD_GROUP (group));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  dzl_gtk_list_store_insert_sorted (self->thread_groups_store,
+                                    &iter, group, 0,
+                                    (GCompareDataFunc)ide_debugger_thread_group_compare,
+                                    NULL);
+  gtk_list_store_set (self->thread_groups_store, &iter, 0, group, -1);
+}
+
+static void
+ide_debugger_threads_view_thread_group_removed (IdeDebuggerThreadsView *self,
+                                                IdeDebuggerThreadGroup *group,
+                                                IdeDebugger            *debugger)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_THREAD_GROUP (group));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  model = GTK_TREE_MODEL (self->thread_groups_store);
+
+  if (gtk_tree_model_get_iter_first (model, &iter))
+    {
+      do
+        {
+          g_autoptr(IdeDebuggerThreadGroup) row = NULL;
+
+          gtk_tree_model_get (model, &iter, 0, &row, -1);
+
+          if (ide_debugger_thread_group_compare (row, group) == 0)
+            {
+              gtk_list_store_remove (self->thread_groups_store, &iter);
+              break;
+            }
+        }
+      while (gtk_tree_model_iter_next (model, &iter));
+    }
+}
+
+static void
+ide_debugger_threads_view_thread_added (IdeDebuggerThreadsView *self,
+                                        IdeDebuggerThread      *thread,
+                                        IdeDebugger            *debugger)
+{
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_THREAD (thread));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  dzl_gtk_list_store_insert_sorted (self->threads_store,
+                                    &iter, thread, 0,
+                                    (GCompareDataFunc)ide_debugger_thread_compare,
+                                    NULL);
+  gtk_list_store_set (self->threads_store, &iter, 0, thread, -1);
+}
+
+static void
+ide_debugger_threads_view_thread_removed (IdeDebuggerThreadsView *self,
+                                          IdeDebuggerThread      *thread,
+                                          IdeDebugger            *debugger)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER_THREAD (thread));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+
+  model = GTK_TREE_MODEL (self->threads_store);
+
+  if (gtk_tree_model_get_iter_first (model, &iter))
+    {
+      do
+        {
+          g_autoptr(IdeDebuggerThread) row = NULL;
+
+          gtk_tree_model_get (model, &iter, 0, &row, -1);
+
+          if (ide_debugger_thread_compare (row, thread) == 0)
+            {
+              gtk_list_store_remove (self->threads_store, &iter);
+              break;
+            }
+        }
+      while (gtk_tree_model_iter_next (model, &iter));
+    }
+}
+
+static void
+ide_debugger_threads_view_list_frames_cb (GObject      *object,
+                                          GAsyncResult *result,
+                                          gpointer      user_data)
+{
+  IdeDebugger *debugger = (IdeDebugger *)object;
+  g_autoptr(IdeDebuggerThreadsView) self = user_data;
+  g_autoptr(GPtrArray) frames = NULL;
+  g_autoptr(GError) error = NULL;
+  GtkTreeSelection *selection;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER (debugger));
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  frames = ide_debugger_list_frames_finish (debugger, result, &error);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (frames, g_object_unref);
+
+  if (frames == NULL)
+    {
+      g_warning ("%s", error->message);
+      return;
+    }
+
+  gtk_list_store_clear (self->frames_store);
+
+  for (guint i = 0; i < frames->len; i++)
+    {
+      IdeDebuggerFrame *frame = g_ptr_array_index (frames, i);
+
+      g_assert (IDE_IS_DEBUGGER_FRAME (frame));
+
+      gtk_list_store_append (self->frames_store, &iter);
+      gtk_list_store_set (self->frames_store, &iter, 0, frame, -1);
+    }
+
+  if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (self->frames_store), &iter))
+    {
+      GtkTreePath *path;
+
+      selection = gtk_tree_view_get_selection (self->frames_tree_view);
+      gtk_tree_selection_select_iter (selection, &iter);
+
+      path = gtk_tree_model_get_path (GTK_TREE_MODEL (self->frames_store), &iter);
+      gtk_tree_view_row_activated (self->frames_tree_view, path, self->depth_column);
+      gtk_tree_path_free (path);
+    }
+}
+
+static void
+ide_debugger_threads_view_bind (IdeDebuggerThreadsView *self,
+                                IdeDebugger            *debugger,
+                                DzlSignalGroup         *debugger_signals)
+{
+  GListModel *thread_groups;
+  GListModel *threads;
+  guint n_items;
+
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (IDE_IS_DEBUGGER (debugger));
+  g_assert (DZL_IS_SIGNAL_GROUP (debugger_signals));
+
+  /* Add any thread groups already loaded by the debugger */
+
+  thread_groups = ide_debugger_get_thread_groups (debugger);
+  n_items = g_list_model_get_n_items (thread_groups);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeDebuggerThreadGroup) group = NULL;
+
+      group = g_list_model_get_item (thread_groups, i);
+      ide_debugger_threads_view_thread_group_added (self, group, debugger);
+    }
+
+  /* Add any threads already loaded by the debugger */
+
+  threads = ide_debugger_get_threads (debugger);
+  n_items = g_list_model_get_n_items (threads);
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeDebuggerThread) thread = NULL;
+
+      thread = g_list_model_get_item (threads, i);
+      ide_debugger_threads_view_thread_added (self, thread, debugger);
+    }
+}
+
+static void
+ide_debugger_threads_view_unbind (IdeDebuggerThreadsView *self,
+                                  DzlSignalGroup         *debugger_signals)
+{
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (DZL_IS_SIGNAL_GROUP (debugger_signals));
+
+  gtk_list_store_clear (self->thread_groups_store);
+  gtk_list_store_clear (self->threads_store);
+  gtk_list_store_clear (self->frames_store);
+}
+
+static void
+argv_property_cell_data_func (GtkCellLayout   *cell_layout,
+                              GtkCellRenderer *cell,
+                              GtkTreeModel    *model,
+                              GtkTreeIter     *iter,
+                              gpointer         user_data)
+{
+  const gchar *property = user_data;
+  g_autoptr(GObject) object = NULL;
+  g_auto(GStrv) strv = NULL;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (property != NULL);
+
+  gtk_tree_model_get (model, iter, 0, &object, -1);
+
+  if (object != NULL)
+    {
+      g_object_get (object, property, &strv, NULL);
+
+      if (strv != NULL)
+        {
+          g_autoptr(GString) str = g_string_new (NULL);
+
+          g_string_append_c (str, '(');
+          for (guint i = 0; strv[i]; i++)
+            {
+              g_string_append (str, strv[i]);
+              if (strv[i+1])
+                g_string_append (str, ", ");
+            }
+          g_string_append_c (str, ')');
+
+          g_object_set (cell, "text", str->str, NULL);
+
+          return;
+        }
+    }
+
+  g_object_set (cell, "text", "", NULL);
+}
+
+static void
+location_property_cell_data_func (GtkCellLayout   *cell_layout,
+                                  GtkCellRenderer *cell,
+                                  GtkTreeModel    *model,
+                                  GtkTreeIter     *iter,
+                                  gpointer         user_data)
+{
+  g_autoptr(IdeDebuggerFrame) frame = NULL;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+
+  gtk_tree_model_get (model, iter, 0, &frame, -1);
+
+  if (frame != NULL)
+    {
+      const gchar *file = ide_debugger_frame_get_file (frame);
+      guint line = ide_debugger_frame_get_line (frame);
+
+      if (file != NULL)
+        {
+          if (line != 0)
+            {
+              g_autofree gchar *text = NULL;
+
+              text = g_strdup_printf ("%s<span fgalpha='32767'>:%u</span>", file, line);
+              g_object_set (cell, "markup", text, NULL);
+            }
+          else
+            {
+              g_object_set (cell, "text", file, NULL);
+            }
+
+          return;
+        }
+    }
+
+  g_object_set (cell, "text", NULL, NULL);
+}
+
+static void
+binary_property_cell_data_func (GtkCellLayout   *cell_layout,
+                                GtkCellRenderer *cell,
+                                GtkTreeModel    *model,
+                                GtkTreeIter     *iter,
+                                gpointer         user_data)
+{
+  IdeDebuggerThreadsView *self = user_data;
+  IdeDebugger *debugger;
+  g_autoptr(IdeDebuggerFrame) frame = NULL;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+
+  debugger = dzl_signal_group_get_target (self->debugger_signals);
+  if (debugger == NULL)
+    return;
+
+  gtk_tree_model_get (model, iter, 0, &frame, -1);
+
+  if (frame != NULL)
+    {
+      IdeDebuggerAddress address;
+      const gchar *name;
+
+      address = ide_debugger_frame_get_address (frame);
+      name = ide_debugger_locate_binary_at_address (debugger, address);
+      g_object_set (cell, "text", name, NULL);
+      return;
+    }
+
+  g_object_set (cell, "text", NULL, NULL);
+}
+
+static void
+string_property_cell_data_func (GtkCellLayout   *cell_layout,
+                                GtkCellRenderer *cell,
+                                GtkTreeModel    *model,
+                                GtkTreeIter     *iter,
+                                gpointer         user_data)
+{
+  const gchar *property = user_data;
+  g_autoptr(GObject) object = NULL;
+  g_auto(GValue) value = G_VALUE_INIT;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (property != NULL);
+
+  g_value_init (&value, G_TYPE_STRING);
+  gtk_tree_model_get (model, iter, 0, &object, -1);
+
+  if (object != NULL)
+    g_object_get_property (object, property, &value);
+
+  g_object_set_property (G_OBJECT (cell), "text", &value);
+}
+
+static void
+int_property_cell_data_func (GtkCellLayout   *cell_layout,
+                             GtkCellRenderer *cell,
+                             GtkTreeModel    *model,
+                             GtkTreeIter     *iter,
+                             gpointer         user_data)
+{
+  const gchar *property = user_data;
+  g_autoptr(GObject) object = NULL;
+  g_auto(GValue) value = G_VALUE_INIT;
+  g_autofree gchar *str = NULL;
+
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (cell_layout));
+  g_assert (GTK_IS_CELL_RENDERER_TEXT (cell));
+  g_assert (GTK_IS_TREE_MODEL (model));
+  g_assert (iter != NULL);
+  g_assert (property != NULL);
+
+  g_value_init (&value, G_TYPE_INT64);
+  gtk_tree_model_get (model, iter, 0, &object, -1);
+
+  if (object != NULL)
+    g_object_get_property (object, property, &value);
+
+  str = g_strdup_printf ("%"G_GINT64_FORMAT, g_value_get_int64 (&value));
+  g_object_set (cell, "text", str, NULL);
+}
+
+static void
+ide_debugger_threads_view_threads_row_activated (IdeDebuggerThreadsView *self,
+                                                 GtkTreePath            *path,
+                                                 GtkTreeViewColumn      *column,
+                                                 GtkTreeView            *tree_view)
+{
+  IdeDebugger *debugger;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (path != NULL);
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  model = gtk_tree_view_get_model (tree_view);
+  debugger = dzl_signal_group_get_target (self->debugger_signals);
+
+  if (debugger == NULL)
+    return;
+
+  if (gtk_tree_model_get_iter (model, &iter, path))
+    {
+      g_autoptr(IdeDebuggerThread) thread = NULL;
+
+      gtk_tree_model_get (model, &iter, 0, &thread, -1);
+      g_assert (!thread || IDE_IS_DEBUGGER_THREAD (thread));
+
+      if (thread != NULL)
+        {
+          ide_debugger_list_frames_async (debugger,
+                                          thread,
+                                          NULL,
+                                          ide_debugger_threads_view_list_frames_cb,
+                                          g_object_ref (self));
+        }
+    }
+}
+
+static void
+ide_debugger_threads_view_frames_row_activated (IdeDebuggerThreadsView *self,
+                                                GtkTreePath            *path,
+                                                GtkTreeViewColumn      *column,
+                                                GtkTreeView            *tree_view)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_assert (path != NULL);
+  g_assert (GTK_IS_TREE_VIEW_COLUMN (column));
+  g_assert (GTK_IS_TREE_VIEW (tree_view));
+
+  model = gtk_tree_view_get_model (tree_view);
+
+  if (gtk_tree_model_get_iter (model, &iter, path))
+    {
+      g_autoptr(IdeDebuggerFrame) frame = NULL;
+
+      gtk_tree_model_get (model, &iter, 0, &frame, -1);
+
+      if (frame != NULL)
+        {
+          g_autoptr(IdeDebuggerThread) thread = NULL;
+
+          thread = ide_debugger_threads_view_get_current_thread (self);
+          if (thread != NULL && frame != NULL)
+            g_signal_emit (self, signals [FRAME_ACTIVATED], 0, thread, frame);
+        }
+    }
+}
+
+static void
+ide_debugger_threads_view_dispose (GObject *object)
+{
+  IdeDebuggerThreadsView *self = (IdeDebuggerThreadsView *)object;
+
+  g_clear_object (&self->debugger_signals);
+
+  G_OBJECT_CLASS (ide_debugger_threads_view_parent_class)->dispose (object);
+}
+
+static void
+ide_debugger_threads_view_get_property (GObject    *object,
+                                        guint       prop_id,
+                                        GValue     *value,
+                                        GParamSpec *pspec)
+{
+  IdeDebuggerThreadsView *self = IDE_DEBUGGER_THREADS_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEBUGGER:
+      g_value_set_object (value, ide_debugger_threads_view_get_debugger (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_debugger_threads_view_set_property (GObject      *object,
+                                        guint         prop_id,
+                                        const GValue *value,
+                                        GParamSpec   *pspec)
+{
+  IdeDebuggerThreadsView *self = IDE_DEBUGGER_THREADS_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_DEBUGGER:
+      ide_debugger_threads_view_set_debugger (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+ide_debugger_threads_view_class_init (IdeDebuggerThreadsViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = ide_debugger_threads_view_dispose;
+  object_class->get_property = ide_debugger_threads_view_get_property;
+  object_class->set_property = ide_debugger_threads_view_set_property;
+
+  properties [PROP_DEBUGGER] =
+    g_param_spec_object ("debugger",
+                         "Debugger",
+                         "Debugger",
+                         IDE_TYPE_DEBUGGER,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [FRAME_ACTIVATED] =
+    g_signal_new ("frame-activated",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0, NULL, NULL, NULL,
+                  G_TYPE_NONE, 2, IDE_TYPE_DEBUGGER_THREAD, IDE_TYPE_DEBUGGER_FRAME);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/debuggerui/ide-debugger-threads-view.ui");
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, args_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, args_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, binary_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, binary_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, depth_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, depth_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, frames_store);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, frames_tree_view);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, function_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, function_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, group_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, group_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, location_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, location_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, thread_cell);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, thread_column);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, thread_groups_store);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, thread_groups_tree_view);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, threads_store);
+  gtk_widget_class_bind_template_child (widget_class, IdeDebuggerThreadsView, threads_tree_view);
+
+  g_type_ensure (IDE_TYPE_DEBUGGER_FRAME);
+  g_type_ensure (IDE_TYPE_DEBUGGER_THREAD);
+  g_type_ensure (IDE_TYPE_DEBUGGER_THREAD_GROUP);
+}
+
+static void
+ide_debugger_threads_view_init (IdeDebuggerThreadsView *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->debugger_signals = dzl_signal_group_new (IDE_TYPE_DEBUGGER);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "running",
+                                    G_CALLBACK (ide_debugger_threads_view_running),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "stopped",
+                                    G_CALLBACK (ide_debugger_threads_view_stopped),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "thread-group-added",
+                                    G_CALLBACK (ide_debugger_threads_view_thread_group_added),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "thread-group-removed",
+                                    G_CALLBACK (ide_debugger_threads_view_thread_group_removed),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "thread-added",
+                                    G_CALLBACK (ide_debugger_threads_view_thread_added),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->debugger_signals,
+                                    "thread-removed",
+                                    G_CALLBACK (ide_debugger_threads_view_thread_removed),
+                                    self);
+
+  g_signal_connect_swapped (self->debugger_signals,
+                            "bind",
+                            G_CALLBACK (ide_debugger_threads_view_bind),
+                            self);
+
+  g_signal_connect_swapped (self->debugger_signals,
+                            "unbind",
+                            G_CALLBACK (ide_debugger_threads_view_unbind),
+                            self);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->group_column),
+                                      GTK_CELL_RENDERER (self->group_cell),
+                                      string_property_cell_data_func, (gchar *)"id", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->thread_column),
+                                      GTK_CELL_RENDERER (self->thread_cell),
+                                      string_property_cell_data_func, (gchar *)"id", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->depth_column),
+                                      GTK_CELL_RENDERER (self->depth_cell),
+                                      int_property_cell_data_func, (gchar *)"depth", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->function_column),
+                                      GTK_CELL_RENDERER (self->function_cell),
+                                      string_property_cell_data_func, (gchar *)"function", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->args_column),
+                                      GTK_CELL_RENDERER (self->args_cell),
+                                      argv_property_cell_data_func, (gchar *)"args", NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->location_column),
+                                      GTK_CELL_RENDERER (self->location_cell),
+                                      location_property_cell_data_func, (gchar *)NULL, NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->binary_column),
+                                      GTK_CELL_RENDERER (self->binary_cell),
+                                      binary_property_cell_data_func, self, NULL);
+
+  g_signal_connect_swapped (self->threads_tree_view,
+                            "row-activated",
+                            G_CALLBACK (ide_debugger_threads_view_threads_row_activated),
+                            self);
+
+  g_signal_connect_swapped (self->frames_tree_view,
+                            "row-activated",
+                            G_CALLBACK (ide_debugger_threads_view_frames_row_activated),
+                            self);
+}
+
+/**
+ * ide_debugger_threads_view_get_debugger:
+ * @self: a #IdeDebuggerThreadsView
+ *
+ * Gets the debugger that is being observed.
+ *
+ * Returns: (transfer none) (nullable): An #IdeDebugger or %NULL
+ *
+ * Since: 3.32
+ */
+IdeDebugger *
+ide_debugger_threads_view_get_debugger (IdeDebuggerThreadsView *self)
+{
+  g_return_val_if_fail (IDE_IS_DEBUGGER_THREADS_VIEW (self), NULL);
+
+  return dzl_signal_group_get_target (self->debugger_signals);
+}
+
+void
+ide_debugger_threads_view_set_debugger (IdeDebuggerThreadsView *self,
+                                        IdeDebugger            *debugger)
+{
+  g_return_if_fail (IDE_IS_DEBUGGER_THREADS_VIEW (self));
+  g_return_if_fail (!debugger || IDE_IS_DEBUGGER (debugger));
+
+  dzl_signal_group_set_target (self->debugger_signals, debugger);
+}
diff --git a/src/plugins/debuggerui/ide-debugger-threads-view.h 
b/src/plugins/debuggerui/ide-debugger-threads-view.h
new file mode 100644
index 000000000..1cdbaab88
--- /dev/null
+++ b/src/plugins/debuggerui/ide-debugger-threads-view.h
@@ -0,0 +1,37 @@
+/* ide-debugger-threads-view.h
+ *
+ * Copyright 2017-2019 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 <gtk/gtk.h>
+
+#include "ide-debugger.h"
+
+G_BEGIN_DECLS
+
+#define IDE_TYPE_DEBUGGER_THREADS_VIEW (ide_debugger_threads_view_get_type())
+
+G_DECLARE_FINAL_TYPE (IdeDebuggerThreadsView, ide_debugger_threads_view, IDE, DEBUGGER_THREADS_VIEW, GtkBin)
+
+IdeDebugger *ide_debugger_threads_view_get_debugger (IdeDebuggerThreadsView *self);
+void         ide_debugger_threads_view_set_debugger (IdeDebuggerThreadsView *self,
+                                                     IdeDebugger            *debugger);
+
+G_END_DECLS
diff --git a/src/libide/debugger/ide-debugger-threads-view.ui 
b/src/plugins/debuggerui/ide-debugger-threads-view.ui
similarity index 100%
rename from src/libide/debugger/ide-debugger-threads-view.ui
rename to src/plugins/debuggerui/ide-debugger-threads-view.ui
diff --git a/src/plugins/debuggerui/meson.build b/src/plugins/debuggerui/meson.build
new file mode 100644
index 000000000..cd77cec81
--- /dev/null
+++ b/src/plugins/debuggerui/meson.build
@@ -0,0 +1,21 @@
+plugins_sources += files([
+  'debuggerui-plugin.c',
+  'ide-debugger-breakpoints-view.c',
+  'ide-debugger-controls.c',
+  'ide-debugger-disassembly-view.c',
+  'ide-debugger-editor-addin.c',
+  'ide-debugger-hover-controls.c',
+  'ide-debugger-hover-provider.c',
+  'ide-debugger-libraries-view.c',
+  'ide-debugger-locals-view.c',
+  'ide-debugger-registers-view.c',
+  'ide-debugger-threads-view.c',
+])
+
+plugin_debuggerui_resources = gnome.compile_resources(
+  'gbp-debuggerui-resources',
+  'debuggerui.gresource.xml',
+  c_name: 'gbp_debuggerui',
+)
+
+plugins_sources += plugin_debuggerui_resources[0]
diff --git a/src/plugins/devhelp/devhelp-plugin.c b/src/plugins/devhelp/devhelp-plugin.c
new file mode 100644
index 000000000..72c5b2fed
--- /dev/null
+++ b/src/plugins/devhelp/devhelp-plugin.c
@@ -0,0 +1,42 @@
+/* devhelp-plugin.c
+ *
+ * Copyright 2015-2019 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
+ */
+
+#include "config.h"
+
+#include <libide-editor.h>
+#include <libpeas/peas.h>
+
+#include "gbp-devhelp-editor-addin.h"
+#include "gbp-devhelp-hover-provider.h"
+#include "gbp-devhelp-frame-addin.h"
+
+_IDE_EXTERN void
+_gbp_devhelp_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_ADDIN,
+                                              GBP_TYPE_DEVHELP_EDITOR_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_HOVER_PROVIDER,
+                                              GBP_TYPE_DEVHELP_HOVER_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_FRAME_ADDIN,
+                                              GBP_TYPE_DEVHELP_FRAME_ADDIN);
+}
diff --git a/src/plugins/devhelp/devhelp.gresource.xml b/src/plugins/devhelp/devhelp.gresource.xml
index 0660faa04..34efd9b5c 100644
--- a/src/plugins/devhelp/devhelp.gresource.xml
+++ b/src/plugins/devhelp/devhelp.gresource.xml
@@ -1,13 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/devhelp">
     <file>devhelp.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/devhelp-plugin">
     <file>gtk/menus.ui</file>
     <file>themes/shared.css</file>
     <file>gbp-devhelp-menu-button.ui</file>
-    <file>gbp-devhelp-view.ui</file>
+    <file>gbp-devhelp-page.ui</file>
     <file>gbp-devhelp-search.ui</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/devhelp/devhelp.plugin b/src/plugins/devhelp/devhelp.plugin
index c629ca26f..21e6122f1 100644
--- a/src/plugins/devhelp/devhelp.plugin
+++ b/src/plugins/devhelp/devhelp.plugin
@@ -1,10 +1,11 @@
 [Plugin]
-Module=devhelp-plugin
-Name=Devhelp
-Description=Integration with devhelp documentation
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
-Depends=editor;webkit;
 Builtin=true
-Embedded=gbp_devhelp_register_types
+Copyright=Copyright © 2015-2018 Christian Hergert
+Depends=editor;webkit;
+Description=Integration with devhelp documentation
+Embedded=_gbp_devhelp_register_types
+Hidden=true
+Module=devhelp
+Name=Devhelp
 X-Hover-Provider-Languages=c,chdr,cpp,cpphdr
diff --git a/src/plugins/devhelp/gbp-devhelp-editor-addin.c b/src/plugins/devhelp/gbp-devhelp-editor-addin.c
index f2fe1b458..982bd2616 100644
--- a/src/plugins/devhelp/gbp-devhelp-editor-addin.c
+++ b/src/plugins/devhelp/gbp-devhelp-editor-addin.c
@@ -1,6 +1,6 @@
 /* gbp-devhelp-editor-addin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,77 +14,82 @@
  *
  * 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-devhelp-editor-addin"
 
 #include "gbp-devhelp-editor-addin.h"
-#include "gbp-devhelp-view.h"
+#include "gbp-devhelp-page.h"
 
 struct _GbpDevhelpEditorAddin
 {
   GObject               parent_instance;
-  IdeEditorPerspective *editor;
+  IdeEditorSurface *editor;
 };
 
 static void
-gbp_devhelp_editor_addin_new_devhelp_view (GSimpleAction *action,
+gbp_devhelp_editor_addin_new_devhelp_page (GSimpleAction *action,
                                            GVariant      *variant,
                                            gpointer       user_data)
 {
   GbpDevhelpEditorAddin *self = user_data;
-  IdeLayoutGrid *grid;
+  IdeGrid *grid;
   GtkWidget *view;
 
   g_assert (G_IS_SIMPLE_ACTION (action));
   g_assert (GBP_IS_DEVHELP_EDITOR_ADDIN (self));
   g_assert (self->editor != NULL);
 
-  view = g_object_new (GBP_TYPE_DEVHELP_VIEW,
+  view = g_object_new (GBP_TYPE_DEVHELP_PAGE,
                        "visible", TRUE,
                        NULL);
-  grid = ide_editor_perspective_get_grid (self->editor);
+  grid = ide_editor_surface_get_grid (self->editor);
   gtk_container_add (GTK_CONTAINER (grid), view);
 }
 
 static GActionEntry actions[] = {
-  { "new-devhelp-view", gbp_devhelp_editor_addin_new_devhelp_view },
+  { "new-devhelp-page", gbp_devhelp_editor_addin_new_devhelp_page },
 };
 
 static void
-gbp_devhelp_editor_addin_load (IdeEditorAddin       *addin,
-                               IdeEditorPerspective *editor)
+gbp_devhelp_editor_addin_load (IdeEditorAddin   *addin,
+                               IdeEditorSurface *editor)
 {
   GbpDevhelpEditorAddin *self = (GbpDevhelpEditorAddin *)addin;
-  IdeWorkbench *workbench;
+  GtkWidget *workspace;
 
   g_assert (GBP_IS_DEVHELP_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
 
   self->editor = editor;
 
-  workbench = ide_widget_get_workbench (GTK_WIDGET (editor));
+  workspace = gtk_widget_get_ancestor (GTK_WIDGET (editor), IDE_TYPE_WORKSPACE);
 
-  if (workbench != NULL)
-    g_action_map_add_action_entries (G_ACTION_MAP (workbench), actions, G_N_ELEMENTS (actions), self);
+  if (workspace != NULL)
+    g_action_map_add_action_entries (G_ACTION_MAP (workspace),
+                                     actions,
+                                     G_N_ELEMENTS (actions),
+                                     self);
 }
 
 static void
 gbp_devhelp_editor_addin_unload (IdeEditorAddin       *addin,
-                                 IdeEditorPerspective *editor)
+                                 IdeEditorSurface *editor)
 {
   GbpDevhelpEditorAddin *self = (GbpDevhelpEditorAddin *)addin;
-  GtkWidget *win;
+  GtkWidget *workspace;
 
   g_assert (GBP_IS_DEVHELP_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
 
-  win = gtk_widget_get_ancestor (GTK_WIDGET (editor), GTK_TYPE_WINDOW);
+  workspace = gtk_widget_get_ancestor (GTK_WIDGET (editor), IDE_TYPE_WORKSPACE);
 
-  if (G_IS_ACTION_MAP (win))
+  if (G_IS_ACTION_MAP (workspace))
     {
       for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
-        g_action_map_remove_action (G_ACTION_MAP (win), actions[i].name);
+        g_action_map_remove_action (G_ACTION_MAP (workspace), actions[i].name);
     }
 
   self->editor = NULL;
diff --git a/src/plugins/devhelp/gbp-devhelp-editor-addin.h b/src/plugins/devhelp/gbp-devhelp-editor-addin.h
index 0828b5cac..d6a5ef564 100644
--- a/src/plugins/devhelp/gbp-devhelp-editor-addin.h
+++ b/src/plugins/devhelp/gbp-devhelp-editor-addin.h
@@ -1,6 +1,6 @@
 /* gbp-devhelp-editor-addin.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-editor.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/devhelp/gbp-devhelp-frame-addin.c b/src/plugins/devhelp/gbp-devhelp-frame-addin.c
new file mode 100644
index 000000000..d9c8ae8f3
--- /dev/null
+++ b/src/plugins/devhelp/gbp-devhelp-frame-addin.c
@@ -0,0 +1,210 @@
+/* gbp-devhelp-frame-addin.c
+ *
+ * Copyright 2017-2019 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-devhelp-frame-addin"
+
+#include "gbp-devhelp-frame-addin.h"
+#include "gbp-devhelp-menu-button.h"
+#include "gbp-devhelp-page.h"
+
+struct _GbpDevhelpFrameAddin
+{
+  GObject               parent_instance;
+  IdeFrame       *stack;
+  GbpDevhelpMenuButton *button;
+};
+
+static void
+gbp_devhelp_frame_addin_search (GSimpleAction *action,
+                                       GVariant      *variant,
+                                       gpointer       user_data)
+{
+  GbpDevhelpFrameAddin *self = user_data;
+  const gchar *keyword;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_DEVHELP_FRAME_ADDIN (self));
+  g_assert (self->stack != NULL);
+  g_assert (IDE_IS_FRAME (self->stack));
+  g_assert (variant != NULL);
+  g_assert (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING));
+
+  if (self->button != NULL)
+    {
+      keyword = g_variant_get_string (variant, NULL);
+      gbp_devhelp_menu_button_search (self->button, keyword);
+    }
+}
+
+static void
+gbp_devhelp_frame_addin_new_view (GSimpleAction *action,
+                                         GVariant      *variant,
+                                         gpointer       user_data)
+{
+  GbpDevhelpFrameAddin *self = user_data;
+  GbpDevhelpPage *view;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_DEVHELP_FRAME_ADDIN (self));
+  g_assert (self->stack != NULL);
+  g_assert (IDE_IS_FRAME (self->stack));
+
+  view = g_object_new (GBP_TYPE_DEVHELP_PAGE,
+                       "visible", TRUE,
+                       NULL);
+  gtk_container_add (GTK_CONTAINER (self->stack), GTK_WIDGET (view));
+}
+
+static void
+gbp_devhelp_frame_addin_navigate_to (GSimpleAction *action,
+                                            GVariant      *variant,
+                                            gpointer       user_data)
+{
+  GbpDevhelpFrameAddin *self = user_data;
+  IdePage *view;
+  const gchar *uri;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_DEVHELP_FRAME_ADDIN (self));
+  g_assert (self->stack != NULL);
+  g_assert (IDE_IS_FRAME (self->stack));
+  g_assert (variant != NULL);
+  g_assert (g_variant_is_of_type (variant, G_VARIANT_TYPE_STRING));
+
+  uri = g_variant_get_string (variant, NULL);
+  view = ide_frame_get_visible_child (self->stack);
+
+  if (GBP_IS_DEVHELP_PAGE (view))
+    gbp_devhelp_page_set_uri (GBP_DEVHELP_PAGE (view), uri);
+}
+
+static GActionEntry actions[] = {
+  { "new-view", gbp_devhelp_frame_addin_new_view },
+  { "search", gbp_devhelp_frame_addin_search, "s" },
+  { "navigate-to", gbp_devhelp_frame_addin_navigate_to, "s" },
+};
+
+static void
+gbp_devhelp_frame_addin_load (IdeFrameAddin *addin,
+                                     IdeFrame      *stack)
+{
+  GbpDevhelpFrameAddin *self = (GbpDevhelpFrameAddin *)addin;
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  g_assert (GBP_IS_DEVHELP_FRAME_ADDIN (self));
+  g_assert (IDE_IS_FRAME (stack));
+
+  self->stack = stack;
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (stack),
+                                  "devhelp",
+                                  G_ACTION_GROUP (group));
+}
+
+static void
+gbp_devhelp_frame_addin_unload (IdeFrameAddin *addin,
+                                       IdeFrame      *stack)
+{
+  GbpDevhelpFrameAddin *self = (GbpDevhelpFrameAddin *)addin;
+
+  g_assert (GBP_IS_DEVHELP_FRAME_ADDIN (self));
+  g_assert (IDE_IS_FRAME (stack));
+
+  self->stack = NULL;
+
+  gtk_widget_insert_action_group (GTK_WIDGET (stack), "devhelp", NULL);
+
+  if (self->button != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->button));
+}
+
+static void
+gbp_devhelp_frame_addin_set_view (IdeFrameAddin *addin,
+                                         IdePage       *view)
+{
+  GbpDevhelpFrameAddin *self = (GbpDevhelpFrameAddin *)addin;
+  gboolean visible = FALSE;
+
+  g_assert (GBP_IS_DEVHELP_FRAME_ADDIN (self));
+  g_assert (!view || IDE_IS_PAGE (view));
+  g_assert (self->stack != NULL);
+  g_assert (IDE_IS_FRAME (self->stack));
+
+  /*
+   * We don't setup self->button until we get our first devhelp
+   * view. This helps reduce startup overhead as well as lower
+   * memory footprint until it is necessary.
+   */
+
+  if (GBP_IS_DEVHELP_PAGE (view))
+    {
+      if (self->button == NULL)
+        {
+          GtkWidget *titlebar;
+
+          titlebar = ide_frame_get_titlebar (self->stack);
+
+          self->button = g_object_new (GBP_TYPE_DEVHELP_MENU_BUTTON,
+                                       "hexpand", TRUE,
+                                       NULL);
+          g_signal_connect (self->button,
+                            "destroy",
+                            G_CALLBACK (gtk_widget_destroyed),
+                            &self->button);
+          ide_frame_header_add_custom_title (IDE_FRAME_HEADER (titlebar),
+                                                    GTK_WIDGET (self->button),
+                                                    100);
+        }
+
+      visible = TRUE;
+    }
+
+  if (self->button != NULL)
+    gtk_widget_set_visible (GTK_WIDGET (self->button), visible);
+}
+
+static void
+frame_addin_iface_init (IdeFrameAddinInterface *iface)
+{
+  iface->load = gbp_devhelp_frame_addin_load;
+  iface->unload = gbp_devhelp_frame_addin_unload;
+  iface->set_page = gbp_devhelp_frame_addin_set_view;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpDevhelpFrameAddin,
+                         gbp_devhelp_frame_addin,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_FRAME_ADDIN,
+                                                frame_addin_iface_init))
+
+static void
+gbp_devhelp_frame_addin_class_init (GbpDevhelpFrameAddinClass *klass)
+{
+}
+
+static void
+gbp_devhelp_frame_addin_init (GbpDevhelpFrameAddin *self)
+{
+}
diff --git a/src/plugins/devhelp/gbp-devhelp-frame-addin.h b/src/plugins/devhelp/gbp-devhelp-frame-addin.h
new file mode 100644
index 000000000..309e358c7
--- /dev/null
+++ b/src/plugins/devhelp/gbp-devhelp-frame-addin.h
@@ -0,0 +1,31 @@
+/* gbp-devhelp-frame-addin.h
+ *
+ * Copyright 2017-2019 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-editor.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_DEVHELP_FRAME_ADDIN (gbp_devhelp_frame_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDevhelpFrameAddin, gbp_devhelp_frame_addin, GBP, DEVHELP_FRAME_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/devhelp/gbp-devhelp-hover-provider.c 
b/src/plugins/devhelp/gbp-devhelp-hover-provider.c
index a64c251fb..9075e4da0 100644
--- a/src/plugins/devhelp/gbp-devhelp-hover-provider.c
+++ b/src/plugins/devhelp/gbp-devhelp-hover-provider.c
@@ -1,6 +1,6 @@
 /* gbp-devhelp-hover-provider.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,16 +14,18 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-devhelp-hover-provider"
 
+#include "config.h"
+
 #include <devhelp/devhelp.h>
+#include <libide-sourceview.h>
 #include <glib/gi18n.h>
 
-#include "sourceview/ide-text-iter.h"
 #include "gbp-devhelp-hover-provider.h"
 
 #define DEVHELP_HOVER_PROVIDER_PRIORITY 200
@@ -45,7 +47,7 @@ hover_free (Hover *h)
 {
   g_clear_object (&h->context);
   g_clear_pointer (&h->word, g_free);
-  g_clear_pointer (&h->symbol, ide_symbol_unref);
+  g_clear_object (&h->symbol);
   g_slice_free (Hover, h);
 }
 
@@ -192,7 +194,7 @@ gbp_devhelp_hover_provider_hover_async (IdeHoverProvider    *provider,
 
   h = g_slice_new0 (Hover);
   h->context = g_object_ref (context);
-  h->word = _ide_text_iter_current_symbol (iter, NULL);
+  h->word = ide_text_iter_current_symbol (iter, NULL);
   ide_task_set_task_data (task, h, hover_free);
 
   buffer = IDE_BUFFER (gtk_text_iter_get_buffer (iter));
diff --git a/src/plugins/devhelp/gbp-devhelp-hover-provider.h 
b/src/plugins/devhelp/gbp-devhelp-hover-provider.h
index c7ffb977d..efb853583 100644
--- a/src/plugins/devhelp/gbp-devhelp-hover-provider.h
+++ b/src/plugins/devhelp/gbp-devhelp-hover-provider.h
@@ -1,6 +1,6 @@
 /* gbp-devhelp-hover-provider.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-editor.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/devhelp/gbp-devhelp-menu-button.c b/src/plugins/devhelp/gbp-devhelp-menu-button.c
index 8a955db1f..35287026f 100644
--- a/src/plugins/devhelp/gbp-devhelp-menu-button.c
+++ b/src/plugins/devhelp/gbp-devhelp-menu-button.c
@@ -1,6 +1,6 @@
 /* gbp-devhelp-menu-button.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,13 +14,15 @@
  *
  * 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-devhelp-menu-button"
 
 #include <devhelp/devhelp.h>
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-editor.h>
 
 #include "gbp-devhelp-menu-button.h"
 
@@ -99,6 +101,8 @@ gbp_devhelp_menu_button_pixbuf_data_func (GtkCellLayout   *cell_layout,
     }
 
   g_object_set (cell, "icon-name", icon_name, NULL);
+
+  g_clear_pointer (&link, dh_link_unref);
 }
 
 static void
@@ -165,7 +169,7 @@ monkey_patch_devhelp (GbpDevhelpMenuButton *self)
   model = gtk_tree_view_get_model (tree_view);
   column_type = gtk_tree_model_get_column_type (model, DH_KEYWORD_MODEL_COL_LINK);
 
-  if (column_type != G_TYPE_POINTER)
+  if (column_type != DH_TYPE_LINK)
     {
       g_warning ("Link type %s does not match expectation",
                  g_type_name (column_type));
@@ -227,7 +231,7 @@ gbp_devhelp_menu_button_class_init (GbpDevhelpMenuButtonClass *klass)
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
   gtk_widget_class_set_template_from_resource (widget_class,
-                                               
"/org/gnome/builder/plugins/devhelp-plugin/gbp-devhelp-menu-button.ui");
+                                               "/plugins/devhelp/gbp-devhelp-menu-button.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpDevhelpMenuButton, popover);
   gtk_widget_class_bind_template_child (widget_class, GbpDevhelpMenuButton, sidebar);
 
diff --git a/src/plugins/devhelp/gbp-devhelp-menu-button.h b/src/plugins/devhelp/gbp-devhelp-menu-button.h
index 1fa65d1ff..3584c7873 100644
--- a/src/plugins/devhelp/gbp-devhelp-menu-button.h
+++ b/src/plugins/devhelp/gbp-devhelp-menu-button.h
@@ -1,6 +1,6 @@
 /* gbp-devhelp-menu-button.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/devhelp/gbp-devhelp-page.c b/src/plugins/devhelp/gbp-devhelp-page.c
new file mode 100644
index 000000000..8bcfc86c3
--- /dev/null
+++ b/src/plugins/devhelp/gbp-devhelp-page.c
@@ -0,0 +1,244 @@
+/* gbp-devhelp-page.c
+ *
+ * Copyright 2015-2019 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
+ */
+
+#include <devhelp/devhelp.h>
+#include <glib/gi18n.h>
+#include <webkit2/webkit2.h>
+
+#include "gbp-devhelp-page.h"
+#include "gbp-devhelp-search.h"
+
+struct _GbpDevhelpPage
+{
+  IdePage         parent_instance;
+
+  WebKitWebView        *web_view1;
+  WebKitFindController *web_controller;
+  GtkClipboard         *clipboard;
+
+  GtkOverlay           *devhelp_overlay;
+  GtkRevealer          *search_revealer;
+  GbpDevhelpSearch     *search;
+ };
+
+enum {
+  PROP_0,
+  PROP_URI,
+  LAST_PROP
+};
+
+enum {
+  SEARCH_REVEAL,
+  LAST_SIGNAL
+};
+
+G_DEFINE_TYPE (GbpDevhelpPage, gbp_devhelp_page, IDE_TYPE_PAGE)
+
+static GParamSpec *properties [LAST_PROP];
+static guint signals [LAST_SIGNAL];
+
+void
+gbp_devhelp_page_set_uri (GbpDevhelpPage *self,
+                          const gchar    *uri)
+{
+  g_return_if_fail (GBP_IS_DEVHELP_PAGE (self));
+
+  if (uri == NULL)
+    return;
+
+  webkit_web_view_load_uri (self->web_view1, uri);
+}
+
+static void
+gbp_devhelp_page_notify_title (GbpDevhelpPage *self,
+                               GParamSpec     *pspec,
+                               WebKitWebView  *web_view)
+{
+  const gchar *title;
+
+  g_assert (GBP_IS_DEVHELP_PAGE (self));
+  g_assert (WEBKIT_IS_WEB_VIEW (web_view));
+
+  title = webkit_web_view_get_title (self->web_view1);
+
+  ide_page_set_title (IDE_PAGE (self), title);
+}
+
+static IdePage *
+gbp_devhelp_page_create_split (IdePage *view)
+{
+  GbpDevhelpPage *self = (GbpDevhelpPage *)view;
+  GbpDevhelpPage *other;
+  const gchar *uri;
+
+  g_assert (GBP_IS_DEVHELP_PAGE (self));
+
+  uri = webkit_web_view_get_uri (self->web_view1);
+  other = g_object_new (GBP_TYPE_DEVHELP_PAGE,
+                        "visible", TRUE,
+                        "uri", uri,
+                        NULL);
+
+  return IDE_PAGE (other);
+}
+
+static void
+gbp_devhelp_page_actions_print (GSimpleAction *action,
+                                GVariant      *param,
+                                gpointer       user_data)
+{
+  GbpDevhelpPage *self = user_data;
+  WebKitPrintOperation *operation;
+  GtkWidget *window;
+
+  g_assert (GBP_IS_DEVHELP_PAGE (self));
+
+  operation = webkit_print_operation_new (self->web_view1);
+  window = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW);
+  webkit_print_operation_run_dialog (operation, GTK_WINDOW (window));
+  g_object_unref (operation);
+}
+
+static void
+gbp_devhelp_page_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  GbpDevhelpPage *self = GBP_DEVHELP_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_URI:
+      gbp_devhelp_page_set_uri (self, g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_devhelp_search_reveal (GbpDevhelpPage *self)
+{
+  g_assert (GBP_IS_DEVHELP_PAGE (self));
+
+  webkit_web_view_can_execute_editing_command (self->web_view1, WEBKIT_EDITING_COMMAND_COPY, NULL, NULL, 
NULL);
+  gtk_revealer_set_reveal_child (self->search_revealer, TRUE);
+}
+
+static void
+gbp_devhelp_focus_in_event (GbpDevhelpPage *self,
+                            GdkEvent       *event)
+{
+  g_assert (GBP_IS_DEVHELP_PAGE (self));
+
+  webkit_find_controller_search_finish (self->web_controller);
+  gtk_revealer_set_reveal_child (self->search_revealer, FALSE);
+}
+
+static void
+gbp_devhelp_page_class_init (GbpDevhelpPageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdePageClass *view_class = IDE_PAGE_CLASS (klass);
+
+  object_class->set_property = gbp_devhelp_page_set_property;
+
+  view_class->create_split = gbp_devhelp_page_create_split;
+
+  properties [PROP_URI] =
+    g_param_spec_string ("uri",
+                         "Uri",
+                         "The uri of the documentation.",
+                         NULL,
+                         (G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
+
+  signals [SEARCH_REVEAL] =
+    g_signal_new_class_handler ("search-reveal",
+                                G_TYPE_FROM_CLASS (klass),
+                                G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                                G_CALLBACK (gbp_devhelp_search_reveal),
+                                NULL, NULL, NULL,
+                                G_TYPE_NONE, 0);
+
+  gtk_binding_entry_add_signal (gtk_binding_set_by_class (klass),
+                                GDK_KEY_f,
+                                GDK_CONTROL_MASK,
+                                "search-reveal", 0);
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/devhelp/gbp-devhelp-page.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpDevhelpPage, web_view1);
+  gtk_widget_class_bind_template_child (widget_class, GbpDevhelpPage, devhelp_overlay);
+
+  g_type_ensure (WEBKIT_TYPE_WEB_VIEW);
+}
+
+static const GActionEntry actions[] = {
+  { "print", gbp_devhelp_page_actions_print },
+};
+
+static void
+gbp_devhelp_page_init (GbpDevhelpPage *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  ide_page_set_title (IDE_PAGE (self), _("Documentation"));
+  ide_page_set_can_split (IDE_PAGE (self), TRUE);
+  ide_page_set_icon_name (IDE_PAGE (self), "org.gnome.Devhelp-symbolic");
+  ide_page_set_menu_id (IDE_PAGE (self), "devhelp-view-document-menu");
+
+  self->search = g_object_new (GBP_TYPE_DEVHELP_SEARCH, NULL);
+  self->search_revealer = gbp_devhelp_search_get_revealer (self->search);
+  self->clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
+  self->web_controller = webkit_web_view_get_find_controller (self->web_view1);
+
+  gtk_overlay_add_overlay (self->devhelp_overlay,
+                           GTK_WIDGET (self->search_revealer));
+
+  gbp_devhelp_search_set_devhelp (self->search,
+                                  self->web_controller,
+                                  self->clipboard);
+
+  g_signal_connect_object (self->web_view1,
+                           "notify::title",
+                           G_CALLBACK (gbp_devhelp_page_notify_title),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (self->web_view1,
+                          "focus-in-event",
+                           G_CALLBACK (gbp_devhelp_focus_in_event),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self),
+                                  "devhelp-view",
+                                  G_ACTION_GROUP (group));
+}
diff --git a/src/plugins/devhelp/gbp-devhelp-page.h b/src/plugins/devhelp/gbp-devhelp-page.h
new file mode 100644
index 000000000..69a32b10f
--- /dev/null
+++ b/src/plugins/devhelp/gbp-devhelp-page.h
@@ -0,0 +1,34 @@
+/* gbp-devhelp-page.h
+ *
+ * Copyright 2015-2019 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-editor.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_DEVHELP_PAGE (gbp_devhelp_page_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDevhelpPage, gbp_devhelp_page, GBP, DEVHELP_PAGE, IdePage)
+
+void gbp_devhelp_page_set_uri (GbpDevhelpPage *self,
+                               const gchar    *uri);
+
+G_END_DECLS
diff --git a/src/plugins/devhelp/gbp-devhelp-page.ui b/src/plugins/devhelp/gbp-devhelp-page.ui
new file mode 100644
index 000000000..28fa7cb95
--- /dev/null
+++ b/src/plugins/devhelp/gbp-devhelp-page.ui
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.18 -->
+  <template class="GbpDevhelpPage" parent="IdePage">
+    <child>
+      <object class="GtkPaned" id="paned">
+        <property name="expand">true</property>
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkOverlay" id="devhelp_overlay">
+            <property name="expand">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="WebKitWebView" id="web_view1">
+                <property name="visible">true</property>
+                <property name="expand">true</property>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/devhelp/gbp-devhelp-search-private.h 
b/src/plugins/devhelp/gbp-devhelp-search-private.h
index c33423a79..b8a9e15bd 100644
--- a/src/plugins/devhelp/gbp-devhelp-search-private.h
+++ b/src/plugins/devhelp/gbp-devhelp-search-private.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-editor.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/devhelp/gbp-devhelp-search.c b/src/plugins/devhelp/gbp-devhelp-search.c
index 91e17255d..4391122a6 100644
--- a/src/plugins/devhelp/gbp-devhelp-search.c
+++ b/src/plugins/devhelp/gbp-devhelp-search.c
@@ -14,6 +14,8 @@
  *
  * 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-devhelp-search"
@@ -21,7 +23,7 @@
 
 #include <fcntl.h>
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-editor.h>
 #include <webkit2/webkit2.h>
 
 #include "gbp-devhelp-search.h"
@@ -93,7 +95,7 @@ gbp_devhelp_search_class_init (GbpDevhelpSearchClass *klass)
 {
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/devhelp-plugin/gbp-devhelp-search.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/devhelp/gbp-devhelp-search.ui");
 
   gtk_widget_class_bind_template_child (widget_class, GbpDevhelpSearch, search_prev_button);
   gtk_widget_class_bind_template_child (widget_class, GbpDevhelpSearch, search_next_button);
diff --git a/src/plugins/devhelp/gbp-devhelp-search.h b/src/plugins/devhelp/gbp-devhelp-search.h
index 5da248804..5d8d8ca02 100644
--- a/src/plugins/devhelp/gbp-devhelp-search.h
+++ b/src/plugins/devhelp/gbp-devhelp-search.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-editor.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/devhelp/gtk/menus.ui b/src/plugins/devhelp/gtk/menus.ui
index 0594ead18..8467da226 100644
--- a/src/plugins/devhelp/gtk/menus.ui
+++ b/src/plugins/devhelp/gtk/menus.ui
@@ -19,7 +19,7 @@
         <attribute name="id">new-documentation-page</attribute>
         <attribute name="after">new-file</attribute>
         <attribute name="label" translatable="yes">New Documentation Page</attribute>
-        <attribute name="action">win.new-devhelp-view</attribute>
+        <attribute name="action">win.new-devhelp-page</attribute>
       </item>
     </section>
   </menu>
diff --git a/src/plugins/devhelp/meson.build b/src/plugins/devhelp/meson.build
index b07e9f952..685f73f51 100644
--- a/src/plugins/devhelp/meson.build
+++ b/src/plugins/devhelp/meson.build
@@ -1,26 +1,25 @@
-if get_option('with_devhelp')
+if get_option('plugin_devhelp')
 
-devhelp_resources = gnome.compile_resources(
-  'devhelp-resources',
-  'devhelp.gresource.xml',
-  c_name: 'gbp_devhelp',
-)
+plugins_deps += [
+  dependency('libdevhelp-3.0', version: '>=3.25.1'),
+]
 
-devhelp_sources = [
-  'gbp-devhelp-menu-button.c',
-  'gbp-devhelp-hover-provider.c',
-  'gbp-devhelp-layout-stack-addin.c',
+plugins_sources += files([
+  'devhelp-plugin.c',
   'gbp-devhelp-editor-addin.c',
-  'gbp-devhelp-plugin.c',
+  'gbp-devhelp-frame-addin.c',
+  'gbp-devhelp-hover-provider.c',
+  'gbp-devhelp-menu-button.c',
+  'gbp-devhelp-page.c',
   'gbp-devhelp-search.c',
-  'gbp-devhelp-view.c',
-]
+])
 
-gnome_builder_plugins_deps += [
-  dependency('libdevhelp-3.0', version: '>=3.25.1'),
-]
+plugin_devhelp_resources = gnome.compile_resources(
+  'devhelp-resources',
+  'devhelp.gresource.xml',
+  c_name: 'gbp_devhelp',
+)
 
-gnome_builder_plugins_sources += files(devhelp_sources)
-gnome_builder_plugins_sources += devhelp_resources[0]
+plugins_sources += plugin_devhelp_resources[0]
 
 endif
diff --git a/src/plugins/deviced/deviced-plugin.c b/src/plugins/deviced/deviced-plugin.c
new file mode 100644
index 000000000..3e314c4ba
--- /dev/null
+++ b/src/plugins/deviced/deviced-plugin.c
@@ -0,0 +1,40 @@
+/* deviced-plugin.c
+ *
+ * Copyright 2018-2019 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 "deviced-plugin"
+
+#include "config.h"
+
+#include <libide-foundry.h>
+#include <libpeas/peas.h>
+
+#include "gbp-deviced-device-provider.h"
+#include "gbp-deviced-deploy-strategy.h"
+
+_IDE_EXTERN void
+_gbp_deviced_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_DEVICE_PROVIDER,
+                                              GBP_TYPE_DEVICED_DEVICE_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_DEPLOY_STRATEGY,
+                                              GBP_TYPE_DEVICED_DEPLOY_STRATEGY);
+}
diff --git a/src/plugins/deviced/deviced.gresource.xml b/src/plugins/deviced/deviced.gresource.xml
index 2fa7cfe02..3d8a98bff 100644
--- a/src/plugins/deviced/deviced.gresource.xml
+++ b/src/plugins/deviced/deviced.gresource.xml
@@ -1,8 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/deviced">
     <file>deviced.plugin</file>
   </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/deviced-plugin">
-  </gresource>
 </gresources>
diff --git a/src/plugins/deviced/deviced.plugin b/src/plugins/deviced/deviced.plugin
index 46f792180..25811b59d 100644
--- a/src/plugins/deviced/deviced.plugin
+++ b/src/plugins/deviced/deviced.plugin
@@ -1,10 +1,9 @@
 [Plugin]
-Module=deviced-plugin
-Name=Deviced
-Description=Integration with deviced devices
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2017 Christian Hergert
-Depends=editor;debugger;terminal;
-Hidden=true
 Builtin=true
-Embedded=gbp_deviced_register_types
+Copyright=Copyright © 2017 Christian Hergert
+Depends=editor;debuggerui;terminal;deviceui;flatpak;
+Description=Integration with deviced devices
+Embedded=_gbp_deviced_register_types
+Module=deviced
+Name=Deviced
diff --git a/src/plugins/deviced/gbp-deviced-deploy-strategy.c 
b/src/plugins/deviced/gbp-deviced-deploy-strategy.c
index 1e6c2461c..46196e4a0 100644
--- a/src/plugins/deviced/gbp-deviced-deploy-strategy.c
+++ b/src/plugins/deviced/gbp-deviced-deploy-strategy.c
@@ -1,6 +1,6 @@
 /* gbp-deviced-deploy-strategy.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -87,7 +87,7 @@ gbp_deviced_deploy_strategy_load_async (IdeDeployStrategy   *strategy,
                                  G_IO_ERROR,
                                  G_IO_ERROR_NOT_SUPPORTED,
                                  "%s is not supported by %s",
-                                 G_OBJECT_TYPE_NAME (device),
+                                 device ?  G_OBJECT_TYPE_NAME (device) : "(nil)",
                                  G_OBJECT_TYPE_NAME (self));
       IDE_EXIT;
     }
diff --git a/src/plugins/deviced/gbp-deviced-deploy-strategy.h 
b/src/plugins/deviced/gbp-deviced-deploy-strategy.h
index a6d837748..cbe3a1547 100644
--- a/src/plugins/deviced/gbp-deviced-deploy-strategy.h
+++ b/src/plugins/deviced/gbp-deviced-deploy-strategy.h
@@ -1,6 +1,6 @@
 /* gbp-deviced-deploy-strategy.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -20,7 +20,7 @@
 
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/deviced/gbp-deviced-device-provider.c 
b/src/plugins/deviced/gbp-deviced-device-provider.c
index a0aab5a11..3d6fdb8a6 100644
--- a/src/plugins/deviced/gbp-deviced-device-provider.c
+++ b/src/plugins/deviced/gbp-deviced-device-provider.c
@@ -1,6 +1,6 @@
 /* gbp-deviced-device-provider.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -45,7 +45,6 @@ gbp_deviced_device_provider_device_added_cb (GbpDevicedDeviceProvider *self,
                                              DevdBrowser              *browser)
 {
   GbpDevicedDevice *wrapped;
-  IdeContext *context;
 
   IDE_ENTRY;
 
@@ -53,9 +52,9 @@ gbp_deviced_device_provider_device_added_cb (GbpDevicedDeviceProvider *self,
   g_assert (DEVD_IS_DEVICE (device));
   g_assert (DEVD_IS_BROWSER (browser));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  wrapped = gbp_deviced_device_new (context, device);
+  wrapped = gbp_deviced_device_new (device);
   g_object_set_data (G_OBJECT (device), "GBP_DEVICED_DEVICE", wrapped);
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (wrapped));
 
   ide_device_provider_emit_device_added (IDE_DEVICE_PROVIDER (self), IDE_DEVICE (wrapped));
 
diff --git a/src/plugins/deviced/gbp-deviced-device-provider.h 
b/src/plugins/deviced/gbp-deviced-device-provider.h
index 52e9d7a44..b033f1c16 100644
--- a/src/plugins/deviced/gbp-deviced-device-provider.h
+++ b/src/plugins/deviced/gbp-deviced-device-provider.h
@@ -1,6 +1,6 @@
 /* gbp-deviced-device-provider.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -20,7 +20,7 @@
 
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/deviced/gbp-deviced-device.c b/src/plugins/deviced/gbp-deviced-device.c
index cf0087a91..0ced14221 100644
--- a/src/plugins/deviced/gbp-deviced-device.c
+++ b/src/plugins/deviced/gbp-deviced-device.c
@@ -1,6 +1,6 @@
 /* gbp-deviced-device.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-deviced-device"
@@ -314,14 +316,13 @@ gbp_deviced_device_init (GbpDevicedDevice *self)
 }
 
 GbpDevicedDevice *
-gbp_deviced_device_new (IdeContext *context,
-                        DevdDevice *device)
+gbp_deviced_device_new (DevdDevice *device)
 {
   g_autofree gchar *id = NULL;
   const gchar *name;
   const gchar *icon_name;
 
-  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
   g_return_val_if_fail (DEVD_IS_DEVICE (device), NULL);
 
   id = g_strdup_printf ("deviced:%s", devd_device_get_id (device));
@@ -330,7 +331,6 @@ gbp_deviced_device_new (IdeContext *context,
 
   return g_object_new (GBP_TYPE_DEVICED_DEVICE,
                        "id", id,
-                       "context", context,
                        "device", device,
                        "display-name", name,
                        "icon-name", icon_name,
diff --git a/src/plugins/deviced/gbp-deviced-device.h b/src/plugins/deviced/gbp-deviced-device.h
index 69be45f8f..e98fbb7a5 100644
--- a/src/plugins/deviced/gbp-deviced-device.h
+++ b/src/plugins/deviced/gbp-deviced-device.h
@@ -1,6 +1,6 @@
 /* gbp-deviced-device.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 #include <libdeviced.h>
 
 G_BEGIN_DECLS
@@ -27,8 +29,7 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpDevicedDevice, gbp_deviced_device, GBP, DEVICED_DEVICE, IdeDevice)
 
-GbpDevicedDevice *gbp_deviced_device_new                   (IdeContext             *context,
-                                                            DevdDevice             *device);
+GbpDevicedDevice *gbp_deviced_device_new                   (DevdDevice             *device);
 void              gbp_deviced_device_get_commit_async      (GbpDevicedDevice       *self,
                                                             const gchar            *commit_id,
                                                             GCancellable           *cancellable,
diff --git a/src/plugins/deviced/meson.build b/src/plugins/deviced/meson.build
index e2393d54b..c728cb5d2 100644
--- a/src/plugins/deviced/meson.build
+++ b/src/plugins/deviced/meson.build
@@ -1,23 +1,26 @@
-if get_option('with_deviced')
+if get_option('plugin_deviced')
 
-deviced_resources = gnome.compile_resources(
-  'deviced-resources',
-  'deviced.gresource.xml',
-  c_name: 'gbp_deviced',
-)
+if not get_option('plugin_flatpak')
+  error('-Dplugin_flatpak=true is required to enable deviced')
+endif
 
-deviced_sources = [
-  'gbp-deviced-plugin.c',
+plugins_sources += files([
+  'deviced-plugin.c',
   'gbp-deviced-deploy-strategy.c',
   'gbp-deviced-device.c',
   'gbp-deviced-device-provider.c',
-]
+])
+
+plugin_deviced_resources = gnome.compile_resources(
+  'deviced-resources',
+  'deviced.gresource.xml',
+  c_name: 'gbp_deviced',
+)
 
-gnome_builder_plugins_deps += [
+plugins_deps += [
   dependency('libdeviced', version: '>=3.27.4'),
 ]
 
-gnome_builder_plugins_sources += files(deviced_sources)
-gnome_builder_plugins_sources += deviced_resources[0]
+plugins_sources += plugin_deviced_resources[0]
 
 endif
diff --git a/src/plugins/deviceui/deviceui-plugin.c b/src/plugins/deviceui/deviceui-plugin.c
new file mode 100644
index 000000000..97f1d7cc4
--- /dev/null
+++ b/src/plugins/deviceui/deviceui-plugin.c
@@ -0,0 +1,36 @@
+/* deviceui-plugin.c
+ *
+ * Copyright 2018-2019 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 "deviceui-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+
+#include "gbp-deviceui-workspace-addin.h"
+
+_IDE_EXTERN void
+_gbp_deviceui_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_DEVICEUI_WORKSPACE_ADDIN);
+}
diff --git a/src/plugins/deviceui/deviceui.gresource.xml b/src/plugins/deviceui/deviceui.gresource.xml
new file mode 100644
index 000000000..088c243af
--- /dev/null
+++ b/src/plugins/deviceui/deviceui.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/deviceui">
+    <file>deviceui.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/deviceui/deviceui.plugin b/src/plugins/deviceui/deviceui.plugin
new file mode 100644
index 000000000..3c0ebea8d
--- /dev/null
+++ b/src/plugins/deviceui/deviceui.plugin
@@ -0,0 +1,11 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2018 Christian Hergert
+Depends=editor;
+Description=Provides user interface components to display devices
+Embedded=_gbp_deviceui_register_types
+Hidden=true
+Module=deviceui
+Name=Device UI
+X-Workspace-Kind=primary;
diff --git a/src/plugins/deviceui/gbp-deviceui-workspace-addin.c 
b/src/plugins/deviceui/gbp-deviceui-workspace-addin.c
new file mode 100644
index 000000000..f7cd7fb2c
--- /dev/null
+++ b/src/plugins/deviceui/gbp-deviceui-workspace-addin.c
@@ -0,0 +1,128 @@
+/* gbp-deviceui-workspace-addin.c
+ *
+ * Copyright 2018-2019 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-deviceui-workspace-addin"
+
+#include "config.h"
+
+#include <libide-foundry.h>
+#include <libide-gui.h>
+
+#include "ide-device-private.h"
+
+#include "gbp-deviceui-workspace-addin.h"
+
+struct _GbpDeviceuiWorkspaceAddin
+{
+  GObject    parent_instance;
+  GtkWidget *button;
+};
+
+static gboolean
+device_to_icon_name (GBinding     *binding,
+                     const GValue *from_value,
+                     GValue       *to_value,
+                     gpointer      user_data)
+{
+  IdeDevice *device;
+  const gchar *icon_name;
+
+  if (G_VALUE_HOLDS (from_value, IDE_TYPE_DEVICE) &&
+      (device = g_value_get_object (from_value)) &&
+      (icon_name = ide_device_get_icon_name (device)))
+    g_value_set_string (to_value, icon_name);
+  else
+    g_value_set_static_string (to_value, "computer-symbolic");
+
+  return TRUE;
+}
+
+static void
+gbp_deviceui_workspace_addin_load (IdeWorkspaceAddin *addin,
+                                   IdeWorkspace      *workspace)
+{
+  GbpDeviceuiWorkspaceAddin *self = (GbpDeviceuiWorkspaceAddin *)addin;
+  IdeDeviceManager *device_manager;
+  IdeHeaderBar *header;
+  IdeContext *context;
+  GMenu *menu;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace));
+
+  header = ide_workspace_get_header_bar (workspace);
+  context = ide_widget_get_context (GTK_WIDGET (workspace));
+  device_manager = ide_device_manager_from_context (context);
+  menu = _ide_device_manager_get_menu (device_manager);
+
+  self->button = g_object_new (DZL_TYPE_MENU_BUTTON,
+                               "focus-on-click", FALSE,
+                               "model", menu,
+                               "show-arrow", TRUE,
+                               "show-icons", TRUE,
+                               "visible", TRUE,
+                               NULL);
+  g_signal_connect (self->button,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->button);
+  ide_header_bar_add_center_left (header, self->button);
+
+  g_object_bind_property_full (device_manager, "device",
+                               self->button, "icon-name",
+                               G_BINDING_SYNC_CREATE,
+                               device_to_icon_name,
+                               NULL, NULL, NULL);
+}
+
+static void
+gbp_deviceui_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                     IdeWorkspace      *workspace)
+{
+  GbpDeviceuiWorkspaceAddin *self = (GbpDeviceuiWorkspaceAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace));
+
+  if (self->button)
+    gtk_widget_destroy (self->button);
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_deviceui_workspace_addin_load;
+  iface->unload = gbp_deviceui_workspace_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpDeviceuiWorkspaceAddin, gbp_deviceui_workspace_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
+
+static void
+gbp_deviceui_workspace_addin_class_init (GbpDeviceuiWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_deviceui_workspace_addin_init (GbpDeviceuiWorkspaceAddin *self)
+{
+}
diff --git a/src/plugins/deviceui/gbp-deviceui-workspace-addin.h 
b/src/plugins/deviceui/gbp-deviceui-workspace-addin.h
new file mode 100644
index 000000000..872c0a7d5
--- /dev/null
+++ b/src/plugins/deviceui/gbp-deviceui-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-deviceui-workspace-addin.h
+ *
+ * Copyright 2018-2019 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_DEVICEUI_WORKSPACE_ADDIN (gbp_deviceui_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDeviceuiWorkspaceAddin, gbp_deviceui_workspace_addin, GBP, 
DEVICEUI_WORKSPACE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/deviceui/meson.build b/src/plugins/deviceui/meson.build
new file mode 100644
index 000000000..d1d00235c
--- /dev/null
+++ b/src/plugins/deviceui/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'deviceui-plugin.c',
+  'gbp-deviceui-workspace-addin.c',
+])
+
+plugin_deviceui_resources = gnome.compile_resources(
+  'deviceui-resources',
+  'deviceui.gresource.xml',
+  c_name: 'gbp_deviceui',
+)
+
+plugins_sources += plugin_deviceui_resources[0]
diff --git a/src/plugins/doap/doap-plugin.c b/src/plugins/doap/doap-plugin.c
new file mode 100644
index 000000000..9e56cb509
--- /dev/null
+++ b/src/plugins/doap/doap-plugin.c
@@ -0,0 +1,37 @@
+/* doap-plugin.c
+ *
+ * Copyright 2018-2019 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 "doap-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+#include <libide-projects.h>
+
+#include "gbp-doap-workbench-addin.h"
+
+_IDE_EXTERN void
+_gbp_doap_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GBP_TYPE_DOAP_WORKBENCH_ADDIN);
+}
diff --git a/src/plugins/doap/doap.gresource.xml b/src/plugins/doap/doap.gresource.xml
new file mode 100644
index 000000000..195e2a448
--- /dev/null
+++ b/src/plugins/doap/doap.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/doap">
+    <file>doap.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/doap/doap.plugin b/src/plugins/doap/doap.plugin
new file mode 100644
index 000000000..4af5b859d
--- /dev/null
+++ b/src/plugins/doap/doap.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2015-2018 Christian Hergert
+Description=Support for the Git version control system
+Embedded=_gbp_doap_register_types
+Hidden=true
+Module=doap
+Name=Description of a Project
diff --git a/src/plugins/doap/gbp-doap-workbench-addin.c b/src/plugins/doap/gbp-doap-workbench-addin.c
new file mode 100644
index 000000000..9057318d1
--- /dev/null
+++ b/src/plugins/doap/gbp-doap-workbench-addin.c
@@ -0,0 +1,176 @@
+/* gbp-doap-workbench-addin.c
+ *
+ * Copyright 2018-2019 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-doap-workbench-addin"
+
+#include "config.h"
+
+#include <libide-gui.h>
+#include <libide-projects.h>
+#include <libide-threading.h>
+
+#include "gbp-doap-workbench-addin.h"
+
+struct _GbpDoapWorkbenchAddin
+{
+  GObject     parent_instance;
+  IdeContext *context;
+};
+
+static void
+gbp_doap_workbench_addin_find_doap_cb (GObject      *object,
+                                       GAsyncResult *result,
+                                       gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GPtrArray) found = NULL;
+  g_autoptr(GError) error = NULL;
+  IdeProjectInfo *project_info;
+  GCancellable *cancellable;
+  GbpDoapWorkbenchAddin *self;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!(found = ide_g_file_find_finish (file, result, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  IDE_PTR_ARRAY_SET_FREE_FUNC (found, g_object_unref);
+
+  self = ide_task_get_source_object (task);
+  cancellable = ide_task_get_cancellable (task);
+  project_info = ide_task_get_task_data (task);
+
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+
+  for (guint i = 0; i < found->len; i++)
+    {
+      GFile *doap_file = g_ptr_array_index (found, 0);
+      g_autoptr(IdeDoap) doap = ide_doap_new ();
+
+      g_assert (G_IS_FILE (doap_file));
+
+      g_debug ("Trying doap file %s for project information",
+               g_file_peek_path (doap_file));
+
+      if (ide_doap_load_from_file (doap, doap_file, cancellable, NULL))
+        {
+          const gchar *name = ide_doap_get_name (doap);
+
+          if (!ide_str_empty0 (name))
+            {
+              ide_project_info_set_name (project_info, name);
+              ide_context_set_title (self->context, name);
+            }
+
+          ide_project_info_set_doap (project_info, doap);
+
+          break;
+        }
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_doap_workbench_addin_load_project_async (IdeWorkbenchAddin   *addin,
+                                             IdeProjectInfo      *project_info,
+                                             GCancellable        *cancellable,
+                                             GAsyncReadyCallback  callback,
+                                             gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  GFile *directory;
+
+  g_assert (GBP_IS_DOAP_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (addin, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_doap_workbench_addin_load_project_async);
+  ide_task_set_task_data (task, g_object_ref (project_info), g_object_unref);
+
+  if (!(directory = ide_project_info_get_directory (project_info)))
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  ide_g_file_find_with_depth_async (directory,
+                                    "*.doap",
+                                    1,
+                                    cancellable,
+                                    gbp_doap_workbench_addin_find_doap_cb,
+                                    g_steal_pointer (&task));
+}
+
+static gboolean
+gbp_doap_workbench_addin_load_project_finish (IdeWorkbenchAddin  *addin,
+                                              GAsyncResult       *result,
+                                              GError            **error)
+{
+  g_assert (GBP_IS_DOAP_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+gbp_doap_workbench_addin_load (IdeWorkbenchAddin *addin,
+                               IdeWorkbench      *workbench)
+{
+  GBP_DOAP_WORKBENCH_ADDIN (addin)->context = ide_workbench_get_context (workbench);
+}
+
+static void
+gbp_doap_workbench_addin_unload (IdeWorkbenchAddin *addin,
+                                 IdeWorkbench      *workbench)
+{
+  GBP_DOAP_WORKBENCH_ADDIN (addin)->context = NULL;
+}
+
+static void
+workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load_project_async = gbp_doap_workbench_addin_load_project_async;
+  iface->load_project_finish = gbp_doap_workbench_addin_load_project_finish;
+  iface->load = gbp_doap_workbench_addin_load;
+  iface->unload = gbp_doap_workbench_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpDoapWorkbenchAddin, gbp_doap_workbench_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKBENCH_ADDIN,
+                                                workbench_addin_iface_init))
+
+static void
+gbp_doap_workbench_addin_class_init (GbpDoapWorkbenchAddinClass *klass)
+{
+}
+
+static void
+gbp_doap_workbench_addin_init (GbpDoapWorkbenchAddin *self)
+{
+}
diff --git a/src/plugins/doap/gbp-doap-workbench-addin.h b/src/plugins/doap/gbp-doap-workbench-addin.h
new file mode 100644
index 000000000..4ab5b1d1a
--- /dev/null
+++ b/src/plugins/doap/gbp-doap-workbench-addin.h
@@ -0,0 +1,31 @@
+/* gbp-doap-workbench-addin.h
+ *
+ * Copyright 2018-2019 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_DOAP_WORKBENCH_ADDIN (gbp_doap_workbench_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpDoapWorkbenchAddin, gbp_doap_workbench_addin, GBP, DOAP_WORKBENCH_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/doap/meson.build b/src/plugins/doap/meson.build
new file mode 100644
index 000000000..ebd2ffb74
--- /dev/null
+++ b/src/plugins/doap/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'doap-plugin.c',
+  'gbp-doap-workbench-addin.c',
+])
+
+plugin_doap_resources = gnome.compile_resources(
+  'doap-resources',
+  'doap.gresource.xml',
+  c_name: 'gbp_doap',
+)
+
+plugins_sources += plugin_doap_resources[0]
diff --git a/src/plugins/editor/default.css b/src/plugins/editor/default.css
new file mode 100644
index 000000000..e1b0177a1
--- /dev/null
+++ b/src/plugins/editor/default.css
@@ -0,0 +1,60 @@
+@import url("resource:///org/gnome/builder/keybindings/shared.css");
+
+@binding-set default-ide-source-view
+{
+  bind "Escape" { "clear-search" ()
+                  "clear-modifier" ()
+                  "clear-selection" ()
+                  "clear-count" ()
+                  "clear-snippets" ()
+                  "hide-completion" ()
+                  "remove-cursors" () };
+  bind "<ctrl><shift>e" { "add-cursor" (column) };
+  bind "<ctrl><shift>d" { "add-cursor" (match) };
+  bind "<alt>period" { "goto-definition" () };
+  bind "<ctrl>k" { "action" ("frame", "show-list", "") };
+  bind "<ctrl>d" { "delete-from-cursor" (paragraphs, 1) };
+  bind "<ctrl>j" { "join-lines" () };
+  bind "<ctrl>u" { "change-case" (upper) };
+  bind "<ctrl>l" { "change-case" (lower) };
+  bind "<ctrl>i" { "action" ("editor-page", "goto-line", "") };
+  bind "<ctrl>asciitilde" { "change-case" (toggle) };
+  bind "<ctrl><alt>d" { "duplicate-entire-line" ()};
+  bind "<ctrl><shift>z" { "clear-count" ()
+                          "clear-selection" ()
+                          "remove-cursors" ()
+                          "redo" () };
+  bind "<ctrl>z" { "clear-count" ()
+                   "clear-selection" ()
+                   "remove-cursors" ()
+                   "undo" () };
+
+  bind "<ctrl>minus" { "decrease-font-size" () };
+  bind "<ctrl>plus" { "increase-font-size" () };
+  bind "<ctrl>equal" { "increase-font-size" () };
+  bind "<ctrl>0" { "reset-font-size" () };
+
+  /* cycle "tabs" */
+  bind "<ctrl><alt>Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl><alt>KP_Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl><alt>Page_Down" { "action" ("frame", "next-page", "") };
+  bind "<ctrl><alt>KP_Page_Down" { "action" ("frame", "next-page", "") };
+
+  bind "F2" { "clear-selection" ()
+              "movement" (previous-word-end, 0, 1, 1)
+              "movement" (next-word-start, 0, 1, 0)
+              "movement" (next-word-end, 1, 0, 1)
+              "request-documentation" ()
+              "clear-count" ()
+              "clear-selection" () };
+  bind "F4" { "action" ("win", "find-other-file", "") };
+
+  bind "<ctrl><alt>i" { "reindent" () };
+
+  /* Add back emoji */
+  bind "<ctrl>semicolon" { "insert-emoji" () };
+}
+
+idesourceviewmode.default {
+  -gtk-key-bindings: default-ide-source-view;
+}
diff --git a/src/plugins/editor/editor-plugin.c b/src/plugins/editor/editor-plugin.c
new file mode 100644
index 000000000..6c266fea4
--- /dev/null
+++ b/src/plugins/editor/editor-plugin.c
@@ -0,0 +1,56 @@
+/* editor-plugin.c
+ *
+ * Copyright 2018-2019 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 "editor-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-editor.h>
+
+#include "gbp-editor-application-addin.h"
+#include "gbp-editor-frame-addin.h"
+#include "gbp-editor-hover-provider.h"
+#include "gbp-editor-session-addin.h"
+#include "gbp-editor-workbench-addin.h"
+#include "gbp-editor-workspace-addin.h"
+
+_IDE_EXTERN void
+_gbp_editor_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_APPLICATION_ADDIN,
+                                              GBP_TYPE_EDITOR_APPLICATION_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_HOVER_PROVIDER,
+                                              GBP_TYPE_EDITOR_HOVER_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_FRAME_ADDIN,
+                                              GBP_TYPE_EDITOR_FRAME_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_SESSION_ADDIN,
+                                              GBP_TYPE_EDITOR_SESSION_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GBP_TYPE_EDITOR_WORKBENCH_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_EDITOR_WORKSPACE_ADDIN);
+}
diff --git a/src/plugins/editor/editor.gresource.xml b/src/plugins/editor/editor.gresource.xml
new file mode 100644
index 000000000..8f9624a5b
--- /dev/null
+++ b/src/plugins/editor/editor.gresource.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/editor">
+    <file>gtk/menus.ui</file>
+    <file>editor.plugin</file>
+    <file>gbp-editor-frame-controls.ui</file>
+  </gresource>
+  <gresource prefix="/org/gnome/builder/keybindings">
+    <file>default.css</file>
+    <file>shared.css</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/editor/editor.plugin b/src/plugins/editor/editor.plugin
new file mode 100644
index 000000000..f534c283b
--- /dev/null
+++ b/src/plugins/editor/editor.plugin
@@ -0,0 +1,11 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Description=Builder's greeter window
+Embedded=_gbp_editor_register_types
+Hidden=true
+Module=editor
+Name=Editor
+X-At-Startup=true
+X-Workspace-Kind=primary;editor;
diff --git a/src/plugins/editor/gbp-editor-application-addin.c 
b/src/plugins/editor/gbp-editor-application-addin.c
new file mode 100644
index 000000000..7a1fff061
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-application-addin.c
@@ -0,0 +1,199 @@
+/* gbp-editor-application-addin.c
+ *
+ * Copyright 2018-2019 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-editor-application-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <stdlib.h>
+
+#include "ide-window-settings-private.h"
+
+#include "gbp-editor-application-addin.h"
+
+struct _GbpEditorApplicationAddin
+{
+  GObject parent_instance;
+};
+
+static GFile *
+get_common_ancestor (GPtrArray *files)
+{
+  GFile *ancestor;
+
+  if (files->len == 0)
+    return NULL;
+
+  ancestor = g_file_get_parent (g_ptr_array_index (files, 0));
+
+  for (guint i = 1; i < files->len; i++)
+    {
+      GFile *file = g_ptr_array_index (files, i);
+
+      while (!g_file_has_prefix (file, ancestor))
+        {
+          GFile *old = ancestor;
+          ancestor = g_file_get_parent (old);
+          if (g_file_equal (ancestor, old))
+            break;
+          g_object_unref (old);
+        }
+    }
+
+  return g_steal_pointer (&ancestor);
+}
+
+static void
+gbp_editor_application_addin_add_option_entries (IdeApplicationAddin *addin,
+                                                 IdeApplication      *app)
+{
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (G_IS_APPLICATION (app));
+
+  g_application_add_main_option (G_APPLICATION (app),
+                                 "editor",
+                                 'e',
+                                 G_OPTION_FLAG_IN_MAIN,
+                                 G_OPTION_ARG_NONE,
+                                 _("Use minial editor interface"),
+                                 NULL);
+}
+
+static void
+gbp_editor_application_addin_open_all_cb (GObject      *object,
+                                          GAsyncResult *result,
+                                          gpointer      user_data)
+{
+  IdeWorkbench *workbench = (IdeWorkbench *)object;
+  g_autoptr(GApplicationCommandLine) cmdline = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  if (!ide_workbench_open_finish (workbench, result, &error))
+    {
+      g_application_command_line_printerr (cmdline, "%s\n", error->message);
+      g_application_command_line_set_exit_status (cmdline, EXIT_FAILURE);
+      return;
+    }
+
+  g_application_command_line_set_exit_status (cmdline, EXIT_SUCCESS);
+}
+
+static void
+gbp_editor_application_addin_handle_command_line (IdeApplicationAddin     *addin,
+                                                  IdeApplication          *application,
+                                                  GApplicationCommandLine *cmdline)
+{
+  g_autoptr(IdeWorkbench) workbench = NULL;
+  IdeEditorWorkspace *workspace;
+  IdeApplication *app = (IdeApplication *)application;
+  g_autoptr(GPtrArray) files = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_auto(GStrv) argv = NULL;
+  GVariantDict *options;
+  IdeContext *context;
+  gint argc;
+
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (app));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  if ((options = g_application_command_line_get_options_dict (cmdline)) &&
+      g_variant_dict_contains (options, "editor"))
+    ide_application_set_workspace_type (application, IDE_TYPE_EDITOR_WORKSPACE);
+
+  /* Ignore if no parameters were passed */
+  argv = g_application_command_line_get_arguments (cmdline, &argc);
+  if (argc < 2)
+    return;
+
+  /*
+   * If the user is trying to open various files using the command line with
+   * something like "gnome-builder x.c y.c z.c" then instead of opening the
+   * full project system, we'll open a simplified editor workspace for just
+   * these files and avoid loading a project altogether. That means that they
+   * wont get all of the IDE experience, but its faster to get quick editing
+   * done and then exit.
+   */
+
+  files = g_ptr_array_new_with_free_func (g_object_unref);
+  for (guint i = 1; i < argc; i++)
+    g_ptr_array_add (files,
+                     g_application_command_line_create_file_for_arg (cmdline, argv[i]));
+
+  workbench = ide_workbench_new ();
+  ide_application_add_workbench (app, workbench);
+
+  workdir = get_common_ancestor (files);
+  context = ide_workbench_get_context (workbench);
+
+  /* Setup the working directory to top-most common ancestor of the
+   * files. That way we can still get somewhat localized search results
+   * and other workspace features.
+   */
+  if (workdir != NULL)
+    ide_context_set_workdir (context, workdir);
+
+  workspace = ide_editor_workspace_new (app);
+  ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+
+  /* Since we are opening a toplevel window, we want to restore it using
+   * the same window sizing as the primary IDE window.
+   */
+  _ide_window_settings_register (GTK_WINDOW (workspace));
+
+  ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+
+  g_assert (files->len > 0);
+
+  ide_workbench_open_all_async (workbench,
+                                (GFile **)(gpointer)files->pdata,
+                                files->len,
+                                "editor",
+                                NULL,
+                                gbp_editor_application_addin_open_all_cb,
+                                g_object_ref (cmdline));
+}
+
+static void
+cmdline_addin_iface_init (IdeApplicationAddinInterface *iface)
+{
+  iface->add_option_entries = gbp_editor_application_addin_add_option_entries;
+  iface->handle_command_line = gbp_editor_application_addin_handle_command_line;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpEditorApplicationAddin, gbp_editor_application_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_APPLICATION_ADDIN, cmdline_addin_iface_init))
+
+static void
+gbp_editor_application_addin_class_init (GbpEditorApplicationAddinClass *klass)
+{
+}
+
+static void
+gbp_editor_application_addin_init (GbpEditorApplicationAddin *self)
+{
+}
diff --git a/src/plugins/editor/gbp-editor-application-addin.h 
b/src/plugins/editor/gbp-editor-application-addin.h
new file mode 100644
index 000000000..7549490a4
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-application-addin.h
@@ -0,0 +1,31 @@
+/* gbp-editor-application-addin.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_EDITOR_APPLICATION_ADDIN (gbp_editor_application_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpEditorApplicationAddin, gbp_editor_application_addin, GBP, 
EDITOR_APPLICATION_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/editor/gbp-editor-frame-addin.c b/src/plugins/editor/gbp-editor-frame-addin.c
new file mode 100644
index 000000000..d9c339603
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-frame-addin.c
@@ -0,0 +1,115 @@
+/* gbp-editor-frame-addin.c
+ *
+ * Copyright 2017-2019 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-editor-frame-addin.h"
+
+#include "config.h"
+
+#include <libide-gui.h>
+#include <libide-editor.h>
+
+#include "gbp-editor-frame-addin.h"
+#include "gbp-editor-frame-controls.h"
+
+struct _GbpEditorFrameAddin
+{
+  GObject                 parent_instance;
+  GbpEditorFrameControls *controls;
+};
+
+static void
+gbp_editor_frame_addin_load (IdeFrameAddin *addin,
+                             IdeFrame      *stack)
+{
+  GbpEditorFrameAddin *self = (GbpEditorFrameAddin *)addin;
+  GtkWidget *header;
+
+  g_assert (GBP_IS_EDITOR_FRAME_ADDIN (self));
+  g_assert (IDE_IS_FRAME (stack));
+
+  header = ide_frame_get_titlebar (stack);
+
+  self->controls = g_object_new (GBP_TYPE_EDITOR_FRAME_CONTROLS, NULL);
+  g_signal_connect (self->controls,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->controls);
+  gtk_container_add_with_properties (GTK_CONTAINER (header), GTK_WIDGET (self->controls),
+                                     "pack-type", GTK_PACK_END,
+                                     "priority", 100,
+                                     NULL);
+}
+
+static void
+gbp_editor_frame_addin_unload (IdeFrameAddin *addin,
+                               IdeFrame      *stack)
+{
+  GbpEditorFrameAddin *self = (GbpEditorFrameAddin *)addin;
+
+  g_assert (GBP_IS_EDITOR_FRAME_ADDIN (self));
+  g_assert (IDE_IS_FRAME (stack));
+
+  if (self->controls != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->controls));
+}
+
+static void
+gbp_editor_frame_addin_set_page (IdeFrameAddin *addin,
+                                 IdePage       *page)
+{
+  GbpEditorFrameAddin *self = (GbpEditorFrameAddin *)addin;
+
+  g_assert (GBP_IS_EDITOR_FRAME_ADDIN (self));
+  g_assert (!page || IDE_IS_PAGE (page));
+
+  if (IDE_IS_EDITOR_PAGE (page))
+    {
+      gbp_editor_frame_controls_set_page (self->controls, IDE_EDITOR_PAGE (page));
+      gtk_widget_show (GTK_WIDGET (self->controls));
+    }
+  else
+    {
+      gbp_editor_frame_controls_set_page (self->controls, NULL);
+      gtk_widget_hide (GTK_WIDGET (self->controls));
+    }
+}
+
+static void
+frame_addin_iface_init (IdeFrameAddinInterface *iface)
+{
+  iface->load = gbp_editor_frame_addin_load;
+  iface->unload = gbp_editor_frame_addin_unload;
+  iface->set_page = gbp_editor_frame_addin_set_page;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpEditorFrameAddin,
+                         gbp_editor_frame_addin,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_FRAME_ADDIN, frame_addin_iface_init))
+
+static void
+gbp_editor_frame_addin_class_init (GbpEditorFrameAddinClass *klass)
+{
+}
+
+static void
+gbp_editor_frame_addin_init (GbpEditorFrameAddin *self)
+{
+}
diff --git a/src/plugins/editor/gbp-editor-frame-addin.h b/src/plugins/editor/gbp-editor-frame-addin.h
new file mode 100644
index 000000000..9acaf2d3c
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-frame-addin.h
@@ -0,0 +1,31 @@
+/* gbp-editor-frame-addin.h
+ *
+ * Copyright 2017-2019 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_EDITOR_FRAME_ADDIN (gbp_editor_frame_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpEditorFrameAddin, gbp_editor_frame_addin, GBP, EDITOR_FRAME_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/editor/gbp-editor-frame-controls.c b/src/plugins/editor/gbp-editor-frame-controls.c
new file mode 100644
index 000000000..15802d3cd
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-frame-controls.c
@@ -0,0 +1,356 @@
+/* gbp-editor-frame-controls.c
+ *
+ * Copyright 2016-2019 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-editor-frame-controls"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-editor.h>
+
+#define IDE_EDITOR_INSIDE
+#include "ide-editor-private.h"
+#undef IDE_EDITOR_INSIDE
+
+#include "gbp-editor-frame-controls.h"
+
+
+G_DEFINE_TYPE (GbpEditorFrameControls, gbp_editor_frame_controls, GTK_TYPE_BOX)
+
+static void
+document_cursor_moved (GbpEditorFrameControls *self,
+                       const GtkTextIter      *iter,
+                       GtkTextBuffer          *buffer)
+{
+  IdeSourceView *source_view;
+  GtkTextIter bounds;
+  GtkTextMark *mark;
+  gchar str[32];
+  guint line;
+  gint column;
+  gint column2;
+
+  g_assert (GBP_IS_EDITOR_FRAME_CONTROLS (self));
+  g_assert (iter != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (self->page == NULL)
+    return;
+
+  if (ide_buffer_get_loading (IDE_BUFFER (buffer)))
+    return;
+
+  source_view = ide_editor_page_get_view (self->page);
+
+  ide_source_view_get_visual_position (source_view, &line, (guint *)&column);
+
+  mark = gtk_text_buffer_get_selection_bound (buffer);
+  gtk_text_buffer_get_iter_at_mark (buffer, &bounds, mark);
+
+  g_snprintf (str, sizeof str, "%d", line + 1);
+  dzl_simple_label_set_label (self->line_label, str);
+
+  g_snprintf (str, sizeof str, "%d", column + 1);
+  dzl_simple_label_set_label (self->column_label, str);
+
+  if (!gtk_widget_has_focus (GTK_WIDGET (source_view)) ||
+      gtk_text_iter_equal (&bounds, iter) ||
+      (gtk_text_iter_get_line (iter) != gtk_text_iter_get_line (&bounds)))
+    {
+      gtk_widget_set_visible (GTK_WIDGET (self->range_label), FALSE);
+      return;
+    }
+
+  /* We have a selection that is on the same line.
+   * Lets give some detail as to how long the selection is.
+   */
+  column2 = gtk_source_view_get_visual_column (GTK_SOURCE_VIEW (source_view), &bounds);
+
+  g_snprintf (str, sizeof str, "%u", ABS (column2 - column));
+  gtk_label_set_label (self->range_label, str);
+  gtk_widget_set_visible (GTK_WIDGET (self->range_label), TRUE);
+}
+
+
+static void
+goto_line_activate (GbpEditorFrameControls *self,
+                    const gchar            *text,
+                    DzlSimplePopover       *popover)
+{
+  gint64 value;
+
+  g_assert (GBP_IS_EDITOR_FRAME_CONTROLS (self));
+  g_assert (DZL_IS_SIMPLE_POPOVER (popover));
+
+  if (self->page == NULL)
+    return;
+
+  if (!dzl_str_empty0 (text))
+    {
+      value = g_ascii_strtoll (text, NULL, 10);
+
+      if ((value > 0) && (value < G_MAXINT))
+        {
+          IdeSourceView *source_view;
+          GtkTextBuffer *buffer = GTK_TEXT_BUFFER (self->page->buffer);
+          GtkTextIter iter;
+
+          source_view = ide_editor_page_get_view (self->page);
+
+          gtk_widget_grab_focus (GTK_WIDGET (self->page));
+          gtk_text_buffer_get_iter_at_line (buffer, &iter, value - 1);
+          gtk_text_buffer_select_range (buffer, &iter, &iter);
+          ide_source_view_scroll_to_iter (source_view, &iter, 0.25, TRUE, 1.0, 0.5, TRUE);
+        }
+    }
+}
+
+static gboolean
+goto_line_insert_text (GbpEditorFrameControls *self,
+                       guint                   position,
+                       const gchar            *chars,
+                       guint                   n_chars,
+                       DzlSimplePopover       *popover)
+{
+  g_assert (GBP_IS_EDITOR_FRAME_CONTROLS (self));
+  g_assert (DZL_IS_SIMPLE_POPOVER (popover));
+  g_assert (chars != NULL);
+
+  for (; *chars; chars = g_utf8_next_char (chars))
+    {
+      if (!g_unichar_isdigit (g_utf8_get_char (chars)))
+        return GDK_EVENT_STOP;
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+goto_line_changed (GbpEditorFrameControls *self,
+                   DzlSimplePopover       *popover)
+{
+  gchar *message;
+  const gchar *text;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (GBP_IS_EDITOR_FRAME_CONTROLS (self));
+  g_assert (DZL_IS_SIMPLE_POPOVER (popover));
+
+  if (self->page == NULL)
+    return;
+
+  text = dzl_simple_popover_get_text (popover);
+
+  gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (self->page->buffer), &begin, &end);
+
+  if (!dzl_str_empty0 (text))
+    {
+      gint64 value;
+
+      value = g_ascii_strtoll (text, NULL, 10);
+
+      if (value > 0)
+        {
+          if (value <= gtk_text_iter_get_line (&end) + 1)
+            {
+              dzl_simple_popover_set_message (popover, NULL);
+              dzl_simple_popover_set_ready (popover, TRUE);
+              return;
+            }
+        }
+    }
+
+  /* translators: the user selected a number outside the value range for the document. */
+  message = g_strdup_printf (_("Provide a number between 1 and %u"),
+                             gtk_text_iter_get_line (&end) + 1);
+  dzl_simple_popover_set_message (popover, message);
+  dzl_simple_popover_set_ready (popover, FALSE);
+
+  g_free (message);
+}
+
+static void
+warning_button_clicked (GbpEditorFrameControls *self,
+                        GtkButton              *button)
+{
+  IdeSourceView *source_view;
+
+  g_assert (GBP_IS_EDITOR_FRAME_CONTROLS (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  if (self->page == NULL)
+    return;
+
+  source_view = ide_editor_page_get_view (self->page);
+  gtk_widget_grab_focus (GTK_WIDGET (source_view));
+  g_signal_emit_by_name (source_view, "move-error", GTK_DIR_DOWN);
+}
+
+static void
+show_goto_line (GSimpleAction          *action,
+                GVariant               *param,
+                GbpEditorFrameControls *self)
+{
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_EDITOR_FRAME_CONTROLS (self));
+
+  gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->goto_line_button), TRUE);
+}
+
+static void
+gbp_editor_frame_controls_bind (GbpEditorFrameControls *self,
+                                GtkTextBuffer          *buffer,
+                                DzlSignalGroup         *buffer_signals)
+{
+  GtkTextIter iter;
+
+  g_assert (GBP_IS_EDITOR_FRAME_CONTROLS (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (DZL_IS_SIGNAL_GROUP (buffer_signals));
+
+  gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_insert (buffer));
+  document_cursor_moved (self, &iter, buffer);
+}
+
+static void
+gbp_editor_frame_controls_finalize (GObject *object)
+{
+  GbpEditorFrameControls *self = (GbpEditorFrameControls *)object;
+
+  g_clear_object (&self->buffer_bindings);
+  g_clear_object (&self->buffer_signals);
+  g_clear_object (&self->goto_line_action);
+
+  self->page = NULL;
+
+  G_OBJECT_CLASS (gbp_editor_frame_controls_parent_class)->finalize (object);
+}
+
+static void
+gbp_editor_frame_controls_class_init (GbpEditorFrameControlsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gbp_editor_frame_controls_finalize;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/editor/gbp-editor-frame-controls.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpEditorFrameControls, column_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpEditorFrameControls, goto_line_popover);
+  gtk_widget_class_bind_template_child (widget_class, GbpEditorFrameControls, goto_line_button);
+  gtk_widget_class_bind_template_child (widget_class, GbpEditorFrameControls, line_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpEditorFrameControls, range_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpEditorFrameControls, warning_button);
+}
+
+static void
+gbp_editor_frame_controls_init (GbpEditorFrameControls *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->goto_line_popover,
+                           "activate",
+                           G_CALLBACK (goto_line_activate),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->goto_line_popover,
+                           "insert-text",
+                           G_CALLBACK (goto_line_insert_text),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->goto_line_popover,
+                           "changed",
+                           G_CALLBACK (goto_line_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->warning_button,
+                           "clicked",
+                           G_CALLBACK (warning_button_clicked),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  self->buffer_bindings = dzl_binding_group_new ();
+
+  dzl_binding_group_bind (self->buffer_bindings, "has-diagnostics",
+                          self->warning_button, "visible",
+                          G_BINDING_SYNC_CREATE);
+
+  self->buffer_signals = dzl_signal_group_new (IDE_TYPE_BUFFER);
+
+  g_signal_connect_swapped (self->buffer_signals,
+                            "bind",
+                            G_CALLBACK (gbp_editor_frame_controls_bind),
+                            self);
+
+  dzl_signal_group_connect_object (self->buffer_signals,
+                                   "cursor-moved",
+                                   G_CALLBACK (document_cursor_moved),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  self->goto_line_action = g_simple_action_new ("goto-line", NULL);
+  g_signal_connect_object (self->goto_line_action,
+                           "activate",
+                           G_CALLBACK (show_goto_line),
+                           self,
+                           0);
+}
+
+void
+gbp_editor_frame_controls_set_page (GbpEditorFrameControls *self,
+                                    IdeEditorPage          *page)
+{
+  GActionGroup *editor_page_group;
+
+  g_return_if_fail (GBP_IS_EDITOR_FRAME_CONTROLS (self));
+  g_return_if_fail (!page || IDE_IS_EDITOR_PAGE (page));
+
+  if (self->page == page)
+    return;
+
+  dzl_binding_group_set_source (self->buffer_bindings, NULL);
+  dzl_signal_group_set_target (self->buffer_signals, NULL);
+
+  if (self->page != NULL)
+    {
+      g_signal_handlers_disconnect_by_func (self->page,
+                                            G_CALLBACK (gtk_widget_destroyed),
+                                            &self->page);
+      self->page = NULL;
+    }
+
+  if (page != NULL)
+    {
+      self->page = page;
+      g_signal_connect (page,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        &self->page);
+      dzl_binding_group_set_source (self->buffer_bindings, page->buffer);
+      dzl_signal_group_set_target (self->buffer_signals, page->buffer);
+
+      if (NULL != (editor_page_group = gtk_widget_get_action_group (GTK_WIDGET (page), "editor-page")))
+        g_action_map_add_action (G_ACTION_MAP (editor_page_group), G_ACTION (self->goto_line_action));
+    }
+}
diff --git a/src/plugins/editor/gbp-editor-frame-controls.h b/src/plugins/editor/gbp-editor-frame-controls.h
new file mode 100644
index 000000000..c9163f2e7
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-frame-controls.h
@@ -0,0 +1,57 @@
+/* gbp-editor-frame-controls.h
+ *
+ * Copyright 2016-2019 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 <gtk/gtk.h>
+#include <dazzle.h>
+
+#include <gtk/gtk.h>
+#include <dazzle.h>
+#include <libide-editor.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_EDITOR_FRAME_CONTROLS (gbp_editor_frame_controls_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpEditorFrameControls, gbp_editor_frame_controls, GBP, EDITOR_FRAME_CONTROLS, GtkBox)
+
+struct _GbpEditorFrameControls
+{
+  GtkBox                parent_instance;
+
+  IdeEditorPage        *page;
+  DzlBindingGroup      *buffer_bindings;
+  DzlSignalGroup       *buffer_signals;
+
+  DzlSimplePopover     *goto_line_popover;
+  GtkMenuButton        *goto_line_button;
+  GtkButton            *warning_button;
+  DzlSimpleLabel       *line_label;
+  DzlSimpleLabel       *column_label;
+  GtkLabel             *range_label;
+
+  GSimpleAction        *goto_line_action;
+};
+
+void gbp_editor_frame_controls_set_page (GbpEditorFrameControls *self,
+                                         IdeEditorPage          *page);
+
+G_END_DECLS
diff --git a/src/plugins/editor/gbp-editor-frame-controls.ui b/src/plugins/editor/gbp-editor-frame-controls.ui
new file mode 100644
index 000000000..d3aa8bc13
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-frame-controls.ui
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpEditorFrameControls" parent="GtkBox">
+    <property name="orientation">horizontal</property>
+    <child>
+      <object class="GtkButton" id="warning_button">
+        <property name="visible">false</property>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">dialog-warning-symbolic</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkMenuButton" id="goto_line_button">
+        <property name="popover">goto_line_popover</property>
+        <property name="focus-on-click">false</property>
+        <property name="tooltip-text" translatable="yes">Go to line number</property>
+        <property name="valign">baseline</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkBox">
+            <property name="valign">baseline</property>
+            <property name="visible">true</property>
+            <child type="center">
+              <object class="GtkLabel">
+                <property name="label">:</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="DzlSimpleLabel" id="line_label">
+                <property name="label">1</property>
+                <property name="width-chars">3</property>
+                <property name="xalign">1.0</property>
+                <property name="valign">baseline</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="pack-type">start</property>
+              </packing>
+            </child>
+            <child>
+              <object class="DzlSimpleLabel" id="column_label">
+                <property name="label">1</property>
+                <property name="width-chars">3</property>
+                <property name="xalign">0.0</property>
+                <property name="valign">baseline</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="position">1</property>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="range_label">
+                <property name="valign">baseline</property>
+                <style>
+                  <class name="dim-label"/>
+                </style>
+              </object>
+              <packing>
+                <property name="position">0</property>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="DzlSimplePopover" id="goto_line_popover">
+    <property name="title" translatable="yes">Go to Line</property>
+    <property name="button-text" translatable="yes">Go</property>
+  </object>
+</interface>
diff --git a/src/plugins/editor/gbp-editor-hover-provider.c b/src/plugins/editor/gbp-editor-hover-provider.c
new file mode 100644
index 000000000..b4319775d
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-hover-provider.c
@@ -0,0 +1,118 @@
+/* gbp-editor-hover-provider.c
+ *
+ * Copyright 2018-2019 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-editor-hover-provider"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-code.h>
+#include <libide-sourceview.h>
+#include <libide-threading.h>
+
+#include "gbp-editor-hover-provider.h"
+
+#define DIAGNOSTICS_HOVER_PRIORITY 500
+
+struct _GbpEditorHoverProvider
+{
+  GObject parent_instance;
+};
+
+static void
+gbp_editor_hover_provider_hover_async (IdeHoverProvider    *provider,
+                                       IdeHoverContext     *context,
+                                       const GtkTextIter   *iter,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  GbpEditorHoverProvider *self = (GbpEditorHoverProvider *)provider;
+  g_autoptr(IdeTask) task = NULL;
+  GtkTextBuffer *buffer;
+
+  g_assert (GBP_IS_EDITOR_HOVER_PROVIDER (self));
+  g_assert (IDE_IS_HOVER_CONTEXT (context));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_editor_hover_provider_hover_async);
+
+  buffer = gtk_text_iter_get_buffer (iter);
+
+  if (IDE_IS_BUFFER (buffer))
+    {
+      GFile *file = ide_buffer_get_file (IDE_BUFFER (buffer));
+      guint line = gtk_text_iter_get_line (iter);
+      IdeDiagnostics *diagnostics;
+      IdeDiagnostic *diag;
+
+      if ((diagnostics = ide_buffer_get_diagnostics (IDE_BUFFER (buffer))) &&
+          (diag = ide_diagnostics_get_diagnostic_at_line (diagnostics, file, line)))
+        {
+          g_autoptr(IdeMarkedContent) content = NULL;
+          g_autofree gchar *text = ide_diagnostic_get_text_for_display (diag);
+
+          content = ide_marked_content_new_from_data (text,
+                                                      strlen (text),
+                                                      IDE_MARKED_KIND_PLAINTEXT);
+          ide_hover_context_add_content (context,
+                                         DIAGNOSTICS_HOVER_PRIORITY,
+                                         _("Diagnostics"),
+                                         content);
+        }
+    }
+
+  ide_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_SUPPORTED,
+                             "No information to display");
+}
+
+static gboolean
+gbp_editor_hover_provider_hover_finish (IdeHoverProvider  *self,
+                                        GAsyncResult      *result,
+                                        GError           **error)
+{
+  g_assert (IDE_IS_HOVER_PROVIDER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+hover_provider_iface_init (IdeHoverProviderInterface *iface)
+{
+  iface->hover_async = gbp_editor_hover_provider_hover_async;
+  iface->hover_finish = gbp_editor_hover_provider_hover_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpEditorHoverProvider, gbp_editor_hover_provider, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_HOVER_PROVIDER, hover_provider_iface_init))
+
+static void
+gbp_editor_hover_provider_class_init (GbpEditorHoverProviderClass *klass)
+{
+}
+
+static void
+gbp_editor_hover_provider_init (GbpEditorHoverProvider *self)
+{
+}
diff --git a/src/plugins/editor/gbp-editor-hover-provider.h b/src/plugins/editor/gbp-editor-hover-provider.h
new file mode 100644
index 000000000..baacdde4d
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-hover-provider.h
@@ -0,0 +1,31 @@
+/* gbp-editor-hover-provider.h
+ *
+ * Copyright 2018-2019 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 "ide-hover-provider.h"
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_EDITOR_HOVER_PROVIDER (gbp_editor_hover_provider_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpEditorHoverProvider, gbp_editor_hover_provider, GBP, EDITOR_HOVER_PROVIDER, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/editor/gbp-editor-session-addin.c b/src/plugins/editor/gbp-editor-session-addin.c
new file mode 100644
index 000000000..61b782242
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-session-addin.c
@@ -0,0 +1,564 @@
+/* gbp-editor-session-addin.c
+ *
+ * Copyright 2018-2019 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-editor-session-addin"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-gui.h>
+#include <libide-editor.h>
+#include <libide-threading.h>
+
+#include "ide-editor-private.h"
+#include "ide-gui-private.h"
+
+#include "gbp-editor-session-addin.h"
+
+struct _GbpEditorSessionAddin
+{
+  IdeObject parent_instance;
+};
+
+typedef struct
+{
+  gchar *uri;
+  gint   column;
+  gint   row;
+  gint   depth;
+  struct {
+    gchar    *keyword;
+    gboolean  case_sensitive;
+    gboolean  regex_enabled;
+    gboolean  at_word_boundaries;
+  } search;
+} Item;
+
+typedef struct
+{
+  IdeWorkspace *workspace;
+  GArray       *items;
+  gint          active;
+} LoadState;
+
+static void
+load_state_free (LoadState *state)
+{
+  g_clear_pointer (&state->items, g_array_unref);
+  g_clear_object (&state->workspace);
+  g_slice_free (LoadState, state);
+}
+
+static IdeWorkspace *
+find_workspace (IdeWorkbench *workbench)
+{
+  IdeWorkspace *workspace;
+
+  if (!(workspace = ide_workbench_get_workspace_by_type (workbench, IDE_TYPE_PRIMARY_WORKSPACE)))
+    workspace = ide_workbench_get_workspace_by_type (workbench, IDE_TYPE_EDITOR_WORKSPACE);
+
+  return workspace;
+}
+
+static gint
+compare_item (gconstpointer a,
+              gconstpointer b)
+{
+  const Item *item_a = a;
+  const Item *item_b = b;
+  gint ret;
+
+  if (!(ret = item_a->column - item_b->column))
+    {
+      if (!(ret = item_a->row - item_b->row))
+        ret = item_a->depth - item_b->depth;
+    }
+
+  return ret;
+}
+
+static void
+clear_item (Item *item)
+{
+  g_clear_pointer (&item->uri, g_free);
+  g_clear_pointer (&item->search.keyword, g_free);
+}
+
+static void
+get_view_position (IdePage *view,
+                   gint    *out_column,
+                   gint    *out_row,
+                   gint    *out_depth)
+{
+  GtkWidget *column;
+  GtkWidget *grid;
+  GtkWidget *lstack;
+  GtkWidget *stack;
+  gint depth;
+  gint index_;
+
+  g_assert (IDE_IS_PAGE (view));
+  g_assert (out_column != NULL);
+  g_assert (out_row != NULL);
+
+  *out_column = 0;
+  *out_row = 0;
+  *out_depth = 0;
+
+  stack = gtk_widget_get_ancestor (GTK_WIDGET (view), GTK_TYPE_STACK);
+  lstack = gtk_widget_get_ancestor (GTK_WIDGET (stack), IDE_TYPE_FRAME);
+  column = gtk_widget_get_ancestor (GTK_WIDGET (stack), IDE_TYPE_GRID_COLUMN);
+  grid = gtk_widget_get_ancestor (GTK_WIDGET (column), IDE_TYPE_GRID);
+
+  gtk_container_child_get (GTK_CONTAINER (stack), GTK_WIDGET (view),
+                           "position", &depth,
+                           NULL);
+  *out_depth = MAX (depth, 0);
+
+  gtk_container_child_get (GTK_CONTAINER (column), GTK_WIDGET (lstack),
+                           "index", &index_,
+                           NULL);
+  *out_row = MAX (index_, 0);
+
+  gtk_container_child_get (GTK_CONTAINER (grid), GTK_WIDGET (column),
+                           "index", &index_,
+                           NULL);
+  *out_column = MAX (index_, 0);
+}
+
+static void
+gbp_editor_session_addin_foreach_page_cb (GtkWidget *widget,
+                                          gpointer   user_data)
+{
+  IdePage *view = (IdePage *)widget;
+  GArray *items = user_data;
+
+  g_assert (IDE_IS_PAGE (view));
+  g_assert (items != NULL);
+
+  if (IDE_IS_EDITOR_PAGE (view))
+    {
+      IdeBuffer *buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (view));
+      GFile *file = ide_buffer_get_file (buffer);
+      IdeEditorSearch *search = ide_editor_page_get_search (IDE_EDITOR_PAGE (view));
+      Item item = { 0 };
+
+      if (!ide_buffer_get_is_temporary (buffer))
+        {
+          item.uri = g_file_get_uri (file);
+          get_view_position (view, &item.column, &item.row, &item.depth);
+
+          item.search.keyword = g_strdup (ide_editor_search_get_search_text (search));
+          item.search.at_word_boundaries = ide_editor_search_get_at_word_boundaries (search);
+          item.search.case_sensitive = ide_editor_search_get_case_sensitive (search);
+          item.search.regex_enabled = ide_editor_search_get_regex_enabled (search);
+
+          IDE_TRACE_MSG ("%u:%u:%u: %s", item.column, item.row, item.depth, item.uri);
+
+          g_array_append_val (items, item);
+        }
+    }
+}
+
+static void
+gbp_editor_session_addin_save_async (IdeSessionAddin     *addin,
+                                     IdeWorkbench        *workbench,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  GbpEditorSessionAddin *self = (GbpEditorSessionAddin *)addin;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GArray) items = NULL;
+  GVariantBuilder builder;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_SESSION_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (addin, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_editor_session_addin_save_async);
+
+  items = g_array_new (FALSE, FALSE, sizeof (Item));
+  g_array_set_clear_func (items, (GDestroyNotify)clear_item);
+
+  ide_workbench_foreach_page (workbench,
+                              gbp_editor_session_addin_foreach_page_cb,
+                              items);
+
+  g_array_sort (items, compare_item);
+
+  g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(siiiv)"));
+
+  for (guint i = 0; i < items->len; i++)
+    {
+      const Item *item = &g_array_index (items, Item, i);
+      GVariantBuilder sub;
+
+      g_variant_builder_init (&sub, G_VARIANT_TYPE ("a{sv}"));
+      g_variant_builder_add_parsed (&sub, "{'search.keyword',<%s>}",item->search.keyword ?: "");
+      g_variant_builder_add_parsed (&sub, "{'search.at-word-boundaries',<%b>}", 
item->search.at_word_boundaries);
+      g_variant_builder_add_parsed (&sub, "{'search.regex-enabled',<%b>}", item->search.regex_enabled);
+      g_variant_builder_add_parsed (&sub, "{'search.case-sensitive',<%b>}", item->search.case_sensitive);
+
+      g_variant_builder_add (&builder,
+                             "(siiiv)",
+                             item->uri,
+                             item->column,
+                             item->row,
+                             item->depth,
+                             g_variant_new_variant (g_variant_builder_end (&sub)));
+    }
+
+  ide_task_return_pointer (task,
+                           g_variant_take_ref (g_variant_builder_end (&builder)),
+                           (GDestroyNotify)g_variant_unref);
+
+  IDE_EXIT;
+}
+
+static GVariant *
+gbp_editor_session_addin_save_finish (IdeSessionAddin  *self,
+                                      GAsyncResult     *result,
+                                      GError          **error)
+{
+  g_assert (GBP_IS_EDITOR_SESSION_ADDIN (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+load_state_finish (GbpEditorSessionAddin *self,
+                   LoadState             *state)
+{
+  IdeBufferManager *bufmgr;
+  IdeContext *context;
+  IdeSurface *editor;
+  IdeGrid *grid;
+
+  g_assert (GBP_IS_EDITOR_SESSION_ADDIN (self));
+  g_assert (state != NULL);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  bufmgr = ide_buffer_manager_from_context (context);
+
+  editor = ide_workspace_get_surface_by_name (state->workspace, "editor");
+  grid = ide_editor_surface_get_grid (IDE_EDITOR_SURFACE (editor));
+
+  /* Now restore views in the proper place */
+
+  for (guint i = 0; i < state->items->len; i++)
+    {
+      const Item *item = &g_array_index (state->items, Item, i);
+      g_autoptr(GFile) file = NULL;
+      IdeGridColumn *column;
+      IdeEditorSearch *search;
+      IdeFrame *stack;
+      IdeEditorPage *view;
+      IdeBuffer *buffer;
+
+      file = g_file_new_for_uri (item->uri);
+
+      if (!(buffer = ide_buffer_manager_find_buffer (bufmgr, file)))
+        {
+          g_warning ("Failed to restore %s", item->uri);
+          continue;
+        }
+
+      column = ide_grid_get_nth_column (grid, item->column);
+      stack = _ide_grid_get_nth_stack_for_column (grid, column, item->row);
+
+      view = g_object_new (IDE_TYPE_EDITOR_PAGE,
+                           "buffer", buffer,
+                           "visible", TRUE,
+                           NULL);
+
+      search = ide_editor_page_get_search (view);
+
+      ide_editor_search_set_search_text (search, item->search.keyword);
+      ide_editor_search_set_at_word_boundaries (search, item->search.at_word_boundaries);
+      ide_editor_search_set_case_sensitive (search, item->search.case_sensitive);
+      ide_editor_search_set_regex_enabled (search, item->search.regex_enabled);
+
+      gtk_container_add (GTK_CONTAINER (stack), GTK_WIDGET (view));
+    }
+}
+
+static void
+gbp_editor_session_addin_load_file_cb (GObject      *object,
+                                       GAsyncResult *result,
+                                       gpointer      user_data)
+{
+  IdeBufferManager *bufmgr = (IdeBufferManager *)object;
+  g_autoptr(IdeBuffer) loaded = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GbpEditorSessionAddin *self;
+  LoadState *state;
+
+  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!(loaded = ide_buffer_manager_load_file_finish (bufmgr, result, &error)))
+    g_warning ("Failed to load buffer: %s", error->message);
+
+  state = ide_task_get_task_data (task);
+  self = ide_task_get_source_object (task);
+
+  g_assert (state != NULL);
+  g_assert (state->items != NULL);
+  g_assert (state->active > 0);
+  g_assert (GBP_IS_EDITOR_SESSION_ADDIN (self));
+
+  state->active--;
+
+  if (state->active == 0)
+    {
+      load_state_finish (self, state);
+      ide_task_return_boolean (task, TRUE);
+    }
+}
+
+static void
+restore_file (GObject      *source,
+              GAsyncResult *result,
+              gpointer      user_data)
+{
+  GFile *file = (GFile *)source;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GFileInfo) info = NULL;
+  GbpEditorSessionAddin *self;
+  LoadState *load_state;
+
+  IDE_ENTRY;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  load_state = ide_task_get_task_data (task);
+
+  g_assert (GBP_IS_EDITOR_SESSION_ADDIN (self));
+  g_assert (load_state != NULL);
+
+  if ((info = g_file_query_info_finish (file, result, &error)))
+    {
+      IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
+      IdeBufferManager *bufmgr = ide_buffer_manager_from_context (context);
+
+      ide_buffer_manager_load_file_async (bufmgr,
+                                          file,
+                                          IDE_BUFFER_OPEN_FLAGS_NO_VIEW,
+                                          ide_task_get_cancellable (task),
+                                          NULL,
+                                          gbp_editor_session_addin_load_file_cb,
+                                          g_object_ref (task));
+    }
+  else
+    {
+      load_state->active--;
+
+      if (load_state->active == 0)
+        {
+          load_state_finish (self, load_state);
+          ide_task_return_boolean (task, TRUE);
+        }
+    }
+}
+
+static void
+load_task_completed_cb (IdeTask          *task,
+                        GParamSpec       *pspec,
+                        IdeEditorSurface *surface)
+{
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_EDITOR_SURFACE (surface));
+
+  /* Always show the grid after the task completes */
+  _ide_editor_surface_set_loading (surface, FALSE);
+}
+
+static void
+gbp_editor_session_addin_restore_async (IdeSessionAddin     *addin,
+                                        IdeWorkbench        *workbench,
+                                        GVariant            *state,
+                                        GCancellable        *cancellable,
+                                        GAsyncReadyCallback  callback,
+                                        gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GHashTable) uris = NULL;
+  g_autoptr(GSettings) settings = NULL;
+  const gchar *uri;
+  LoadState *load_state;
+  IdeWorkspace *workspace;
+  IdeSurface *editor;
+  GVariantIter iter;
+  GVariant *extra = NULL;
+  const gchar *format = "(&siii)";
+  gint column, row, depth;
+
+  g_assert (GBP_IS_EDITOR_SESSION_ADDIN (addin));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (addin, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_editor_session_addin_restore_async);
+
+  if (state == NULL)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  if (!(workspace = find_workspace (workbench)) ||
+      !(editor = ide_workspace_get_surface_by_name (workspace, "editor")))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "NO editor surface to restore documents");
+      return;
+    }
+
+  g_signal_connect_object (task,
+                           "notify::completed",
+                           G_CALLBACK (load_task_completed_cb),
+                           editor,
+                           0);
+
+  settings = g_settings_new ("org.gnome.builder");
+
+  if (!g_settings_get_boolean (settings, "restore-previous-files"))
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  uris = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+  load_state = g_slice_new0 (LoadState);
+  load_state->items = g_array_new (FALSE, FALSE, sizeof (Item));
+  load_state->workspace = g_object_ref (workspace);
+  g_array_set_clear_func (load_state->items, (GDestroyNotify)clear_item);
+  ide_task_set_task_data (task, load_state, load_state_free);
+
+  g_variant_iter_init (&iter, state);
+
+  load_state->active++;
+
+  if (g_variant_is_of_type (state, G_VARIANT_TYPE ("a(siiiv)")))
+    format = "(&siiiv)";
+
+  while (g_variant_iter_next (&iter, format, &uri, &column, &row, &depth, &extra))
+    {
+      g_autoptr(GFile) gfile = NULL;
+      Item item = {0};
+
+      IDE_TRACE_MSG ("Restore URI \"%s\" at %d:%d:%d", uri, column, row, depth);
+
+      item.uri = g_strdup (uri);
+      item.column = column;
+      item.row = row;
+      item.depth = depth;
+
+      if (extra != NULL)
+        {
+          g_autoptr(GVariantDict) dict = NULL;
+
+          dict = g_variant_dict_new (g_variant_get_variant (extra));
+
+          g_variant_dict_lookup (dict, "search.keyword", "s", &item.search.keyword);
+          g_variant_dict_lookup (dict, "search.at-word-boundaries", "b", &item.search.at_word_boundaries);
+          g_variant_dict_lookup (dict, "search.case-sensitive", "b", &item.search.case_sensitive);
+          g_variant_dict_lookup (dict, "search.regex-enabled", "b", &item.search.regex_enabled);
+        }
+
+      g_array_append_val (load_state->items, item);
+
+      /* Skip loading buffer if already loading */
+      if (!g_hash_table_contains (uris, uri))
+        {
+          g_hash_table_add (uris, g_strdup (uri));
+          gfile = g_file_new_for_uri (uri);
+
+          load_state->active++;
+
+          g_file_query_info_async (gfile,
+                                   G_FILE_ATTRIBUTE_STANDARD_NAME,
+                                   G_FILE_QUERY_INFO_NONE,
+                                   G_PRIORITY_LOW,
+                                   cancellable,
+                                   restore_file,
+                                   g_object_ref (task));
+        }
+
+      g_clear_pointer (&extra, g_variant_unref);
+    }
+
+  load_state->active--;
+
+  if (load_state->active == 0)
+    {
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  /* Hide the grid until we've loaded */
+  _ide_editor_surface_set_loading (IDE_EDITOR_SURFACE (editor), TRUE);
+}
+
+static gboolean
+gbp_editor_session_addin_restore_finish (IdeSessionAddin  *self,
+                                         GAsyncResult     *result,
+                                         GError          **error)
+{
+  g_assert (GBP_IS_EDITOR_SESSION_ADDIN (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+session_addin_iface_init (IdeSessionAddinInterface *iface)
+{
+  iface->save_async = gbp_editor_session_addin_save_async;
+  iface->save_finish = gbp_editor_session_addin_save_finish;
+  iface->restore_async = gbp_editor_session_addin_restore_async;
+  iface->restore_finish = gbp_editor_session_addin_restore_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpEditorSessionAddin, gbp_editor_session_addin, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_SESSION_ADDIN, session_addin_iface_init))
+
+static void
+gbp_editor_session_addin_class_init (GbpEditorSessionAddinClass *klass)
+{
+}
+
+static void
+gbp_editor_session_addin_init (GbpEditorSessionAddin *self)
+{
+}
diff --git a/src/plugins/editor/gbp-editor-session-addin.h b/src/plugins/editor/gbp-editor-session-addin.h
new file mode 100644
index 000000000..7fa6b938c
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-session-addin.h
@@ -0,0 +1,31 @@
+/* gbp-editor-session-addin.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_EDITOR_SESSION_ADDIN (gbp_editor_session_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpEditorSessionAddin, gbp_editor_session_addin, GBP, EDITOR_SESSION_ADDIN, IdeObject)
+
+G_END_DECLS
diff --git a/src/plugins/editor/gbp-editor-workbench-addin.c b/src/plugins/editor/gbp-editor-workbench-addin.c
new file mode 100644
index 000000000..6d6079bd5
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-workbench-addin.c
@@ -0,0 +1,341 @@
+/* gbp-editor-workbench-addin.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-editor-workbench-addin"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+#include <libide-code.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <libide-io.h>
+#include <libide-threading.h>
+#include <string.h>
+
+#include "gbp-editor-workbench-addin.h"
+
+struct _GbpEditorWorkbenchAddin
+{
+  GObject       parent_instance;
+  IdeWorkbench *workbench;
+};
+
+typedef struct
+{
+  GFile              *file;
+  IdeBufferOpenFlags  flags;
+  gint                at_line;
+  gint                at_line_offset;
+} OpenFileTaskData;
+
+static void ide_workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (GbpEditorWorkbenchAddin, gbp_editor_workbench_addin, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKBENCH_ADDIN,
+                                               ide_workbench_addin_iface_init))
+
+static void
+open_file_task_data_free (gpointer data)
+{
+  OpenFileTaskData *td = data;
+
+  g_clear_object (&td->file);
+  g_slice_free (OpenFileTaskData, td);
+}
+
+static void
+gbp_editor_workbench_addin_class_init (GbpEditorWorkbenchAddinClass *klass)
+{
+}
+
+static void
+gbp_editor_workbench_addin_init (GbpEditorWorkbenchAddin *self)
+{
+}
+
+static void
+gbp_editor_workbench_addin_load (IdeWorkbenchAddin *addin,
+                                 IdeWorkbench      *workbench)
+{
+  GbpEditorWorkbenchAddin *self = (GbpEditorWorkbenchAddin *)addin;
+
+  g_assert (GBP_IS_EDITOR_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (self->workbench == NULL);
+
+  self->workbench = workbench;
+}
+
+static void
+gbp_editor_workbench_addin_unload (IdeWorkbenchAddin *addin,
+                                   IdeWorkbench      *workbench)
+{
+  GbpEditorWorkbenchAddin *self = (GbpEditorWorkbenchAddin *)addin;
+
+  g_assert (GBP_IS_EDITOR_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+
+  self->workbench = NULL;
+}
+
+static gboolean
+gbp_editor_workbench_addin_can_open (IdeWorkbenchAddin *addin,
+                                     GFile             *file,
+                                     const gchar       *content_type,
+                                     gint              *priority)
+{
+  const gchar *path;
+
+  g_assert (GBP_IS_EDITOR_WORKBENCH_ADDIN (addin));
+  g_assert (G_IS_FILE (file));
+  g_assert (priority != NULL);
+
+  *priority = 0;
+
+  path = g_file_peek_path (file);
+
+  if (path != NULL || content_type != NULL)
+    {
+      GtkSourceLanguageManager *manager;
+      GtkSourceLanguage *language;
+
+      manager = gtk_source_language_manager_get_default ();
+      language = gtk_source_language_manager_guess_language (manager, path, content_type);
+
+      if (language != NULL)
+        return TRUE;
+    }
+
+  if (content_type != NULL)
+    {
+      g_autofree gchar *text_type = NULL;
+
+      text_type = g_content_type_from_mime_type ("text/plain");
+      return g_content_type_is_a (content_type, text_type);
+    }
+
+  return FALSE;
+}
+
+static void
+find_workspace_surface_cb (GtkWidget *widget,
+                           gpointer   user_data)
+{
+  IdeSurface **surface = user_data;
+
+  g_assert (IDE_IS_WORKSPACE (widget));
+  g_assert (surface != NULL);
+  g_assert (*surface == NULL || IDE_IS_SURFACE (*surface));
+
+  if (*surface == NULL)
+    {
+      *surface = ide_workspace_get_surface_by_name (IDE_WORKSPACE (widget), "editor");
+      if (!IDE_IS_EDITOR_SURFACE (*surface))
+        *surface = NULL;
+    }
+}
+
+static void
+gbp_editor_workbench_addin_open_at_cb (GObject      *object,
+                                       GAsyncResult *result,
+                                       gpointer      user_data)
+{
+  IdeBufferManager *buffer_manager = (IdeBufferManager *)object;
+  GbpEditorWorkbenchAddin *self;
+  g_autoptr(IdeBuffer) buffer = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  OpenFileTaskData *state;
+  IdeEditorSurface *surface = NULL;
+
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  g_assert (GBP_IS_EDITOR_WORKBENCH_ADDIN (self));
+
+  buffer = ide_buffer_manager_load_file_finish (buffer_manager, result, &error);
+
+  if (buffer == NULL)
+    {
+      IDE_TRACE_MSG ("%s", error->message);
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  if (self->workbench == NULL)
+    goto failure;
+
+  ide_workbench_foreach_workspace (self->workbench,
+                                   find_workspace_surface_cb,
+                                   &surface);
+
+  if (!IDE_IS_EDITOR_SURFACE (surface))
+    goto failure;
+
+  state = ide_task_get_task_data (task);
+
+  if (state->at_line > -1)
+    {
+      g_autoptr(IdeLocation) location = NULL;
+
+      location = ide_location_new (state->file,
+                                   state->at_line,
+                                   state->at_line_offset);
+      ide_editor_surface_focus_location (surface, location);
+    }
+
+  if (surface != NULL &&
+      !(state->flags & IDE_BUFFER_OPEN_FLAGS_NO_VIEW) &&
+      !(state->flags & IDE_BUFFER_OPEN_FLAGS_BACKGROUND))
+    ide_editor_surface_focus_buffer_in_current_stack (surface, buffer);
+
+failure:
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_editor_workbench_addin_open_at_async (IdeWorkbenchAddin   *addin,
+                                          GFile               *file,
+                                          const gchar         *content_type,
+                                          gint                 at_line,
+                                          gint                 at_line_offset,
+                                          IdeBufferOpenFlags   flags,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
+{
+  GbpEditorWorkbenchAddin *self = (GbpEditorWorkbenchAddin *)addin;
+  IdeBufferManager *buffer_manager;
+  IdeContext *context;
+  OpenFileTaskData *state;
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (GBP_IS_EDITOR_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (IDE_IS_WORKBENCH (self->workbench));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  state = g_slice_new0 (OpenFileTaskData);
+  state->flags = flags;
+  state->file = g_object_ref (file);
+  state->at_line = at_line;
+  state->at_line_offset = at_line_offset;
+  ide_task_set_task_data (task, state, open_file_task_data_free);
+
+  context = ide_workbench_get_context (self->workbench);
+  buffer_manager = ide_buffer_manager_from_context (context);
+
+  ide_buffer_manager_load_file_async (buffer_manager,
+                                      file,
+                                      state->flags,
+                                      cancellable,
+                                      NULL,
+                                      gbp_editor_workbench_addin_open_at_cb,
+                                      g_steal_pointer (&task));
+}
+
+static void
+gbp_editor_workbench_addin_open_async (IdeWorkbenchAddin   *addin,
+                                       GFile               *file,
+                                       const gchar         *content_type,
+                                       IdeBufferOpenFlags   flags,
+                                       GCancellable        *cancellable,
+                                       GAsyncReadyCallback  callback,
+                                       gpointer             user_data)
+{
+  gbp_editor_workbench_addin_open_at_async (addin, file, content_type, -1, -1, flags, cancellable, callback, 
user_data);
+}
+
+static gboolean
+gbp_editor_workbench_addin_open_finish (IdeWorkbenchAddin  *addin,
+                                        GAsyncResult       *result,
+                                        GError            **error)
+{
+  g_assert (GBP_IS_EDITOR_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+new_editor_workspace_cb (GSimpleAction *action,
+                         GVariant      *param,
+                         gpointer       user_data)
+{
+  GbpEditorWorkbenchAddin *self = user_data;
+  IdeWorkspace *workspace;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_EDITOR_WORKBENCH_ADDIN (self));
+
+  workspace = g_object_new (IDE_TYPE_EDITOR_WORKSPACE,
+                            "application", IDE_APPLICATION_DEFAULT,
+                            NULL);
+  ide_workbench_add_workspace (self->workbench, workspace);
+  gtk_window_present (GTK_WINDOW (workspace));
+}
+
+static GActionEntry actions[] = {
+  { "new-editor-workspace", new_editor_workspace_cb },
+};
+
+static void
+gbp_editor_workbench_addin_workspace_added (IdeWorkbenchAddin *addin,
+                                            IdeWorkspace      *workspace)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  g_action_map_add_action_entries (G_ACTION_MAP (workspace),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   addin);
+}
+
+static void
+gbp_editor_workbench_addin_workspace_removed (IdeWorkbenchAddin *addin,
+                                              IdeWorkspace      *workspace)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
+    g_action_map_remove_action (G_ACTION_MAP (workspace), actions[i].name);
+}
+
+static void
+ide_workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->can_open = gbp_editor_workbench_addin_can_open;
+  iface->load = gbp_editor_workbench_addin_load;
+  iface->open_at_async = gbp_editor_workbench_addin_open_at_async;
+  iface->open_async = gbp_editor_workbench_addin_open_async;
+  iface->open_finish = gbp_editor_workbench_addin_open_finish;
+  iface->unload = gbp_editor_workbench_addin_unload;
+  iface->workspace_added = gbp_editor_workbench_addin_workspace_added;
+  iface->workspace_removed = gbp_editor_workbench_addin_workspace_removed;
+}
diff --git a/src/plugins/editor/gbp-editor-workbench-addin.h b/src/plugins/editor/gbp-editor-workbench-addin.h
new file mode 100644
index 000000000..9f844ac95
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-workbench-addin.h
@@ -0,0 +1,31 @@
+/* gbp-editor-workbench-addin.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_EDITOR_WORKBENCH_ADDIN (gbp_editor_workbench_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpEditorWorkbenchAddin, gbp_editor_workbench_addin, GBP, EDITOR_WORKBENCH_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/editor/gbp-editor-workspace-addin.c b/src/plugins/editor/gbp-editor-workspace-addin.c
new file mode 100644
index 000000000..bbeaabdbf
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-workspace-addin.c
@@ -0,0 +1,317 @@
+/* gbp-editor-workspace-addin.c
+ *
+ * Copyright 2018-2019 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-editor-workspace-addin"
+
+#include "config.h"
+
+#include <libide-editor.h>
+#include <libide-gui.h>
+
+#include "gbp-editor-workspace-addin.h"
+
+struct _GbpEditorWorkspaceAddin
+{
+  GObject               parent_instance;
+
+  DzlSignalGroup       *buffer_manager_signals;
+  DzlShortcutTooltip   *tooltip1;
+  DzlShortcutTooltip   *tooltip2;
+
+  IdeWorkspace         *workspace;
+  IdeEditorSurface     *surface;
+  GtkBox               *panels_box;
+  DzlMenuButton        *new_button;
+};
+
+static void
+find_topmost_editor (GtkWidget *widget,
+                     gpointer   user_data)
+{
+  IdeWorkspace **workspace = user_data;
+  IdeSurface *surface;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKSPACE (widget));
+  g_assert (workspace != NULL);
+
+  if (*workspace)
+    return;
+
+  if ((surface = ide_workspace_get_surface_by_name (IDE_WORKSPACE (widget), "editor")) &&
+      IDE_IS_EDITOR_SURFACE (surface))
+    *workspace = IDE_WORKSPACE (widget);
+}
+
+static gboolean
+is_topmost_workspace_with_editor (GbpEditorWorkspaceAddin *self)
+{
+  IdeWorkbench *workbench;
+  IdeWorkspace *topmost = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_EDITOR_WORKSPACE_ADDIN (self));
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self->workspace));
+  ide_workbench_foreach_workspace (workbench, find_topmost_editor, &topmost);
+
+  return topmost == self->workspace;
+}
+
+static void
+on_load_buffer (GbpEditorWorkspaceAddin *self,
+                IdeBuffer               *buffer,
+                gboolean                 create_new_view,
+                IdeBufferManager        *buffer_manager)
+{
+  g_autofree gchar *title = NULL;
+
+  g_assert (GBP_IS_EDITOR_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+
+  /* We only want to create a new view when the buffer is originally created,
+   * not when it's reloaded.
+   */
+  if (!create_new_view)
+    return;
+
+  /* If another workspace is active and it has an editor surface, then we
+   * don't want to open the buffer in this window.
+   */
+  if (!is_topmost_workspace_with_editor (self))
+    return;
+
+  title = ide_buffer_dup_title (buffer);
+  g_debug ("Loading editor page for \"%s\"", title);
+
+  ide_editor_surface_focus_buffer (self->surface, buffer);
+}
+
+static void
+bind_buffer_manager (GbpEditorWorkspaceAddin *self,
+                     IdeBufferManager        *buffer_manager,
+                     DzlSignalGroup          *signal_group)
+{
+  guint n_items;
+
+  g_assert (GBP_IS_EDITOR_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+  g_assert (DZL_IS_SIGNAL_GROUP (signal_group));
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (buffer_manager));
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      g_autoptr(IdeBuffer) buffer = NULL;
+
+      buffer = g_list_model_get_item (G_LIST_MODEL (buffer_manager), i);
+      ide_editor_surface_focus_buffer (self->surface, buffer);
+    }
+}
+
+static void
+add_buttons (GbpEditorWorkspaceAddin *self,
+             IdeHeaderBar            *header)
+{
+  GtkWidget *button;
+
+  g_assert (GBP_IS_EDITOR_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_HEADER_BAR (header));
+
+  self->new_button = g_object_new (DZL_TYPE_MENU_BUTTON,
+                                   "icon-name", "document-open-symbolic",
+                                   "focus-on-click", FALSE,
+                                   "show-arrow", TRUE,
+                                   "show-icons", FALSE,
+                                   "show-accels", FALSE,
+                                   "menu-id", "new-document-menu",
+                                   "visible", TRUE,
+                                   NULL);
+  g_signal_connect (self->new_button,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->new_button);
+  ide_header_bar_add_primary (header, GTK_WIDGET (self->new_button));
+
+  self->panels_box = g_object_new (GTK_TYPE_BOX,
+                                   "margin-start", 6,
+                                   "margin-end", 6,
+                                   "visible", TRUE,
+                                   NULL);
+  g_signal_connect (self->panels_box,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->panels_box);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->panels_box), "linked");
+  ide_header_bar_add_primary (header, GTK_WIDGET (self->panels_box));
+
+  button = g_object_new (GTK_TYPE_TOGGLE_BUTTON,
+                         "action-name", "dockbin.left-visible",
+                         "focus-on-click", FALSE,
+                         "child", g_object_new (GTK_TYPE_IMAGE,
+                                                "icon-name", "builder-view-left-pane-symbolic",
+                                                "margin-start", 12,
+                                                "margin-end", 12,
+                                                "visible", TRUE,
+                                                NULL),
+                         "visible", TRUE,
+                         NULL);
+  self->tooltip1 = g_object_new (DZL_TYPE_SHORTCUT_TOOLTIP,
+                                 "command-id", "org.gnome.builder.editor.navigation-panel",
+                                 "widget", button,
+                                 NULL);
+  gtk_container_add (GTK_CONTAINER (self->panels_box), button);
+
+  button = g_object_new (GTK_TYPE_TOGGLE_BUTTON,
+                         "action-name", "dockbin.bottom-visible",
+                         "focus-on-click", FALSE,
+                         "child", g_object_new (GTK_TYPE_IMAGE,
+                                                "icon-name", "builder-view-bottom-pane-symbolic",
+                                                "margin-start", 12,
+                                                "margin-end", 12,
+                                                "visible", TRUE,
+                                                NULL),
+                         "visible", TRUE,
+                         NULL);
+  self->tooltip2 = g_object_new (DZL_TYPE_SHORTCUT_TOOLTIP,
+                                 "command-id", "org.gnome.builder.editor.utilities-panel",
+                                 "widget", button,
+                                 NULL);
+  gtk_container_add (GTK_CONTAINER (self->panels_box), button);
+}
+
+static void
+gbp_editor_workspace_addin_load (IdeWorkspaceAddin *addin,
+                                 IdeWorkspace      *workspace)
+{
+  GbpEditorWorkspaceAddin *self = (GbpEditorWorkspaceAddin *)addin;
+  IdeBufferManager *buffer_manager;
+  IdeHeaderBar *header_bar;
+  IdeContext *context;
+
+  g_assert (GBP_IS_EDITOR_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace) ||
+            IDE_IS_EDITOR_WORKSPACE (workspace));
+
+  self->workspace = workspace;
+
+  /* Get our buffer manager for future use */
+  context = ide_widget_get_context (GTK_WIDGET (workspace));
+  buffer_manager = ide_buffer_manager_from_context (context);
+
+  /* Monitor buffer manager for new buffers */
+  self->buffer_manager_signals = dzl_signal_group_new (IDE_TYPE_BUFFER_MANAGER);
+  g_signal_connect_swapped (self->buffer_manager_signals,
+                            "bind",
+                            G_CALLBACK (bind_buffer_manager),
+                            self);
+  dzl_signal_group_connect_swapped (self->buffer_manager_signals,
+                                    "load-buffer",
+                                    G_CALLBACK (on_load_buffer),
+                                    self);
+  dzl_signal_group_set_target (self->buffer_manager_signals, buffer_manager);
+
+  /* Add buttons to the header bar */
+  header_bar = ide_workspace_get_header_bar (workspace);
+  add_buttons (self, header_bar);
+
+  /* Add the editor surface to the workspace */
+  self->surface = g_object_new (IDE_TYPE_EDITOR_SURFACE,
+                                "name", "editor",
+                                "visible", TRUE,
+                                NULL);
+  g_signal_connect (self->surface,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->surface);
+  ide_workspace_add_surface (IDE_WORKSPACE (workspace), IDE_SURFACE (self->surface));
+  ide_workspace_set_visible_surface_name (IDE_WORKSPACE (workspace), "editor");
+}
+
+static void
+gbp_editor_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                   IdeWorkspace      *workspace)
+{
+  GbpEditorWorkspaceAddin *self = (GbpEditorWorkspaceAddin *)addin;
+
+  g_assert (GBP_IS_EDITOR_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace) ||
+            IDE_IS_EDITOR_WORKSPACE (workspace));
+
+  dzl_signal_group_set_target (self->buffer_manager_signals, NULL);
+  g_clear_object (&self->buffer_manager_signals);
+
+  if (self->surface != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->surface));
+
+  if (self->panels_box)
+    gtk_widget_destroy (GTK_WIDGET (self->panels_box));
+
+  if (self->new_button)
+    gtk_widget_destroy (GTK_WIDGET (self->new_button));
+
+  g_clear_object (&self->tooltip1);
+  g_clear_object (&self->tooltip2);
+
+  self->workspace = NULL;
+}
+
+static void
+gbp_editor_workspace_addin_surface_set (IdeWorkspaceAddin *addin,
+                                        IdeSurface        *surface)
+{
+  GbpEditorWorkspaceAddin *self = (GbpEditorWorkspaceAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_EDITOR_WORKSPACE_ADDIN (self));
+  g_assert (!surface || IDE_IS_SURFACE (surface));
+
+  if (self->panels_box)
+    gtk_widget_set_visible (GTK_WIDGET (self->panels_box),
+                            IDE_IS_EDITOR_SURFACE (surface));
+  if (self->new_button)
+    gtk_widget_set_visible (GTK_WIDGET (self->new_button),
+                            IDE_IS_EDITOR_SURFACE (surface));
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_editor_workspace_addin_load;
+  iface->unload = gbp_editor_workspace_addin_unload;
+  iface->surface_set = gbp_editor_workspace_addin_surface_set;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpEditorWorkspaceAddin, gbp_editor_workspace_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN,
+                                                workspace_addin_iface_init))
+
+static void
+gbp_editor_workspace_addin_class_init (GbpEditorWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_editor_workspace_addin_init (GbpEditorWorkspaceAddin *self)
+{
+}
diff --git a/src/plugins/editor/gbp-editor-workspace-addin.h b/src/plugins/editor/gbp-editor-workspace-addin.h
new file mode 100644
index 000000000..f7d204739
--- /dev/null
+++ b/src/plugins/editor/gbp-editor-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-editor-workspace-addin.h
+ *
+ * Copyright 2018-2019 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-editor.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_EDITOR_WORKSPACE_ADDIN (gbp_editor_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpEditorWorkspaceAddin, gbp_editor_workspace_addin, GBP, EDITOR_WORKSPACE_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/editor/gtk/menus.ui b/src/plugins/editor/gtk/menus.ui
new file mode 100644
index 000000000..a521c44f0
--- /dev/null
+++ b/src/plugins/editor/gtk/menus.ui
@@ -0,0 +1,171 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-primary-workspace-menu">
+    <section id="ide-primary-workspace-menu-placeholder1">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-new-editor-workspace</attribute>
+        <attribute name="label" translatable="yes">New Workspace…</attribute>
+        <attribute name="action">win.new-editor-workspace</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-editor-workspace-menu">
+    <section id="ide-editor-workspace-menu-projects-section"/>
+    <section id="ide-editor-workspace-menu-placeholder1">
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-new-editor-workspace</attribute>
+        <attribute name="label" translatable="yes">New Workspace…</attribute>
+        <attribute name="action">win.new-editor-workspace</attribute>
+      </item>
+    </section>
+    <section id="ide-editor-workspace-menu-open-section">
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-open</attribute>
+        <attribute name="label" translatable="yes">Open File…</attribute>
+        <attribute name="action">workbench.open</attribute>
+        <attribute name="accel">&lt;primary&gt;o</attribute>
+      </item>
+    </section>
+    <section id="ide-editor-workspace-menu-app-section">
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-preferences</attribute>
+        <attribute name="label" translatable="yes">Preferences</attribute>
+        <attribute name="action">app.preferences</attribute>
+        <attribute name="accel">&lt;primary&gt;comma</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-shortcuts</attribute>
+        <attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
+        <attribute name="action">app.shortcuts</attribute>
+        <attribute name="accel">&lt;primary&gt;question</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-help</attribute>
+        <attribute name="label" translatable="yes">Help</attribute>
+        <attribute name="action">app.help</attribute>
+        <attribute name="accel">F1</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-about</attribute>
+        <attribute name="label" translatable="yes">About Builder</attribute>
+        <attribute name="action">app.about</attribute>
+      </item>
+    </section>
+    <section id="ide-editor-workspace-menu-quit-section">
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-quit</attribute>
+        <attribute name="label" translatable="yes">_Quit</attribute>
+        <attribute name="action">app.quit</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-primary-workspace-surfaces-menu">
+    <section id="ide-primary-workspace-surfaces-menu-section">
+      <item>
+        <attribute name="accel">&lt;alt&gt;1</attribute>
+        <attribute name="id">ide-primary-workspace-menu-surfaces-menu-editor</attribute>
+        <attribute name="label" translatable="yes">Editor</attribute>
+        <attribute name="role">normal</attribute>
+        <attribute name="action">win.surface</attribute>
+        <attribute name="target">editor</attribute>
+        <attribute name="verb-icon-name">builder-editor-symbolic</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-editor-workspace-surfaces-menu">
+    <section id="ide-editor-workspace-surfaces-menu-section">
+      <attribute name="label" translatable="yes">Switch Surface</attribute>
+      <item>
+        <attribute name="accel">&lt;alt&gt;1</attribute>
+        <attribute name="id">ide-primary-workspace-menu-surfaces-menu-editor</attribute>
+        <attribute name="label" translatable="yes">Editor</attribute>
+        <attribute name="role">normal</attribute>
+        <attribute name="action">win.surface</attribute>
+        <attribute name="target">editor</attribute>
+        <attribute name="verb-icon-name">builder-editor-symbolic</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="new-document-menu">
+    <section id="new-document-section">
+      <item>
+        <attribute name="id">new-file</attribute>
+        <attribute name="label" translatable="yes">New File</attribute>
+        <attribute name="action">editor.new-file</attribute>
+      </item>
+    </section>
+    <section id="open-document-section">
+      <item>
+        <attribute name="id">open-file</attribute>
+        <attribute name="label" translatable="yes">Open File…</attribute>
+        <attribute name="action">workbench.open</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-frame-menu">
+    <section id="ide-frame-section">
+      <attribute name="label" translatable="yes">Frame</attribute>
+      <item>
+        <attribute name="label" translatable="yes">Move Left</attribute>
+        <attribute name="action">frame.move-left</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Move Right</attribute>
+        <attribute name="action">frame.move-right</attribute>
+      </item>
+      <item>
+        <attribute name="action">grid.close-stack</attribute>
+        <attribute name="label" translatable="yes">Close</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-editor-page-document-menu">
+    <section id="editor-document-section">
+      <attribute name="label" translatable="yes">Document</attribute>
+      <item>
+        <attribute name="id">editor-document-open-in-new-frame</attribute>
+        <attribute name="label" translatable="yes">Open in New Frame</attribute>
+        <attribute name="action">frame.open-in-new-frame</attribute>
+        <attribute name="target" type="s">""</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Split</attribute>
+        <attribute name="action">frame.split-page</attribute>
+        <attribute name="target" type="s">""</attribute>
+        <attribute name="verb-icon-name">builder-split-tab-symbolic</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Print…</attribute>
+        <attribute name="action">editor-page.print</attribute>
+        <attribute name="accel">&lt;primary&gt;p</attribute>
+      </item>
+    </section>
+    <section id="editor-document-preferences-section">
+      <attribute name="after">editor-document-section</attribute>
+      <item>
+        <attribute name="label" translatable="yes">Document Properties</attribute>
+        <attribute name="action">editor-page.properties</attribute>
+      </item>
+    </section>
+    <section id="editor-document-save-section">
+      <attribute name="after">editor-document-preferences-section</attribute>
+      <item>
+        <attribute name="action">editor-page.save</attribute>
+        <attribute name="label" translatable="yes">_Save</attribute>
+        <attribute name="accel">&lt;primary&gt;s</attribute>
+      </item>
+      <item>
+        <attribute name="action">editor-page.save-as</attribute>
+        <attribute name="label" translatable="yes">Save _As</attribute>
+        <attribute name="accel">&lt;primary&gt;&lt;shift&gt;s</attribute>
+      </item>
+    </section>
+    <section id="editor-document-close-section">
+      <attribute name="after">editor-document-save-section</attribute>
+      <item>
+        <attribute name="action">frame.close-page</attribute>
+        <attribute name="label" translatable="yes">Close</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/plugins/editor/meson.build b/src/plugins/editor/meson.build
new file mode 100644
index 000000000..7c0101be7
--- /dev/null
+++ b/src/plugins/editor/meson.build
@@ -0,0 +1,18 @@
+plugins_sources += files([
+  'editor-plugin.c',
+  'gbp-editor-application-addin.c',
+  'gbp-editor-frame-addin.c',
+  'gbp-editor-frame-controls.c',
+  'gbp-editor-hover-provider.c',
+  'gbp-editor-session-addin.c',
+  'gbp-editor-workbench-addin.c',
+  'gbp-editor-workspace-addin.c',
+])
+
+plugin_editor_resources = gnome.compile_resources(
+  'gbp-editor-resources',
+  'editor.gresource.xml',
+  c_name: 'gbp_editor',
+)
+
+plugins_sources += plugin_editor_resources[0]
diff --git a/src/libide/keybindings/shared.css b/src/plugins/editor/shared.css
similarity index 100%
rename from src/libide/keybindings/shared.css
rename to src/plugins/editor/shared.css
diff --git a/src/plugins/editorconfig/editorconfig-glib.c b/src/plugins/editorconfig/editorconfig-glib.c
new file mode 100644
index 000000000..c12f1605a
--- /dev/null
+++ b/src/plugins/editorconfig/editorconfig-glib.c
@@ -0,0 +1,125 @@
+/* editorconfig-glib.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "editorconfig-glib"
+
+#include "config.h"
+
+#include <editorconfig.h>
+
+#include "editorconfig-glib.h"
+
+static void
+_g_value_free (gpointer data)
+{
+  GValue *value = data;
+
+  g_value_unset (value);
+  g_free (value);
+}
+
+GHashTable *
+editorconfig_glib_read (GFile         *file,
+                        GCancellable  *cancellable,
+                        GError       **error)
+{
+  editorconfig_handle handle = { 0 };
+  GHashTable *ret = NULL;
+  gchar *filename = NULL;
+  gint code;
+  gint count;
+  guint i;
+
+  filename = g_file_get_path (file);
+
+  if (!filename)
+    {
+      /*
+       * This sucks, but we need to basically rewrite editorconfig library
+       * to support this. Not out of the question, but it is for today.
+       */
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_NOT_SUPPORTED,
+                   "only local files are currently supported");
+      return NULL;
+    }
+
+  handle = editorconfig_handle_init ();
+  code = editorconfig_parse (filename, handle);
+
+  switch (code)
+    {
+    case 0:
+      break;
+
+    case EDITORCONFIG_PARSE_NOT_FULL_PATH:
+    case EDITORCONFIG_PARSE_MEMORY_ERROR:
+    case EDITORCONFIG_PARSE_VERSION_TOO_NEW:
+    default:
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_FAILED,
+                   "Failed to parse editorconfig.");
+      goto cleanup;
+    }
+
+  count = editorconfig_handle_get_name_value_count (handle);
+
+  ret = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, _g_value_free);
+
+  for (i = 0; i < count; i++)
+    {
+      GValue *value;
+      const gchar *key = NULL;
+      const gchar *valuestr = NULL;
+
+      value = g_new0 (GValue, 1);
+
+      editorconfig_handle_get_name_value (handle, i, &key, &valuestr);
+
+      if ((g_strcmp0 (key, "tab_width") == 0) ||
+          (g_strcmp0 (key, "max_line_length") == 0) ||
+          (g_strcmp0 (key, "indent_size") == 0))
+        {
+          g_value_init (value, G_TYPE_INT);
+          g_value_set_int (value, g_ascii_strtoll (valuestr, NULL, 10));
+        }
+      else if ((g_strcmp0 (key, "insert_final_newline") == 0) ||
+               (g_strcmp0 (key, "trim_trailing_whitespace") == 0))
+        {
+          g_value_init (value, G_TYPE_BOOLEAN);
+          g_value_set_boolean (value, g_str_equal (valuestr, "true"));
+        }
+      else
+        {
+          g_value_init (value, G_TYPE_STRING);
+          g_value_set_string (value, valuestr);
+        }
+
+      g_hash_table_replace (ret, g_strdup (key), value);
+    }
+
+cleanup:
+  editorconfig_handle_destroy (handle);
+  g_free (filename);
+
+  return ret;
+}
diff --git a/src/plugins/editorconfig/editorconfig-glib.h b/src/plugins/editorconfig/editorconfig-glib.h
new file mode 100644
index 000000000..f4ce075e7
--- /dev/null
+++ b/src/plugins/editorconfig/editorconfig-glib.h
@@ -0,0 +1,31 @@
+/* editorconfig-glib.h
+ *
+ * Copyright 2018-2019 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
+
+GHashTable *editorconfig_glib_read (GFile         *file,
+                                    GCancellable  *cancellable,
+                                    GError       **error);
+
+G_BEGIN_DECLS
diff --git a/src/plugins/editorconfig/editorconfig-plugin.c b/src/plugins/editorconfig/editorconfig-plugin.c
new file mode 100644
index 000000000..0b55b1881
--- /dev/null
+++ b/src/plugins/editorconfig/editorconfig-plugin.c
@@ -0,0 +1,37 @@
+/* editorconfig-plugin.c
+ *
+ * Copyright 2018-2019 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 "editorconfig-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-code.h>
+
+#include "gbp-editorconfig-file-settings.h"
+
+_IDE_EXTERN void
+_gbp_editorconfig_register_types (PeasObjectModule *module)
+{
+  g_io_extension_point_implement (IDE_FILE_SETTINGS_EXTENSION_POINT,
+                                  GBP_TYPE_EDITORCONFIG_FILE_SETTINGS,
+                                  IDE_FILE_SETTINGS_EXTENSION_POINT".editorconfig",
+                                  -200);
+}
diff --git a/src/plugins/editorconfig/editorconfig.gresource.xml 
b/src/plugins/editorconfig/editorconfig.gresource.xml
new file mode 100644
index 000000000..efa19b0c2
--- /dev/null
+++ b/src/plugins/editorconfig/editorconfig.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/editorconfig">
+    <file>editorconfig.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/editorconfig/editorconfig.plugin b/src/plugins/editorconfig/editorconfig.plugin
new file mode 100644
index 000000000..3bb96bde9
--- /dev/null
+++ b/src/plugins/editorconfig/editorconfig.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Depends=editor;
+Description=Editorconfig integration
+Embedded=_gbp_editorconfig_register_types
+Module=editorconfig
+Name=Editorconfig
diff --git a/src/plugins/editorconfig/gbp-editorconfig-file-settings.c 
b/src/plugins/editorconfig/gbp-editorconfig-file-settings.c
new file mode 100644
index 000000000..eecd43b42
--- /dev/null
+++ b/src/plugins/editorconfig/gbp-editorconfig-file-settings.c
@@ -0,0 +1,188 @@
+/* gbp-editorconfig-file-settings.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-editorconfig-file-settings"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+
+#include "editorconfig-glib.h"
+#include "gbp-editorconfig-file-settings.h"
+
+struct _GbpEditorconfigFileSettings
+{
+  IdeFileSettings parent_instance;
+};
+
+static void async_initable_iface_init (GAsyncInitableIface *iface);
+
+G_DEFINE_TYPE_EXTENDED (GbpEditorconfigFileSettings,
+                        gbp_editorconfig_file_settings,
+                        IDE_TYPE_FILE_SETTINGS,
+                        0,
+                        G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE,
+                                               async_initable_iface_init))
+
+static void
+gbp_editorconfig_file_settings_class_init (GbpEditorconfigFileSettingsClass *klass)
+{
+}
+
+static void
+gbp_editorconfig_file_settings_init (GbpEditorconfigFileSettings *self)
+{
+}
+
+static void
+gbp_editorconfig_file_settings_init_worker (IdeTask      *task,
+                                            gpointer      source_object,
+                                            gpointer      task_data,
+                                            GCancellable *cancellable)
+{
+  GFile *file = task_data;
+  g_autoptr(GError) error = NULL;
+  GHashTableIter iter;
+  GHashTable *ht;
+  gpointer k, v;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_EDITORCONFIG_FILE_SETTINGS (source_object));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ht = editorconfig_glib_read (file, cancellable, &error);
+
+  if (!ht)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  g_hash_table_iter_init (&iter, ht);
+
+  while (g_hash_table_iter_next (&iter, &k, &v))
+    {
+      const gchar *key = k;
+      const GValue *value = v;
+
+      if (g_str_equal (key, "indent_size"))
+        g_object_set_property (source_object, "indent-width", value);
+      else if (g_str_equal (key, "tab_width") ||
+               g_str_equal (key, "trim_trailing_whitespace"))
+        g_object_set_property (source_object, key, value);
+      else if (g_str_equal (key, "insert_final_newline"))
+        g_object_set_property (source_object, "insert-trailing-newline", value);
+      else if (g_str_equal (key, "charset"))
+        g_object_set_property (source_object, "encoding", value);
+      else if (g_str_equal (key, "max_line_length"))
+        {
+          g_object_set_property (source_object, "right-margin-position", value);
+          g_object_set (source_object, "show-right-margin", TRUE, NULL);
+        }
+      else if (g_str_equal (key, "end_of_line"))
+        {
+          GtkSourceNewlineType newline_type = GTK_SOURCE_NEWLINE_TYPE_LF;
+          const gchar *str;
+
+          str = g_value_get_string (value);
+          if (g_strcmp0 (str, "cr") == 0)
+            newline_type = GTK_SOURCE_NEWLINE_TYPE_CR;
+          else if (g_strcmp0 (str, "crlf") == 0)
+            newline_type = GTK_SOURCE_NEWLINE_TYPE_CR_LF;
+
+          ide_file_settings_set_newline_type (source_object, newline_type);
+        }
+      else if (g_str_equal (key, "indent_style"))
+        {
+          IdeIndentStyle indent_style = IDE_INDENT_STYLE_SPACES;
+          const gchar *str;
+
+          str = g_value_get_string (value);
+
+          if (g_strcmp0 (str, "tab") == 0)
+            indent_style = IDE_INDENT_STYLE_TABS;
+
+          ide_file_settings_set_indent_style (source_object, indent_style);
+        }
+    }
+
+  ide_task_return_boolean (task, TRUE);
+
+  g_hash_table_unref (ht);
+}
+
+static void
+gbp_editorconfig_file_settings_init_async (GAsyncInitable      *initable,
+                                           gint                 io_priority,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  GbpEditorconfigFileSettings *self = (GbpEditorconfigFileSettings *)initable;
+  g_autoptr(IdeTask) task = NULL;
+  GFile *file;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (GBP_IS_EDITORCONFIG_FILE_SETTINGS (self));
+  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_editorconfig_file_settings_init_async);
+
+  if (!(file = ide_file_settings_get_file (IDE_FILE_SETTINGS (self))))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_FOUND,
+                                 _("No file was provided."));
+      IDE_EXIT;
+    }
+
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
+  ide_task_run_in_thread (task, gbp_editorconfig_file_settings_init_worker);
+
+  IDE_EXIT;
+}
+
+static gboolean
+gbp_editorconfig_file_settings_init_finish (GAsyncInitable  *initable,
+                                            GAsyncResult    *result,
+                                            GError         **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+async_initable_iface_init (GAsyncInitableIface *iface)
+{
+  iface->init_async = gbp_editorconfig_file_settings_init_async;
+  iface->init_finish = gbp_editorconfig_file_settings_init_finish;
+}
diff --git a/src/plugins/editorconfig/gbp-editorconfig-file-settings.h 
b/src/plugins/editorconfig/gbp-editorconfig-file-settings.h
new file mode 100644
index 000000000..43bc76cdd
--- /dev/null
+++ b/src/plugins/editorconfig/gbp-editorconfig-file-settings.h
@@ -0,0 +1,31 @@
+/* gbp-editorconfig-file-settings.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-code.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_EDITORCONFIG_FILE_SETTINGS (gbp_editorconfig_file_settings_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpEditorconfigFileSettings, gbp_editorconfig_file_settings, GBP, 
EDITORCONFIG_FILE_SETTINGS, IdeFileSettings)
+
+G_END_DECLS
diff --git a/src/plugins/editorconfig/libeditorconfig/ec_glob.c 
b/src/plugins/editorconfig/libeditorconfig/ec_glob.c
new file mode 100644
index 000000000..86b57ad1d
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/ec_glob.c
@@ -0,0 +1,371 @@
+/*
+ * Copyright 2014 Hong Xu <hong AT topbug DOT net>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include "global.h"
+
+#include <ctype.h>
+#include <string.h>
+#include <pcre.h>
+
+#include "utarray.h"
+#include "misc.h"
+
+#include "ec_glob.h"
+
+typedef struct int_pair
+{
+    int     num1;
+    int     num2;
+} int_pair;
+static const UT_icd ut_int_pair_icd = {sizeof(int_pair),NULL,NULL,NULL};
+
+/* concatenate the string then move the pointer to the end */
+#define STRING_CAT(p, string, end)  do {    \
+    size_t string_len = strlen(string); \
+    if (p + string_len >= end) \
+        return -1; \
+    strcat(p, string); \
+    p += string_len; \
+} while(0)
+
+#define PATTERN_MAX  300
+/*
+ * Whether the string matches the given glob pattern
+ */
+EDITORCONFIG_LOCAL
+int ec_glob(const char *pattern, const char *string)
+{
+    size_t                    i;
+    int_pair *                p;
+    char *                    c;
+    char                      pcre_str[2 * PATTERN_MAX] = "^";
+    char *                    p_pcre;
+    char *                    pcre_str_end;
+    int                       brace_level = 0;
+    _Bool                     is_in_bracket = 0;
+    const char *              error_msg;
+    int                       erroffset;
+    pcre *                    re;
+    int                       rc;
+    int *                     pcre_result;
+    size_t                    pcre_result_len;
+    char                      l_pattern[2 * PATTERN_MAX];
+    _Bool                     are_brace_paired;
+    UT_array *                nums;     /* number ranges */
+    int                       ret = 0;
+
+    if (pattern == NULL || string == NULL || (strlen (pattern) > PATTERN_MAX))
+      return -1;
+
+    strcpy(l_pattern, pattern);
+    p_pcre = pcre_str + 1;
+    pcre_str_end = pcre_str + 2 * PATTERN_MAX;
+
+    {
+        int     left_count = 0;
+        int     right_count = 0;
+        for (c = l_pattern; *c; ++ c)
+        {
+            if (*c == '\\' && *(c+1) != '\0')
+            {
+                ++ c;
+                continue;
+            }
+
+            if (*c == '}')
+                ++ right_count;
+            if (*c == '{')
+                ++ left_count;
+        }
+
+        are_brace_paired = (right_count == left_count);
+    }
+
+    /* used to search for {num1..num2} case */
+    re = pcre_compile("^\\{[\\+\\-]?\\d+\\.\\.[\\+\\-]?\\d+\\}$", 0,
+            &error_msg, &erroffset, NULL);
+    if (!re)        /* failed to compile */
+        return -1;
+
+    utarray_new(nums, &ut_int_pair_icd);
+
+    for (c = l_pattern; *c; ++ c)
+    {
+        switch (*c)
+        {
+        case '\\':      /* also skip the next one */
+            if (*(c+1) != '\0')
+            {
+                *(p_pcre ++) = *(c++);
+                *(p_pcre ++) = *c;
+            }
+            else
+                STRING_CAT(p_pcre, "\\\\", pcre_str_end);
+
+            break;
+        case '?':
+            *(p_pcre ++) = '.';
+            break;
+        case '*':
+            if (*(c+1) == '*')      /* case of ** */
+            {
+                STRING_CAT(p_pcre, ".*", pcre_str_end);
+                ++ c;
+            }
+            else                    /* case of * */
+                STRING_CAT(p_pcre, "[^\\/]*", pcre_str_end);
+
+            break;
+        case '[':
+            if (is_in_bracket)     /* inside brackets, we really mean bracket */
+            {
+                STRING_CAT(p_pcre, "\\[", pcre_str_end);
+                break;
+            }
+
+            {
+                /* check whether we have slash within the bracket */
+                _Bool           has_slash = 0;
+                char *          cc;
+                for (cc = c; *cc && *cc != ']'; ++ cc)
+                {
+                    if (*cc == '\\' && *(cc+1) != '\0')
+                    {
+                        ++ cc;
+                        continue;
+                    }
+
+                    if (*cc == '/')
+                    {
+                        has_slash = 1;
+                        break;
+                    }
+                }
+
+                /* if we have slash in the brackets, just do it literally */
+                if (has_slash)
+                {
+                    char *           right_bracket = strchr(c, ']');
+
+                    strcat(p_pcre, "\\");
+                    strncat(p_pcre, c, right_bracket - c);
+                    strcat(p_pcre, "\\]");
+                    p_pcre += strlen(p_pcre);
+                    c = right_bracket;
+                    break;
+                }
+            }
+
+            is_in_bracket = 1;
+            if (*(c+1) == '!')     /* case of [!...] */
+            {
+                STRING_CAT(p_pcre, "[^", pcre_str_end);
+                ++ c;
+            }
+            else
+                *(p_pcre ++) = '[';
+
+            break;
+
+        case ']':
+            is_in_bracket = 0;
+            *(p_pcre ++) = *c;
+            break;
+
+        case '-':
+            if (is_in_bracket)      /* in brackets, - indicates range */
+                *(p_pcre ++) = *c;
+            else
+                STRING_CAT(p_pcre, "\\-", pcre_str_end);
+
+            break;
+        case '{':
+            if (!are_brace_paired)
+            {
+                STRING_CAT(p_pcre, "\\{", pcre_str_end);
+                break;
+            }
+
+            /* Check the case of {single}, where single can be empty */
+            {
+                char *                   cc;
+                _Bool                    is_single = 1;
+
+                for (cc = c + 1; *cc != '\0' && *cc != '}'; ++ cc)
+                {
+                    if (*cc == '\\' && *(cc+1) != '\0')
+                    {
+                        ++ cc;
+                        continue;
+                    }
+
+                    if (*cc == ',')
+                    {
+                        is_single = 0;
+                        break;
+                    }
+                }
+
+                if (*cc == '\0')
+                    is_single = 0;
+
+                if (is_single)      /* escape the { and the corresponding } */
+                {
+                    const char *        double_dots;
+                    int_pair            pair;
+                    int                 pcre_res[3];
+
+                    /* Check the case of {num1..num2} */
+                    rc = pcre_exec(re, NULL, c, (int) (cc - c + 1), 0, 0,
+                            pcre_res, 3);
+
+                    if (rc < 0)    /* not {num1..num2} case */
+                    {
+                        STRING_CAT(p_pcre, "\\{", pcre_str_end);
+
+                        memmove(cc+1, cc, strlen(cc) + 1);
+                        *cc = '\\';
+
+                        break;
+                    }
+
+                    /* Get the range */
+                    double_dots = strstr(c, "..");
+                    pair.num1 = atoi(c + 1);
+                    pair.num2 = atoi(double_dots + 2);
+
+                    utarray_push_back(nums, &pair);
+
+                    STRING_CAT(p_pcre, "([\\+\\-]?\\d+)", pcre_str_end);
+                    c = cc;
+
+                    break;
+                }
+            }
+
+            ++ brace_level;
+            STRING_CAT(p_pcre, "(?:", pcre_str_end);
+            break;
+
+        case '}':
+            if (!are_brace_paired)
+            {
+                STRING_CAT(p_pcre, "\\}", pcre_str_end);
+                break;
+            }
+
+            -- brace_level;
+            *(p_pcre ++) = ')';
+            break;
+
+        case ',':
+            if (brace_level > 0)  /* , inside {...} */
+                *(p_pcre ++) = '|';
+            else
+                STRING_CAT(p_pcre, "\\,", pcre_str_end);
+            break;
+
+        case '/':
+            // /**/ case, match both single / and /anything/
+            if (!strncmp(c, "/**/", 4))
+            {
+                STRING_CAT(p_pcre, "(\\/|\\/.*\\/)", pcre_str_end);
+                c += 3;
+            }
+            else
+                STRING_CAT(p_pcre, "\\/", pcre_str_end);
+
+            break;
+
+        default:
+            if (!isalnum(*c))
+                *(p_pcre ++) = '\\';
+
+            *(p_pcre ++) = *c;
+        }
+    }
+
+    *(p_pcre ++) = '$';
+
+    pcre_free(re); /* ^\\d+\\.\\.\\d+$ */
+
+    re = pcre_compile(pcre_str, 0, &error_msg, &erroffset, NULL);
+
+    if (!re)        /* failed to compile */
+    {
+      utarray_free(nums);
+      return -1;
+    }
+
+    pcre_result_len = 3 * (utarray_len(nums) + 1);
+    pcre_result = (int *) calloc(pcre_result_len, sizeof(int_pair));
+    rc = pcre_exec(re, NULL, string, (int) strlen(string), 0, 0,
+            pcre_result, pcre_result_len);
+
+    if (rc < 0)     /* failed to match */
+    {
+        if (rc == PCRE_ERROR_NOMATCH)
+            ret = EC_GLOB_NOMATCH;
+        else
+            ret = rc;
+
+        pcre_free(re);
+        free(pcre_result);
+        utarray_free(nums);
+
+        return ret;
+    }
+
+    /* Whether the numbers are in the desired range? */
+    for(p = (int_pair *) utarray_front(nums), i = 1; p;
+            ++ i, p = (int_pair *) utarray_next(nums, p))
+    {
+        const char * substring_start = string + pcre_result[2 * i];
+        size_t  substring_length = pcre_result[2 * i + 1] - pcre_result[2 * i];
+        char *       num_string;
+        int          num;
+
+        /* we don't consider 0digits such as 010 as matched */
+        if (*substring_start == '0')
+            break;
+
+        num_string = strndup(substring_start, substring_length);
+        num = atoi(num_string);
+        free(num_string);
+
+        if (num < p->num1 || num > p->num2) /* not matched */
+            break;
+    }
+
+    if (p != NULL)      /* numbers not matched */
+        ret = EC_GLOB_NOMATCH;
+
+    pcre_free(re);
+    free(pcre_result);
+    utarray_free(nums);
+
+    return ret;
+}
diff --git a/src/plugins/editorconfig/libeditorconfig/ec_glob.h 
b/src/plugins/editorconfig/libeditorconfig/ec_glob.h
new file mode 100644
index 000000000..7da19df99
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/ec_glob.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2014 Hong Xu <hong AT topbug DOT net>
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef __EC_GLOB_H__
+#define __EC_GLOB_H__
+
+#include "global.h"
+
+#define EC_GLOB_NOMATCH  1   /* Match failed. */
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+EDITORCONFIG_LOCAL
+int ec_glob(const char * pattern, const char * string);
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* !__EC_GLOB_H__ */
diff --git a/src/plugins/editorconfig/libeditorconfig/editorconfig.c 
b/src/plugins/editorconfig/libeditorconfig/editorconfig.c
new file mode 100644
index 000000000..5042c2092
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/editorconfig.c
@@ -0,0 +1,547 @@
+/*
+ * Copyright 2011-2012 EditorConfig Team
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include "global.h"
+#include "editorconfig.h"
+#include "misc.h"
+#include "ini.h"
+#include "ec_glob.h"
+
+/* could be used to fast locate these properties in an
+ * array_editorconfig_name_value */
+typedef struct
+{
+    const editorconfig_name_value*        indent_style;
+    const editorconfig_name_value*        indent_size;
+    const editorconfig_name_value*        tab_width;
+} special_property_name_value_pointers;
+
+typedef struct
+{
+    editorconfig_name_value*                name_values;
+    int                                     current_value_count;
+    int                                     max_value_count;
+    special_property_name_value_pointers    spnvp;
+} array_editorconfig_name_value;
+
+typedef struct
+{
+    char*                           full_filename;
+    char*                           editorconfig_file_dir;
+    array_editorconfig_name_value   array_name_value;
+} handler_first_param;
+
+/*
+ * Set the special pointers for a name
+ */
+static void set_special_property_name_value_pointers(
+        const editorconfig_name_value* nv,
+        special_property_name_value_pointers* spnvp)
+{
+    /* set speical pointers */
+    if (!strcmp(nv->name, "indent_style"))
+        spnvp->indent_style = nv;
+    else if (!strcmp(nv->name, "indent_size"))
+        spnvp->indent_size = nv;
+    else if (!strcmp(nv->name, "tab_width"))
+        spnvp->tab_width = nv;
+}
+
+/*
+ * Set the name and value of a editorconfig_name_value structure
+ */
+static void set_name_value(editorconfig_name_value* nv, const char* name,
+        const char* value, special_property_name_value_pointers* spnvp)
+{
+    if (name)
+        nv->name = strdup(name);
+    if (value)
+        nv->value = strdup(value);
+    /* lowercase the value when the name is one of the following */
+    if (!strcmp(nv->name, "end_of_line") ||
+            !strcmp(nv->name, "indent_style") ||
+            !strcmp(nv->name, "indent_size") ||
+            !strcmp(nv->name, "insert_final_newline") ||
+            !strcmp(nv->name, "trim_trailing_whitespace") ||
+            !strcmp(nv->name, "charset"))
+        strlwr(nv->value);
+
+    /* set speical pointers */
+    set_special_property_name_value_pointers(nv, spnvp);
+}
+
+/*
+ * reset special property name value pointers
+ */
+static void reset_special_property_name_value_pointers(
+        array_editorconfig_name_value* aenv)
+{
+    int         i;
+
+    for (i = 0; i < aenv->current_value_count; ++ i)
+        set_special_property_name_value_pointers(
+                &aenv->name_values[i], &aenv->spnvp);
+}
+
+/*
+ * Find the editorconfig_name_value from name in a editorconfig_name_value
+ * array.
+ */
+static int find_name_value_from_name(const editorconfig_name_value* env,
+        int count, const char* name)
+{
+    int         i;
+
+    for (i = 0; i < count; ++i)
+        if (!strcmp(env[i].name, name)) /* found */
+            return i;
+
+    return -1;
+}
+
+/* initialize array_editorconfig_name_value */
+static void array_editorconfig_name_value_init(
+        array_editorconfig_name_value* aenv)
+{
+    memset(aenv, 0, sizeof(array_editorconfig_name_value));
+}
+
+static int array_editorconfig_name_value_add(
+        array_editorconfig_name_value* aenv,
+        const char* name, const char* value)
+{
+#define VALUE_COUNT_INITIAL      30
+#define VALUE_COUNT_INCREASEMENT 10
+    int         name_value_pos;
+    /* always use name_lwr but not name, since property names are case
+     * insensitive */
+    char        name_lwr[MAX_PROPERTY_NAME];
+
+    if (strlen (name) > (MAX_PROPERTY_NAME - 1))
+      return -1;
+
+    /* For the first time we came here, aenv->name_values is NULL */
+    if (aenv->name_values == NULL) {
+        aenv->name_values = (editorconfig_name_value*)malloc(
+                sizeof(editorconfig_name_value) * VALUE_COUNT_INITIAL);
+
+        if (aenv->name_values == NULL)
+            return -1;
+
+        aenv->max_value_count = VALUE_COUNT_INITIAL;
+        aenv->current_value_count = 0;
+    }
+
+
+    /* name_lwr is the lowercase property name */
+    strlwr(strcpy(name_lwr, name));
+
+    name_value_pos = find_name_value_from_name(
+            aenv->name_values, aenv->current_value_count, name_lwr);
+
+    if (name_value_pos >= 0) { /* current name has already been used */
+        free(aenv->name_values[name_value_pos].value);
+        set_name_value(&aenv->name_values[name_value_pos],
+                (const char*)NULL, value, &aenv->spnvp);
+        return 0;
+    }
+
+    /* if the space is not enough, allocate more before add the new name and
+     * value */
+    if (aenv->current_value_count >= aenv->max_value_count) {
+
+        editorconfig_name_value*        new_values;
+        int                             new_max_value_count;
+
+        new_max_value_count = aenv->current_value_count +
+            VALUE_COUNT_INCREASEMENT;
+        new_values = (editorconfig_name_value*)realloc(aenv->name_values,
+                sizeof(editorconfig_name_value) * new_max_value_count);
+
+        if (new_values == NULL) /* error occured */
+            return -1;
+
+        aenv->name_values = new_values;
+        aenv->max_value_count = new_max_value_count;
+
+        /* reset special pointers */
+        reset_special_property_name_value_pointers(aenv);
+    }
+
+    set_name_value(&aenv->name_values[aenv->current_value_count],
+            name_lwr, value, &aenv->spnvp);
+    ++ aenv->current_value_count;
+
+    return 0;
+#undef VALUE_COUNT_INITIAL
+#undef VALUE_COUNT_INCREASEMENT
+}
+
+static void array_editorconfig_name_value_clear(
+        array_editorconfig_name_value* aenv)
+{
+    int             i;
+
+    for (i = 0; i < aenv->current_value_count; ++i) {
+        free(aenv->name_values[i].name);
+        free(aenv->name_values[i].value);
+    }
+
+    free(aenv->name_values);
+}
+
+/*
+ * Accept INI property value and store known values in handler_first_param
+ * struct.
+ */
+static int ini_handler(void* hfp, const char* section, const char* name,
+        const char* value)
+{
+    handler_first_param* hfparam = (handler_first_param*)hfp;
+    /* prepend ** to pattern */
+    char*                pattern;
+
+    /* root = true, clear all previous values */
+    if (*section == '\0' && !strcasecmp(name, "root") &&
+            !strcasecmp(value, "true")) {
+        array_editorconfig_name_value_clear(&hfparam->array_name_value);
+        array_editorconfig_name_value_init(&hfparam->array_name_value);
+        return 1;
+    }
+
+    /* pattern would be: /dir/of/editorconfig/file[double_star]/[section] if
+     * section does not contain '/', or /dir/of/editorconfig/file[section]
+     * if section starts with a '/', or /dir/of/editorconfig/file/[section] if
+     * section contains '/' but does not start with '/' */
+    pattern = (char*)malloc(
+            strlen(hfparam->editorconfig_file_dir) * sizeof(char) +
+            sizeof("**/") + strlen(section) * sizeof(char));
+    if (!pattern)
+        return 0;
+    strcpy(pattern, hfparam->editorconfig_file_dir);
+
+    if (strchr(section, '/') == NULL) /* No / is found, append '[star][star]/' */
+        strcat(pattern, "**/");
+    else if (*section != '/') /* The first char is not '/' but section contains
+                                 '/', append a '/' */
+        strcat(pattern, "/");
+
+    strcat(pattern, section);
+
+    if (ec_glob(pattern, hfparam->full_filename) == 0) {
+        if (array_editorconfig_name_value_add(&hfparam->array_name_value, name,
+                value)) {
+            free(pattern);
+            return 0;
+        }
+    }
+
+    free(pattern);
+    return 1;
+}
+
+/*
+ * Split an absolute file path into directory and filename parts.
+ *
+ * If absolute_path does not contain a path separator, set directory and
+ * filename to NULL pointers.
+ */
+static void split_file_path(char** directory, char** filename,
+        const char* absolute_path)
+{
+    char* path_char = strrchr(absolute_path, '/');
+
+    if (path_char == NULL) {
+        if (directory)
+            *directory = NULL;
+        if (filename)
+            *filename = NULL;
+        return;
+    }
+
+    if (directory != NULL) {
+        *directory = strndup(absolute_path,
+                (size_t)(path_char - absolute_path));
+    }
+    if (filename != NULL) {
+        *filename = strndup(path_char+1, strlen(path_char)-1);
+    }
+}
+
+/*
+ * Return the number of slashes in given filename
+ */
+static int count_slashes(const char* filename)
+{
+    int slash_count;
+    for (slash_count = 0; *filename != '\0'; filename++) {
+        if (*filename == '/') {
+            slash_count++;
+        }
+    }
+    return slash_count;
+}
+
+/*
+ * Return an array of full filenames for files in every directory in and above
+ * the given path with the name of the relative filename given.
+ */
+static char** get_filenames(const char* path, const char* filename)
+{
+    char* currdir;
+    char* currdir1;
+    char** files;
+    int slashes = count_slashes(path);
+    int i;
+
+    files = (char**) calloc(slashes+1, sizeof(char*));
+
+    currdir = strdup(path);
+    for (i = slashes - 1; i >= 0; --i) {
+        currdir1 = currdir;
+        split_file_path(&currdir, NULL, currdir1);
+        free(currdir1);
+        files[i] = malloc(strlen(currdir) + strlen(filename) + 2);
+        strcpy(files[i], currdir);
+        strcat(files[i], "/");
+        strcat(files[i], filename);
+    }
+
+    free(currdir);
+
+    files[slashes] = NULL;
+
+    return files;
+}
+
+/*
+ * version number comparison
+ */
+static int editorconfig_compare_version(
+        const struct editorconfig_version* v0,
+        const struct editorconfig_version* v1)
+{
+    /* compare major */
+    if (v0->major > v1->major)
+        return 1;
+    else if (v0->major < v1->major)
+        return -1;
+
+    /* compare minor */
+    if (v0->minor > v1->minor)
+        return 1;
+    else if (v0->minor < v1->minor)
+        return -1;
+
+    /* compare patch */
+    if (v0->patch > v1->patch)
+        return 1;
+    else if (v0->patch < v1->patch)
+        return -1;
+
+    return 0;
+}
+
+EDITORCONFIG_EXPORT
+const char* editorconfig_get_error_msg(int err_num)
+{
+    if(err_num > 0)
+        return "Failed to parse file.";
+
+    switch(err_num) {
+    case 0:
+        return "No error occurred.";
+    case EDITORCONFIG_PARSE_NOT_FULL_PATH:
+        return "Input file must be a full path name.";
+    case EDITORCONFIG_PARSE_MEMORY_ERROR:
+        return "Memory error.";
+    case EDITORCONFIG_PARSE_VERSION_TOO_NEW:
+        return "Required version is greater than the current version.";
+    default:
+        break;
+    }
+
+    return "Unknown error.";
+}
+
+/*
+ * See the header file for the use of this function
+ */
+EDITORCONFIG_EXPORT
+int editorconfig_parse(const char* full_filename, editorconfig_handle h)
+{
+    handler_first_param                 hfp;
+    char**                              config_file;
+    char**                              config_files;
+    int                                 err_num;
+    int                                 i;
+    struct editorconfig_handle*         eh = (struct editorconfig_handle*)h;
+    struct editorconfig_version         cur_ver;
+    struct editorconfig_version         tmp_ver;
+
+    /* get current version */
+    editorconfig_get_version(&cur_ver.major, &cur_ver.minor,
+            &cur_ver.patch);
+
+    /* if version is set to 0.0.0, we set it to current version */
+    if (eh->ver.major == 0 &&
+            eh->ver.minor == 0 &&
+            eh->ver.patch == 0)
+        eh->ver = cur_ver;
+
+    if (editorconfig_compare_version(&eh->ver, &cur_ver) > 0)
+        return EDITORCONFIG_PARSE_VERSION_TOO_NEW;
+
+    if (!eh->err_file) {
+        free(eh->err_file);
+        eh->err_file = NULL;
+    }
+
+    /* if eh->conf_file_name is NULL, we set ".editorconfig" as the default
+     * conf file name */
+    if (!eh->conf_file_name)
+        eh->conf_file_name = ".editorconfig";
+
+    if (eh->name_values) {
+        /* free name_values */
+        for (i = 0; i < eh->name_value_count; ++i) {
+            free(eh->name_values[i].name);
+            free(eh->name_values[i].value);
+        }
+        free(eh->name_values);
+
+        eh->name_values = NULL;
+        eh->name_value_count = 0;
+    }
+    memset(&hfp, 0, sizeof(hfp));
+
+    hfp.full_filename = strdup(full_filename);
+
+    /* return an error if file path is not absolute */
+    if (!is_file_path_absolute(full_filename)) {
+        return EDITORCONFIG_PARSE_NOT_FULL_PATH;
+    }
+
+#ifdef WIN32
+    /* replace all backslashes with slashes on Windows */
+    str_replace(hfp.full_filename, '\\', '/');
+#endif
+
+    array_editorconfig_name_value_init(&hfp.array_name_value);
+
+    config_files = get_filenames(hfp.full_filename, eh->conf_file_name);
+    for (config_file = config_files; *config_file != NULL; config_file++) {
+        split_file_path(&hfp.editorconfig_file_dir, NULL, *config_file);
+        if ((err_num = ini_parse(*config_file, ini_handler, &hfp)) != 0 &&
+                /* ignore error caused by I/O, maybe caused by non exist file */
+                err_num != -1) {
+            eh->err_file = strdup(*config_file);
+            free(*config_file);
+            free(hfp.full_filename);
+            free(hfp.editorconfig_file_dir);
+            return err_num;
+        }
+
+        free(hfp.editorconfig_file_dir);
+        free(*config_file);
+    }
+
+    /* value proprocessing */
+
+    /* For v0.9 */
+    SET_EDITORCONFIG_VERSION(&tmp_ver, 0, 9, 0);
+    if (editorconfig_compare_version(&eh->ver, &tmp_ver) >= 0) {
+    /* Set indent_size to "tab" if indent_size is not specified and
+     * indent_style is set to "tab". Only should be done after v0.9 */
+        if (hfp.array_name_value.spnvp.indent_style &&
+                !hfp.array_name_value.spnvp.indent_size &&
+                !strcmp(hfp.array_name_value.spnvp.indent_style->value, "tab"))
+            array_editorconfig_name_value_add(&hfp.array_name_value,
+                    "indent_size", "tab");
+    /* Set indent_size to tab_width if indent_size is "tab" and tab_width is
+     * specified. This behavior is specified for v0.9 and up. */
+        if (hfp.array_name_value.spnvp.indent_size &&
+            hfp.array_name_value.spnvp.tab_width &&
+            !strcmp(hfp.array_name_value.spnvp.indent_size->value, "tab"))
+        array_editorconfig_name_value_add(&hfp.array_name_value, "indent_size",
+                hfp.array_name_value.spnvp.tab_width->value);
+    }
+
+    /* Set tab_width to indent_size if indent_size is specified. If version is
+     * not less than 0.9.0, we also need to check when the indent_size is set
+     * to "tab", we should not duplicate the value to tab_width */
+    if (hfp.array_name_value.spnvp.indent_size &&
+            !hfp.array_name_value.spnvp.tab_width &&
+            (editorconfig_compare_version(&eh->ver, &tmp_ver) < 0 ||
+             strcmp(hfp.array_name_value.spnvp.indent_size->value, "tab")))
+        array_editorconfig_name_value_add(&hfp.array_name_value, "tab_width",
+                hfp.array_name_value.spnvp.indent_size->value);
+
+    eh->name_value_count = hfp.array_name_value.current_value_count;
+
+    if (eh->name_value_count == 0) {  /* no value is set, just return 0. */
+        free(hfp.full_filename);
+        free(config_files);
+        return 0;
+    }
+    eh->name_values = hfp.array_name_value.name_values;
+    eh->name_values = realloc(      /* realloc to truncate the unused spaces */
+            eh->name_values,
+            sizeof(editorconfig_name_value) * eh->name_value_count);
+    if (eh->name_values == NULL) {
+        free(hfp.full_filename);
+        free(config_files);
+        return EDITORCONFIG_PARSE_MEMORY_ERROR;
+    }
+
+    free(hfp.full_filename);
+    free(config_files);
+
+    return 0;
+}
+
+/*
+ * See header file
+ */
+EDITORCONFIG_EXPORT
+void editorconfig_get_version(int* major, int* minor, int* patch)
+{
+    if (major)
+        *major = editorconfig_VERSION_MAJOR;
+    if (minor)
+        *minor = editorconfig_VERSION_MINOR;
+    if (patch)
+        *patch = editorconfig_VERSION_PATCH;
+}
+
+/*
+ * See header file
+ */
+EDITORCONFIG_EXPORT
+const char* editorconfig_get_version_suffix(void)
+{
+    return editorconfig_VERSION_SUFFIX;
+}
diff --git a/src/plugins/editorconfig/libeditorconfig/editorconfig.h 
b/src/plugins/editorconfig/libeditorconfig/editorconfig.h
new file mode 100644
index 000000000..f0fa95733
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/editorconfig.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2011-2012 EditorConfig Team
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef __EDITORCONFIG_H__
+#define __EDITORCONFIG_H__
+
+#include "editorconfig/editorconfig.h"
+
+#include "editorconfig_handle.h"
+
+typedef struct editorconfig_name_value editorconfig_name_value;
+
+#endif /* !__EDITORCONFIG_H__ */
+
diff --git a/src/plugins/editorconfig/libeditorconfig/editorconfig/editorconfig.h 
b/src/plugins/editorconfig/libeditorconfig/editorconfig/editorconfig.h
new file mode 100644
index 000000000..a4ae1ad74
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/editorconfig/editorconfig.h
@@ -0,0 +1,309 @@
+/*
+ * Copyright 2011-2013 EditorConfig Team
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+/*!
+ * @mainpage EditorConfig C Core Documentation
+ *
+ * This is the documentation of EditorConfig C Core. In this documentation, you
+ * could find the document of the @ref editorconfig and the document of
+ * EditorConfig Core C APIs in editorconfig.h and editorconfig_handle.h.
+ */
+
+/*!
+ * @page editorconfig EditorConfig Command
+ *
+ * @section usage Usage of the `editorconfig` command line tool
+ *
+ * Usage: editorconfig <em>[OPTIONS]</em> FILEPATH1 [FILEPATH2 FILEPATH3 ...]
+ *
+ * FILEPATH can be a hyphen (-) if you want to path(s) to be read from stdin.
+ * Hyphen can also be specified with other file names. In this way, both file
+ * paths from stdin and the paths specified on the command line will be used.
+ * If more than one path specified on the command line, or the paths are
+ * reading from stdin (even only one path is read from stdin), the output
+ * format would be INI format, instead of the simple "key=value" lines.
+ *
+ * @htmlonly
+ * <table cellpadding="5" cellspacing="5">
+ *
+ * <tr>
+ * <td><em>-f</em></td>
+ * <td>Specify conf filename other than ".editorconfig".</td>
+ * </tr>
+ *
+ * <tr>
+ * <td><em>-b</em></td>
+ * <td>Specify version (used by devs to test compatibility).</td>
+ * </tr>
+ *
+ * <tr>
+ * <td><em>-h</em> OR <em>--help</em></td>
+ * <td>Print this help message.</td>
+ * </tr>
+ *
+ * <tr>
+ * <td><em>--version</em></td>
+ * <td>Display version information.</td>
+ * </tr>
+ *
+ * </table>
+ * @endhtmlonly
+ * @manonly
+ *
+ * -f             Specify conf filename other than ".editorconfig".
+ *
+ * -b             Specify version (used by devs to test compatibility).
+ *
+ * -h OR --help   Print this help message.
+ *
+ * --version      Display version information.
+ *
+ * @endmanonly
+ *
+ * @section related Related Pages
+ *
+ * @ref editorconfig-format
+ */
+
+/*!
+ * @page editorconfig-format EditorConfig File Format
+ *
+ * @section format EditorConfig File Format
+ *
+ * EditorConfig files use an INI format that is compatible with the format used
+ * by Python ConfigParser Library, but [ and ] are allowed in the section names.
+ * The section names are filepath globs, similar to the format accepted by
+ * gitignore. Forward slashes (/) are used as path separators and semicolons (;)
+ * or octothorpes (#) are used for comments. Comments should go individual lines.
+ * EditorConfig files should be UTF-8 encoded, with either CRLF or LF line
+ * separators.
+ *
+ * Filename globs containing path separators (/) match filepaths in the same
+ * way as the filename globs used by .gitignore files.  Backslashes (\\) are
+ * not allowed as path separators.
+ *
+ * A semicolon character (;) starts a line comment that terminates at the end
+ * of the line. Line comments and blank lines are ignored when parsing.
+ * Comments may be added to the ends of non-empty lines. An octothorpe
+ * character (#) may be used instead of a semicolon to denote the start of a
+ * comment.
+ *
+ * @section file-location Filename and Location
+ *
+ * When a filename is given to EditorConfig a search is performed in the
+ * directory of the given file and all parent directories for an EditorConfig
+ * file (named ".editorconfig" by default).  All found EditorConfig files are
+ * searched for sections with section names matching the given filename. The
+ * search will stop if an EditorConfig file is found with the root property set
+ * to true or when reaching the root filesystem directory.
+ *
+ * Files are read top to bottom and the most recent rules found take
+ * precedence. If multiple EditorConfig files have matching sections, the rules
+ * from the closer EditorConfig file are read last, so properties in closer
+ * files take precedence.
+ *
+ * @section patterns Wildcard Patterns
+ *
+ * Section names in EditorConfig files are filename globs that support pattern
+ * matching through Unix shell-style wildcards. These filename globs recognize
+ * the following as special characters for wildcard matching:
+ *
+ * @htmlonly
+ * <table>
+ *   <tr><td><code>*</code></td><td>Matches any string of characters, except path separators 
(<code>/</code>)</td></tr>
+ *   <tr><td><code>**</code></td><td>Matches any string of characters</td></tr>
+ *   <tr><td><code>?</code></td><td>Matches any single character</td></tr>
+ *   <tr><td><code>[seq]</code></td><td>Matches any single character in <i>seq</i></td></tr>
+ *   <tr><td><code>[!seq]</code></td><td>Matches any single character not in <i>seq</i></td></tr>
+ *   <tr><td><code>{s1,s2,s3}</code></td><td>Matches any of the strings given (separated by commas, can be 
nested)</td></tr>
+ *   <tr><td><code>{num1..num2}</code></td><td>Matches any integer numbers between num1 and num2, where num1 
and num2 can be either positive or negative</td></tr>
+ * </table>
+ * @endhtmlonly
+ * @manonly
+ * *            Matches any string of characters, except path separators (/)
+ *
+ * **           Matches any string of characters
+ *
+ * ?            Matches any single character
+ *
+ * [seq]        Matches any single character in seq
+ *
+ * [!seq]       Matches any single character not in seq
+ *
+ * {s1,s2,s3}   Matches any of the strings given (separated by commas, can be nested)
+ *
+ * {num1..num2} Matches any integer numbers between num1 and num2, where num1 and num2 can be either 
positive or negative
+ *
+ * @endmanonly
+ *
+ * The backslash character (\) can be used to escape a character so it is not interpreted as a special 
character.
+ *
+ * @section properties Supported Properties
+ *
+ * EditorConfig file sections contain properties, which are name-value pairs separated by an equal sign (=). 
EditorConfig plugins will ignore unrecognized property names and properties with invalid values.
+ *
+ * Here is the list of all property names understood by EditorConfig and all valid values for these 
properties:
+ *
+ * <ul>
+ * <li><strong>indent_style</strong>: set to "tab" or "space" to use hard tabs or soft tabs respectively. 
The values are case insensitive.</li>
+ * <li><strong>indent_size</strong>: a whole number defining the number of columns used for each indentation 
level and the width of soft tabs (when supported). If this equals to "tab", the <strong>indent_size</strong> 
will be set to the tab size, which should be tab_width if <strong>tab_width</strong> is specified, or the tab 
size set by editor if <strong>tab_width</strong> is not specified. The values are case insensitive.</li>
+ * <li><strong>tab_width</strong>: a whole number defining the number of columns used to represent a tab 
character. This defaults to the value of <strong>indent_size</strong> and should not usually need to be 
specified.</li>
+ * <li><strong>end_of_line</strong>: set to "lf", "cr", or "crlf" to control how line breaks are 
represented. The values are case insensitive.</li>
+ * <li><strong>charset</strong>: set to "latin1", "utf-8", "utf-8-bom", "utf-16be" or "utf-16le" to control 
the character set. Use of "utf-8-bom" is discouraged.</li>
+ * <li><strong>trim_trailing_whitespace</strong>:  set to "true" to remove any whitespace characters 
preceeding newline characters and "false" to ensure it doesn't.</li>
+ * <li><strong>insert_final_newline</strong>: set to "true" ensure file ends with a newline when saving and 
"false" to ensure it doesn't.</li>
+ * <li><strong>root</strong>: special property that should be specified at the top of the file outside of 
any sections. Set to "true" to stop <code>.editorconfig</code> files search on current file. The value is 
case insensitive.</li>
+ * </ul>
+ *
+ * Property names are case insensitive and all property names are lowercased when parsing.
+ */
+
+/*!
+ * @file editorconfig/editorconfig.h
+ * @brief Header file of EditorConfig.
+ *
+ * Related page: @ref editorconfig-format
+ *
+ * @author EditorConfig Team
+ */
+
+#ifndef __EDITORCONFIG_EDITORCONFIG_H__
+#define __EDITORCONFIG_EDITORCONFIG_H__
+
+/* When included from a user program, EDITORCONFIG_EXPORT may not be defined,
+ * and we define it here*/
+#ifndef EDITORCONFIG_EXPORT
+# define EDITORCONFIG_EXPORT
+#endif
+
+#include <editorconfig/editorconfig_handle.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*!
+ * @brief Parse editorconfig files corresponding to the file path given by
+ * full_filename, and related information is input and output in h.
+ *
+ * An example is available at
+ * <a href=https://github.com/editorconfig/editorconfig-core/blob/master/src/bin/main.c>src/bin/main.c</a>
+ * in EditorConfig C Core source code.
+ *
+ * @param full_filename The full path of a file that is edited by the editor
+ * for which the parsing result is.
+ *
+ * @param h The @ref editorconfig_handle to be used and returned from this
+ * function (including the parsing result). The @ref editorconfig_handle should
+ * be created by editorconfig_handle_init().
+ *
+ * @retval 0 Everything is OK.
+ *
+ * @retval "Positive Integer" A parsing error occurs. The return value would be
+ * the line number of parsing error. err_file obtained from h by calling
+ * editorconfig_handle_get_err_file() will also be filled with the file path
+ * that caused the parsing error.
+ *
+ * @retval "Negative Integer" Some error occured. See below for the reason of
+ * the error for each return value.
+ *
+ * @retval EDITORCONFIG_PARSE_NOT_FULL_PATH The full_filename is not a full
+ * path name.
+ *
+ * @retval EDITORCONFIG_PARSE_MEMORY_ERROR A memory error occurs.
+ *
+ * @retval EDITORCONFIG_PARSE_VERSION_TOO_NEW The required version specified in
+ * @ref editorconfig_handle is greater than the current version.
+ *
+ */
+EDITORCONFIG_EXPORT
+int editorconfig_parse(const char* full_filename, editorconfig_handle h);
+
+/*!
+ * @brief Get the error message from the error number returned by
+ * editorconfig_parse().
+ *
+ * An example is available at
+ * <a href=https://github.com/editorconfig/editorconfig-core/blob/master/src/bin/main.c>src/bin/main.c</a>
+ * in EditorConfig C Core source code.
+ *
+ * @param err_num The error number that is used to obtain the error message.
+ *
+ * @return The error message corresponding to err_num.
+ */
+EDITORCONFIG_EXPORT
+const char* editorconfig_get_error_msg(int err_num);
+
+/*!
+ * editorconfig_parse() return value: the full_filename parameter of
+ * editorconfig_parse() is not a full path name
+ */
+#define EDITORCONFIG_PARSE_NOT_FULL_PATH                (-2)
+/*!
+ * editorconfig_parse() return value: a memory error occurs.
+ */
+#define EDITORCONFIG_PARSE_MEMORY_ERROR                 (-3)
+/*!
+ * editorconfig_parse() return value: the required version specified in @ref
+ * editorconfig_handle is greater than the current version.
+ */
+#define EDITORCONFIG_PARSE_VERSION_TOO_NEW              (-4)
+
+/*!
+ * @brief Get the version number of EditorConfig.
+ *
+ * An example is available at
+ * <a href=https://github.com/editorconfig/editorconfig-core/blob/master/src/bin/main.c>src/bin/main.c</a>
+ * in EditorConfig C Core source code.
+ *
+ * @param major If not null, the integer pointed by major will be filled with
+ * the major version of EditorConfig.
+ *
+ * @param minor If not null, the integer pointed by minor will be filled with
+ * the minor version of EditorConfig.
+ *
+ * @param patch If not null, the integer pointed by patch will be filled
+ * with the patch version of EditorConfig.
+ *
+ * @return None.
+ */
+EDITORCONFIG_EXPORT
+void editorconfig_get_version(int* major, int* minor, int* patch);
+
+/*!
+ * @brief Get the version suffix.
+ *
+ * @return The version suffix, such as "-development" for a development
+ * version, empty string for a stable version.
+ */
+EDITORCONFIG_EXPORT
+const char* editorconfig_get_version_suffix(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* !__EDITORCONFIG_EDITORCONFIG_H__ */
+
diff --git a/src/plugins/editorconfig/libeditorconfig/editorconfig/editorconfig_handle.h 
b/src/plugins/editorconfig/libeditorconfig/editorconfig/editorconfig_handle.h
new file mode 100644
index 000000000..35f4be91e
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/editorconfig/editorconfig_handle.h
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2011-2012 EditorConfig Team
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*!
+ * @file editorconfig/editorconfig_handle.h
+ * @brief Header file of EditorConfig handle.
+ *
+ * @author EditorConfig Team
+ */
+
+#ifndef __EDITORCONFIG_EDITORCONFIG_HANDLE_H__
+#define __EDITORCONFIG_EDITORCONFIG_HANDLE_H__
+
+/* When included from a user program, EDITORCONFIG_EXPORT may not be defined,
+ * and we define it here*/
+#ifndef EDITORCONFIG_EXPORT
+# define EDITORCONFIG_EXPORT
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*!
+ * @brief The editorconfig handle object type
+ */
+typedef void*   editorconfig_handle;
+
+/*!
+ * @brief Create and intialize a default editorconfig_handle object.
+ *
+ * @retval NULL Failed to create the editorconfig_handle object.
+ *
+ * @retval non-NULL The created editorconfig_handle object is returned.
+ */
+EDITORCONFIG_EXPORT
+editorconfig_handle editorconfig_handle_init(void);
+
+/*!
+ * @brief Destroy an editorconfig_handle object
+ *
+ * @param h The editorconfig_handle object needs to be destroyed.
+ *
+ * @retval zero The editorconfig_handle object is destroyed successfully.
+ * 
+ * @retval non-zero Failed to destroy the editorconfig_handle object.
+ */
+EDITORCONFIG_EXPORT
+int editorconfig_handle_destroy(editorconfig_handle h);
+
+/*!
+ * @brief Get the err_file field of an editorconfig_handle object
+ *
+ * @param h The editorconfig_handle object whose err_file needs to be obtained.
+ *
+ * @retval NULL No error file exists.
+ *
+ * @retval non-NULL The pointer to the path of the file caused the parsing
+ * error is returned.
+ */
+EDITORCONFIG_EXPORT
+const char* editorconfig_handle_get_err_file(editorconfig_handle h);
+
+/*!
+ * @brief Get the version fields of an editorconfig_handle object.
+ *
+ * @param h The editorconfig_handle object whose version field need to be
+ * obtained.
+ *
+ * @param major If not null, the integer pointed by major will be filled with
+ * the major version field of the editorconfig_handle object.
+ *
+ * @param minor If not null, the integer pointed by minor will be filled with
+ * the minor version field of the editorconfig_handle object.
+ *
+ * @param patch If not null, the integer pointed by patch will be filled
+ * with the patch version field of the editorconfig_handle object.
+ *
+ * @return None.
+ */
+EDITORCONFIG_EXPORT
+void editorconfig_handle_get_version(const editorconfig_handle h, int* major,
+        int* minor, int* patch);
+
+/*!
+ * @brief Set the version fields of an editorconfig_handle object.
+ *
+ * @param h The editorconfig_handle object whose version fields need to be set.
+ *
+ * @param major If not less than 0, the major version field will be set to
+ * major. If this parameter is less than 0, the major version field of the
+ * editorconfig_handle object will remain unchanged.
+ *
+ * @param minor If not less than 0, the minor version field will be set to
+ * minor. If this parameter is less than 0, the minor version field of the
+ * editorconfig_handle object will remain unchanged.
+ *
+ * @param patch If not less than 0, the patch version field will be set to
+ * patch. If this parameter is less than 0, the patch version field of the
+ * editorconfig_handle object will remain unchanged.
+ *
+ * @return None.
+ */
+EDITORCONFIG_EXPORT
+void editorconfig_handle_set_version(const editorconfig_handle h, int major,
+        int minor, int patch);
+/*!
+ * @brief Set the conf_file_name field of an editorconfig_handle object.
+ *
+ * @param h The editorconfig_handle object whose conf_file_name field needs to
+ * be set.
+ *
+ * @param conf_file_name The new value of the conf_file_name field of the
+ * editorconfig_handle object.
+ *
+ * @return None.
+ */
+EDITORCONFIG_EXPORT
+void editorconfig_handle_set_conf_file_name(editorconfig_handle h,
+        const char* conf_file_name);
+
+/*!
+ * @brief Get the conf_file_name field of an editorconfig_handle object.
+ *
+ * @param h The editorconfig_handle object whose conf_file_name field needs to
+ * be obtained.
+ *
+ * @return The value of the conf_file_name field of the editorconfig_handle
+ * object.
+ */
+EDITORCONFIG_EXPORT
+const char* editorconfig_handle_get_conf_file_name(const editorconfig_handle h);
+
+/*!
+ * @brief Get the nth name and value fields of an editorconfig_handle object.
+ *
+ * @param h The editorconfig_handle object whose name and value fields need to
+ * be obtained.
+ *
+ * @param n The zero-based index of the name and value fields to be obtained.
+ *
+ * @param name If not null, *name will be set to point to the obtained name.
+ *
+ * @param value If not null, *value will be set to point to the obtained value.
+ *
+ * @return None.
+ */
+EDITORCONFIG_EXPORT
+void editorconfig_handle_get_name_value(const editorconfig_handle h, int n,
+        const char** name, const char** value);
+
+/*!
+ * @brief Get the count of name and value fields of an editorconfig_handle
+ * object.
+ *
+ * @param h The editorconfig_handle object whose count of name and value fields
+ * need to be obtained.
+ *
+ * @return the count of name and value fields of the editorconfig_handle
+ * object.
+ */
+EDITORCONFIG_EXPORT
+int editorconfig_handle_get_name_value_count(const editorconfig_handle h);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* !__EDITORCONFIG_EDITORCONFIG_HANDLE_H__ */
+
diff --git a/src/plugins/editorconfig/libeditorconfig/editorconfig_handle.c 
b/src/plugins/editorconfig/libeditorconfig/editorconfig_handle.c
new file mode 100644
index 000000000..e00039837
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/editorconfig_handle.c
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2011-2012 EditorConfig Team
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "editorconfig_handle.h"
+
+/*
+ * See header file
+ */
+EDITORCONFIG_EXPORT
+editorconfig_handle editorconfig_handle_init(void)
+{
+    editorconfig_handle     h;
+    
+    h = (editorconfig_handle)malloc(sizeof(struct editorconfig_handle));
+
+    if (!h)
+        return (editorconfig_handle)NULL;
+
+    memset(h, 0, sizeof(struct editorconfig_handle));
+
+    return h;
+}
+
+/*
+ * See header file
+ */
+EDITORCONFIG_EXPORT
+int editorconfig_handle_destroy(editorconfig_handle h)
+{
+    int                             i;
+    struct editorconfig_handle*     eh = (struct editorconfig_handle*)h;
+
+
+    if (h == NULL)
+        return 0;
+
+    /* free name_values */
+    for (i = 0; i < eh->name_value_count; ++i) {
+        free(eh->name_values[i].name);
+        free(eh->name_values[i].value);
+    }
+    free(eh->name_values);
+
+    /* free err_file */
+    if (eh->err_file)
+        free(eh->err_file);
+
+    /* free eh itself */
+    free(eh);
+
+    return 0;
+}
+
+/*
+ * See header file
+ */
+EDITORCONFIG_EXPORT
+const char* editorconfig_handle_get_err_file(const editorconfig_handle h)
+{
+    return ((const struct editorconfig_handle*)h)->err_file;
+}
+
+/*
+ * See header file
+ */
+EDITORCONFIG_EXPORT
+void editorconfig_handle_get_version(const editorconfig_handle h, int* major,
+        int* minor, int* patch)
+{
+    if (major)
+        *major = ((const struct editorconfig_handle*)h)->ver.major;
+    if (minor)
+        *minor = ((const struct editorconfig_handle*)h)->ver.minor;
+    if (patch)
+        *patch = ((const struct editorconfig_handle*)h)->ver.patch;
+}
+
+/*
+ * See header file
+ */
+EDITORCONFIG_EXPORT
+void editorconfig_handle_set_version(editorconfig_handle h, int major,
+        int minor, int patch)
+{
+    if (major >= 0)
+        ((struct editorconfig_handle*)h)->ver.major = major;
+
+    if (minor >= 0)
+        ((struct editorconfig_handle*)h)->ver.minor = minor;
+
+    if (patch >= 0)
+        ((struct editorconfig_handle*)h)->ver.patch = patch;
+}
+
+/*
+ * See header file
+ */
+EDITORCONFIG_EXPORT
+void editorconfig_handle_set_conf_file_name(editorconfig_handle h,
+        const char* conf_file_name)
+{
+    ((struct editorconfig_handle*)h)->conf_file_name = conf_file_name;
+}
+
+/*
+ * See header file
+ */
+EDITORCONFIG_EXPORT
+const char* editorconfig_handle_get_conf_file_name(const editorconfig_handle h)
+{
+    return ((const struct editorconfig_handle*)h)->conf_file_name;
+}
+
+EDITORCONFIG_EXPORT
+void editorconfig_handle_get_name_value(const editorconfig_handle h, int n,
+        const char** name, const char** value)
+{
+    struct editorconfig_name_value* name_value = &((
+                const struct editorconfig_handle*)h)->name_values[n];
+
+    if (name)
+        *name = name_value->name;
+
+    if (value)
+        *value = name_value->value;
+}
+
+EDITORCONFIG_EXPORT
+int editorconfig_handle_get_name_value_count(const editorconfig_handle h)
+{
+    return ((const struct editorconfig_handle*)h)->name_value_count;
+}
diff --git a/src/plugins/editorconfig/libeditorconfig/editorconfig_handle.h 
b/src/plugins/editorconfig/libeditorconfig/editorconfig_handle.h
new file mode 100644
index 000000000..f36f34224
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/editorconfig_handle.h
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2011-2012 EditorConfig Team
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef __EDITORCONFIG_HANDLE_H__
+#define __EDITORCONFIG_HANDLE_H__
+
+#include "global.h"
+#include <editorconfig/editorconfig_handle.h>
+
+/*!
+ * @brief A structure containing a name and its corresponding value.
+ * @author EditorConfig Team
+ */
+struct editorconfig_name_value
+{
+    /*! EditorConfig config item's name. */ 
+    char*       name;
+    /*! EditorConfig config item's value. */ 
+    char*       value;
+};
+
+/*!
+ * @brief A structure that descripts version number.
+ * @author EditorConfig Team
+ */
+struct editorconfig_version
+{
+    /*! major version */
+    int                     major;
+    /*! minor version */
+    int                     minor;
+    /*! patch version */
+    int                     patch;
+};
+
+struct editorconfig_handle
+{
+    /*!
+     * The file name of EditorConfig conf file. If this pointer is set to NULL,
+     * the file name is set to ".editorconfig" by default.
+     */
+    const char*                         conf_file_name;
+
+    /*!
+     * When a parsing error occured, this will point to a file that caused the
+     * parsing error.
+     */
+    char*                               err_file;
+
+    /*!
+     * version number it should act as. Set this to 0.0.0 to act like the
+     * current version.
+     */
+    struct editorconfig_version         ver;
+
+    /*! Pointer to a list of editorconfig_name_value structures containing
+     * names and values of the parsed result */
+    struct editorconfig_name_value*     name_values;
+
+    /*! The total count of name_values structures pointed by name_values
+     * pointer */
+    int                                 name_value_count;
+};
+
+#endif /* !__EDITORCONFIG_HANDLE_H__ */
+
diff --git a/src/plugins/editorconfig/libeditorconfig/global.h 
b/src/plugins/editorconfig/libeditorconfig/global.h
new file mode 100644
index 000000000..235693f25
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/global.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2011-2012 EditorConfig Team
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef __GLOBAL_H__
+#define __GLOBAL_H__
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/*
+ * Microsoft Visual C++ and some other Windows C Compilers requires exported
+ * functions in shared library to be defined with __declspec(dllexport)
+ * declarator. Also, gcc >= 4 supports hiding symbols that do not need to be
+ * exported.
+ */
+#ifdef editorconfig_shared_EXPORTS /* We are building shared lib if defined */
+# ifdef WIN32
+#  ifdef __GNUC__
+#   define EDITORCONFIG_EXPORT  __attribute__ ((dllexport))
+#  else /* __GNUC__ */
+#   define EDITORCONFIG_EXPORT __declspec(dllexport)
+#  endif /* __GNUC__ */
+# else /* WIN32 */
+#  if defined(__GNUC__) && __GNUC__ >= 4
+#   define EDITORCONFIG_EXPORT __attribute__ ((visibility ("default")))
+#   define EDITORCONFIG_LOCAL __attribute__ ((visibility ("hidden")))
+#  endif /* __GNUC__ && __GNUC >= 4 */
+# endif /* WIN32 */
+#endif /* editorconfig_shared_EXPORTS */
+
+/*
+ * For other cases, just define EDITORCONFIG_EXPORT and EDITORCONFIG_LOCAL, to
+ * make compilation successful
+ */
+#ifndef EDITORCONFIG_EXPORT
+# define EDITORCONFIG_EXPORT
+#endif
+#ifndef EDITORCONFIG_LOCAL
+# define EDITORCONFIG_LOCAL
+#endif
+
+/* a macro to set editorconfig_version struct */
+#define SET_EDITORCONFIG_VERSION(editorconfig_ver, maj, min, submin) \
+    do { \
+        (editorconfig_ver)->major = (maj); \
+        (editorconfig_ver)->minor = (min); \
+        (editorconfig_ver)->patch = (submin); \
+    } while(0)
+
+#endif /* !__GLOBAL_H__ */
+
diff --git a/src/plugins/editorconfig/libeditorconfig/ini.c b/src/plugins/editorconfig/libeditorconfig/ini.c
new file mode 100644
index 000000000..08fc0eae0
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/ini.c
@@ -0,0 +1,200 @@
+/* inih -- simple .INI file parser
+
+The "inih" library is distributed under the New BSD license:
+
+Copyright 2009, Brush Technology
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of Brush Technology nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY BRUSH TECHNOLOGY ''AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL BRUSH TECHNOLOGY BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Go to the project home page for more info:
+http://code.google.com/p/inih/
+
+*/
+
+#include "global.h"
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+
+#include "ini.h"
+
+#define MAX_LINE 200
+#define MAX_SECTION MAX_SECTION_NAME
+#define MAX_NAME MAX_PROPERTY_NAME
+
+/* Strip whitespace chars off end of given string, in place. Return s. */
+static char* rstrip(char* s)
+{
+    char* p = s + strlen(s);
+    while (p > s && isspace(*--p))
+        *p = '\0';
+    return s;
+}
+
+/* Return pointer to first non-whitespace char in given string. */
+static char* lskip(const char* s)
+{
+    while (*s && isspace(*s))
+        s++;
+    return (char*)s;
+}
+
+/* Return pointer to first char c or ';' comment in given string, or pointer to
+   null at end of string if neither found. ';' must be prefixed by a whitespace
+   character to register as a comment. */
+static char* find_char_or_comment(const char* s, char c)
+{
+    int was_whitespace = 0;
+    while (*s && *s != c && !(was_whitespace && (*s == ';' || *s == '#'))) {
+        was_whitespace = isspace(*s);
+        s++;
+    }
+    return (char*)s;
+}
+
+static char* find_last_char_or_comment(const char* s, char c)
+{
+    const char* last_char = s;
+    int was_whitespace = 0;
+    while (*s && !(was_whitespace && (*s == ';' || *s == '#'))) {
+        if (*s == c)
+            last_char = s;
+        was_whitespace = isspace(*s);
+        s++;
+    }
+    return (char*)last_char;
+}
+
+/* Version of strncpy that ensures dest (size bytes) is null-terminated. */
+static char* strncpy0(char* dest, const char* src, size_t size)
+{
+    strncpy(dest, src, size - 1);
+    dest[size - 1] = '\0';
+    return dest;
+}
+
+/* See documentation in header file. */
+EDITORCONFIG_LOCAL
+int ini_parse_file(FILE* file,
+                   int (*handler)(void*, const char*, const char*,
+                                  const char*),
+                   void* user)
+{
+    /* Uses a fair bit of stack (use heap instead if you need to) */
+    char line[MAX_LINE];
+    char section[MAX_SECTION] = "";
+    char prev_name[MAX_NAME] = "";
+
+    char* start;
+    char* end;
+    char* name;
+    char* value;
+    int lineno = 0;
+    int error = 0;
+
+    /* Scan through file line by line */
+    while (fgets(line, sizeof(line), file) != NULL) {
+        lineno++;
+
+        start = line;
+#if INI_ALLOW_BOM
+        if (lineno == 1 && (unsigned char)start[0] == 0xEF &&
+                           (unsigned char)start[1] == 0xBB &&
+                           (unsigned char)start[2] == 0xBF) {
+            start += 3;
+        }
+#endif
+        start = lskip(rstrip(start));
+
+        if (*start == ';' || *start == '#') {
+            /* Per Python ConfigParser, allow '#' comments at start of line */
+        }
+#if INI_ALLOW_MULTILINE
+        else if (*prev_name && *start && start > line) {
+            /* Non-black line with leading whitespace, treat as continuation
+               of previous name's value (as per Python ConfigParser). */
+            if (!handler(user, section, prev_name, start) && !error)
+                error = lineno;
+        }
+#endif
+        else if (*start == '[') {
+            /* A "[section]" line */
+            end = find_last_char_or_comment(start + 1, ']');
+            if (*end == ']') {
+                *end = '\0';
+                strncpy0(section, start + 1, sizeof(section));
+                *prev_name = '\0';
+            }
+            else if (!error) {
+                /* No ']' found on section line */
+                error = lineno;
+            }
+        }
+        else if (*start && (*start != ';' || *start == '#')) {
+            /* Not a comment, must be a name[=:]value pair */
+            end = find_char_or_comment(start, '=');
+            if (*end != '=') {
+                end = find_char_or_comment(start, ':');
+            }
+            if (*end == '=' || *end == ':') {
+                *end = '\0';
+                name = rstrip(start);
+                value = lskip(end + 1);
+                end = find_char_or_comment(value, '\0');
+                if (*end == ';' || *end == '#')
+                    *end = '\0';
+                rstrip(value);
+
+                /* Valid name[=:]value pair found, call handler */
+                strncpy0(prev_name, name, sizeof(prev_name));
+                if (!handler(user, section, name, value) && !error)
+                    error = lineno;
+            }
+            else if (!error) {
+                /* No '=' or ':' found on name[=:]value line */
+                error = lineno;
+            }
+        }
+    }
+
+    return error;
+}
+
+/* See documentation in header file. */
+EDITORCONFIG_LOCAL
+int ini_parse(const char* filename,
+              int (*handler)(void*, const char*, const char*, const char*),
+              void* user)
+{
+    FILE* file;
+    int error;
+
+    file = fopen(filename, "r");
+    if (!file)
+        return -1;
+    error = ini_parse_file(file, handler, user);
+    fclose(file);
+    return error;
+}
diff --git a/src/plugins/editorconfig/libeditorconfig/ini.h b/src/plugins/editorconfig/libeditorconfig/ini.h
new file mode 100644
index 000000000..321da462d
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/ini.h
@@ -0,0 +1,93 @@
+/* inih -- simple .INI file parser
+
+The "inih" library is distributed under the New BSD license:
+
+Copyright 2009, Brush Technology
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright
+      notice, this list of conditions and the following disclaimer in the
+      documentation and/or other materials provided with the distribution.
+    * Neither the name of Brush Technology nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY BRUSH TECHNOLOGY ''AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL BRUSH TECHNOLOGY BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Go to the project home page for more info:
+http://code.google.com/p/inih/
+
+*/
+
+#ifndef __INI_H__
+#define __INI_H__
+
+#include "global.h"
+
+/* Make this header file easier to include in C++ code */
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stdio.h>
+
+/* Parse given INI-style file. May have [section]s, name=value pairs
+   (whitespace stripped), and comments starting with ';' (semicolon). Section
+   is "" if name=value pair parsed before any section heading. name:value
+   pairs are also supported as a concession to Python's ConfigParser.
+
+   For each name=value pair parsed, call handler function with given user
+   pointer as well as section, name, and value (data only valid for duration
+   of handler call). Handler should return nonzero on success, zero on error.
+
+   Returns 0 on success, line number of first error on parse error (doesn't
+   stop on first error), or -1 on file open error.
+*/
+EDITORCONFIG_LOCAL
+int ini_parse(const char* filename,
+              int (*handler)(void* user, const char* section,
+                             const char* name, const char* value),
+              void* user);
+
+/* Same as ini_parse(), but takes a FILE* instead of filename. This doesn't
+   close the file when it's finished -- the caller must do that. */
+EDITORCONFIG_LOCAL
+int ini_parse_file(FILE* file,
+                   int (*handler)(void* user, const char* section,
+                                  const char* name, const char* value),
+                   void* user);
+
+/* Nonzero to allow multi-line value parsing, in the style of Python's
+   ConfigParser. If allowed, ini_parse() will call the handler with the same
+   name for each subsequent line parsed. */
+#ifndef INI_ALLOW_MULTILINE
+#define INI_ALLOW_MULTILINE 0
+#endif
+
+#define MAX_SECTION_NAME 500
+#define MAX_PROPERTY_NAME 500
+
+/* Nonzero to allow a UTF-8 BOM sequence (0xEF 0xBB 0xBF) at the start of
+   the file. See http://code.google.com/p/inih/issues/detail?id=21 */
+#ifndef INI_ALLOW_BOM
+#define INI_ALLOW_BOM 1
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __INI_H__ */
diff --git a/src/plugins/editorconfig/libeditorconfig/meson.build 
b/src/plugins/editorconfig/libeditorconfig/meson.build
new file mode 100644
index 000000000..d30561e1f
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/meson.build
@@ -0,0 +1,45 @@
+libeditorconfig_sources = [
+  'ec_glob.c',
+  'ec_glob.h',
+  'editorconfig.c',
+  'editorconfig.h',
+  'editorconfig/editorconfig.h',
+  'editorconfig/editorconfig_handle.h',
+  'editorconfig_handle.c',
+  'editorconfig_handle.h',
+  'global.h',
+  'ini.c',
+  'ini.h',
+  'misc.c',
+  'misc.h',
+  'utarray.h',
+]
+
+libeditorconfig_deps = [
+  dependency('libpcre')
+]
+
+# FIXME: Actually test these
+libeditorconfig_args = [
+  '-DHAVE_STRCASECMP',
+  '-DHAVE_STRICMP',
+  '-DHAVE_STRDUP',
+  '-DHAVE_STRNDUP',
+  '-DUNIX',
+  '-Deditorconfig_VERSION_MAJOR=0',
+  '-Deditorconfig_VERSION_MINOR=0',
+  '-Deditorconfig_VERSION_PATCH=0',
+  '-Deditorconfig_VERSION_SUFFIX=0',
+]
+
+libeditorconfig = static_library('editorconfig',
+  libeditorconfig_sources,
+  dependencies: libeditorconfig_deps,
+        c_args: libeditorconfig_args,
+           pic: true,
+)
+
+libeditorconfig_dep = declare_dependency(
+            link_with: libeditorconfig,
+  include_directories: include_directories('.'),
+)
diff --git a/src/plugins/editorconfig/libeditorconfig/misc.c b/src/plugins/editorconfig/libeditorconfig/misc.c
new file mode 100644
index 000000000..143d618f5
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/misc.c
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2011-2012 EditorConfig Team
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+#include "misc.h"
+
+#ifdef WIN32
+# include <Shlwapi.h>
+#endif
+
+#if !defined(HAVE_STRCASECMP) && !defined(HAVE_STRICMP)
+/*
+ * strcasecmp function from FreeBSD
+ *
+ * Copyright 1987, 1993
+ * The Regents of the University of California. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 4. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <string.h>
+#include <ctype.h>
+
+typedef unsigned char u_char;
+
+EDITORCONFIG_LOCAL
+int ec_strcasecmp(const char *s1, const char *s2)
+{
+    const unsigned char
+        *us1 = (const unsigned char*)s1,
+        *us2 = (const unsigned char*)s2;
+
+    while (tolower(*us1) == tolower(*us2++))
+        if (*us1++ == '\0')
+            return (0);
+    return (tolower(*us1) - tolower(*--us2));
+}
+#endif /* !HAVE_STRCASECMP && !HAVE_STRICMP */
+
+#ifndef HAVE_STRDUP
+/*
+ * strdup function from FreeBSD
+ *
+ * Copyright 1988, 1993
+ * The Regents of the University of California. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 4. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+EDITORCONFIG_LOCAL
+char* ec_strdup(const char *str)
+{
+    size_t      len;
+    char*       copy;
+
+    len = strlen(str) + 1;
+    if ((copy = malloc(len)) == NULL)
+        return (NULL);
+    memcpy(copy, str, len);
+    return (copy);
+}
+
+#endif /* !HAVE_STRDUP */
+
+
+#ifndef HAVE_STRNDUP
+/*
+ * strndup function from NetBSD
+ *
+ * $NetBSD: strndup.c,v 1.3 2007/01/14 23:41:24 cbiere Exp $ 
+ *
+ * Copyright 1988, 1993
+ *  The Regents of the University of California.  All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "global.h"
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+EDITORCONFIG_LOCAL
+char* ec_strndup(const char* str, size_t n)
+{
+    size_t      len;
+    char*       copy;
+
+    for (len = 0; len < n && str[len]; len++)
+        continue;
+
+    if ((copy = malloc(len + 1)) == NULL)
+        return (NULL);
+    memcpy(copy, str, len);
+    copy[len] = '\0';
+    return (copy);
+}
+
+#endif /* HAVE_STRNDUP */
+
+/*
+ * replace oldc with newc in the string str
+ */
+EDITORCONFIG_LOCAL
+char* str_replace(char* str, char oldc, char newc)
+{
+    char*   p;
+
+    if (str == NULL)
+        return NULL;
+
+    for (p = str; *p != '\0'; p++)
+        if (*p == oldc)
+            *p = newc;
+
+    return str;
+}
+
+#ifndef HAVE_STRLWR
+
+#include <ctype.h>
+/*
+ * convert the string to lowercase
+ */
+EDITORCONFIG_LOCAL
+char* ec_strlwr(char* str)
+{
+    char*       p;
+
+    for (p = str; *p; ++p)
+        *p = (char)tolower((unsigned char)*p);
+
+    return str;
+}
+
+#endif /* !HAVE_STRLWR */
+
+/*
+ * is path an abosolute file path
+ */
+EDITORCONFIG_LOCAL
+_Bool is_file_path_absolute(const char* path)
+{
+    if (!path)
+        return 0;
+
+#if defined(UNIX)
+    if (*path == '/')
+        return 1;
+    return 0;
+#elif defined(WIN32)
+    return !PathIsRelative(path);
+#else
+# error "Either UNIX or WIN32 must be defined."
+#endif
+}
diff --git a/src/plugins/editorconfig/libeditorconfig/misc.h b/src/plugins/editorconfig/libeditorconfig/misc.h
new file mode 100644
index 000000000..f73554429
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/misc.h
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2011-2012 EditorConfig Team
+ * All rights reserved.
+ * 
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ * 
+ * 1. Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ * 
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef __MISC_H__
+#define __MISC_H__
+
+#include "global.h"
+
+#include <stddef.h>
+
+#ifndef HAVE_STRCASECMP
+# ifdef HAVE_STRICMP
+#  define strcasecmp stricmp
+# else /* HAVE_STRICMP */
+EDITORCONFIG_LOCAL
+int ec_strcasecmp(const char *s1, const char *s2);
+# define strcasecmp ec_strcasecmp
+# endif /* HAVE_STRICMP */
+#endif /* !HAVE_STRCASECMP */
+#ifndef HAVE_STRDUP
+EDITORCONFIG_LOCAL
+char* ec_strdup(const char *str);
+# define strdup ec_strdup
+#endif
+#ifndef HAVE_STRNDUP
+EDITORCONFIG_LOCAL
+char* ec_strndup(const char* str, size_t n);
+# define strndup ec_strndup
+#endif
+EDITORCONFIG_LOCAL
+char* str_replace(char* str, char oldc, char newc);
+#ifndef HAVE_STRLWR
+EDITORCONFIG_LOCAL
+char* ec_strlwr(char* str);
+# define strlwr ec_strlwr
+#endif
+EDITORCONFIG_LOCAL
+_Bool is_file_path_absolute(const char* path);
+#endif /* __MISC_H__ */
diff --git a/src/plugins/editorconfig/libeditorconfig/utarray.h 
b/src/plugins/editorconfig/libeditorconfig/utarray.h
new file mode 100644
index 000000000..e4fdf77e4
--- /dev/null
+++ b/src/plugins/editorconfig/libeditorconfig/utarray.h
@@ -0,0 +1,232 @@
+/*
+Copyright 2008-2014, Troy D. Hanson   http://troydhanson.github.com/uthash/
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
+OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* a dynamic array implementation using macros 
+ */
+#ifndef UTARRAY_H
+#define UTARRAY_H
+
+#define UTARRAY_VERSION 1.9.9
+
+#ifdef __GNUC__
+#define _UNUSED_ __attribute__ ((__unused__)) 
+#else
+#define _UNUSED_ 
+#endif
+
+#include <stddef.h>  /* size_t */
+#include <string.h>  /* memset, etc */
+#include <stdlib.h>  /* exit */
+
+#define oom() exit(-1)
+
+typedef void (ctor_f)(void *dst, const void *src);
+typedef void (dtor_f)(void *elt);
+typedef void (init_f)(void *elt);
+typedef struct {
+    size_t sz;
+    init_f *init;
+    ctor_f *copy;
+    dtor_f *dtor;
+} UT_icd;
+
+typedef struct {
+    unsigned i,n;/* i: index of next available slot, n: num slots */
+    UT_icd icd;  /* initializer, copy and destructor functions */
+    char *d;     /* n slots of size icd->sz*/
+} UT_array;
+
+#define utarray_init(a,_icd) do {                                             \
+  memset(a,0,sizeof(UT_array));                                               \
+  (a)->icd=*_icd;                                                             \
+} while(0)
+
+#define utarray_done(a) do {                                                  \
+  if ((a)->n) {                                                               \
+    if ((a)->icd.dtor) {                                                      \
+      size_t _ut_i;                                                           \
+      for(_ut_i=0; _ut_i < (a)->i; _ut_i++) {                                 \
+        (a)->icd.dtor(utarray_eltptr(a,_ut_i));                               \
+      }                                                                       \
+    }                                                                         \
+    free((a)->d);                                                             \
+  }                                                                           \
+  (a)->n=0;                                                                   \
+} while(0)
+
+#define utarray_new(a,_icd) do {                                              \
+  a=(UT_array*)malloc(sizeof(UT_array));                                      \
+  utarray_init(a,_icd);                                                       \
+} while(0)
+
+#define utarray_free(a) do {                                                  \
+  utarray_done(a);                                                            \
+  free(a);                                                                    \
+} while(0)
+
+#define utarray_reserve(a,by) do {                                            \
+  if (((a)->i+by) > ((a)->n)) {                                               \
+    while(((a)->i+by) > ((a)->n)) { (a)->n = ((a)->n ? (2*(a)->n) : 8); }     \
+    if ( ((a)->d=(char*)realloc((a)->d, (a)->n*(a)->icd.sz)) == NULL) oom();  \
+  }                                                                           \
+} while(0)
+
+#define utarray_push_back(a,p) do {                                           \
+  utarray_reserve(a,1);                                                       \
+  if ((a)->icd.copy) { (a)->icd.copy( _utarray_eltptr(a,(a)->i++), p); }      \
+  else { memcpy(_utarray_eltptr(a,(a)->i++), p, (a)->icd.sz); };              \
+} while(0)
+
+#define utarray_pop_back(a) do {                                              \
+  if ((a)->icd.dtor) { (a)->icd.dtor( _utarray_eltptr(a,--((a)->i))); }       \
+  else { (a)->i--; }                                                          \
+} while(0)
+
+#define utarray_extend_back(a) do {                                           \
+  utarray_reserve(a,1);                                                       \
+  if ((a)->icd.init) { (a)->icd.init(_utarray_eltptr(a,(a)->i)); }            \
+  else { memset(_utarray_eltptr(a,(a)->i),0,(a)->icd.sz); }                   \
+  (a)->i++;                                                                   \
+} while(0)
+
+#define utarray_len(a) ((a)->i)
+
+#define utarray_eltptr(a,j) (((j) < (a)->i) ? _utarray_eltptr(a,j) : NULL)
+#define _utarray_eltptr(a,j) ((char*)((a)->d + ((a)->icd.sz*(j) )))
+
+#define utarray_insert(a,p,j) do {                                            \
+  if (j > (a)->i) utarray_resize(a,j);                                        \
+  utarray_reserve(a,1);                                                       \
+  if ((j) < (a)->i) {                                                         \
+    memmove( _utarray_eltptr(a,(j)+1), _utarray_eltptr(a,j),                  \
+             ((a)->i - (j))*((a)->icd.sz));                                   \
+  }                                                                           \
+  if ((a)->icd.copy) { (a)->icd.copy( _utarray_eltptr(a,j), p); }             \
+  else { memcpy(_utarray_eltptr(a,j), p, (a)->icd.sz); };                     \
+  (a)->i++;                                                                   \
+} while(0)
+
+#define utarray_inserta(a,w,j) do {                                           \
+  if (utarray_len(w) == 0) break;                                             \
+  if (j > (a)->i) utarray_resize(a,j);                                        \
+  utarray_reserve(a,utarray_len(w));                                          \
+  if ((j) < (a)->i) {                                                         \
+    memmove(_utarray_eltptr(a,(j)+utarray_len(w)),                            \
+            _utarray_eltptr(a,j),                                             \
+            ((a)->i - (j))*((a)->icd.sz));                                    \
+  }                                                                           \
+  if ((a)->icd.copy) {                                                        \
+    size_t _ut_i;                                                             \
+    for(_ut_i=0;_ut_i<(w)->i;_ut_i++) {                                       \
+      (a)->icd.copy(_utarray_eltptr(a,j+_ut_i), _utarray_eltptr(w,_ut_i));    \
+    }                                                                         \
+  } else {                                                                    \
+    memcpy(_utarray_eltptr(a,j), _utarray_eltptr(w,0),                        \
+           utarray_len(w)*((a)->icd.sz));                                     \
+  }                                                                           \
+  (a)->i += utarray_len(w);                                                   \
+} while(0)
+
+#define utarray_resize(dst,num) do {                                          \
+  size_t _ut_i;                                                               \
+  if (dst->i > (size_t)(num)) {                                               \
+    if ((dst)->icd.dtor) {                                                    \
+      for(_ut_i=num; _ut_i < dst->i; _ut_i++) {                               \
+        (dst)->icd.dtor(utarray_eltptr(dst,_ut_i));                           \
+      }                                                                       \
+    }                                                                         \
+  } else if (dst->i < (size_t)(num)) {                                        \
+    utarray_reserve(dst,num-dst->i);                                          \
+    if ((dst)->icd.init) {                                                    \
+      for(_ut_i=dst->i; _ut_i < num; _ut_i++) {                               \
+        (dst)->icd.init(utarray_eltptr(dst,_ut_i));                           \
+      }                                                                       \
+    } else {                                                                  \
+      memset(_utarray_eltptr(dst,dst->i),0,(dst)->icd.sz*(num-dst->i));       \
+    }                                                                         \
+  }                                                                           \
+  dst->i = num;                                                               \
+} while(0)
+
+#define utarray_concat(dst,src) do {                                          \
+  utarray_inserta((dst),(src),utarray_len(dst));                              \
+} while(0)
+
+#define utarray_erase(a,pos,len) do {                                         \
+  if ((a)->icd.dtor) {                                                        \
+    size_t _ut_i;                                                             \
+    for(_ut_i=0; _ut_i < len; _ut_i++) {                                      \
+      (a)->icd.dtor(utarray_eltptr((a),pos+_ut_i));                           \
+    }                                                                         \
+  }                                                                           \
+  if ((a)->i > (pos+len)) {                                                   \
+    memmove( _utarray_eltptr((a),pos), _utarray_eltptr((a),pos+len),          \
+            (((a)->i)-(pos+len))*((a)->icd.sz));                              \
+  }                                                                           \
+  (a)->i -= (len);                                                            \
+} while(0)
+
+#define utarray_renew(a,u) do {                                               \
+  if (a) utarray_clear(a); \
+  else utarray_new((a),(u));   \
+} while(0) 
+
+#define utarray_clear(a) do {                                                 \
+  if ((a)->i > 0) {                                                           \
+    if ((a)->icd.dtor) {                                                      \
+      size_t _ut_i;                                                           \
+      for(_ut_i=0; _ut_i < (a)->i; _ut_i++) {                                 \
+        (a)->icd.dtor(utarray_eltptr(a,_ut_i));                               \
+      }                                                                       \
+    }                                                                         \
+    (a)->i = 0;                                                               \
+  }                                                                           \
+} while(0)
+
+#define utarray_sort(a,cmp) do {                                              \
+  qsort((a)->d, (a)->i, (a)->icd.sz, cmp);                                    \
+} while(0)
+
+#define utarray_find(a,v,cmp) bsearch((v),(a)->d,(a)->i,(a)->icd.sz,cmp)
+
+#define utarray_front(a) (((a)->i) ? (_utarray_eltptr(a,0)) : NULL)
+#define utarray_next(a,e) (((e)==NULL) ? utarray_front(a) : ((((a)->i) > (utarray_eltidx(a,e)+1)) ? 
_utarray_eltptr(a,utarray_eltidx(a,e)+1) : NULL))
+#define utarray_prev(a,e) (((e)==NULL) ? utarray_back(a) : ((utarray_eltidx(a,e) > 0) ? 
_utarray_eltptr(a,utarray_eltidx(a,e)-1) : NULL))
+#define utarray_back(a) (((a)->i) ? (_utarray_eltptr(a,(a)->i-1)) : NULL)
+#define utarray_eltidx(a,e) (((char*)(e) >= (char*)((a)->d)) ? (((char*)(e) - 
(char*)((a)->d))/(size_t)(a)->icd.sz) : -1)
+
+/* last we pre-define a few icd for common utarrays of ints and strings */
+static void utarray_str_cpy(void *dst, const void *src) {
+  char **_src = (char**)src, **_dst = (char**)dst;
+  *_dst = (*_src == NULL) ? NULL : strdup(*_src);
+}
+static void utarray_str_dtor(void *elt) {
+  char **eltc = (char**)elt;
+  if (*eltc) free(*eltc);
+}
+static const UT_icd ut_str_icd _UNUSED_ = {sizeof(char*),NULL,utarray_str_cpy,utarray_str_dtor};
+static const UT_icd ut_int_icd _UNUSED_ = {sizeof(int),NULL,NULL,NULL};
+static const UT_icd ut_ptr_icd _UNUSED_ = {sizeof(void*),NULL,NULL,NULL};
+
+
+#endif /* UTARRAY_H */
diff --git a/src/plugins/editorconfig/meson.build b/src/plugins/editorconfig/meson.build
new file mode 100644
index 000000000..8cbf269d1
--- /dev/null
+++ b/src/plugins/editorconfig/meson.build
@@ -0,0 +1,20 @@
+if get_option('plugin_editorconfig')
+
+subdir('libeditorconfig')
+
+plugins_sources += files([
+  'editorconfig-glib.c',
+  'editorconfig-plugin.c',
+  'gbp-editorconfig-file-settings.c',
+])
+
+plugin_editorconfig_resources = gnome.compile_resources(
+  'gbp-editorconfig-resources',
+  'editorconfig.gresource.xml',
+  c_name: 'gbp_editorconfig',
+)
+
+plugins_sources += plugin_editorconfig_resources[0]
+plugins_deps += libeditorconfig_dep
+
+endif
diff --git a/src/plugins/emacs/emacs-plugin.c b/src/plugins/emacs/emacs-plugin.c
new file mode 100644
index 000000000..fb60cc225
--- /dev/null
+++ b/src/plugins/emacs/emacs-plugin.c
@@ -0,0 +1,36 @@
+/* emacs-plugin.c
+ *
+ * Copyright 2018-2019 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 "emacs-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+
+#include "gbp-emacs-preferences-addin.h"
+
+_IDE_EXTERN void
+_gbp_emacs_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_EMACS_PREFERENCES_ADDIN);
+}
diff --git a/src/plugins/emacs/emacs.gresource.xml b/src/plugins/emacs/emacs.gresource.xml
new file mode 100644
index 000000000..d269de173
--- /dev/null
+++ b/src/plugins/emacs/emacs.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/emacs">
+    <file>emacs.plugin</file>
+    <file>keybindings/emacs.css</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/emacs/emacs.plugin b/src/plugins/emacs/emacs.plugin
new file mode 100644
index 000000000..9e9a8147f
--- /dev/null
+++ b/src/plugins/emacs/emacs.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Christian Hergert <chergert redhat com>
+Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Description=Emulation of various Emacs features
+Embedded=_gbp_emacs_register_types
+Hidden=true
+Module=emacs
+Name=Emacs Emulation
diff --git a/src/plugins/emacs/gbp-emacs-preferences-addin.c b/src/plugins/emacs/gbp-emacs-preferences-addin.c
new file mode 100644
index 000000000..ac2222525
--- /dev/null
+++ b/src/plugins/emacs/gbp-emacs-preferences-addin.c
@@ -0,0 +1,89 @@
+/* gbp-emacs-preferences-addin.c
+ *
+ * Copyright 2018-2019 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-emacs-preferences-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+
+#include "gbp-emacs-preferences-addin.h"
+
+struct _GbpEmacsPreferencesAddin
+{
+  GObject parent_instance;
+  guint   keybinding_id;
+};
+
+static void
+gbp_emacs_preferences_addin_load (IdePreferencesAddin *addin,
+                                  DzlPreferences      *preferences)
+{
+  GbpEmacsPreferencesAddin *self = (GbpEmacsPreferencesAddin *)addin;
+
+  g_assert (GBP_IS_EMACS_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  self->keybinding_id = dzl_preferences_add_radio (preferences,
+                                                   "keyboard",
+                                                   "mode",
+                                                   "org.gnome.builder.editor",
+                                                   "keybindings",
+                                                   NULL,
+                                                   "\"emacs\"",
+                                                   _("Emacs"),
+                                                   _("Emulates the Emacs text editor"),
+                                                   NULL,
+                                                   10);
+}
+
+static void
+gbp_emacs_preferences_addin_unload (IdePreferencesAddin *addin,
+                                    DzlPreferences      *preferences)
+{
+  GbpEmacsPreferencesAddin *self = (GbpEmacsPreferencesAddin *)addin;
+
+  g_assert (GBP_IS_EMACS_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  dzl_preferences_remove_id (preferences, self->keybinding_id);
+}
+
+static void
+preferences_addin_iface_init (IdePreferencesAddinInterface *iface)
+{
+  iface->load = gbp_emacs_preferences_addin_load;
+  iface->unload = gbp_emacs_preferences_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpEmacsPreferencesAddin, gbp_emacs_preferences_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_PREFERENCES_ADDIN,
+                                                preferences_addin_iface_init))
+
+static void
+gbp_emacs_preferences_addin_class_init (GbpEmacsPreferencesAddinClass *klass)
+{
+}
+
+static void
+gbp_emacs_preferences_addin_init (GbpEmacsPreferencesAddin *self)
+{
+}
diff --git a/src/plugins/emacs/gbp-emacs-preferences-addin.h b/src/plugins/emacs/gbp-emacs-preferences-addin.h
new file mode 100644
index 000000000..5fe2dca51
--- /dev/null
+++ b/src/plugins/emacs/gbp-emacs-preferences-addin.h
@@ -0,0 +1,31 @@
+/* gbp-emacs-preferences-addin.h
+ *
+ * Copyright 2018-2019 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_EMACS_PREFERENCES_ADDIN (gbp_emacs_preferences_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpEmacsPreferencesAddin, gbp_emacs_preferences_addin, GBP, EMACS_PREFERENCES_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/emacs/keybindings/emacs.css b/src/plugins/emacs/keybindings/emacs.css
new file mode 100644
index 000000000..f5028fccb
--- /dev/null
+++ b/src/plugins/emacs/keybindings/emacs.css
@@ -0,0 +1,232 @@
+@import url("resource:///org/gnome/builder/keybindings/shared.css");
+
+@binding-set builder-emacs-text-entry
+{
+  bind "<ctrl>b" { "move-cursor" (logical-positions, -1, 0) };
+  bind "<shift><ctrl>b" { "move-cursor" (logical-positions, -1, 1) };
+  bind "<ctrl>f" { "move-cursor" (logical-positions, 1, 0) };
+  bind "<shift><ctrl>f" { "move-cursor" (logical-positions, 1, 1) };
+
+  bind "<alt>b" { "move-cursor" (words, -1, 0) };
+  bind "<shift><alt>b" { "move-cursor" (words, -1, 1) };
+  bind "<alt>f" { "move-cursor" (words, 1, 0) };
+  bind "<shift><alt>f" { "move-cursor" (words, 1, 1) };
+
+  bind "<ctrl>a" { "move-cursor" (paragraph-ends, -1, 0) };
+  bind "<shift><ctrl>a" { "move-cursor" (paragraph-ends, -1, 1) };
+  bind "<ctrl>e" { "move-cursor" (paragraph-ends, 1, 0) };
+  bind "<shift><ctrl>e" { "move-cursor" (paragraph-ends, 1, 1) };
+
+  bind "<ctrl>w" { "cut-clipboard" () };
+  bind "<alt>w" { "copy-clipboard" () };
+  bind "<ctrl>y" { "paste-clipboard" () };
+
+  bind "<ctrl>d" { "delete-from-cursor" (chars, 1) };
+  bind "<alt>d" { "delete-from-cursor" (word-ends, 1) };
+  bind "<ctrl>k" { "delete-from-cursor" (paragraph-ends, 1) };
+  bind "<alt>backslash" { "delete-from-cursor" (whitespace, 1) };
+
+  bind "<alt>space" { "delete-from-cursor" (whitespace, 1)
+                      "insert-at-cursor" (" ") };
+  bind "<alt>KP_Space" { "delete-from-cursor" (whitespace, 1)
+                         "insert-at-cursor" (" ")  };
+}
+
+/*
+ * Bindings for GtkTextView
+ */
+@binding-set builder-emacs-text-view
+{
+  bind "<ctrl>p" { "move-cursor" (display-lines, -1, 0) };
+  bind "<shift><ctrl>p" { "move-cursor" (display-lines, -1, 1) };
+  bind "<ctrl>n" { "move-cursor" (display-lines, 1, 0) };
+  bind "<shift><ctrl>n" { "move-cursor" (display-lines, 1, 1) };
+
+  bind "<ctrl>space" { "set-anchor" () };
+  bind "<ctrl>KP_Space" { "set-anchor" () };
+}
+
+@binding-set builder-emacs-source-view
+{
+  bind "Escape" { "clear-search" ()
+                  "clear-modifier" ()
+                  "clear-selection" ()
+                  "clear-count" ()
+                  "clear-snippets" ()
+                  "hide-completion" ()
+                  "remove-cursors" () };
+  bind "<ctrl><shift>e" { "add-cursor" (column) };
+  bind "<ctrl><shift>d" { "add-cursor" (match) };
+  bind "<ctrl>x" { "set-mode" ("emacs-x", transient) };
+  bind "<ctrl>c" { "set-mode" ("emacs-c", transient) };
+  bind "<ctrl>underscore" { "clear-count" ()
+                            "clear-selection" ()
+                            "remove-cursors" ()
+                            "undo" () };
+  bind "<alt>x" { "action" ("win", "show-command-bar", "") };
+  bind "<ctrl>r" { "action" ("editor-page", "find", "") };
+  bind "<ctrl>s" { "action" ("editor-page", "find", "") };
+  bind "<alt>dollar" { "action" ("spellcheck", "spellcheck", "") };
+  bind "<alt>period" { "goto-definition" () };
+  bind "<alt>n" { "move-error" (down) };
+  bind "<alt>p" { "move-error" (up) };
+  bind "<ctrl>j" { "action" ("grid", "focus-neighbor", "3") };
+  bind "<shift><ctrl>j" { "action" ("frame", "split-page", "''") };
+  bind "F2" { "clear-selection" ()
+              "movement" (previous-word-end, 0, 1, 1)
+              "movement" (next-word-start, 0, 1, 0)
+              "movement" (next-word-end, 1, 0, 1)
+              "request-documentation" ()
+              "clear-count" ()
+              "clear-selection" () };
+
+  bind "<ctrl>minus" { "decrease-font-size" () };
+  bind "<ctrl>plus" { "increase-font-size" () };
+  bind "<ctrl>equal" { "increase-font-size" () };
+  bind "<ctrl>0" { "reset-font-size" () };
+
+  bind "<ctrl>Right" { "movement" (next-sub-word-start, 0, 0, 0) };
+  bind "<ctrl>Left" { "movement" (previous-sub-word-start, 0, 0, 0) };
+  bind "<ctrl><shift>Right" { "movement" (next-sub-word-start, 1, 0, 0) };
+  bind "<ctrl><shift>Left" { "movement" (previous-sub-word-start, 1, 0, 0) };
+
+  /* allow entering raw code */
+  bind "<ctrl>q" { "capture-modifier" ()
+                   "insert-modifier" (0)
+                   "clear-modifier" () };
+
+  /* swap between header/source */
+  bind "<alt>o" { "action" ("win", "find-other-file", "") };
+
+  /* cycle "tabs" */
+  bind "<ctrl><alt>Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl><alt>KP_Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl><alt>Page_Down" { "action" ("frame", "next-page", "") };
+  bind "<ctrl><alt>KP_Page_Down" { "action" ("frame", "next-page", "") };
+
+  bind "<alt>0" { "append-to-count" (0) };
+  bind "<alt>1" { "append-to-count" (1) };
+  bind "<alt>2" { "append-to-count" (2) };
+  bind "<alt>3" { "append-to-count" (3) };
+  bind "<alt>4" { "append-to-count" (4) };
+  bind "<alt>5" { "append-to-count" (5) };
+  bind "<alt>6" { "append-to-count" (6) };
+  bind "<alt>7" { "append-to-count" (7) };
+  bind "<alt>8" { "append-to-count" (8) };
+  bind "<alt>9" { "append-to-count" (9) };
+
+  /* Add back emoji */
+  bind "<ctrl>semicolon" { "insert-emoji" () };
+
+  bind "<ctrl>m" { "insert-at-cursor" ("\n") };
+}
+
+@binding-set builder-emacs-source-view-has-indenter
+{
+  bind "Tab" { "reindent" () };
+}
+
+@binding-set builder-emacs-source-view-x
+{
+  bind "<ctrl>c" { "action" ("app", "quit", "") };
+  bind "0" { "action" ("frame", "close-page", "") };
+  bind "k" { "action" ("frame", "close-page", "") };
+  bind "<ctrl>f" { "action" ("win", "open-with-dialog", "") };
+  bind "<ctrl>s" { "action" ("editor-page", "save", "") };
+  bind "s" { "action" ("win", "save-all", "") };
+  bind "<ctrl>b" { "action" ("frame", "show-list", "") };
+  bind "<ctrl>w" { "action" ("editor-page", "save-as", "") };
+  bind "u" { "clear-count" ()
+             "clear-selection" ()
+             "remove-cursors" ()
+             "redo" () };
+  bind "2" { "action" ("frame", "split-page", "''") };
+  bind "3" { "action" ("frame", "open-in-new-frame", "''") };
+  bind "o" { "action" ("grid", "focus-neighbor", "0") };
+  bind "grave" { "move-error" (down) };
+  bind "h" { "select-all" (1) };
+  bind "<ctrl>space" { "action" ("history", "move-previous-edit", "") };
+}
+
+@binding-set builder-emacs-source-view-c
+{
+  bind "<ctrl>f" { "format-selection" () };
+}
+
+/*
+ * Bindings for GtkTreeView
+ */
+@binding-set builder-emacs-tree-view
+{
+  bind "<ctrl>s" { "start-interactive-search" () };
+  bind "<ctrl>f" { "move-cursor" (logical-positions, 1) };
+  bind "<ctrl>b" { "move-cursor" (logical-positions, -1) };
+}
+
+@binding-set builder-emacs-list-box
+{
+  bind "<ctrl>f" { "move-cursor" (display-lines, 1) };
+  bind "<ctrl>b" { "move-cursor" (display-lines, -1) };
+}
+
+@binding-set builder-emacs-editor-search
+{
+  bind "<ctrl>r" { "action" ("frame", "previous-search-result", "") };
+  bind "<ctrl>s" { "action" ("frame", "next-search-result", "") };
+}
+
+frame.gb-search-frame entry {
+  -gtk-key-bindings: builder-emacs-editor-search,
+                     builder-emacs-text-entry;
+}
+
+entry {
+  -gtk-key-bindings: builder-emacs-text-entry;
+}
+
+textview {
+  -gtk-key-bindings: builder-emacs-text-entry, builder-emacs-text-view;
+}
+
+.sourceview,
+idesourceviewmode.default
+{
+  -IdeSourceViewMode-repeat-insert-with-count: true;
+
+  -gtk-key-bindings: builder-emacs-text-entry, builder-emacs-source-view, builder-emacs-text-view;
+}
+
+.sourceview,
+idesourceviewmode.default.has-indenter
+{
+  -IdeSourceViewMode-repeat-insert-with-count: true;
+
+  -gtk-key-bindings: builder-emacs-text-entry,
+                     builder-emacs-source-view-has-indenter,
+                     builder-emacs-source-view,
+                     builder-emacs-text-view;
+}
+
+idesourceviewmode.emacs-x {
+  -IdeSourceViewMode-display-name: "C-x";
+
+  -gtk-key-bindings: builder-emacs-source-view-x;
+}
+
+idesourceviewmode.emacs-c {
+  -IdeSourceViewMode-display-name: "C-c";
+
+  -gtk-key-bindings: builder-emacs-source-view-c;
+}
+
+treeview {
+  -gtk-key-bindings: builder-emacs-tree-view;
+}
+
+listbox {
+  -gtk-key-bindings: builder-emacs-list-box;
+}
+
+treeview.project-tree {
+  -gtk-key-bindings: builder-emacs-tree-view;
+}
diff --git a/src/plugins/emacs/meson.build b/src/plugins/emacs/meson.build
new file mode 100644
index 000000000..a568a5666
--- /dev/null
+++ b/src/plugins/emacs/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'emacs-plugin.c',
+  'gbp-emacs-preferences-addin.c',
+])
+
+plugin_emacs_resources = gnome.compile_resources(
+  'emacs-resources',
+  'emacs.gresource.xml',
+  c_name: 'gbp_emacs',
+)
+
+plugins_sources += plugin_emacs_resources[0]
diff --git a/src/plugins/eslint/eslint.plugin b/src/plugins/eslint/eslint.plugin
index 13730315c..80e13c99b 100644
--- a/src/plugins/eslint/eslint.plugin
+++ b/src/plugins/eslint/eslint.plugin
@@ -1,9 +1,11 @@
 [Plugin]
-Module=eslint_plugin
-Name=eslint
-Loader=python3
-Description=Provides javascript lints
 Authors=Georg Vienna <georg vienna himbarsoft com>
 Copyright=Copyright © 2017 Georg Vienna <georg vienna himbarsoft com>
-X-Diagnostic-Provider-Languages=js
+Description=Provides javascript lints
+Loader=python3
+Hidden=true
+Module=eslint_plugin
+Name=eslint
 X-Diagnostic-Provider-Languages-Priority=100
+X-Diagnostic-Provider-Languages=js
+X-Builder-ABI=@PACKAGE_ABI@
diff --git a/src/plugins/eslint/eslint_plugin.py b/src/plugins/eslint/eslint_plugin.py
index 91e6075b6..847c35f1e 100644
--- a/src/plugins/eslint/eslint_plugin.py
+++ b/src/plugins/eslint/eslint_plugin.py
@@ -24,15 +24,11 @@ import gi
 import json
 import threading
 
-gi.require_version('Ide', '1.0')
-
-from gi.repository import (
-    GLib,
-    GObject,
-    Gio,
-    Gtk,
-    Ide,
-)
+from gi.repository import GLib
+from gi.repository import GObject
+from gi.repository import Gio
+from gi.repository import Gtk
+from gi.repository import Ide
 
 _ = Ide.gettext
 
@@ -52,24 +48,34 @@ class ESLintDiagnosticProvider(Ide.Object, Ide.DiagnosticProvider):
         else:
             return 'eslint'  # Just rely on PATH
 
-    def do_diagnose_async(self, file, buffer, cancellable, callback, user_data):
-        self.diagnostics_list = []
-        task = Gio.Task.new(self, cancellable, callback)
-        task.diagnostics_list = []
-
+    def create_launcher(self):
         context = self.get_context()
-        unsaved_file = context.get_unsaved_files().get_unsaved_file(file.get_file())
-        pipeline = self.get_context().get_build_manager().get_pipeline()
-        srcdir = pipeline.get_srcdir()
-        runtime = pipeline.get_configuration().get_runtime()
-        launcher = runtime.create_launcher()
+        srcdir = context.ref_workdir().get_path()
+        launcher = None
+
+        if context.has_project():
+            build_manager = Ide.BuildManager.from_context(context)
+            pipeline = build_manager.get_pipeline()
+            if pipeline is not None:
+                srcdir = pipeline.get_srcdir()
+            runtime = pipeline.get_configuration().get_runtime()
+            launcher = runtime.create_launcher()
+
+        if launcher is None:
+            launcher = Ide.SubprocessLauncher.new(0)
+
         launcher.set_flags(Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE)
         launcher.set_cwd(srcdir)
 
-        if unsaved_file:
-            file_content = unsaved_file.get_content().get_data().decode('utf-8')
-        else:
-            file_content = None
+        return launcher
+
+    def do_diagnose_async(self, file, file_content, lang_id, cancellable, callback, user_data):
+        self.diagnostics_list = []
+        task = Gio.Task.new(self, cancellable, callback)
+        task.diagnostics_list = []
+
+        launcher = self.create_launcher()
+        srcdir = launcher.get_cwd()
 
         threading.Thread(target=self.execute, args=(task, launcher, srcdir, file, file_content),
                          name='eslint-thread').start()
@@ -87,7 +93,8 @@ class ESLintDiagnosticProvider(Ide.Object, Ide.DiagnosticProvider):
                 launcher.push_argv(file.get_path())
 
             sub_process = launcher.spawn()
-            success, stdout, stderr = sub_process.communicate_utf8(file_content, None)
+            stdin = file_content.get_data().decode('UTF-8')
+            success, stdout, stderr = sub_process.communicate_utf8(stdin, None)
 
             if not success:
                 task.return_boolean(False)
@@ -100,17 +107,17 @@ class ESLintDiagnosticProvider(Ide.Object, Ide.DiagnosticProvider):
                         continue
                     start_line = max(message['line'] - 1, 0)
                     start_col = max(message['column'] - 1, 0)
-                    start = Ide.SourceLocation.new(file, start_line, start_col, 0)
+                    start = Ide.Location.new(file, start_line, start_col)
                     end = None
                     if 'endLine' in message:
                         end_line = max(message['endLine'] - 1, 0)
                         end_col = max(message['endColumn'] - 1, 0)
-                        end = Ide.SourceLocation.new(file, end_line, end_col, 0)
+                        end = Ide.Location.new(file, end_line, end_col)
 
                     severity = SEVERITY_MAP[message['severity']]
                     diagnostic = Ide.Diagnostic.new(severity, message['message'], start)
                     if end is not None:
-                        range_ = Ide.SourceRange.new(start, end)
+                        range_ = Ide.Range.new(start, end)
                         diagnostic.add_range(range_)
                         # if 'fix' in message:
                         # Fixes often come without end* information so we
@@ -129,7 +136,10 @@ class ESLintDiagnosticProvider(Ide.Object, Ide.DiagnosticProvider):
 
     def do_diagnose_finish(self, result):
         if result.propagate_boolean():
-            return Ide.Diagnostics.new(result.diagnostics_list)
+            diagnostics = Ide.Diagnostics()
+            for diag in result.diagnostics_list:
+                diagnostics.add(diag)
+            return diagnostics
 
 
 class ESLintPreferencesAddin(GObject.Object, Ide.PreferencesAddin):
diff --git a/src/plugins/eslint/meson.build b/src/plugins/eslint/meson.build
index d13bcb9c5..754eb171c 100644
--- a/src/plugins/eslint/meson.build
+++ b/src/plugins/eslint/meson.build
@@ -1,4 +1,4 @@
-if get_option('with_eslint')
+if get_option('plugin_eslint')
 
 install_data('eslint_plugin.py', install_dir: plugindir)
 
@@ -8,7 +8,7 @@ install_data('org.gnome.builder.plugins.eslint.gschema.xml',
 configure_file(
           input: 'eslint.plugin',
          output: 'eslint.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/file-search/file-search-plugin.c b/src/plugins/file-search/file-search-plugin.c
new file mode 100644
index 000000000..52d25f9b8
--- /dev/null
+++ b/src/plugins/file-search/file-search-plugin.c
@@ -0,0 +1,36 @@
+/* file-search-plugin.c
+ *
+ * Copyright 2015-2019 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 "file-search-plugin"
+
+#include "config.h"
+
+#include <libide-search.h>
+#include <libpeas/peas.h>
+
+#include "gbp-file-search-provider.h"
+
+void
+_gbp_file_search_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_SEARCH_PROVIDER,
+                                              GBP_TYPE_FILE_SEARCH_PROVIDER);
+}
diff --git a/src/plugins/file-search/file-search.gresource.xml 
b/src/plugins/file-search/file-search.gresource.xml
index 344062a83..a00b8c87c 100644
--- a/src/plugins/file-search/file-search.gresource.xml
+++ b/src/plugins/file-search/file-search.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/file-search">
     <file>file-search.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/file-search/file-search.plugin b/src/plugins/file-search/file-search.plugin
index b356c4313..b4721368e 100644
--- a/src/plugins/file-search/file-search.plugin
+++ b/src/plugins/file-search/file-search.plugin
@@ -1,8 +1,8 @@
 [Plugin]
-Module=file-search
-Name=File Search
-Description=Search for files in the global search bar
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015-2017 Christian Hergert
 Builtin=true
-Embedded=gb_file_search_register_types
+Copyright=Copyright © 2015-2018 Christian Hergert
+Description=Search for files in the global search bar
+Embedded=_gbp_file_search_register_types
+Module=file-search
+Name=File Search
diff --git a/src/plugins/file-search/gbp-file-search-index.c b/src/plugins/file-search/gbp-file-search-index.c
new file mode 100644
index 000000000..b2059bdb2
--- /dev/null
+++ b/src/plugins/file-search/gbp-file-search-index.c
@@ -0,0 +1,420 @@
+/* gbp-file-search-index.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-file-search-index"
+
+#include <glib/gi18n.h>
+#include <libide-search.h>
+#include <libide-code.h>
+#include <libide-vcs.h>
+#include <string.h>
+
+#include "gbp-file-search-index.h"
+#include "gbp-file-search-result.h"
+
+struct _GbpFileSearchIndex
+{
+  IdeObject             parent_instance;
+
+  GFile                *root_directory;
+  DzlFuzzyMutableIndex *fuzzy;
+};
+
+G_DEFINE_TYPE (GbpFileSearchIndex, gbp_file_search_index, IDE_TYPE_OBJECT)
+
+enum {
+  PROP_0,
+  PROP_ROOT_DIRECTORY,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+gbp_file_search_index_set_root_directory (GbpFileSearchIndex *self,
+                                         GFile             *root_directory)
+{
+  g_return_if_fail (GBP_IS_FILE_SEARCH_INDEX (self));
+  g_return_if_fail (!root_directory || G_IS_FILE (root_directory));
+
+  if (g_set_object (&self->root_directory, root_directory))
+    {
+      g_clear_pointer (&self->fuzzy, dzl_fuzzy_mutable_index_unref);
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_ROOT_DIRECTORY]);
+    }
+}
+
+static void
+gbp_file_search_index_finalize (GObject *object)
+{
+  GbpFileSearchIndex *self = (GbpFileSearchIndex *)object;
+
+  g_clear_object (&self->root_directory);
+  g_clear_pointer (&self->fuzzy, dzl_fuzzy_mutable_index_unref);
+
+  G_OBJECT_CLASS (gbp_file_search_index_parent_class)->finalize (object);
+}
+
+static void
+gbp_file_search_index_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  GbpFileSearchIndex *self = GBP_FILE_SEARCH_INDEX (object);
+
+  switch (prop_id)
+    {
+    case PROP_ROOT_DIRECTORY:
+      g_value_set_object (value, self->root_directory);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_file_search_index_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  GbpFileSearchIndex *self = GBP_FILE_SEARCH_INDEX (object);
+
+  switch (prop_id)
+    {
+    case PROP_ROOT_DIRECTORY:
+      gbp_file_search_index_set_root_directory (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_file_search_index_class_init (GbpFileSearchIndexClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_file_search_index_finalize;
+  object_class->get_property = gbp_file_search_index_get_property;
+  object_class->set_property = gbp_file_search_index_set_property;
+
+  properties [PROP_ROOT_DIRECTORY] =
+    g_param_spec_object ("root-directory",
+                         "Root Directory",
+                         "Root Directory",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+gbp_file_search_index_init (GbpFileSearchIndex *self)
+{
+}
+
+static void
+populate_from_dir (DzlFuzzyMutableIndex *fuzzy,
+                   IdeVcs               *vcs,
+                   const gchar          *relpath,
+                   GFile                *directory,
+                   GCancellable         *cancellable)
+{
+  GFileEnumerator *enumerator;
+  GPtrArray *children = NULL;
+  gpointer file_info_ptr;
+
+  g_assert (fuzzy != NULL);
+  g_assert (G_IS_FILE (directory));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (ide_vcs_is_ignored (vcs, directory, NULL))
+    return;
+
+  enumerator = g_file_enumerate_children (directory,
+                                          G_FILE_ATTRIBUTE_STANDARD_IS_SYMLINK","
+                                          G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME","
+                                          G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                                          G_FILE_QUERY_INFO_NONE,
+                                          cancellable,
+                                          NULL);
+
+  if (enumerator == NULL)
+    return;
+
+  while ((file_info_ptr = g_file_enumerator_next_file (enumerator, cancellable, NULL)))
+    {
+      g_autoptr(GFileInfo) file_info = file_info_ptr;
+      g_autofree gchar *path = NULL;
+      g_autoptr(GFile) file = NULL;
+      GFileType file_type;
+      const gchar *name;
+
+      if (g_file_info_get_is_symlink (file_info))
+        continue;
+
+      name = g_file_info_get_display_name (file_info);
+      file = g_file_get_child (directory, name);
+
+      file_type = g_file_info_get_file_type (file_info);
+
+      if (file_type == G_FILE_TYPE_DIRECTORY)
+        {
+          if (children == NULL)
+            children = g_ptr_array_new_with_free_func (g_object_unref);
+          g_ptr_array_add (children, g_object_ref (file));
+          continue;
+        }
+
+      /* We only want to index regular files, and ignore symlinks.  If the
+       * symlink points to something else in-tree, we'll index it in the
+       * rightful place.
+       */
+      if (file_type != G_FILE_TYPE_REGULAR)
+        continue;
+
+      if (ide_vcs_is_ignored (vcs, file, NULL))
+        continue;
+
+      if (relpath != NULL)
+        name = path = g_build_filename (relpath, name, NULL);
+
+      dzl_fuzzy_mutable_index_insert (fuzzy, name, NULL);
+    }
+
+  g_clear_object (&enumerator);
+
+  if (children != NULL)
+    {
+      gsize i;
+
+      for (i = 0; i < children->len; i++)
+        {
+          g_autofree gchar *path = NULL;
+          g_autofree gchar *name = NULL;
+          GFile *child;
+
+          child = g_ptr_array_index (children, i);
+          name = g_file_get_basename (child);
+
+          if (relpath != NULL)
+            path = g_build_filename (relpath, name, NULL);
+
+          populate_from_dir (fuzzy, vcs, path ? path : name, child, cancellable);
+        }
+    }
+
+  g_clear_pointer (&children, g_ptr_array_unref);
+}
+
+static void
+gbp_file_search_index_builder (IdeTask      *task,
+                              gpointer      source_object,
+                              gpointer      task_data,
+                              GCancellable *cancellable)
+{
+  GbpFileSearchIndex *self = source_object;
+  g_autoptr(GTimer) timer = NULL;
+  g_autoptr(IdeVcs) vcs = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  GFile *directory = task_data;
+  DzlFuzzyMutableIndex *fuzzy;
+  gdouble elapsed;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_FILE_SEARCH_INDEX (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (G_IS_FILE (directory));
+
+  context = ide_object_ref_context (IDE_OBJECT (self));
+  vcs = ide_vcs_ref_from_context (context);
+
+  timer = g_timer_new ();
+
+  fuzzy = dzl_fuzzy_mutable_index_new (FALSE);
+  dzl_fuzzy_mutable_index_begin_bulk_insert (fuzzy);
+  populate_from_dir (fuzzy, vcs, NULL, directory, cancellable);
+  dzl_fuzzy_mutable_index_end_bulk_insert (fuzzy);
+
+  self->fuzzy = fuzzy;
+
+  g_timer_stop (timer);
+  elapsed = g_timer_elapsed (timer, NULL);
+
+  g_message ("File index built in %lf seconds.", elapsed);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+void
+gbp_file_search_index_build_async (GbpFileSearchIndex  *self,
+                                   GCancellable        *cancellable,
+                                   GAsyncReadyCallback  callback,
+                                   gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (GBP_IS_FILE_SEARCH_INDEX (self));
+  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_file_search_index_build_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  if (self->root_directory == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_FILENAME,
+                                 "Root directory has not been set.");
+      return;
+    }
+
+  ide_task_set_task_data (task, g_object_ref (self->root_directory), g_object_unref);
+  ide_task_run_in_thread (task, gbp_file_search_index_builder);
+}
+
+gboolean
+gbp_file_search_index_build_finish (GbpFileSearchIndex  *self,
+                                   GAsyncResult       *result,
+                                   GError            **error)
+{
+  IdeTask *task = (IdeTask *)result;
+
+  g_return_val_if_fail (GBP_IS_FILE_SEARCH_INDEX (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (task), FALSE);
+
+  return ide_task_propagate_boolean (task, error);
+}
+
+GPtrArray *
+gbp_file_search_index_populate (GbpFileSearchIndex *self,
+                               const gchar       *query,
+                               gsize              max_results)
+{
+  g_auto(IdeSearchReducer) reducer = { 0 };
+  g_autoptr(GString) delimited = NULL;
+  g_autoptr(GArray) ar = NULL;
+  const gchar *iter = query;
+  IdeContext *context;
+  gsize i;
+
+  g_return_val_if_fail (GBP_IS_FILE_SEARCH_INDEX (self), NULL);
+  g_return_val_if_fail (query != NULL, NULL);
+
+  if (self->fuzzy == NULL)
+    return g_ptr_array_new_with_free_func (g_object_unref);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  ide_search_reducer_init (&reducer, max_results);
+
+  delimited = g_string_new (NULL);
+
+  for (; *iter; iter = g_utf8_next_char (iter))
+    {
+      gunichar ch = g_utf8_get_char (iter);
+
+      if (!g_unichar_isspace (ch))
+        g_string_append_unichar (delimited, ch);
+    }
+
+  ar = dzl_fuzzy_mutable_index_match (self->fuzzy, delimited->str, max_results);
+
+  for (i = 0; i < ar->len; i++)
+    {
+      const DzlFuzzyMutableIndexMatch *match = &g_array_index (ar, DzlFuzzyMutableIndexMatch, i);
+
+      if (ide_search_reducer_accepts (&reducer, match->score))
+        {
+          g_autoptr(GbpFileSearchResult) result = NULL;
+          g_autofree gchar *escaped = NULL;
+          g_autofree gchar *markup = NULL;
+          g_autofree gchar *free_me = NULL;
+          g_autofree gchar *free_me_sym = NULL;
+          const gchar *filename = match->key;
+          g_autofree gchar *content_type = NULL;
+          g_autoptr(GIcon) themed_icon = NULL;
+
+          escaped = g_markup_escape_text (match->key, -1);
+          markup = dzl_fuzzy_highlight (escaped, delimited->str, FALSE);
+
+          /*
+           * Try to get a more appropriate icon, but by filename only.
+           * Sniffing would be way too slow here.
+           */
+          if ((content_type = g_content_type_guess (filename, NULL, 0, NULL)))
+            themed_icon = ide_g_content_type_get_symbolic_icon (content_type);
+
+          result = g_object_new (GBP_TYPE_FILE_SEARCH_RESULT,
+                                 "context", context,
+                                 "score", match->score,
+                                 "title", markup,
+                                 "path", filename,
+                                 NULL);
+
+          if (themed_icon != NULL)
+            ide_search_result_set_icon (IDE_SEARCH_RESULT (result), themed_icon);
+
+          ide_search_reducer_take (&reducer, IDE_SEARCH_RESULT (g_steal_pointer (&result)));
+        }
+    }
+
+  return ide_search_reducer_free (&reducer, FALSE);
+}
+
+gboolean
+gbp_file_search_index_contains (GbpFileSearchIndex *self,
+                               const gchar       *relative_path)
+{
+  g_return_val_if_fail (GBP_IS_FILE_SEARCH_INDEX (self), FALSE);
+  g_return_val_if_fail (relative_path != NULL, FALSE);
+  g_return_val_if_fail (self->fuzzy != NULL, FALSE);
+
+  return dzl_fuzzy_mutable_index_contains (self->fuzzy, relative_path);
+}
+
+void
+gbp_file_search_index_insert (GbpFileSearchIndex *self,
+                             const gchar       *relative_path)
+{
+  g_return_if_fail (GBP_IS_FILE_SEARCH_INDEX (self));
+  g_return_if_fail (relative_path != NULL);
+  g_return_if_fail (self->fuzzy != NULL);
+
+  dzl_fuzzy_mutable_index_insert (self->fuzzy, relative_path, NULL);
+}
+
+void
+gbp_file_search_index_remove (GbpFileSearchIndex *self,
+                             const gchar       *relative_path)
+{
+  g_return_if_fail (GBP_IS_FILE_SEARCH_INDEX (self));
+  g_return_if_fail (relative_path != NULL);
+  g_return_if_fail (self->fuzzy != NULL);
+
+  dzl_fuzzy_mutable_index_remove (self->fuzzy, relative_path);
+}
diff --git a/src/plugins/file-search/gbp-file-search-index.h b/src/plugins/file-search/gbp-file-search-index.h
new file mode 100644
index 000000000..feb45ca58
--- /dev/null
+++ b/src/plugins/file-search/gbp-file-search-index.h
@@ -0,0 +1,48 @@
+/* gbp-file-search-index.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-search.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_FILE_SEARCH_INDEX (gbp_file_search_index_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpFileSearchIndex, gbp_file_search_index, GBP, FILE_SEARCH_INDEX, IdeObject)
+
+GPtrArray *gbp_file_search_index_populate     (GbpFileSearchIndex    *self,
+                                              const gchar          *query,
+                                              gsize                 max_results);
+void       gbp_file_search_index_build_async  (GbpFileSearchIndex    *self,
+                                              GCancellable         *cancellable,
+                                              GAsyncReadyCallback   callback,
+                                              gpointer              user_data);
+gboolean   gbp_file_search_index_build_finish (GbpFileSearchIndex    *self,
+                                              GAsyncResult         *result,
+                                              GError              **error);
+gboolean   gbp_file_search_index_contains     (GbpFileSearchIndex    *self,
+                                              const gchar          *relative_path);
+void       gbp_file_search_index_insert       (GbpFileSearchIndex    *self,
+                                              const gchar          *relative_path);
+void       gbp_file_search_index_remove       (GbpFileSearchIndex    *self,
+                                              const gchar          *relative_path);
+
+G_END_DECLS
diff --git a/src/plugins/file-search/gbp-file-search-provider.c 
b/src/plugins/file-search/gbp-file-search-provider.c
new file mode 100644
index 000000000..ad9b3b13a
--- /dev/null
+++ b/src/plugins/file-search/gbp-file-search-provider.c
@@ -0,0 +1,361 @@
+/* gbp-file-search-provider.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-file-search-provider"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-code.h>
+#include <libide-projects.h>
+#include <libide-search.h>
+#include <libide-vcs.h>
+#include <libpeas/peas.h>
+
+#include "gbp-file-search-provider.h"
+#include "gbp-file-search-index.h"
+
+struct _GbpFileSearchProvider
+{
+  IdeObject           parent_instance;
+  GbpFileSearchIndex *index;
+};
+
+static void search_provider_iface_init (IdeSearchProviderInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GbpFileSearchProvider,
+                         gbp_file_search_provider,
+                         IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_SEARCH_PROVIDER, search_provider_iface_init))
+
+static void
+gbp_file_search_provider_search_async (IdeSearchProvider   *provider,
+                                      const gchar         *search_terms,
+                                      guint                max_results,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  GbpFileSearchProvider *self = (GbpFileSearchProvider *)provider;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) results = NULL;
+
+  g_assert (GBP_IS_FILE_SEARCH_PROVIDER (self));
+  g_assert (search_terms != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_file_search_provider_search_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  if (self->index != NULL)
+    results = gbp_file_search_index_populate (self->index, search_terms, max_results);
+  else
+    results = g_ptr_array_new_with_free_func (g_object_unref);
+
+  ide_task_return_pointer (task, g_steal_pointer (&results), (GDestroyNotify)g_ptr_array_unref);
+}
+
+static GPtrArray *
+gbp_file_search_provider_search_finish (IdeSearchProvider  *provider,
+                                       GAsyncResult       *result,
+                                       GError            **error)
+{
+  GPtrArray *ret;
+
+  g_assert (GBP_IS_FILE_SEARCH_PROVIDER (provider));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ret);
+}
+
+static void
+on_buffer_loaded (GbpFileSearchProvider *self,
+                  IdeBuffer            *buffer,
+                  IdeBufferManager     *bufmgr)
+{
+  g_autofree gchar *relative_path = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  IdeVcs *vcs;
+  GFile *file;
+
+  g_assert (GBP_IS_FILE_SEARCH_PROVIDER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+
+  if (self->index == NULL)
+    return;
+
+  file = ide_buffer_get_file (buffer);
+  context = ide_buffer_ref_context (buffer);
+  vcs = ide_vcs_from_context (context);
+  workdir = ide_context_ref_workdir (context);
+  relative_path = g_file_get_relative_path (workdir, file);
+
+  if ((relative_path != NULL) &&
+      !ide_vcs_is_ignored (vcs, file, NULL) &&
+      !gbp_file_search_index_contains (self->index, relative_path))
+    gbp_file_search_index_insert (self->index, relative_path);
+}
+
+static void
+on_file_renamed (GbpFileSearchProvider *self,
+                 GFile                *src_file,
+                 GFile                *dst_file,
+                 IdeProject           *project)
+{
+  g_autofree gchar *old_path = NULL;
+  g_autofree gchar *new_path = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  IdeContext *context;
+
+  g_assert (GBP_IS_FILE_SEARCH_PROVIDER (self));
+  g_assert (G_IS_FILE (src_file));
+  g_assert (G_IS_FILE (dst_file));
+  g_assert (IDE_IS_PROJECT (project));
+  g_assert (GBP_IS_FILE_SEARCH_INDEX (self->index));
+
+  context = ide_object_get_context (IDE_OBJECT (project));
+  workdir = ide_context_ref_workdir (context);
+
+  if (NULL != (old_path = g_file_get_relative_path (workdir, src_file)))
+    gbp_file_search_index_remove (self->index, old_path);
+
+  if (NULL != (new_path = g_file_get_relative_path (workdir, dst_file)))
+    gbp_file_search_index_insert (self->index, new_path);
+}
+
+static void
+on_file_trashed (GbpFileSearchProvider *self,
+                 GFile                *file,
+                 IdeProject           *project)
+{
+  g_autofree gchar *path = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  IdeContext *context;
+
+  g_assert (GBP_IS_FILE_SEARCH_PROVIDER (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (IDE_IS_PROJECT (project));
+  g_assert (GBP_IS_FILE_SEARCH_INDEX (self->index));
+
+  context = ide_object_get_context (IDE_OBJECT (project));
+  workdir = ide_context_ref_workdir (context);
+
+  if (NULL != (path = g_file_get_relative_path (workdir, file)))
+    gbp_file_search_index_remove (self->index, path);
+}
+
+static void
+gbp_file_search_provider_build_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  GbpFileSearchIndex *index = (GbpFileSearchIndex *)object;
+  g_autoptr(GbpFileSearchProvider) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_FILE_SEARCH_INDEX (index));
+  g_assert (GBP_IS_FILE_SEARCH_PROVIDER (self));
+
+  if (!gbp_file_search_index_build_finish (index, result, &error))
+    {
+      g_warning ("%s", error->message);
+      return;
+    }
+
+  g_set_object (&self->index, index);
+}
+
+#if 0
+static void
+gbp_file_search_provider_activate (IdeSearchProvider *provider,
+                                  GtkWidget         *row,
+                                  IdeSearchResult   *result)
+{
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_SEARCH_PROVIDER (provider));
+  g_assert (GTK_IS_WIDGET (row));
+  g_assert (IDE_IS_SEARCH_RESULT (result));
+
+  toplevel = gtk_widget_get_toplevel (row);
+
+  if (IDE_IS_WORKBENCH (toplevel))
+    {
+      g_autofree gchar *path = NULL;
+      g_autoptr(GFile) file = NULL;
+      g_autoptr(GFile) workdir = NULL;
+      IdeContext *context;
+      IdeVcs *vcs;
+
+      context = ide_workbench_get_context (IDE_WORKBENCH (toplevel));
+      vcs = ide_vcs_from_context (context);
+      workdir = ide_context_ref_workdir (context);
+      g_object_get (result, "path", &path, NULL);
+      file = g_file_get_child (workdir, path);
+
+      ide_workbench_open_files_async (IDE_WORKBENCH (toplevel),
+                                      &file,
+                                      1,
+                                      NULL,
+                                      IDE_WORKBENCH_OPEN_FLAGS_NONE,
+                                      NULL,
+                                      NULL,
+                                      NULL);
+    }
+}
+#endif
+
+static void
+gbp_file_search_provider_vcs_changed_cb (GbpFileSearchProvider *self,
+                                         IdeVcs                *vcs)
+{
+  g_autoptr(GbpFileSearchIndex) index = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (GBP_IS_FILE_SEARCH_PROVIDER (self));
+  g_return_if_fail (IDE_IS_VCS (vcs));
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  workdir = ide_context_ref_workdir (context);
+
+  index = g_object_new (GBP_TYPE_FILE_SEARCH_INDEX,
+                        "root-directory", workdir,
+                        NULL);
+
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (index));
+
+  gbp_file_search_index_build_async (index,
+                                     NULL,
+                                     gbp_file_search_provider_build_cb,
+                                     g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_file_search_provider_parent_set (IdeObject *object,
+                                     IdeObject *parent)
+{
+  GbpFileSearchProvider *self = (GbpFileSearchProvider *)object;
+  g_autoptr(GbpFileSearchIndex) index = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  IdeBufferManager *bufmgr;
+  IdeContext *context;
+  IdeProject *project;
+  IdeVcs *vcs;
+
+  g_assert (GBP_IS_FILE_SEARCH_PROVIDER (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (parent == NULL)
+    return;
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+
+  bufmgr = ide_buffer_manager_from_context (context);
+  project = ide_project_from_context (context);
+  vcs = ide_vcs_from_context (context);
+
+  workdir = ide_context_ref_workdir (context);
+
+  g_signal_connect_object (vcs,
+                           "changed",
+                           G_CALLBACK (gbp_file_search_provider_vcs_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (bufmgr,
+                           "buffer-loaded",
+                           G_CALLBACK (on_buffer_loaded),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (project,
+                           "file-renamed",
+                           G_CALLBACK (on_file_renamed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (project,
+                           "file-trashed",
+                           G_CALLBACK (on_file_trashed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  index = g_object_new (GBP_TYPE_FILE_SEARCH_INDEX,
+                        "root-directory", workdir,
+                        NULL);
+
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (index));
+
+  gbp_file_search_index_build_async (index,
+                                     NULL,
+                                     gbp_file_search_provider_build_cb,
+                                     g_object_ref (self));
+}
+
+static void
+gbp_file_search_provider_finalize (GObject *object)
+{
+  GbpFileSearchProvider *self = (GbpFileSearchProvider *)object;
+
+  g_clear_object (&self->index);
+
+  G_OBJECT_CLASS (gbp_file_search_provider_parent_class)->finalize (object);
+}
+
+static void
+gbp_file_search_provider_class_init (GbpFileSearchProviderClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_file_search_provider_finalize;
+
+  i_object_class->parent_set = gbp_file_search_provider_parent_set;
+}
+
+static void
+gbp_file_search_provider_init (GbpFileSearchProvider *self)
+{
+}
+
+static void
+search_provider_iface_init (IdeSearchProviderInterface *iface)
+{
+  iface->search_async = gbp_file_search_provider_search_async;
+  iface->search_finish = gbp_file_search_provider_search_finish;
+}
+
+void
+gbp_file_search_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_SEARCH_PROVIDER,
+                                              GBP_TYPE_FILE_SEARCH_PROVIDER);
+}
diff --git a/src/plugins/file-search/gbp-file-search-provider.h 
b/src/plugins/file-search/gbp-file-search-provider.h
new file mode 100644
index 000000000..95844c3d3
--- /dev/null
+++ b/src/plugins/file-search/gbp-file-search-provider.h
@@ -0,0 +1,31 @@
+/* gbp-file-search-provider.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-search.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_FILE_SEARCH_PROVIDER (gbp_file_search_provider_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpFileSearchProvider, gbp_file_search_provider, GBP, FILE_SEARCH_PROVIDER, IdeObject)
+
+G_END_DECLS
diff --git a/src/plugins/file-search/gbp-file-search-result.c 
b/src/plugins/file-search/gbp-file-search-result.c
new file mode 100644
index 000000000..e57eba9f7
--- /dev/null
+++ b/src/plugins/file-search/gbp-file-search-result.c
@@ -0,0 +1,156 @@
+/* gbp-file-search-result.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-file-search-result"
+
+#include <libide-gui.h>
+
+#include "gbp-file-search-result.h"
+
+struct _GbpFileSearchResult
+{
+  IdeSearchResult  parent_instance;
+
+  IdeContext      *context;
+  gchar           *path;
+};
+
+G_DEFINE_TYPE (GbpFileSearchResult, gbp_file_search_result, IDE_TYPE_SEARCH_RESULT)
+
+enum {
+  PROP_0,
+  PROP_CONTEXT,
+  PROP_PATH,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+static void
+gbp_file_search_result_activate (IdeSearchResult *result,
+                                 GtkWidget       *last_focus)
+{
+  g_autoptr(GFile) workdir = NULL;
+  IdeWorkbench *workbench;
+  IdeContext *context;
+  GFile *file;
+
+  g_assert (GBP_IS_FILE_SEARCH_RESULT (result));
+  g_assert (!last_focus || GTK_IS_WIDGET (last_focus));
+
+  if (!last_focus)
+    return;
+
+  if (!(workbench = ide_widget_get_workbench (last_focus)) ||
+      !(context = ide_workbench_get_context (workbench)) ||
+      !(workdir = ide_context_ref_workdir (context)))
+    return;
+
+  file = g_file_get_child (workdir, GBP_FILE_SEARCH_RESULT (result)->path);
+
+  ide_workbench_open_async (workbench, file, NULL, 0,NULL, NULL, NULL);
+}
+
+static void
+gbp_file_search_result_finalize (GObject *object)
+{
+  GbpFileSearchResult *self = (GbpFileSearchResult *)object;
+
+  g_clear_weak_pointer (&self->context);
+  g_clear_pointer (&self->path, g_free);
+
+  G_OBJECT_CLASS (gbp_file_search_result_parent_class)->finalize (object);
+}
+
+static void
+gbp_file_search_result_get_property (GObject    *object,
+                                    guint       prop_id,
+                                    GValue     *value,
+                                    GParamSpec *pspec)
+{
+  GbpFileSearchResult *self = (GbpFileSearchResult *)object;
+
+  switch (prop_id)
+    {
+    case PROP_PATH:
+      g_value_set_string (value, self->path);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_file_search_result_set_property (GObject      *object,
+                                    guint         prop_id,
+                                    const GValue *value,
+                                    GParamSpec   *pspec)
+{
+  GbpFileSearchResult *self = (GbpFileSearchResult *)object;
+
+  switch (prop_id)
+    {
+    case PROP_CONTEXT:
+      g_set_weak_pointer (&self->context, g_value_get_object (value));
+      break;
+
+    case PROP_PATH:
+      self->path = g_value_dup_string (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_file_search_result_class_init (GbpFileSearchResultClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeSearchResultClass *result_class = IDE_SEARCH_RESULT_CLASS (klass);
+
+  object_class->finalize = gbp_file_search_result_finalize;
+  object_class->get_property = gbp_file_search_result_get_property;
+  object_class->set_property = gbp_file_search_result_set_property;
+
+  result_class->activate = gbp_file_search_result_activate;
+
+  properties [PROP_CONTEXT] =
+    g_param_spec_object ("context",
+                         "Context",
+                         "The context for the result",
+                         IDE_TYPE_CONTEXT,
+                         (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_PATH] =
+    g_param_spec_string ("path",
+                         "Path",
+                         "The relative path to the file.",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+gbp_file_search_result_init (GbpFileSearchResult *self)
+{
+}
diff --git a/src/plugins/file-search/gbp-file-search-result.h 
b/src/plugins/file-search/gbp-file-search-result.h
new file mode 100644
index 000000000..b1ed2152a
--- /dev/null
+++ b/src/plugins/file-search/gbp-file-search-result.h
@@ -0,0 +1,31 @@
+/* gbp-file-search-result.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-search.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_FILE_SEARCH_RESULT (gbp_file_search_result_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpFileSearchResult, gbp_file_search_result, GBP, FILE_SEARCH_RESULT, IdeSearchResult)
+
+G_END_DECLS
diff --git a/src/plugins/file-search/meson.build b/src/plugins/file-search/meson.build
index aa3260400..8ae908f90 100644
--- a/src/plugins/file-search/meson.build
+++ b/src/plugins/file-search/meson.build
@@ -1,21 +1,18 @@
-if get_option('with_file_search')
+if get_option('plugin_file_search')
 
-file_search_resources = gnome.compile_resources(
+plugins_sources += files([
+  'gbp-file-search-provider.c',
+  'gbp-file-search-result.c',
+  'gbp-file-search-index.c',
+  'file-search-plugin.c',
+])
+
+plugin_file_search_resources = gnome.compile_resources(
   'file-search-resources',
   'file-search.gresource.xml',
-  c_name: 'gb_file_search',
+  c_name: 'gbp_file_search',
 )
 
-file_search_sources = [
-  'gb-file-search-provider.c',
-  'gb-file-search-provider.h',
-  'gb-file-search-result.c',
-  'gb-file-search-result.h',
-  'gb-file-search-index.c',
-  'gb-file-search-index.h',
-]
-
-gnome_builder_plugins_sources += files(file_search_sources)
-gnome_builder_plugins_sources += file_search_resources[0]
+plugins_sources += plugin_file_search_resources[0]
 
 endif
diff --git a/src/plugins/find-other-file/find-other-file.plugin 
b/src/plugins/find-other-file/find-other-file.plugin
index ae00a077c..bb9e3c7ad 100644
--- a/src/plugins/find-other-file/find-other-file.plugin
+++ b/src/plugins/find-other-file/find-other-file.plugin
@@ -1,9 +1,12 @@
 [Plugin]
-Module=find_other_file
-Loader=python3
-Name=Find other files
-Description=Allows the user to rotate through other files similarly named to the open document.
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2017 Christian Hergert
 Builtin=true
-Depends=editor
+Copyright=Copyright © 2017 Christian Hergert
+Depends=editor;
+Description=Allows the user to rotate through other files similarly named to the open document.
+Hidden=true
+Loader=python3
+Module=find_other_file
+Name=Find other files
+X-Builder-ABI=@PACKAGE_ABI@
+X-Workspace-Kind=primary;editor;
diff --git a/src/plugins/find-other-file/find_other_file.py b/src/plugins/find-other-file/find_other_file.py
index a65985a8f..b44d9c92c 100644
--- a/src/plugins/find-other-file/find_other_file.py
+++ b/src/plugins/find-other-file/find_other_file.py
@@ -33,30 +33,33 @@ _ATTRIBUTES = ",".join([
     Gio.FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON,
 ])
 
-class FindOtherFile(GObject.Object, Ide.WorkbenchAddin):
+class FindOtherFile(GObject.Object, Ide.WorkspaceAddin):
     context = None
+    workspace = None
     workbench = None
 
-    def do_load(self, workbench):
-        self.workbench = workbench
-        self.context = workbench.get_context()
+    def do_load(self, workspace):
+        self.workspace = workspace
+        self.workbench = Ide.widget_get_workbench(workspace)
+        self.context = self.workbench.get_context()
 
         action = Gio.SimpleAction.new('find-other-file', None)
         action.connect('activate', self.on_activate)
-        self.workbench.add_action(action)
+        self.workspace.add_action(action)
 
-    def do_unload(self, workbench):
+    def do_unload(self, workspace):
         self.workbench = None
+        self.workspace = None
         self.context = None
 
     def on_activate(self, *args):
-        editor = self.workbench.get_perspective_by_name('editor')
-        view = editor.get_active_view()
-        if type(view) is not Ide.EditorView:
+        editor = self.workspace.get_surface_by_name('editor')
+        page = editor.get_active_page()
+        if type(page) is not Ide.EditorPage:
             return
 
-        buffer = view.get_buffer()
-        file = buffer.get_file().get_file()
+        buffer = page.get_buffer()
+        file = buffer.get_file()
         parent = file.get_parent()
 
         basename = file.get_basename()
@@ -85,9 +88,8 @@ class FindOtherFile(GObject.Object, Ide.WorkbenchAddin):
                     display_name = info.get_attribute_string(Gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME)
                     icon = info.get_attribute_object(Gio.FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON)
                     icon_name = icon.to_string() if icon else None
-                    gfile = parent.get_child(name)
-                    ifile = Ide.File.new(self.context, gfile)
-                    result = OtherFileSearchResult(file=ifile, icon_name=icon_name, title=display_name)
+                    file = parent.get_child(name)
+                    result = OtherFileSearchResult(file=file, icon_name=icon_name, title=display_name)
                     files.append(result)
 
                 info = enumerator.next_file(None)
@@ -97,8 +99,8 @@ class FindOtherFile(GObject.Object, Ide.WorkbenchAddin):
             count = files.get_n_items()
 
             if count == 1:
-                file = files.get_item(0).file.get_file()
-                self.workbench.open_files_async([file], 'editor', 0, None, None)
+                file = files.get_item(0).file
+                self.workbench.open_async(file, 'editor', 0, None, None)
             elif count:
                 self.present_results(files, basename)
 
@@ -107,7 +109,7 @@ class FindOtherFile(GObject.Object, Ide.WorkbenchAddin):
             return
 
     def present_results(self, results, name):
-        headerbar = self.workbench.get_headerbar()
+        headerbar = self.workspace.get_header_bar()
         search = Dazzle.gtk_widget_find_child_typed(headerbar, Ide.SearchEntry)
         search.set_text('')
         search.set_model(results)
@@ -116,7 +118,10 @@ class FindOtherFile(GObject.Object, Ide.WorkbenchAddin):
 
 
 class OtherFileSearchResult(Ide.SearchResult):
-    file = GObject.Property(type=Ide.File)
+    file = GObject.Property(type=Gio.File)
 
-    def do_get_source_location(self):
-        return Ide.SourceLocation.new(self.file, 0, 0, 0)
+    def do_activate(self, last_focus):
+        workspace = Ide.widget_get_workspace(last_focus)
+        editor = workspace.get_surface_by_name('editor')
+        loc = Ide.Location.new(self.file, -1, -1)
+        editor.focus_location(loc)
diff --git a/src/plugins/find-other-file/meson.build b/src/plugins/find-other-file/meson.build
index 7b2e848ec..4df9128b6 100644
--- a/src/plugins/find-other-file/meson.build
+++ b/src/plugins/find-other-file/meson.build
@@ -1,13 +1,9 @@
-if get_option('with_find_other_file')
-
 install_data('find_other_file.py', install_dir: plugindir)
 
 configure_file(
           input: 'find-other-file.plugin',
          output: 'find-other-file.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
-
-endif
diff --git a/src/plugins/flatpak/flatpak-plugin.c b/src/plugins/flatpak/flatpak-plugin.c
new file mode 100644
index 000000000..375e3db0c
--- /dev/null
+++ b/src/plugins/flatpak/flatpak-plugin.c
@@ -0,0 +1,72 @@
+/* flatpak-plugin.c
+ *
+ * Copyright 2016-2019 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 "flatpak-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-code.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+
+#include "gbp-flatpak-application-addin.h"
+#include "gbp-flatpak-build-system-discovery.h"
+#include "gbp-flatpak-build-target-provider.h"
+#include "gbp-flatpak-configuration-provider.h"
+#include "gbp-flatpak-dependency-updater.h"
+#include "gbp-flatpak-pipeline-addin.h"
+#include "gbp-flatpak-preferences-addin.h"
+#include "gbp-flatpak-runtime-provider.h"
+#include "gbp-flatpak-workbench-addin.h"
+
+_IDE_EXTERN void
+_gbp_flatpak_register_types (PeasObjectModule *module)
+{
+  ide_g_file_add_ignored_pattern (".flatpak-builder");
+
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_SYSTEM_DISCOVERY,
+                                              GBP_TYPE_FLATPAK_BUILD_SYSTEM_DISCOVERY);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_TARGET_PROVIDER,
+                                              GBP_TYPE_FLATPAK_BUILD_TARGET_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_CONFIGURATION_PROVIDER,
+                                              GBP_TYPE_FLATPAK_CONFIGURATION_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_DEPENDENCY_UPDATER,
+                                              GBP_TYPE_FLATPAK_DEPENDENCY_UPDATER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_RUNTIME_PROVIDER,
+                                              GBP_TYPE_FLATPAK_RUNTIME_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_APPLICATION_ADDIN,
+                                              GBP_TYPE_FLATPAK_APPLICATION_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                              GBP_TYPE_FLATPAK_PIPELINE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_FLATPAK_PREFERENCES_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GBP_TYPE_FLATPAK_WORKBENCH_ADDIN);
+}
diff --git a/src/plugins/flatpak/flatpak.gresource.xml b/src/plugins/flatpak/flatpak.gresource.xml
index 2523c2a28..e24f61d3d 100644
--- a/src/plugins/flatpak/flatpak.gresource.xml
+++ b/src/plugins/flatpak/flatpak.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/flatpak">
     <file>flatpak.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/flatpak-plugin">
-    <file>gbp-flatpak-clone-widget.ui</file>
+    <file preprocess="xml-stripblanks">gbp-flatpak-clone-widget.ui</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/flatpak/flatpak.plugin b/src/plugins/flatpak/flatpak.plugin
index 1116a3366..2d7e9bc7c 100644
--- a/src/plugins/flatpak/flatpak.plugin
+++ b/src/plugins/flatpak/flatpak.plugin
@@ -1,9 +1,11 @@
 [Plugin]
-Module=flatpak-plugin
-Name=Flatpak
-Description=Provides support for building with Flatpak
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2016 Christian Hergert
 Builtin=true
-Depends=git-plugin;
-Embedded=gbp_flatpak_register_types
+Copyright=Copyright © 2016-2018 Christian Hergert
+Depends=buildui;editor;git;
+Description=Provides support for building with Flatpak
+Embedded=_gbp_flatpak_register_types
+Hidden=true
+Module=flatpak
+Name=Flatpak
+X-At-Startup=true
diff --git a/src/plugins/flatpak/gbp-flatpak-application-addin.c 
b/src/plugins/flatpak/gbp-flatpak-application-addin.c
index 459160cdc..feddde9ed 100644
--- a/src/plugins/flatpak/gbp-flatpak-application-addin.c
+++ b/src/plugins/flatpak/gbp-flatpak-application-addin.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-application-addin.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,16 +14,24 @@
  *
  * 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-flatpak-application-addin"
 
+#include "config.h"
+
+#include <glib/gi18n.h>
 #include <glib/gstdio.h>
+#include <libide-greeter.h>
+#include <libide-gui.h>
 #include <errno.h>
 #include <flatpak.h>
 #include <unistd.h>
 
 #include "gbp-flatpak-application-addin.h"
+#include "gbp-flatpak-clone-widget.h"
 #include "gbp-flatpak-runtime.h"
 #include "gbp-flatpak-util.h"
 
@@ -40,7 +48,7 @@ typedef struct
   gchar               *arch;
   gchar               *branch;
   GPtrArray           *installations;
-  IdeProgress         *progress;
+  IdeNotification     *progress;
   FlatpakInstalledRef *ref;
   guint                did_added : 1;
 } InstallRequest;
@@ -67,6 +75,13 @@ struct _GbpFlatpakApplicationAddin
    * ptrarray) will not be affected.
    */
   GPtrArray *installations;
+
+  /* The addin attempts to delay loading any flatpak information until
+   * it has been requested (by the runtime provider for example). Doing
+   * so helps speed up initial application startup at the cost of a bit
+   * slower project setup time.
+   */
+  guint      has_loaded : 1;
 };
 
 typedef struct
@@ -89,7 +104,7 @@ static BuiltinFlatpakRepo builtin_flatpak_repos[] = {
   { "gnome-nightly", "https://sdk.gnome.org/gnome-nightly.flatpakrepo"; },
 };
 
-static void gbp_flatpak_application_addin_reload (GbpFlatpakApplicationAddin *self);
+static void gbp_flatpak_application_addin_lazy_reload (GbpFlatpakApplicationAddin *self);
 
 static void
 copy_devhelp_docs_into_user_data_dir_worker (IdeTask      *task,
@@ -231,7 +246,7 @@ install_info_installation_changed (GFileMonitor      *monitor,
 
   self = g_object_ref (info->self);
 
-  gbp_flatpak_application_addin_reload (self);
+  gbp_flatpak_application_addin_lazy_reload (self);
 
   IDE_EXIT;
 }
@@ -308,7 +323,7 @@ locate_sdk_free (LocateSdk *locate)
 }
 
 static void
-gbp_flatpak_application_addin_reload (GbpFlatpakApplicationAddin *self)
+gbp_flatpak_application_addin_lazy_reload (GbpFlatpakApplicationAddin *self)
 {
   g_autofree gchar *user_path = NULL;
   g_autoptr(GFile) user_file = NULL;
@@ -320,6 +335,8 @@ gbp_flatpak_application_addin_reload (GbpFlatpakApplicationAddin *self)
 
   g_assert (GBP_IS_FLATPAK_APPLICATION_ADDIN (self));
 
+  self->has_loaded = TRUE;
+
   /* Clear any previous installations */
   g_clear_pointer (&self->installations, g_ptr_array_unref);
   self->installations = g_ptr_array_new_with_free_func ((GDestroyNotify)install_info_free);
@@ -393,8 +410,6 @@ gbp_flatpak_application_addin_load (IdeApplicationAddin *addin,
 
   instance = self;
 
-  gbp_flatpak_application_addin_reload (self);
-
   settings = g_settings_new ("org.gnome.builder");
 
   if (g_settings_get_boolean (settings, "clear-cache-at-startup"))
@@ -459,6 +474,9 @@ gbp_flatpak_application_addin_get_runtimes (GbpFlatpakApplicationAddin *self)
 
   g_assert (GBP_IS_FLATPAK_APPLICATION_ADDIN (self));
 
+  if (!self->has_loaded)
+    gbp_flatpak_application_addin_lazy_reload (self);
+
   ret = g_ptr_array_new_with_free_func (g_object_unref);
 
   for (guint i = 0; i < self->installations->len; i++)
@@ -501,6 +519,9 @@ gbp_flatpak_application_addin_get_installations (GbpFlatpakApplicationAddin *sel
 
   g_assert (GBP_IS_FLATPAK_APPLICATION_ADDIN (self));
 
+  if (!self->has_loaded)
+    gbp_flatpak_application_addin_lazy_reload (self);
+
   ret = g_ptr_array_new_with_free_func (g_object_unref);
 
   /* Might be NULL before things have loaded at startup */
@@ -645,7 +666,7 @@ gbp_flatpak_application_addin_install_runtime_worker (IdeTask      *task,
                                                           id,
                                                           arch,
                                                           branch,
-                                                          ide_progress_flatpak_progress_callback,
+                                                          ide_notification_flatpak_progress_callback,
                                                           request->progress,
                                                           cancellable,
                                                           &error);
@@ -710,7 +731,7 @@ gbp_flatpak_application_addin_install_runtime_worker (IdeTask      *task,
                                                                id,
                                                                arch,
                                                                branch,
-                                                               ide_progress_flatpak_progress_callback,
+                                                               ide_notification_flatpak_progress_callback,
                                                                request->progress,
                                                                cancellable,
                                                                &error);
@@ -741,7 +762,7 @@ gbp_flatpak_application_addin_install_runtime_async (GbpFlatpakApplicationAddin
                                                      const gchar                 *arch,
                                                      const gchar                 *branch,
                                                      GCancellable                *cancellable,
-                                                     IdeProgress                **progress,
+                                                     IdeNotification            **progress,
                                                      GAsyncReadyCallback          callback,
                                                      gpointer                     user_data)
 {
@@ -767,7 +788,7 @@ gbp_flatpak_application_addin_install_runtime_async (GbpFlatpakApplicationAddin
   request->arch = g_strdup (arch);
   request->branch = g_strdup (branch);
   request->installations = g_ptr_array_ref (self->installations);
-  request->progress = ide_progress_new ();
+  request->progress = ide_notification_new ();
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, gbp_flatpak_application_addin_install_runtime_async);
@@ -867,11 +888,92 @@ gbp_flatpak_application_addin_has_runtime (GbpFlatpakApplicationAddin *self,
   IDE_RETURN (FALSE);
 }
 
+static void
+gbp_flatpak_application_addin_add_option_entries (IdeApplicationAddin *addin,
+                                                  IdeApplication      *app)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_FLATPAK_APPLICATION_ADDIN (addin));
+  g_assert (G_IS_APPLICATION (app));
+
+  g_application_add_main_option (G_APPLICATION (app),
+                                 "manifest",
+                                 'm',
+                                 G_OPTION_FLAG_IN_MAIN,
+                                 G_OPTION_ARG_FILENAME,
+                                 _("Clone a project using flatpak manifest"),
+                                 _("MANIFEST"));
+}
+
+static void
+gbp_flatpak_application_addin_clone_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  GbpFlatpakCloneWidget *clone = (GbpFlatpakCloneWidget *)object;
+  g_autoptr(IdeGreeterWorkspace) workspace = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_FLATPAK_CLONE_WIDGET (clone));
+  g_assert (IDE_IS_GREETER_WORKSPACE (workspace));
+
+  if (!gbp_flatpak_clone_widget_clone_finish (clone, result, &error))
+    g_warning ("%s", error->message);
+
+  ide_greeter_workspace_end (workspace);
+}
+
+static void
+gbp_flatpak_application_addin_handle_command_line (IdeApplicationAddin     *addin,
+                                                   IdeApplication          *application,
+                                                   GApplicationCommandLine *cmdline)
+{
+  g_autofree gchar *manifest = NULL;
+  GbpFlatpakCloneWidget *clone;
+  IdeGreeterWorkspace *workspace;
+  IdeWorkbench *workbench;
+  GVariantDict *options;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_FLATPAK_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (application));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  if (!(options = g_application_command_line_get_options_dict (cmdline)) ||
+      !g_variant_dict_contains (options, "manifest") ||
+      !g_variant_dict_lookup (options, "manifest", "^ay", &manifest))
+    return;
+
+  workbench = ide_workbench_new ();
+  ide_application_add_workbench (application, workbench);
+
+  workspace = ide_greeter_workspace_new (application);
+  ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+
+  clone = g_object_new (GBP_TYPE_FLATPAK_CLONE_WIDGET,
+                        "manifest", manifest,
+                        "visible", TRUE,
+                        NULL);
+  ide_workspace_add_surface (IDE_WORKSPACE (workspace), IDE_SURFACE (clone));
+  ide_workspace_set_visible_surface (IDE_WORKSPACE (workspace), IDE_SURFACE (clone));
+
+  ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+
+  ide_greeter_workspace_begin (workspace);
+  gbp_flatpak_clone_widget_clone_async (clone,
+                                        NULL,
+                                        gbp_flatpak_application_addin_clone_cb,
+                                        g_object_ref (workspace));
+}
+
 static void
 application_addin_iface_init (IdeApplicationAddinInterface *iface)
 {
   iface->load = gbp_flatpak_application_addin_load;
   iface->unload = gbp_flatpak_application_addin_unload;
+  iface->add_option_entries = gbp_flatpak_application_addin_add_option_entries;
+  iface->handle_command_line = gbp_flatpak_application_addin_handle_command_line;
 }
 
 G_DEFINE_TYPE_EXTENDED (GbpFlatpakApplicationAddin,
diff --git a/src/plugins/flatpak/gbp-flatpak-application-addin.h 
b/src/plugins/flatpak/gbp-flatpak-application-addin.h
index 15829e07c..ce4ae7820 100644
--- a/src/plugins/flatpak/gbp-flatpak-application-addin.h
+++ b/src/plugins/flatpak/gbp-flatpak-application-addin.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-application-addin.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,12 +14,14 @@
  *
  * 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 <flatpak.h>
-#include <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
@@ -46,7 +48,7 @@ void                        gbp_flatpak_application_addin_install_runtime_async
                                                                                   const gchar                
 *arch,
                                                                                   const gchar                
 *branch,
                                                                                   GCancellable               
 *cancellable,
-                                                                                  IdeProgress                
**progress,
+                                                                                  IdeNotification            
**progress,
                                                                                   GAsyncReadyCallback        
  callback,
                                                                                   gpointer                   
  user_data);
 gboolean                    gbp_flatpak_application_addin_install_runtime_finish (GbpFlatpakApplicationAddin 
 *self,
diff --git a/src/plugins/flatpak/gbp-flatpak-build-system-discovery.c 
b/src/plugins/flatpak/gbp-flatpak-build-system-discovery.c
index b285a6e7b..c42647453 100644
--- a/src/plugins/flatpak/gbp-flatpak-build-system-discovery.c
+++ b/src/plugins/flatpak/gbp-flatpak-build-system-discovery.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-build-system-discovery.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-build-system-discovery"
diff --git a/src/plugins/flatpak/gbp-flatpak-build-system-discovery.h 
b/src/plugins/flatpak/gbp-flatpak-build-system-discovery.h
index 4cfbfc5d5..f047f95a1 100644
--- a/src/plugins/flatpak/gbp-flatpak-build-system-discovery.h
+++ b/src/plugins/flatpak/gbp-flatpak-build-system-discovery.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-build-system-discovery.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/gbp-flatpak-build-target-provider.c 
b/src/plugins/flatpak/gbp-flatpak-build-target-provider.c
index b45731e6b..92906226d 100644
--- a/src/plugins/flatpak/gbp-flatpak-build-target-provider.c
+++ b/src/plugins/flatpak/gbp-flatpak-build-target-provider.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-build-target-provider.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-build-target-provider"
@@ -48,7 +50,7 @@ gbp_flatpak_build_target_provider_get_targets_async (IdeBuildTargetProvider *pro
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  config_manager = ide_context_get_configuration_manager (context);
+  config_manager = ide_configuration_manager_from_context (context);
   config = ide_configuration_manager_get_current (config_manager);
 
   targets = g_ptr_array_new_with_free_func (g_object_unref);
@@ -61,7 +63,6 @@ gbp_flatpak_build_target_provider_get_targets_async (IdeBuildTargetProvider *pro
       command = gbp_flatpak_manifest_get_command (GBP_FLATPAK_MANIFEST (config));
 
       target = g_object_new (GBP_TYPE_FLATPAK_BUILD_TARGET,
-                             "context", context,
                              "command", command,
                              NULL);
 
diff --git a/src/plugins/flatpak/gbp-flatpak-build-target-provider.h 
b/src/plugins/flatpak/gbp-flatpak-build-target-provider.h
index 6d77ee91f..285694c04 100644
--- a/src/plugins/flatpak/gbp-flatpak-build-target-provider.h
+++ b/src/plugins/flatpak/gbp-flatpak-build-target-provider.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-build-target-provider.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/gbp-flatpak-build-target.c b/src/plugins/flatpak/gbp-flatpak-build-target.c
index ed92ad1b1..f8fb477e2 100644
--- a/src/plugins/flatpak/gbp-flatpak-build-target.c
+++ b/src/plugins/flatpak/gbp-flatpak-build-target.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-build-target.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-build-target"
diff --git a/src/plugins/flatpak/gbp-flatpak-build-target.h b/src/plugins/flatpak/gbp-flatpak-build-target.h
index bc3186105..10c29e59a 100644
--- a/src/plugins/flatpak/gbp-flatpak-build-target.h
+++ b/src/plugins/flatpak/gbp-flatpak-build-target.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-build-target.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/gbp-flatpak-clone-widget.c b/src/plugins/flatpak/gbp-flatpak-clone-widget.c
index 5fb2561b2..89cc550cc 100644
--- a/src/plugins/flatpak/gbp-flatpak-clone-widget.c
+++ b/src/plugins/flatpak/gbp-flatpak-clone-widget.c
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-clone-widget"
@@ -22,7 +24,10 @@
 #include <glib/gi18n.h>
 #include <json-glib/json-glib.h>
 #include <libgit2-glib/ggit.h>
-#include <ide.h>
+#include <libide-greeter.h>
+#include <libide-gui.h>
+#include <libide-threading.h>
+#include <libide-vcs.h>
 
 #include "gbp-flatpak-clone-widget.h"
 #include "gbp-flatpak-sources.h"
@@ -31,7 +36,7 @@
 
 struct _GbpFlatpakCloneWidget
 {
-  GtkBin          parent_instance;
+  IdeSurface      parent_instance;
 
   GtkProgressBar *clone_progress;
 
@@ -52,12 +57,12 @@ typedef enum {
 
 typedef struct
 {
-  SourceType type;
-  IdeVcsUri  *uri;
-  gchar      *branch;
-  gchar      *sha;
-  gchar      *name;
-  gchar     **patches;
+  SourceType   type;
+  IdeVcsUri   *uri;
+  gchar       *branch;
+  gchar       *sha;
+  gchar       *name;
+  gchar      **patches;
 } ModuleSource;
 
 typedef struct
@@ -74,7 +79,7 @@ enum {
   LAST_PROP
 };
 
-G_DEFINE_TYPE (GbpFlatpakCloneWidget, gbp_flatpak_clone_widget, GTK_TYPE_BIN)
+G_DEFINE_TYPE (GbpFlatpakCloneWidget, gbp_flatpak_clone_widget, IDE_TYPE_SURFACE)
 
 static void
 module_source_free (void *data)
@@ -120,24 +125,33 @@ static void
 gbp_flatpak_clone_widget_set_manifest (GbpFlatpakCloneWidget *self,
                                        const gchar           *manifest)
 {
-  gchar *ptr;
+  g_autofree gchar *name = NULL;
+  g_autofree gchar *title = NULL;
+  const gchar *ptr;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_FLATPAK_CLONE_WIDGET (self));
+  g_assert (manifest != NULL);
+
+  g_clear_pointer (&self->manifest, g_free);
+  g_clear_pointer (&self->app_id_override, g_free);
 
-  g_free (self->manifest);
-  g_free (self->app_id_override);
+  name = g_path_get_basename (manifest);
+  /* translators: %s is replaced with the name of the flatpak manifest */
+  title = g_strdup_printf (_("Cloning project %s"), name);
+  ide_surface_set_title (IDE_SURFACE (self), title);
 
   /* if the filename does not end with .json, just set it right away,
    * even if it may fail later.
    */
-  ptr = g_strrstr (manifest, ".json");
-  if (!ptr)
+  if (!(ptr = g_strrstr (manifest, ".json")))
     {
       self->manifest = g_strdup (manifest);
       return;
     }
 
   /* search for the first '+' after the .json extension */
-  ptr = strchr (ptr, '+');
-  if (!ptr)
+  if (!(ptr = strchr (ptr, '+')))
     {
       self->manifest = g_strdup (manifest);
       return;
@@ -228,7 +242,7 @@ gbp_flatpak_clone_widget_class_init (GbpFlatpakCloneWidgetClass *klass)
                                                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)));
 
   gtk_widget_class_set_css_name (widget_class, "flatpakclonewidget");
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/flatpak-plugin/gbp-flatpak-clone-widget.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/flatpak/gbp-flatpak-clone-widget.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpFlatpakCloneWidget, clone_progress);
 }
 
@@ -242,21 +256,30 @@ gbp_flatpak_clone_widget_init (GbpFlatpakCloneWidget *self)
 static gboolean
 open_after_timeout (gpointer user_data)
 {
+  g_autoptr(IdeProjectInfo) project_info = NULL;
   g_autoptr(IdeTask) task = user_data;
-  DownloadRequest *req;
   GbpFlatpakCloneWidget *self;
-  IdeWorkbench *workbench;
+  DownloadRequest *req;
+  GtkWidget *workspace;
 
   IDE_ENTRY;
 
   req = ide_task_get_task_data (task);
   self = ide_task_get_source_object (task);
+
   g_assert (GBP_IS_FLATPAK_CLONE_WIDGET (self));
+  g_assert (req != NULL);
+  g_assert (G_IS_FILE (req->project_file));
+
+  /* Maybe we were shut mid-operation? */
+  if (!(workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GREETER_WORKSPACE)))
+    IDE_RETURN (G_SOURCE_REMOVE);
 
-  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
-  g_assert (IDE_IS_WORKBENCH (workbench));
+  project_info = ide_project_info_new ();
+  ide_project_info_set_file (project_info, req->project_file);
+  ide_project_info_set_directory (project_info, req->project_file);
 
-  ide_workbench_open_project_async (workbench, req->project_file, NULL, NULL, NULL);
+  ide_greeter_workspace_open_project (IDE_GREETER_WORKSPACE (workspace), project_info);
 
   IDE_RETURN (G_SOURCE_REMOVE);
 }
@@ -368,7 +391,6 @@ download_flatpak_sources_if_required (GbpFlatpakCloneWidget  *self,
       g_autoptr(GgitObject) parsed_rev = NULL;
       g_autoptr(GgitRemoteCallbacks) callbacks = NULL;
       g_autoptr(GgitRepository) repository = NULL;
-      g_autoptr(IdeProgress) progress = NULL;
       GType git_callbacks_type;
 
       /* First, try to open an existing repository at this path */
@@ -385,16 +407,19 @@ download_flatpak_sources_if_required (GbpFlatpakCloneWidget  *self,
 
       if (repository == NULL)
         {
+          g_autoptr(IdeNotification) progress = ide_notification_new ();
+
           /* HACK: we don't want libide to depend on libgit2 just yet, so for
            * now, we just lookup the GType of the object we need from the git
            * plugin by name.
            */
-          git_callbacks_type = g_type_from_name ("IdeGitRemoteCallbacks");
+          git_callbacks_type = g_type_from_name ("GbpGitRemoteCallbacks");
           g_assert (git_callbacks_type != 0);
 
-          callbacks = g_object_new (git_callbacks_type, NULL);
-          g_object_get (callbacks, "progress", &progress, NULL);
-          g_object_bind_property (progress, "fraction", self->clone_progress, "fraction", 0);
+          callbacks = g_object_new (git_callbacks_type,
+                                    "progress", progress,
+                                    NULL);
+          g_object_bind_property (progress, "progress", self->clone_progress, "fraction", 0);
 
           fetch_options = ggit_fetch_options_new ();
           ggit_fetch_options_set_remote_callbacks (fetch_options, callbacks);
@@ -743,8 +768,7 @@ gbp_flatpak_clone_widget_clone_async (GbpFlatpakCloneWidget   *self,
         }
     }
 
-  destination = ide_application_get_projects_directory (IDE_APPLICATION_DEFAULT);
-  g_assert (G_IS_FILE (destination));
+  destination = g_file_new_for_path (ide_get_projects_dir ());
 
   if (self->child_name)
     {
diff --git a/src/plugins/flatpak/gbp-flatpak-clone-widget.h b/src/plugins/flatpak/gbp-flatpak-clone-widget.h
index d6d9ae703..70bf5c178 100644
--- a/src/plugins/flatpak/gbp-flatpak-clone-widget.h
+++ b/src/plugins/flatpak/gbp-flatpak-clone-widget.h
@@ -14,6 +14,8 @@
  *
  * 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
@@ -24,7 +26,7 @@ G_BEGIN_DECLS
 
 #define GBP_TYPE_FLATPAK_CLONE_WIDGET (gbp_flatpak_clone_widget_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpFlatpakCloneWidget, gbp_flatpak_clone_widget, GBP, FLATPAK_CLONE_WIDGET, GtkBin)
+G_DECLARE_FINAL_TYPE (GbpFlatpakCloneWidget, gbp_flatpak_clone_widget, GBP, FLATPAK_CLONE_WIDGET, IdeSurface)
 
 void     gbp_flatpak_clone_widget_clone_async  (GbpFlatpakCloneWidget *self,
                                                 GCancellable          *cancellable,
diff --git a/src/plugins/flatpak/gbp-flatpak-clone-widget.ui b/src/plugins/flatpak/gbp-flatpak-clone-widget.ui
index a626cf66c..c49580178 100644
--- a/src/plugins/flatpak/gbp-flatpak-clone-widget.ui
+++ b/src/plugins/flatpak/gbp-flatpak-clone-widget.ui
@@ -1,48 +1,41 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <!-- interface-requires gtk+ 3.18 -->
-  <template class="GbpFlatpakCloneWidget" parent="GtkBin">
+  <!-- interface-requires gtk+ 3.24 -->
+  <template class="GbpFlatpakCloneWidget" parent="IdeSurface">
     <child>
-      <object class="GtkOverlay" id="page_clone_remote">
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="spacing">12</property>
+        <property name="valign">center</property>
+        <property name="vexpand">true</property>
         <property name="visible">true</property>
-        <child type="overlay">
-          <object class="GtkProgressBar" id="clone_progress">
-            <property name="valign">start</property>
-            <property name="fraction">0.0</property>
+        <child>
+          <object class="GtkImage">
+            <property name="icon-name">document-save-symbolic</property>
+            <property name="pixel-size">128</property>
             <property name="visible">true</property>
+            <property name="margin">12</property>
             <style>
-              <class name="osd"/>
+              <class name="dim-label"/>
             </style>
           </object>
         </child>
         <child>
-          <object class="GtkBox">
-            <property name="orientation">vertical</property>
-            <property name="spacing">12</property>
-            <property name="valign">center</property>
-            <property name="vexpand">true</property>
+          <object class="GtkProgressBar" id="clone_progress">
+            <property name="halign">center</property>
+            <property name="width-request">500</property>
+            <property name="fraction">0.0</property>
             <property name="visible">true</property>
-            <child>
-              <object class="GtkImage">
-                <property name="icon-name">document-save-symbolic</property>
-                <property name="pixel-size">128</property>
-                <property name="visible">true</property>
-                <property name="margin">12</property>
-                <style>
-                  <class name="dim-label"/>
-                </style>
-              </object>
-            </child>
-            <child>
-              <object class="GtkLabel">
-                <property name="label" translatable="yes">Downloading application sources…</property>
-                <property name="margin-bottom">24</property>
-                <property name="visible">true</property>
-                <style>
-                  <class name="dim-label"/>
-                </style>
-              </object>
-            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel">
+            <property name="label" translatable="yes">Downloading application sources…</property>
+            <property name="margin-bottom">24</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
           </object>
         </child>
       </object>
diff --git a/src/plugins/flatpak/gbp-flatpak-configuration-provider.c 
b/src/plugins/flatpak/gbp-flatpak-configuration-provider.c
index 6a8f2c159..5f2c7bfab 100644
--- a/src/plugins/flatpak/gbp-flatpak-configuration-provider.c
+++ b/src/plugins/flatpak/gbp-flatpak-configuration-provider.c
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-configuration-provider"
@@ -21,6 +23,7 @@
 #include <flatpak.h>
 #include <glib/gi18n.h>
 #include <json-glib/json-glib.h>
+#include <libide-vcs.h>
 #include <string.h>
 
 #include "gbp-flatpak-configuration-provider.h"
@@ -150,7 +153,6 @@ load_manifest_worker (IdeTask      *task,
   g_autoptr(GbpFlatpakManifest) manifest = NULL;
   g_autoptr(GError) error = NULL;
   g_autofree gchar *name = NULL;
-  IdeContext *context;
   GFile *file = task_data;
 
   g_assert (IDE_IS_TASK (task));
@@ -158,12 +160,13 @@ load_manifest_worker (IdeTask      *task,
   g_assert (G_IS_FILE (file));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
   name = g_file_get_basename (file);
-  manifest = gbp_flatpak_manifest_new (context, file, name);
+  manifest = gbp_flatpak_manifest_new (file, name);
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (manifest));
 
   if (!g_initable_init (G_INITABLE (manifest), cancellable, &error))
     {
+      ide_clear_and_destroy_object (&manifest);
       ide_task_return_error (task, g_steal_pointer (&error));
       return;
     }
@@ -246,7 +249,7 @@ reload_manifest_cb (GObject      *object,
   g_ptr_array_add (self->configs, g_object_ref (new_manifest));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  manager = ide_context_get_configuration_manager (context);
+  manager = ide_configuration_manager_from_context (context);
   current = ide_configuration_manager_get_current (manager);
 
   is_active = current == IDE_CONFIGURATION (old_manifest);
@@ -296,7 +299,6 @@ gbp_flatpak_configuration_provider_load_worker (IdeTask      *task,
 {
   GbpFlatpakConfigurationProvider *self = source_object;
   g_autoptr(GPtrArray) manifests = NULL;
-  IdeContext *context;
   GPtrArray *files = task_data;
 
   g_assert (IDE_IS_TASK (task));
@@ -304,7 +306,6 @@ gbp_flatpak_configuration_provider_load_worker (IdeTask      *task,
   g_assert (files != NULL);
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
   manifests = g_ptr_array_new_with_free_func (g_object_unref);
 
   for (guint i = 0; i < files->len; i++)
@@ -317,10 +318,12 @@ gbp_flatpak_configuration_provider_load_worker (IdeTask      *task,
       g_assert (G_IS_FILE (file));
 
       name = g_file_get_basename (file);
-      manifest = gbp_flatpak_manifest_new (context, file, name);
+      manifest = gbp_flatpak_manifest_new (file, name);
+      ide_object_append (IDE_OBJECT (self), IDE_OBJECT (manifest));
 
       if (!g_initable_init (G_INITABLE (manifest), cancellable, &error))
         {
+          ide_clear_and_destroy_object (&manifest);
           g_message ("%s is not a flatpak manifest, skipping: %s",
                      name, error->message);
           continue;
@@ -423,13 +426,13 @@ gbp_flatpak_configuration_provider_monitor_changed (GbpFlatpakConfigurationProvi
         {
           g_autoptr(GbpFlatpakManifest) manifest = NULL;
           g_autoptr(GError) error = NULL;
-          IdeContext *context;
 
-          context = ide_object_get_context (IDE_OBJECT (self));
-          manifest = gbp_flatpak_manifest_new (context, file, name);
+          manifest = gbp_flatpak_manifest_new (file, name);
+          ide_object_append (IDE_OBJECT (self), IDE_OBJECT (manifest));
 
           if (!g_initable_init (G_INITABLE (manifest), NULL, &error))
             {
+              ide_clear_and_destroy_object (&manifest);
               g_message ("%s is not a flatpak manifest, skipping: %s",
                          name, error->message);
               return;
@@ -469,9 +472,9 @@ gbp_flatpak_configuration_provider_load_async (IdeConfigurationProvider *provide
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
-  monitor = ide_context_get_monitor (context);
+  vcs = ide_vcs_from_context (context);
+  workdir = ide_vcs_get_workdir (vcs);
+  monitor = ide_context_peek_child_typed (context, IDE_TYPE_VCS_MONITOR);
 
   task = ide_task_new (provider, cancellable, callback, user_data);
   ide_task_set_source_tag (task, gbp_flatpak_configuration_provider_load_async);
@@ -561,7 +564,7 @@ gbp_flatpak_configuration_provider_load_finish (IdeConfigurationProvider  *provi
     {
       IdeConfiguration *config = guess_best_config (configs);
       IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
-      IdeConfigurationManager *manager = ide_context_get_configuration_manager (context);
+      IdeConfigurationManager *manager = ide_configuration_manager_from_context (context);
 
       g_assert (IDE_IS_CONFIGURATION (config));
 
diff --git a/src/plugins/flatpak/gbp-flatpak-configuration-provider.h 
b/src/plugins/flatpak/gbp-flatpak-configuration-provider.h
index 0f0217265..fd5bccaf9 100644
--- a/src/plugins/flatpak/gbp-flatpak-configuration-provider.h
+++ b/src/plugins/flatpak/gbp-flatpak-configuration-provider.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/gbp-flatpak-dependency-updater.c 
b/src/plugins/flatpak/gbp-flatpak-dependency-updater.c
index 3ebfb87a4..a427250c4 100644
--- a/src/plugins/flatpak/gbp-flatpak-dependency-updater.c
+++ b/src/plugins/flatpak/gbp-flatpak-dependency-updater.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-dependency-updater.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-dependency-updater"
@@ -81,7 +83,7 @@ gbp_flatpak_dependency_updater_update_async (IdeDependencyUpdater *updater,
   context = ide_object_get_context (IDE_OBJECT (self));
   g_assert (IDE_IS_CONTEXT (context));
 
-  manager = ide_context_get_build_manager (context);
+  manager = ide_build_manager_from_context (context);
   g_assert (IDE_IS_BUILD_MANAGER (manager));
 
   pipeline = ide_build_manager_get_pipeline (manager);
@@ -117,6 +119,7 @@ gbp_flatpak_dependency_updater_update_async (IdeDependencyUpdater *updater,
   ide_build_manager_rebuild_async (manager,
                                    IDE_BUILD_PHASE_CONFIGURE,
                                    NULL,
+                                   NULL,
                                    gbp_flatpak_dependency_updater_update_cb,
                                    g_steal_pointer (&task));
 }
diff --git a/src/plugins/flatpak/gbp-flatpak-dependency-updater.h 
b/src/plugins/flatpak/gbp-flatpak-dependency-updater.h
index 8a0e7e872..7d2b32f56 100644
--- a/src/plugins/flatpak/gbp-flatpak-dependency-updater.h
+++ b/src/plugins/flatpak/gbp-flatpak-dependency-updater.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-dependency-updater.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/gbp-flatpak-download-stage.c 
b/src/plugins/flatpak/gbp-flatpak-download-stage.c
index 4da62fc0e..5651b781e 100644
--- a/src/plugins/flatpak/gbp-flatpak-download-stage.c
+++ b/src/plugins/flatpak/gbp-flatpak-download-stage.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-download-stage.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,14 @@
  *
  * 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-flatpak-download-stage"
 
 #include <glib/gi18n.h>
+#include <libide-gui.h>
 
 #include "gbp-flatpak-download-stage.h"
 #include "gbp-flatpak-manifest.h"
@@ -47,6 +50,7 @@ static GParamSpec *properties [N_PROPS];
 static void
 gbp_flatpak_download_stage_query (IdeBuildStage    *stage,
                                   IdeBuildPipeline *pipeline,
+                                  GPtrArray        *targets,
                                   GCancellable     *cancellable)
 {
   GbpFlatpakDownloadStage *self = (GbpFlatpakDownloadStage *)stage;
diff --git a/src/plugins/flatpak/gbp-flatpak-download-stage.h 
b/src/plugins/flatpak/gbp-flatpak-download-stage.h
index 0a045049d..3dff748e8 100644
--- a/src/plugins/flatpak/gbp-flatpak-download-stage.h
+++ b/src/plugins/flatpak/gbp-flatpak-download-stage.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-download-stage.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,21 +14,19 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
 #define GBP_TYPE_FLATPAK_DOWNLOAD_STAGE (gbp_flatpak_download_stage_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpFlatpakDownloadStage,
-                      gbp_flatpak_download_stage,
-                      GBP,
-                      FLATPAK_DOWNLOAD_STAGE,
-                      IdeBuildStageLauncher)
+G_DECLARE_FINAL_TYPE (GbpFlatpakDownloadStage, gbp_flatpak_download_stage, GBP, FLATPAK_DOWNLOAD_STAGE, 
IdeBuildStageLauncher)
 
 void gbp_flatpak_download_stage_force_update (GbpFlatpakDownloadStage *self);
 
diff --git a/src/plugins/flatpak/gbp-flatpak-manifest.c b/src/plugins/flatpak/gbp-flatpak-manifest.c
index c2e421d80..2a5a02bd3 100644
--- a/src/plugins/flatpak/gbp-flatpak-manifest.c
+++ b/src/plugins/flatpak/gbp-flatpak-manifest.c
@@ -1,7 +1,7 @@
 /* gbp-flatpak-manifest.c
  *
  * Copyright 2016 Matthew Leeds <mleeds redhat com>
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -15,6 +15,8 @@
  *
  * 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-flatpak-manifest"
@@ -341,12 +343,11 @@ gbp_flatpak_manifest_initable_init (GInitable     *initable,
   g_auto(GStrv) build_commands = NULL;
   g_auto(GStrv) post_install = NULL;
   const gchar *app_id_field = "app-id";
-  IdeContext *context;
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(GFile) workdir = NULL;
   JsonObject *root_obj;
   JsonObject *primary;
   JsonNode *root;
-  IdeVcs *vcs;
-  GFile *workdir;
   gsize len = 0;
 
   g_assert (GBP_IS_FLATPAK_MANIFEST (self));
@@ -375,9 +376,8 @@ gbp_flatpak_manifest_initable_init (GInitable     *initable,
   display_name = g_file_get_basename (self->file);
   ide_configuration_set_display_name (IDE_CONFIGURATION (self), display_name);
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
+  context = ide_object_ref_context (IDE_OBJECT (self));
+  workdir = ide_context_ref_workdir (context);
   dir_name = g_file_get_basename (workdir);
   root_obj = json_node_get_object (root);
 
@@ -653,12 +653,13 @@ gbp_flatpak_manifest_init (GbpFlatpakManifest *self)
 }
 
 GbpFlatpakManifest *
-gbp_flatpak_manifest_new (IdeContext  *context,
-                          GFile       *file,
+gbp_flatpak_manifest_new (GFile       *file,
                           const gchar *id)
 {
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
+  g_return_val_if_fail (id != NULL, NULL);
+
   return g_object_new (GBP_TYPE_FLATPAK_MANIFEST,
-                       "context", context,
                        "id", id,
                        "file", file,
                        NULL);
diff --git a/src/plugins/flatpak/gbp-flatpak-manifest.h b/src/plugins/flatpak/gbp-flatpak-manifest.h
index a51c69e13..2103ee0ed 100644
--- a/src/plugins/flatpak/gbp-flatpak-manifest.h
+++ b/src/plugins/flatpak/gbp-flatpak-manifest.h
@@ -1,7 +1,7 @@
 /* gbp-flatpak-manifest.h
  *
  * Copyright 2016 Matthew Leeds <mleeds redhat com>
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
@@ -27,8 +29,7 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpFlatpakManifest, gbp_flatpak_manifest, GBP, FLATPAK_MANIFEST, IdeConfiguration)
 
-GbpFlatpakManifest  *gbp_flatpak_manifest_new                (IdeContext           *context,
-                                                              GFile                *file,
+GbpFlatpakManifest  *gbp_flatpak_manifest_new                (GFile                *file,
                                                               const gchar          *id);
 GFile               *gbp_flatpak_manifest_get_file           (GbpFlatpakManifest   *self);
 const gchar         *gbp_flatpak_manifest_get_primary_module (GbpFlatpakManifest   *self);
diff --git a/src/plugins/flatpak/gbp-flatpak-pipeline-addin.c 
b/src/plugins/flatpak/gbp-flatpak-pipeline-addin.c
index c6154fe9b..e70f5c17e 100644
--- a/src/plugins/flatpak/gbp-flatpak-pipeline-addin.c
+++ b/src/plugins/flatpak/gbp-flatpak-pipeline-addin.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-pipeline-addin.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-pipeline-addin"
@@ -110,10 +112,15 @@ sniff_flatpak_builder_version (GbpFlatpakPipelineAddin *self)
 
 static void
 always_run_query_handler (IdeBuildStage    *stage,
-                          IdeBuildPipeline *pipeline)
+                          GPtrArray        *targets,
+                          IdeBuildPipeline *pipeline,
+                          GCancellable     *cancellable,
+                          gpointer          user_data)
 {
-  g_return_if_fail (IDE_IS_BUILD_STAGE (stage));
-  g_return_if_fail (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   ide_build_stage_set_completed (stage, FALSE);
 }
@@ -155,7 +162,7 @@ register_mkdirs_stage (GbpFlatpakPipelineAddin  *self,
   ide_build_stage_mkdirs_add_path (IDE_BUILD_STAGE_MKDIRS (mkdirs), repo_dir, TRUE, 0750, FALSE);
   ide_build_stage_mkdirs_add_path (IDE_BUILD_STAGE_MKDIRS (mkdirs), staging_dir, TRUE, 0750, TRUE);
 
-  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_PREPARE, PREPARE_MKDIRS, mkdirs);
+  stage_id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_PREPARE, PREPARE_MKDIRS, mkdirs);
 
   ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
 
@@ -190,6 +197,7 @@ reap_staging_dir_cb (GObject      *object,
 static void
 check_for_build_init_files (IdeBuildStage    *stage,
                             IdeBuildPipeline *pipeline,
+                            GPtrArray        *targets,
                             GCancellable     *cancellable,
                             const gchar      *staging_dir)
 {
@@ -199,6 +207,7 @@ check_for_build_init_files (IdeBuildStage    *stage,
   gboolean completed = FALSE;
   gboolean parent_exists;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_BUILD_STAGE (stage));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
   g_assert (staging_dir != NULL);
@@ -327,7 +336,6 @@ register_build_init_stage (GbpFlatpakPipelineAddin  *self,
 
   stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
                         "name", _("Preparing build directory"),
-                        "context", context,
                         "launcher", launcher,
                         NULL);
 
@@ -354,7 +362,7 @@ register_build_init_stage (GbpFlatpakPipelineAddin  *self,
                          (GClosureNotify)g_free,
                          0);
 
-  stage_id = ide_build_pipeline_connect (pipeline,
+  stage_id = ide_build_pipeline_attach (pipeline,
                                          IDE_BUILD_PHASE_PREPARE,
                                          PREPARE_BUILD_INIT,
                                          stage);
@@ -378,10 +386,9 @@ register_downloads_stage (GbpFlatpakPipelineAddin  *self,
 
   stage = g_object_new (GBP_TYPE_FLATPAK_DOWNLOAD_STAGE,
                         "name", _("Downloading dependencies"),
-                        "context", context,
                         "state-dir", self->state_dir,
                         NULL);
-  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_DOWNLOADS, 0, stage);
+  stage_id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_DOWNLOADS, 0, stage);
   ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
 
   return TRUE;
@@ -458,11 +465,10 @@ register_dependencies_stage (GbpFlatpakPipelineAddin  *self,
 
   stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
                         "name", _("Building dependencies"),
-                        "context", context,
                         "launcher", launcher,
                         NULL);
 
-  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_DEPENDENCIES, 0, stage);
+  stage_id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_DEPENDENCIES, 0, stage);
   ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
 
   return TRUE;
@@ -510,11 +516,10 @@ register_build_finish_stage (GbpFlatpakPipelineAddin  *self,
 
   stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
                         "name", _("Finalizing flatpak build"),
-                        "context", context,
                         "launcher", launcher,
                         NULL);
 
-  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_COMMIT, COMMIT_BUILD_FINISH, stage);
+  stage_id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_COMMIT, COMMIT_BUILD_FINISH, stage);
   ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
 
   return TRUE;
@@ -556,7 +561,6 @@ register_build_export_stage (GbpFlatpakPipelineAddin  *self,
 
   stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
                         "name", _("Exporting staging directory"),
-                        "context", context,
                         "launcher", launcher,
                         NULL);
 
@@ -565,7 +569,7 @@ register_build_export_stage (GbpFlatpakPipelineAddin  *self,
                     G_CALLBACK (always_run_query_handler),
                     NULL);
 
-  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_COMMIT, COMMIT_BUILD_EXPORT, stage);
+  stage_id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_COMMIT, COMMIT_BUILD_EXPORT, stage);
   ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
 
   return TRUE;
@@ -642,7 +646,6 @@ register_build_bundle_stage (GbpFlatpakPipelineAddin  *self,
 
   stage = g_object_new (IDE_TYPE_BUILD_STAGE_LAUNCHER,
                         "name", _("Creating flatpak bundle"),
-                        "context", context,
                         "launcher", launcher,
                         NULL);
 
@@ -658,7 +661,7 @@ register_build_bundle_stage (GbpFlatpakPipelineAddin  *self,
                          (GClosureNotify)g_free,
                          0);
 
-  stage_id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_EXPORT, EXPORT_BUILD_BUNDLE, stage);
+  stage_id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_EXPORT, EXPORT_BUILD_BUNDLE, stage);
   ide_build_pipeline_addin_track (IDE_BUILD_PIPELINE_ADDIN (self), stage_id);
 
   return TRUE;
diff --git a/src/plugins/flatpak/gbp-flatpak-pipeline-addin.h 
b/src/plugins/flatpak/gbp-flatpak-pipeline-addin.h
index 4b2258e6e..4a307ec56 100644
--- a/src/plugins/flatpak/gbp-flatpak-pipeline-addin.h
+++ b/src/plugins/flatpak/gbp-flatpak-pipeline-addin.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-pipeline-addin.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/gbp-flatpak-preferences-addin.c 
b/src/plugins/flatpak/gbp-flatpak-preferences-addin.c
index 3b60ac647..04e4f2087 100644
--- a/src/plugins/flatpak/gbp-flatpak-preferences-addin.c
+++ b/src/plugins/flatpak/gbp-flatpak-preferences-addin.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-preferences-addin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-preferences-addin"
@@ -21,6 +23,7 @@
 
 #include <flatpak.h>
 #include <glib/gi18n.h>
+#include <libide-gui.h>
 
 #include "gbp-flatpak-application-addin.h"
 #include "gbp-flatpak-preferences-addin.h"
diff --git a/src/plugins/flatpak/gbp-flatpak-preferences-addin.h 
b/src/plugins/flatpak/gbp-flatpak-preferences-addin.h
index 437ec6719..207d1b22d 100644
--- a/src/plugins/flatpak/gbp-flatpak-preferences-addin.h
+++ b/src/plugins/flatpak/gbp-flatpak-preferences-addin.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-preferences-addin.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/gbp-flatpak-runner.c b/src/plugins/flatpak/gbp-flatpak-runner.c
index f13f8c84b..2925784df 100644
--- a/src/plugins/flatpak/gbp-flatpak-runner.c
+++ b/src/plugins/flatpak/gbp-flatpak-runner.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-runner.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-runner"
@@ -66,7 +68,7 @@ gbp_flatpak_runner_fixup_launcher (IdeRunner             *runner,
   g_assert (IDE_IS_SUBPROCESS_LAUNCHER (launcher));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  config_manager = ide_context_get_configuration_manager (context);
+  config_manager = ide_configuration_manager_from_context (context);
   config = ide_configuration_manager_get_current (config_manager);
   app_id = ide_configuration_get_app_id (config);
 
@@ -150,9 +152,7 @@ gbp_flatpak_runner_new (IdeContext  *context,
 
   g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
 
-  self = g_object_new (GBP_TYPE_FLATPAK_RUNNER,
-                       "context", context,
-                       NULL);
+  self = g_object_new (GBP_TYPE_FLATPAK_RUNNER, NULL);
 
   if (binary_path != NULL)
     ide_runner_append_argv (IDE_RUNNER (self), binary_path);
diff --git a/src/plugins/flatpak/gbp-flatpak-runner.h b/src/plugins/flatpak/gbp-flatpak-runner.h
index dab11b0cf..0f7d226fd 100644
--- a/src/plugins/flatpak/gbp-flatpak-runner.h
+++ b/src/plugins/flatpak/gbp-flatpak-runner.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-runner.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/gbp-flatpak-runtime-provider.c 
b/src/plugins/flatpak/gbp-flatpak-runtime-provider.c
index 9bbdcfb7b..3e71a0f77 100644
--- a/src/plugins/flatpak/gbp-flatpak-runtime-provider.c
+++ b/src/plugins/flatpak/gbp-flatpak-runtime-provider.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-runtime-provider.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-runtime-provider"
@@ -22,8 +24,6 @@
 #include <ostree.h>
 #include <string.h>
 
-#include "util/ide-posix.h"
-
 #include "gbp-flatpak-application-addin.h"
 #include "gbp-flatpak-manifest.h"
 #include "gbp-flatpak-runtime.h"
@@ -55,7 +55,7 @@ typedef struct
 
 struct _GbpFlatpakRuntimeProvider
 {
-  GObject            parent_instance;
+  IdeObject          parent_instance;
   IdeRuntimeManager *manager;
   GPtrArray         *runtimes;
 };
@@ -66,7 +66,7 @@ static void gbp_flatpak_runtime_provider_load   (IdeRuntimeProvider          *pr
 static void gbp_flatpak_runtime_provider_unload (IdeRuntimeProvider          *provider,
                                                  IdeRuntimeManager           *manager);
 
-G_DEFINE_TYPE_WITH_CODE (GbpFlatpakRuntimeProvider, gbp_flatpak_runtime_provider, G_TYPE_OBJECT,
+G_DEFINE_TYPE_WITH_CODE (GbpFlatpakRuntimeProvider, gbp_flatpak_runtime_provider, IDE_TYPE_OBJECT,
                          G_IMPLEMENT_INTERFACE (IDE_TYPE_RUNTIME_PROVIDER, runtime_provider_iface_init))
 
 static void
@@ -113,6 +113,20 @@ is_same_runtime (GbpFlatpakRuntime   *runtime,
                      gbp_flatpak_runtime_get_branch (runtime)) == 0);
 }
 
+static void
+monitor_transfer (GbpFlatpakRuntimeProvider *self,
+                  GbpFlatpakTransfer        *transfer)
+{
+  g_autoptr(IdeNotification) notif = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_FLATPAK_RUNTIME_PROVIDER (self));
+  g_assert (GBP_IS_FLATPAK_TRANSFER (transfer));
+
+  notif = ide_transfer_create_notification (IDE_TRANSFER (transfer));
+  ide_notification_attach (notif, IDE_OBJECT (self));
+}
+
 static void
 runtime_added_cb (GbpFlatpakRuntimeProvider  *self,
                   FlatpakInstalledRef        *ref,
@@ -121,7 +135,6 @@ runtime_added_cb (GbpFlatpakRuntimeProvider  *self,
   g_autoptr(GbpFlatpakRuntime) new_runtime = NULL;
   g_autoptr(GError) error = NULL;
   const gchar *name;
-  IdeContext *context;
 
   IDE_ENTRY;
 
@@ -151,13 +164,16 @@ runtime_added_cb (GbpFlatpakRuntimeProvider  *self,
    * We didn't already have this runtime, so go ahead and just
    * add it now (and keep a copy so we can find it later).
    */
-  context = ide_object_get_context (IDE_OBJECT (self->manager));
-  new_runtime = gbp_flatpak_runtime_new (context, ref, NULL, &error);
+  new_runtime = gbp_flatpak_runtime_new (ref, NULL, &error);
 
   if (new_runtime == NULL)
-    g_warning ("Failed to create GbpFlatpakRuntime: %s", error->message);
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+        g_warning ("Failed to create GbpFlatpakRuntime: %s", error->message);
+    }
   else
     {
+      ide_object_append (IDE_OBJECT (self), IDE_OBJECT (new_runtime));
       ide_runtime_manager_add (self->manager, IDE_RUNTIME (new_runtime));
       g_ptr_array_add (self->runtimes, g_steal_pointer (&new_runtime));
     }
@@ -325,6 +341,7 @@ gbp_flatpak_runtime_provider_locate_sdk_cb (GObject      *object,
   g_autoptr(IdeTask) task = user_data;
   g_autoptr(GError) error = NULL;
   g_autofree gchar *docs_id = NULL;
+  GbpFlatpakRuntimeProvider *self;
   IdeTransferManager *transfer_manager;
   InstallRuntime *install;
   GCancellable *cancellable;
@@ -337,13 +354,15 @@ gbp_flatpak_runtime_provider_locate_sdk_cb (GObject      *object,
   g_assert (IDE_IS_TASK (task));
   g_assert (!ide_task_get_completed (task));
 
+  self = ide_task_get_source_object (task);
   install = ide_task_get_task_data (task);
   cancellable = ide_task_get_cancellable (task);
 
+  g_assert (GBP_IS_FLATPAK_RUNTIME_PROVIDER (self));
   g_assert (install != NULL);
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  transfer_manager = ide_application_get_transfer_manager (IDE_APPLICATION_DEFAULT);
+  transfer_manager = ide_transfer_manager_get_default ();
 
   if (!gbp_flatpak_application_addin_locate_sdk_finish (app_addin,
                                                         result,
@@ -375,6 +394,7 @@ gbp_flatpak_runtime_provider_locate_sdk_cb (GObject      *object,
                                            install->arch,
                                            install->branch,
                                            FALSE);
+      monitor_transfer (self, transfer);
       ide_transfer_manager_execute_async (transfer_manager,
                                           IDE_TRANSFER (transfer),
                                           cancellable,
@@ -400,6 +420,7 @@ gbp_flatpak_runtime_provider_locate_sdk_cb (GObject      *object,
                                            install->sdk_arch,
                                            install->sdk_branch,
                                            FALSE);
+      monitor_transfer (self, transfer);
       ide_transfer_manager_execute_async (transfer_manager,
                                           IDE_TRANSFER (transfer),
                                           cancellable,
@@ -423,6 +444,7 @@ gbp_flatpak_runtime_provider_locate_sdk_cb (GObject      *object,
                                            install->arch,
                                            install->branch,
                                            FALSE);
+      monitor_transfer (self, transfer);
       ide_transfer_manager_execute_async (transfer_manager,
                                           IDE_TRANSFER (transfer),
                                           cancellable,
@@ -581,7 +603,7 @@ gbp_flatpak_runtime_provider_bootstrap_cb (GObject      *object,
                                     state->branch);
 
       context = ide_object_get_context (IDE_OBJECT (self->manager));
-      runtime_manager = ide_context_get_runtime_manager (context);
+      runtime_manager = ide_runtime_manager_from_context (context);
       runtime = ide_runtime_manager_get_runtime (runtime_manager, runtime_id);
 
       if (runtime == NULL)
@@ -654,7 +676,7 @@ gbp_flatpak_runtime_provider_bootstrap_async (IdeRuntimeProvider  *provider,
       GbpFlatpakApplicationAddin *addin;
       const gchar * const *sdk_exts;
 
-      transfer_manager = ide_application_get_transfer_manager (IDE_APPLICATION_DEFAULT);
+      transfer_manager = ide_transfer_manager_get_default ();
       addin = gbp_flatpak_application_addin_get_default ();
       sdk_exts = gbp_flatpak_manifest_get_sdk_extensions (GBP_FLATPAK_MANIFEST (state->config));
 
diff --git a/src/plugins/flatpak/gbp-flatpak-runtime-provider.h 
b/src/plugins/flatpak/gbp-flatpak-runtime-provider.h
index 058ce5900..25aab0cb2 100644
--- a/src/plugins/flatpak/gbp-flatpak-runtime-provider.h
+++ b/src/plugins/flatpak/gbp-flatpak-runtime-provider.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-runtime-provider.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
@@ -24,6 +26,6 @@ G_BEGIN_DECLS
 
 #define GBP_TYPE_FLATPAK_RUNTIME_PROVIDER (gbp_flatpak_runtime_provider_get_type())
 
-G_DECLARE_FINAL_TYPE (GbpFlatpakRuntimeProvider, gbp_flatpak_runtime_provider, GBP, 
FLATPAK_RUNTIME_PROVIDER, GObject)
+G_DECLARE_FINAL_TYPE (GbpFlatpakRuntimeProvider, gbp_flatpak_runtime_provider, GBP, 
FLATPAK_RUNTIME_PROVIDER, IdeObject)
 
 G_END_DECLS
diff --git a/src/plugins/flatpak/gbp-flatpak-runtime.c b/src/plugins/flatpak/gbp-flatpak-runtime.c
index 9536eb9ce..d53a2ef7c 100644
--- a/src/plugins/flatpak/gbp-flatpak-runtime.c
+++ b/src/plugins/flatpak/gbp-flatpak-runtime.c
@@ -1,6 +1,6 @@
 /* gb-flatpak-runtime.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-runtime"
@@ -67,20 +69,19 @@ strv_empty (gchar **strv)
 static const gchar *
 get_builddir (GbpFlatpakRuntime *self)
 {
-  IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
-  IdeBuildManager *build_manager = ide_context_get_build_manager (context);
-  IdeBuildPipeline *pipeline = ide_build_manager_get_pipeline (build_manager);
-  const gchar *builddir = ide_build_pipeline_get_builddir (pipeline);
+  g_autoptr(IdeContext) context = ide_object_ref_context (IDE_OBJECT (self));
+  g_autoptr(IdeBuildManager) build_manager = ide_build_manager_ref_from_context (context);
+  g_autoptr(IdeBuildPipeline) pipeline = ide_build_manager_ref_pipeline (build_manager);
 
-  return builddir;
+  return ide_build_pipeline_get_builddir (pipeline);
 }
 
 static gchar *
 get_staging_directory (GbpFlatpakRuntime *self)
 {
-  IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
-  IdeBuildManager *build_manager = ide_context_get_build_manager (context);
-  IdeBuildPipeline *pipeline = ide_build_manager_get_pipeline (build_manager);
+  g_autoptr(IdeContext) context = ide_object_ref_context (IDE_OBJECT (self));
+  g_autoptr(IdeBuildManager) build_manager = ide_build_manager_ref_from_context (context);
+  g_autoptr(IdeBuildPipeline) pipeline = ide_build_manager_ref_pipeline (build_manager);
 
   return gbp_flatpak_get_staging_dir (pipeline);
 }
@@ -141,7 +142,7 @@ gbp_flatpak_runtime_contains_program_in_path (IdeRuntime   *runtime,
                                      NULL);
             }
 
-          return !dzl_str_empty0 (stdout_buf);
+          return !ide_str_empty0 (stdout_buf);
         }
     }
 
@@ -167,21 +168,21 @@ gbp_flatpak_runtime_create_launcher (IdeRuntime  *runtime,
       const gchar *builddir = NULL;
       const gchar *project_path = NULL;
       const gchar * const *build_args = NULL;
-      IdeContext *context;
-      IdeConfigurationManager *config_manager;
+      g_autoptr(IdeConfigurationManager) config_manager = NULL;
+      g_autoptr(IdeContext) context = NULL;
       IdeConfiguration *configuration;
       IdeVcs *vcs;
 
-      context = ide_object_get_context (IDE_OBJECT (self));
-      config_manager = ide_context_get_configuration_manager (context);
-      configuration = ide_configuration_manager_get_current (config_manager);
+      context = ide_object_ref_context (IDE_OBJECT (self));
+      config_manager = ide_configuration_manager_ref_from_context (context);
+      configuration = ide_configuration_manager_ref_current (config_manager);
 
       build_path = get_staging_directory (self);
       builddir = get_builddir (self);
 
       /* Find the project directory path */
-      vcs = ide_context_get_vcs (context);
-      project_path = g_file_peek_path (ide_vcs_get_working_directory (vcs));
+      vcs = ide_vcs_ref_from_context (context);
+      project_path = g_file_peek_path (ide_vcs_get_workdir (vcs));
 
       /* Add 'flatpak build' and the specified arguments to the launcher */
       ide_subprocess_launcher_push_argv (ret, "flatpak");
@@ -208,7 +209,7 @@ gbp_flatpak_runtime_create_launcher (IdeRuntime  *runtime,
                                      NULL);
       ide_subprocess_launcher_setenv (ret, "CCACHE_DIR", ccache_dir, FALSE);
 
-      if (!dzl_str_empty0 (project_path))
+      if (!ide_str_empty0 (project_path))
         {
           g_autofree gchar *filesystem_option_src = NULL;
           g_autofree gchar *filesystem_option_build = NULL;
@@ -272,7 +273,7 @@ get_binary_name (GbpFlatpakRuntime *self,
                  IdeBuildTarget    *build_target)
 {
   IdeContext *context = ide_object_get_context (IDE_OBJECT (self));
-  IdeConfigurationManager *config_manager = ide_context_get_configuration_manager (context);
+  IdeConfigurationManager *config_manager = ide_configuration_manager_from_context (context);
   IdeConfiguration *config = ide_configuration_manager_get_current (config_manager);
   g_autofree gchar *build_target_name = ide_build_target_get_name (build_target);
   g_auto(GStrv) argv = ide_build_target_get_argv (build_target);
@@ -290,21 +291,16 @@ get_binary_name (GbpFlatpakRuntime *self,
       const gchar *command;
 
       command = gbp_flatpak_manifest_get_command (GBP_FLATPAK_MANIFEST (config));
-      if (!dzl_str_empty0 (command))
+      if (!ide_str_empty0 (command))
         return g_strdup (command);
     }
 
   /* Use the build target name if there's no command in the manifest */
-  if (!dzl_str_empty0 (build_target_name))
+  if (!ide_str_empty0 (build_target_name))
     return g_steal_pointer (&build_target_name);
 
-  /* Use the project name as a last resort */
-  {
-    IdeProject *project;
-
-    project = ide_context_get_project (context);
-    return g_strdup (ide_project_get_name (project));
-  }
+  /* Use the project id as a last resort */
+  return ide_context_dup_project_id (context);
 }
 
 static IdeRunner *
@@ -326,7 +322,9 @@ gbp_flatpak_runtime_create_runner (IdeRuntime     *runtime,
   if (build_target != NULL)
     binary_name = get_binary_name (self, build_target);
 
-  runner = IDE_RUNNER (gbp_flatpak_runner_new (context, build_path, binary_name));
+  if ((runner = IDE_RUNNER (gbp_flatpak_runner_new (context, build_path, binary_name))))
+    ide_object_append (IDE_OBJECT (self), IDE_OBJECT (runner));
+
   if (build_target != NULL)
     ide_runner_set_build_target (runner, build_target);
 
@@ -772,8 +770,7 @@ locate_deploy_dir (const gchar *sdk_id)
 }
 
 GbpFlatpakRuntime *
-gbp_flatpak_runtime_new (IdeContext           *context,
-                         FlatpakInstalledRef  *ref,
+gbp_flatpak_runtime_new (FlatpakInstalledRef  *ref,
                          GCancellable         *cancellable,
                          GError              **error)
 {
@@ -785,30 +782,54 @@ gbp_flatpak_runtime_new (IdeContext           *context,
   g_autofree gchar *display_name = NULL;
   g_autofree gchar *triplet = NULL;
   g_autoptr(IdeTriplet) triplet_object = NULL;
+  g_autoptr(GString) category = NULL;
   const gchar *name;
   const gchar *arch;
   const gchar *branch;
   const gchar *deploy_dir;
 
-  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
   g_return_val_if_fail (FLATPAK_IS_INSTALLED_REF (ref), NULL);
+  g_return_val_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable), NULL);
 
-  name = flatpak_ref_get_name (FLATPAK_REF (ref));
   arch = flatpak_ref_get_arch (FLATPAK_REF (ref));
+
+  name = flatpak_ref_get_name (FLATPAK_REF (ref));
   branch = flatpak_ref_get_branch (FLATPAK_REF (ref));
   deploy_dir = flatpak_installed_ref_get_deploy_dir (ref);
   triplet_object = ide_triplet_new (arch);
   triplet = g_strdup_printf ("%s/%s/%s", name, arch, branch);
   id = g_strdup_printf ("flatpak:%s", triplet);
 
-  metadata = flatpak_installed_ref_load_metadata (ref, cancellable, error);
-  if (metadata == NULL)
+  category = g_string_new ("Flatpak/");
+
+  if (g_str_has_prefix (name, "org.gnome."))
+    g_string_append (category, "GNOME/");
+  else if (g_str_has_prefix (name, "org.freedesktop."))
+    g_string_append (category, "FreeDesktop.org/");
+  else if (g_str_has_prefix (name, "org.kde."))
+    g_string_append (category, "KDE/");
+
+  if (ide_str_equal0 (flatpak_get_default_arch (), arch))
+    g_string_append (category, name);
+  else
+    g_string_append_printf (category, "%s (%s)", name, arch);
+
+  if (!(metadata = flatpak_installed_ref_load_metadata (ref, cancellable, error)))
     return NULL;
 
   keyfile = g_key_file_new ();
   if (!g_key_file_load_from_bytes (keyfile, metadata, 0, error))
     return NULL;
 
+  if (g_key_file_has_group (keyfile, "ExtensionOf"))
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_NOT_SUPPORTED,
+                   "Runtime is an extension");
+      return NULL;
+    }
+
   sdk = g_key_file_get_string (keyfile, "Runtime", "sdk", NULL);
 
   if (g_str_equal (arch, flatpak_get_default_arch ()))
@@ -824,10 +845,10 @@ gbp_flatpak_runtime_new (IdeContext           *context,
     deploy_dir = sdk_deploy_dir;
 
   return g_object_new (GBP_TYPE_FLATPAK_RUNTIME,
-                       "context", context,
                        "id", id,
                        "triplet", triplet_object,
                        "branch", branch,
+                       "category", category->str,
                        "deploy-dir", deploy_dir,
                        "display-name", display_name,
                        "platform", name,
diff --git a/src/plugins/flatpak/gbp-flatpak-runtime.h b/src/plugins/flatpak/gbp-flatpak-runtime.h
index b147f9502..b41705ee0 100644
--- a/src/plugins/flatpak/gbp-flatpak-runtime.h
+++ b/src/plugins/flatpak/gbp-flatpak-runtime.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-runtime.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,12 +14,14 @@
  *
  * 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 <flatpak.h>
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
@@ -27,8 +29,7 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpFlatpakRuntime, gbp_flatpak_runtime, GBP, FLATPAK_RUNTIME, IdeRuntime)
 
-GbpFlatpakRuntime   *gbp_flatpak_runtime_new          (IdeContext           *context,
-                                                       FlatpakInstalledRef  *ref,
+GbpFlatpakRuntime   *gbp_flatpak_runtime_new          (FlatpakInstalledRef  *ref,
                                                        GCancellable         *cancellable,
                                                        GError              **error);
 IdeTriplet          *gbp_flatpak_runtime_get_triplet  (GbpFlatpakRuntime    *self);
diff --git a/src/plugins/flatpak/gbp-flatpak-sources.c b/src/plugins/flatpak/gbp-flatpak-sources.c
index 821f4315b..c7cc1a686 100644
--- a/src/plugins/flatpak/gbp-flatpak-sources.c
+++ b/src/plugins/flatpak/gbp-flatpak-sources.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <errno.h>
@@ -26,7 +28,7 @@
 
 /* This file includes modified code from
  * flatpak/builder/builder-source-archive.c
- * Written by Alexander Larsson, originally licensed under GPL 2.1.
+ * Written by Alexander Larsson, originally licensed under GPL 2.1+.
  * Copyright Red Hat, Inc. 2015
  */
 
diff --git a/src/plugins/flatpak/gbp-flatpak-sources.h b/src/plugins/flatpak/gbp-flatpak-sources.h
index 0726f3e6b..9e43c6086 100644
--- a/src/plugins/flatpak/gbp-flatpak-sources.h
+++ b/src/plugins/flatpak/gbp-flatpak-sources.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/flatpak/gbp-flatpak-subprocess-launcher.c 
b/src/plugins/flatpak/gbp-flatpak-subprocess-launcher.c
index d3762a07d..7cd275db6 100644
--- a/src/plugins/flatpak/gbp-flatpak-subprocess-launcher.c
+++ b/src/plugins/flatpak/gbp-flatpak-subprocess-launcher.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-subprocess-launcher.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-subprocess-launcher"
diff --git a/src/plugins/flatpak/gbp-flatpak-subprocess-launcher.h 
b/src/plugins/flatpak/gbp-flatpak-subprocess-launcher.h
index 1a6396749..4536d8054 100644
--- a/src/plugins/flatpak/gbp-flatpak-subprocess-launcher.h
+++ b/src/plugins/flatpak/gbp-flatpak-subprocess-launcher.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-subprocess-launcher.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-threading.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/gbp-flatpak-transfer.c b/src/plugins/flatpak/gbp-flatpak-transfer.c
index 3a7c33971..4a4ef344c 100644
--- a/src/plugins/flatpak/gbp-flatpak-transfer.c
+++ b/src/plugins/flatpak/gbp-flatpak-transfer.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-transfer.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-flatpak-transfer"
@@ -123,20 +125,20 @@ task_completed (GbpFlatpakTransfer *self,
 static void
 proxy_notify (GbpFlatpakTransfer *self,
               GParamSpec         *pspec,
-              IdeProgress        *progress)
+              IdeNotification    *progress)
 {
   g_assert (GBP_IS_FLATPAK_TRANSFER (self));
   g_assert (pspec != NULL);
-  g_assert (IDE_IS_PROGRESS (progress));
+  g_assert (IDE_IS_NOTIFICATION (progress));
 
-  if (g_strcmp0 (pspec->name, "message") == 0)
+  if (g_strcmp0 (pspec->name, "body") == 0)
     {
-      g_autofree gchar *message = ide_progress_get_message (progress);
+      g_autofree gchar *message = ide_notification_dup_body (progress);
       ide_transfer_set_status (IDE_TRANSFER (self), message);
     }
 
-  if (g_strcmp0 (pspec->name, "fraction") == 0)
-    ide_transfer_set_progress (IDE_TRANSFER (self), ide_progress_get_fraction (progress));
+  if (g_strcmp0 (pspec->name, "progress") == 0)
+    ide_transfer_set_progress (IDE_TRANSFER (self), ide_notification_get_progress (progress));
 }
 
 static void
@@ -170,7 +172,7 @@ gbp_flatpak_transfer_execute_async (IdeTransfer         *transfer,
   GbpFlatpakTransfer *self = (GbpFlatpakTransfer *)transfer;
   GbpFlatpakApplicationAddin *addin;
   g_autoptr(IdeTask) task = NULL;
-  g_autoptr(IdeProgress) progress = NULL;
+  g_autoptr(IdeNotification) progress = NULL;
 
   IDE_ENTRY;
 
@@ -218,13 +220,13 @@ gbp_flatpak_transfer_execute_async (IdeTransfer         *transfer,
                                                        g_steal_pointer (&task));
 
   g_signal_connect_object (progress,
-                           "notify::fraction",
+                           "notify::progress",
                            G_CALLBACK (proxy_notify),
                            self,
                            G_CONNECT_SWAPPED);
 
   g_signal_connect_object (progress,
-                           "notify::message",
+                           "notify::body",
                            G_CALLBACK (proxy_notify),
                            self,
                            G_CONNECT_SWAPPED);
diff --git a/src/plugins/flatpak/gbp-flatpak-transfer.h b/src/plugins/flatpak/gbp-flatpak-transfer.h
index 31f4df12b..5091cd294 100644
--- a/src/plugins/flatpak/gbp-flatpak-transfer.h
+++ b/src/plugins/flatpak/gbp-flatpak-transfer.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-transfer.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
@@ -26,9 +28,9 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpFlatpakTransfer, gbp_flatpak_transfer, GBP, FLATPAK_TRANSFER, IdeTransfer)
 
-GbpFlatpakTransfer *gbp_flatpak_transfer_new (const gchar        *id,
-                                              const gchar        *arch,
-                                              const gchar        *branch,
-                                              gboolean            force_update);
+GbpFlatpakTransfer *gbp_flatpak_transfer_new (const gchar *id,
+                                              const gchar *arch,
+                                              const gchar *branch,
+                                              gboolean     force_update);
 
 G_END_DECLS
diff --git a/src/plugins/flatpak/gbp-flatpak-util.c b/src/plugins/flatpak/gbp-flatpak-util.c
index e20995c92..2bf9cd5db 100644
--- a/src/plugins/flatpak/gbp-flatpak-util.c
+++ b/src/plugins/flatpak/gbp-flatpak-util.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-util.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,12 +14,16 @@
  *
  * 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-flatpak-util"
 
 #include <flatpak.h>
 #include <string.h>
+#include <libide-foundry.h>
+#include <libide-vcs.h>
 
 #include "gbp-flatpak-util.h"
 
@@ -35,16 +39,16 @@ gbp_flatpak_get_staging_dir (IdeBuildPipeline *pipeline)
   g_autofree gchar *branch = NULL;
   g_autofree gchar *name = NULL;
   g_autoptr (IdeTriplet) triplet = NULL;
-  IdeContext *context;
-  IdeVcs *vcs;
-  IdeToolchain *toolchain;
+  g_autoptr(IdeContext) context = NULL;
+  g_autoptr(IdeVcs) vcs = NULL;
+  g_autoptr(IdeToolchain) toolchain = NULL;
 
   g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
-  context = ide_object_get_context (IDE_OBJECT (pipeline));
-  vcs = ide_context_get_vcs (context);
+  context = ide_object_ref_context (IDE_OBJECT (pipeline));
+  vcs = ide_vcs_ref_from_context (context);
   branch = ide_vcs_get_branch_name (vcs);
-  toolchain = ide_build_pipeline_get_toolchain (pipeline);
+  toolchain = ide_build_pipeline_ref_toolchain (pipeline);
   triplet = ide_toolchain_get_host_triplet (toolchain);
   name = g_strdup_printf ("%s-%s", ide_triplet_get_arch (triplet), branch);
 
diff --git a/src/plugins/flatpak/gbp-flatpak-util.h b/src/plugins/flatpak/gbp-flatpak-util.h
index 78818eaa8..eb599badd 100644
--- a/src/plugins/flatpak/gbp-flatpak-util.h
+++ b/src/plugins/flatpak/gbp-flatpak-util.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-util.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/gbp-flatpak-workbench-addin.c 
b/src/plugins/flatpak/gbp-flatpak-workbench-addin.c
index 397e6eba8..4dbceb4cd 100644
--- a/src/plugins/flatpak/gbp-flatpak-workbench-addin.c
+++ b/src/plugins/flatpak/gbp-flatpak-workbench-addin.c
@@ -1,6 +1,6 @@
 /* gbp-flatpak-workbench-addin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,14 @@
  *
  * 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-flatpak-workbench-addin"
 
 #include <glib/gi18n.h>
+#include <libide-io.h>
 
 #include "gbp-flatpak-application-addin.h"
 #include "gbp-flatpak-download-stage.h"
@@ -30,7 +33,7 @@ struct _GbpFlatpakWorkbenchAddin
 
   GSimpleActionGroup  *actions;
   IdeWorkbench        *workbench;
-  IdeWorkbenchMessage *message;
+  IdeNotification     *message;
 };
 
 static void
@@ -39,13 +42,13 @@ check_sysdeps_cb (GObject      *object,
                   gpointer      user_data)
 {
   GbpFlatpakApplicationAddin *app_addin = (GbpFlatpakApplicationAddin *)object;
-  g_autoptr(IdeWorkbenchMessage) message = user_data;
+  g_autoptr(GbpFlatpakWorkbenchAddin) self = user_data;
   g_autoptr(GError) error = NULL;
   gboolean has_sysdeps;
 
   g_assert (GBP_IS_FLATPAK_APPLICATION_ADDIN (app_addin));
   g_assert (G_IS_ASYNC_RESULT (result));
-  g_assert (IDE_IS_WORKBENCH_MESSAGE (message));
+  g_assert (GBP_IS_FLATPAK_WORKBENCH_ADDIN (self));
 
   has_sysdeps = gbp_flatpak_application_addin_check_sysdeps_finish (app_addin, result, &error);
 
@@ -54,57 +57,95 @@ check_sysdeps_cb (GObject      *object,
     IDE_TRACE_MSG ("which flatpak-builder resulted in %s", error->message);
 #endif
 
-  gtk_widget_set_visible (GTK_WIDGET (message), has_sysdeps == FALSE);
+  if (!has_sysdeps)
+    {
+      IdeContext *context;
+
+      context = ide_workbench_get_context (self->workbench);
+      ide_notification_attach (self->message, IDE_OBJECT (context));
+    }
 }
 
 static void
-gbp_flatpak_workbench_addin_load (IdeWorkbenchAddin *addin,
-                                  IdeWorkbench      *workbench)
+gbp_flatpak_workbench_addin_workspace_added (IdeWorkbenchAddin *addin,
+                                             IdeWorkspace      *workspace)
 {
   GbpFlatpakWorkbenchAddin *self = (GbpFlatpakWorkbenchAddin *)addin;
   GbpFlatpakApplicationAddin *app_addin;
-  IdeContext *context;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (GBP_IS_FLATPAK_WORKBENCH_ADDIN (self));
-  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (IDE_IS_WORKSPACE (workspace));
 
-  self->workbench = workbench;
+  if (!IDE_IS_PRIMARY_WORKSPACE (workspace))
+    return;
 
-  context = ide_workbench_get_context (workbench);
-  if (context != NULL)
-    gtk_widget_insert_action_group (GTK_WIDGET (workbench), "flatpak", G_ACTION_GROUP (self->actions));
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "flatpak",
+                                  G_ACTION_GROUP (self->actions));
 
-  self->message = g_object_new (IDE_TYPE_WORKBENCH_MESSAGE,
-                                "id", "org.gnome.builder.flatpak.install",
-                                "title", _("Your computer is missing flatpak-builder"),
-                                "subtitle", _("This program is necessary for building Flatpak applications. 
Would you like to install it?"),
-                                "show-close-button", TRUE,
-                                "visible", FALSE,
+  self->message = g_object_new (IDE_TYPE_NOTIFICATION,
+                                "title", _("Missing system dependencies"),
+                                "body", _("The “flatpak-builder” program is necessary for building 
Flatpak-based applications. Builder can install it for you."),
+                                "icon-name", "dialog-warning-symbolic",
+                                "urgent", TRUE,
                                 NULL);
-  ide_workbench_message_add_action (self->message, _("Install"), "flatpak.install-flatpak-builder");
-  ide_workbench_push_message (workbench, self->message);
+  ide_notification_add_button (self->message,
+                               _("Install"),
+                               NULL,
+                               "flatpak.install-flatpak-builder");
 
   app_addin = gbp_flatpak_application_addin_get_default ();
   gbp_flatpak_application_addin_check_sysdeps_async (app_addin,
                                                      NULL,
                                                      check_sysdeps_cb,
-                                                     g_object_ref (self->message));
+                                                     g_object_ref (self));
+
 }
 
 static void
-gbp_flatpak_workbench_addin_unload (IdeWorkbenchAddin *addin,
-                                    IdeWorkbench      *workbench)
+gbp_flatpak_workbench_addin_workspace_removed (IdeWorkbenchAddin *addin,
+                                               IdeWorkspace      *workspace)
 {
   GbpFlatpakWorkbenchAddin *self = (GbpFlatpakWorkbenchAddin *)addin;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_FLATPAK_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  if (!IDE_IS_PRIMARY_WORKSPACE (workspace))
+    return;
+
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "flatpak", NULL);
+
+  if (self->message != NULL)
+    {
+      ide_notification_withdraw (self->message);
+      g_clear_object (&self->message);
+    }
+}
+
+static void
+gbp_flatpak_workbench_addin_load (IdeWorkbenchAddin *addin,
+                                  IdeWorkbench      *workbench)
+{
+  GbpFlatpakWorkbenchAddin *self = (GbpFlatpakWorkbenchAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (GBP_IS_FLATPAK_WORKBENCH_ADDIN (self));
   g_assert (IDE_IS_WORKBENCH (workbench));
 
-  gtk_widget_insert_action_group (GTK_WIDGET (workbench), "flatpak", NULL);
+  self->workbench = workbench;
+}
+
+static void
+gbp_flatpak_workbench_addin_unload (IdeWorkbenchAddin *addin,
+                                    IdeWorkbench      *workbench)
+{
+  GbpFlatpakWorkbenchAddin *self = (GbpFlatpakWorkbenchAddin *)addin;
 
-  gtk_widget_destroy (GTK_WIDGET (self->message));
+  g_assert (GBP_IS_FLATPAK_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
 
-  self->message = NULL;
   self->workbench = NULL;
 }
 
@@ -113,6 +154,8 @@ workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
 {
   iface->load = gbp_flatpak_workbench_addin_load;
   iface->unload = gbp_flatpak_workbench_addin_unload;
+  iface->workspace_added = gbp_flatpak_workbench_addin_workspace_added;
+  iface->workspace_removed = gbp_flatpak_workbench_addin_workspace_removed;
 }
 
 static void
@@ -146,7 +189,7 @@ gbp_flatpak_workbench_addin_install_cb (GObject      *object,
   else
     {
       IdeContext *context = ide_workbench_get_context (self->workbench);
-      IdeConfigurationManager *config_manager = ide_context_get_configuration_manager (context);
+      IdeConfigurationManager *config_manager = ide_configuration_manager_from_context (context);
 
       /* TODO: It would be nice to have a cleaner way to re-setup the pipeline
        *       because we know it is invalidated.
@@ -178,7 +221,7 @@ gbp_flatpak_workbench_addin_install_flatpak_builder (GSimpleAction *action,
   g_assert (GBP_IS_FLATPAK_WORKBENCH_ADDIN (self));
 
   transfer = ide_pkcon_transfer_new (packages);
-  manager = ide_application_get_transfer_manager (IDE_APPLICATION_DEFAULT);
+  manager = ide_transfer_manager_get_default ();
 
   g_simple_action_set_enabled (action, FALSE);
 
diff --git a/src/plugins/flatpak/gbp-flatpak-workbench-addin.h 
b/src/plugins/flatpak/gbp-flatpak-workbench-addin.h
index eaf7be930..4c13b0066 100644
--- a/src/plugins/flatpak/gbp-flatpak-workbench-addin.h
+++ b/src/plugins/flatpak/gbp-flatpak-workbench-addin.h
@@ -1,6 +1,6 @@
 /* gbp-flatpak-workbench-addin.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/flatpak/meson.build b/src/plugins/flatpak/meson.build
index 28982bb74..c0f8746b7 100644
--- a/src/plugins/flatpak/meson.build
+++ b/src/plugins/flatpak/meson.build
@@ -1,63 +1,44 @@
-if get_option('with_flatpak')
+if get_option('plugin_flatpak')
 
-flatpak_resources = gnome.compile_resources(
-  'flatpak-resources',
-  'flatpak.gresource.xml',
-  c_name: 'gbp_flatpak'
-)
+if not get_option('plugin_git')
+  error('-Dplugin_git=true is required for flatpak')
+endif
 
-flatpak_sources = [
+plugins_sources += files([
+  'flatpak-plugin.c',
   'gbp-flatpak-application-addin.c',
-  'gbp-flatpak-application-addin.h',
   'gbp-flatpak-build-system-discovery.c',
-  'gbp-flatpak-build-system-discovery.h',
-  'gbp-flatpak-build-target.c',
-  'gbp-flatpak-build-target.h',
   'gbp-flatpak-build-target-provider.c',
-  'gbp-flatpak-build-target-provider.h',
+  'gbp-flatpak-build-target.c',
   'gbp-flatpak-clone-widget.c',
-  'gbp-flatpak-clone-widget.h',
   'gbp-flatpak-configuration-provider.c',
-  'gbp-flatpak-configuration-provider.h',
   'gbp-flatpak-dependency-updater.c',
-  'gbp-flatpak-dependency-updater.h',
   'gbp-flatpak-download-stage.c',
-  'gbp-flatpak-download-stage.h',
-  'gbp-flatpak-genesis-addin.c',
-  'gbp-flatpak-genesis-addin.h',
   'gbp-flatpak-manifest.c',
-  'gbp-flatpak-manifest.h',
   'gbp-flatpak-pipeline-addin.c',
-  'gbp-flatpak-pipeline-addin.h',
   'gbp-flatpak-preferences-addin.c',
-  'gbp-flatpak-preferences-addin.h',
-  'gbp-flatpak-plugin.c',
   'gbp-flatpak-runner.c',
-  'gbp-flatpak-runner.h',
   'gbp-flatpak-runtime-provider.c',
-  'gbp-flatpak-runtime-provider.h',
   'gbp-flatpak-runtime.c',
-  'gbp-flatpak-runtime.h',
   'gbp-flatpak-sources.c',
-  'gbp-flatpak-sources.h',
   'gbp-flatpak-subprocess-launcher.c',
-  'gbp-flatpak-subprocess-launcher.h',
   'gbp-flatpak-transfer.c',
-  'gbp-flatpak-transfer.h',
   'gbp-flatpak-util.c',
-  'gbp-flatpak-util.h',
   'gbp-flatpak-workbench-addin.c',
-  'gbp-flatpak-workbench-addin.h',
-]
+])
+
+plugin_flatpak_resources = gnome.compile_resources(
+  'flatpak-resources',
+  'flatpak.gresource.xml',
+  c_name: 'gbp_flatpak'
+)
 
-gnome_builder_plugins_deps += [
+plugins_deps += [
   dependency('flatpak', version: '>= 0.8.0'),
   dependency('ostree-1'),
   dependency('libsoup-2.4', version: '>= 2.52.0'),
-  libgit_dep,
 ]
 
-gnome_builder_plugins_sources += files(flatpak_sources)
-gnome_builder_plugins_sources += flatpak_resources[0]
+plugins_sources += plugin_flatpak_resources[0]
 
 endif
diff --git a/src/plugins/gcc/gbp-gcc-pipeline-addin.c b/src/plugins/gcc/gbp-gcc-pipeline-addin.c
index 6d43654ae..65766f0d3 100644
--- a/src/plugins/gcc/gbp-gcc-pipeline-addin.c
+++ b/src/plugins/gcc/gbp-gcc-pipeline-addin.c
@@ -1,6 +1,6 @@
 /* gbp-gcc-pipeline-addin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,10 +14,16 @@
  *
  * 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-gcc-pipeline-addin"
 
+#include "config.h"
+
+#include "libide-foundry.h"
+
 #include "gbp-gcc-pipeline-addin.h"
 
 #define ERROR_FORMAT_REGEX                  \
diff --git a/src/plugins/gcc/gbp-gcc-pipeline-addin.h b/src/plugins/gcc/gbp-gcc-pipeline-addin.h
index fe6ed2bde..f54189204 100644
--- a/src/plugins/gcc/gbp-gcc-pipeline-addin.h
+++ b/src/plugins/gcc/gbp-gcc-pipeline-addin.h
@@ -1,6 +1,6 @@
 /* gbp-gcc-pipeline-addin.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/gcc/gbp-gcc-toolchain-provider.c b/src/plugins/gcc/gbp-gcc-toolchain-provider.c
index 42bd13090..564245c75 100644
--- a/src/plugins/gcc/gbp-gcc-toolchain-provider.c
+++ b/src/plugins/gcc/gbp-gcc-toolchain-provider.c
@@ -16,11 +16,14 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gbp-gcc-toolchain-provider"
 
 #include <glib/gi18n.h>
+#include <libide-foundry.h>
 
 #include "gbp-gcc-toolchain-provider.h"
 
@@ -80,13 +83,11 @@ gbp_gcc_toolchain_provider_get_toolchain_from_file (GbpGccToolchainProvider *sel
   g_autofree gchar *sdk_ld_path = NULL;
   g_autofree gchar *sdk_strip_path = NULL;
   g_autofree gchar *sdk_pkg_config_path = NULL;
-  IdeContext *context;
 
   gcc_path = g_file_get_path (file);
   toolchain_id = g_strdup_printf ("gcc:%s", gcc_path);
   display_name = g_strdup_printf (_("GCC %s Cross-Compiler (System)"), arch);
-  context = ide_object_get_context (IDE_OBJECT (self));
-  toolchain = ide_simple_toolchain_new (context, toolchain_id, display_name);
+  toolchain = ide_simple_toolchain_new (toolchain_id, display_name);
   ide_toolchain_set_host_triplet (IDE_TOOLCHAIN (toolchain), triplet);
   ide_simple_toolchain_set_tool_for_language (toolchain, IDE_TOOLCHAIN_LANGUAGE_C, IDE_TOOLCHAIN_TOOL_CC, 
gcc_path);
 
diff --git a/src/plugins/gcc/gbp-gcc-toolchain-provider.h b/src/plugins/gcc/gbp-gcc-toolchain-provider.h
index e9b217e72..c3c350811 100644
--- a/src/plugins/gcc/gbp-gcc-toolchain-provider.h
+++ b/src/plugins/gcc/gbp-gcc-toolchain-provider.h
@@ -16,11 +16,13 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #pragma once
 
-#include <ide.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/gcc/gcc-plugin.c b/src/plugins/gcc/gcc-plugin.c
new file mode 100644
index 000000000..87deae432
--- /dev/null
+++ b/src/plugins/gcc/gcc-plugin.c
@@ -0,0 +1,38 @@
+/* gbp-gcc-plugin.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+ */
+
+#include "config.h"
+
+#include <libide-foundry.h>
+#include <libpeas/peas.h>
+
+#include "gbp-gcc-pipeline-addin.h"
+#include "gbp-gcc-toolchain-provider.h"
+
+_IDE_EXTERN void
+_gbp_gcc_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                              GBP_TYPE_GCC_PIPELINE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_TOOLCHAIN_PROVIDER,
+                                              GBP_TYPE_GCC_TOOLCHAIN_PROVIDER);
+}
diff --git a/src/plugins/gcc/gcc.gresource.xml b/src/plugins/gcc/gcc.gresource.xml
index f9b9c6007..238baef47 100644
--- a/src/plugins/gcc/gcc.gresource.xml
+++ b/src/plugins/gcc/gcc.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/gcc">
     <file>gcc.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/gcc/gcc.plugin b/src/plugins/gcc/gcc.plugin
index e6b8fea5e..4bc7068ad 100644
--- a/src/plugins/gcc/gcc.plugin
+++ b/src/plugins/gcc/gcc.plugin
@@ -1,8 +1,9 @@
 [Plugin]
-Module=gcc-plugin
-Name=GCC
-Description=Provides various GCC integration hooks
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
-Embedded=gbp_gcc_register_types
 Builtin=true
+Copyright=Copyright © 2015-2018 Christian Hergert
+Description=Provides various GCC integration hooks
+Embedded=_gbp_gcc_register_types
+Hidden=true
+Module=gcc
+Name=GCC
diff --git a/src/plugins/gcc/meson.build b/src/plugins/gcc/meson.build
index 4127bde80..932bcdc92 100644
--- a/src/plugins/gcc/meson.build
+++ b/src/plugins/gcc/meson.build
@@ -1,20 +1,15 @@
-if get_option('with_gcc')
-
-gcc_resources = gnome.compile_resources(
-  'gcc-resources',
-  'gcc.gresource.xml',
-  c_name: 'gbp_gcc',
-)
-
-gcc_sources = [
+plugins_sources += files([
   'gbp-gcc-pipeline-addin.c',
   'gbp-gcc-pipeline-addin.h',
-  'gbp-gcc-plugin.c',
   'gbp-gcc-toolchain-provider.c',
   'gbp-gcc-toolchain-provider.h',
-]
+  'gcc-plugin.c',
+])
 
-gnome_builder_plugins_sources += files(gcc_sources)
-gnome_builder_plugins_sources += gcc_resources[0]
+plugin_gcc_resources = gnome.compile_resources(
+  'gcc-resources',
+  'gcc.gresource.xml',
+  c_name: 'gbp_gcc',
+)
 
-endif
+plugins_sources += plugin_gcc_resources[0]
diff --git a/src/plugins/gdb/gbp-gdb-debugger.c b/src/plugins/gdb/gbp-gdb-debugger.c
index 8b1eb0764..df5e9416e 100644
--- a/src/plugins/gdb/gbp-gdb-debugger.c
+++ b/src/plugins/gdb/gbp-gdb-debugger.c
@@ -1,6 +1,6 @@
 /* gbp-gdb-debugger.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,15 +14,17 @@
  *
  * 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-gdb-debugger"
 
 #include <dazzle.h>
+#include <libide-io.h>
 #include <string.h>
 
 #include "gbp-gdb-debugger.h"
-#include "util/ide-glib.h"
 
 #define READ_BUFFER_LEN 4096
 
@@ -77,35 +79,35 @@ G_DEFINE_TYPE (GbpGdbDebugger, gbp_gdb_debugger, IDE_TYPE_DEBUGGER)
   } G_STMT_END
 
 static void
-gbp_gdb_debugger_set_context (IdeObject  *object,
-                              IdeContext *context)
+gbp_gdb_debugger_parent_set (IdeObject *object,
+                             IdeObject *parent)
 {
   GbpGdbDebugger *self = (GbpGdbDebugger *)object;
+  IdeBuildManager *build_manager;
+  IdeBuildPipeline *pipeline;
+  const gchar *builddir;
+  IdeContext *context;
 
   g_assert (GBP_IS_GDB_DEBUGGER (self));
-  g_assert (!context || IDE_IS_CONTEXT (context));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
 
-  IDE_OBJECT_CLASS (gbp_gdb_debugger_parent_class)->set_context (object, context);
+  if (parent == NULL)
+    return;
 
-  if (context != NULL)
-    {
-      IdeBuildManager *build_manager;
-      IdeBuildPipeline *pipeline;
-      const gchar *builddir;
+  context = ide_object_get_context (parent);
 
-      /*
-       * We need to save the build directory so that we can translate
-       * relative paths coming from gdb into the path within the project
-       * source tree.
-       */
+  /*
+   * We need to save the build directory so that we can translate
+   * relative paths coming from gdb into the path within the project
+   * source tree.
+   */
 
-      build_manager = ide_context_get_build_manager (context);
-      pipeline = ide_build_manager_get_pipeline (build_manager);
-      builddir = ide_build_pipeline_get_builddir (pipeline);
+  build_manager = ide_build_manager_from_context (context);
+  pipeline = ide_build_manager_get_pipeline (build_manager);
+  builddir = ide_build_pipeline_get_builddir (pipeline);
 
-      g_clear_object (&self->builddir);
-      self->builddir = g_file_new_for_path (builddir);
-    }
+  g_clear_object (&self->builddir);
+  self->builddir = g_file_new_for_path (builddir);
 }
 
 static gchar *
@@ -2532,7 +2534,7 @@ gbp_gdb_debugger_class_init (GbpGdbDebuggerClass *klass)
   object_class->dispose = gbp_gdb_debugger_dispose;
   object_class->finalize = gbp_gdb_debugger_finalize;
 
-  ide_object_class->set_context = gbp_gdb_debugger_set_context;
+  ide_object_class->parent_set = gbp_gdb_debugger_parent_set;
 
   debugger_class->supports_runner = gbp_gdb_debugger_supports_runner;
   debugger_class->prepare = gbp_gdb_debugger_prepare;
@@ -2765,7 +2767,7 @@ gbp_gdb_debugger_write_cb (GObject      *object,
  * from the debugger with the result, or the connection has closed. Whichever
  * is first.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 gbp_gdb_debugger_exec_async (GbpGdbDebugger      *self,
@@ -2877,6 +2879,8 @@ gbp_gdb_debugger_exec_async (GbpGdbDebugger      *self,
  *
  * Returns: a gdbwire_mi_output which should be freed with
  *   gdbwire_mi_output_free() when no longer in use.
+ *
+ * Since: 3.32
  */
 struct gdbwire_mi_output *
 gbp_gdb_debugger_exec_finish (GbpGdbDebugger  *self,
diff --git a/src/plugins/gdb/gbp-gdb-debugger.h b/src/plugins/gdb/gbp-gdb-debugger.h
index 176bbaf20..b03116859 100644
--- a/src/plugins/gdb/gbp-gdb-debugger.h
+++ b/src/plugins/gdb/gbp-gdb-debugger.h
@@ -1,6 +1,6 @@
 /* gbp-gdb-debugger.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-debugger.h>
 
 #pragma GCC diagnostic push
 #pragma GCC diagnostic ignored "-Wredundant-decls"
diff --git a/src/plugins/gdb/gdb-plugin.c b/src/plugins/gdb/gdb-plugin.c
new file mode 100644
index 000000000..40ef19ace
--- /dev/null
+++ b/src/plugins/gdb/gdb-plugin.c
@@ -0,0 +1,34 @@
+/* gdb-plugin.c
+ *
+ * Copyright 2017-2019 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
+ */
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-debugger.h>
+
+#include "gbp-gdb-debugger.h"
+
+_IDE_EXTERN void
+_gbp_gdb_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_DEBUGGER,
+                                              GBP_TYPE_GDB_DEBUGGER);
+}
diff --git a/src/plugins/gdb/gdb.gresource.xml b/src/plugins/gdb/gdb.gresource.xml
index a899d1f22..16f6006cf 100644
--- a/src/plugins/gdb/gdb.gresource.xml
+++ b/src/plugins/gdb/gdb.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/gdb">
     <file>gdb.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/gdb/gdb.plugin b/src/plugins/gdb/gdb.plugin
index 2406f9a81..90600c78f 100644
--- a/src/plugins/gdb/gdb.plugin
+++ b/src/plugins/gdb/gdb.plugin
@@ -1,10 +1,11 @@
 [Plugin]
-Module=gdb-plugin
-Name=Gdb
-Description=Provides integration with the GNU Debugger
 Authors=Christian Hergert <chergert redhat com>
-Copyright=Copyright © 2017 Christian Hergert
-Depends=debugger;editor;terminal;
 Builtin=true
-Embedded=gbp_gdb_register_types
+Copyright=Copyright © 2017-2018 Christian Hergert
+Depends=editor;terminal;buildui;debuggerui;
+Description=Provides integration with the GNU Debugger
+Embedded=_gbp_gdb_register_types
+Hidden=true
+Module=gdb
+Name=Gdb
 X-Debugger-Languages=c,chdr,cpp,cpphdr,fortran,rust,vala,asm
diff --git a/src/plugins/gdb/gdbwire.c b/src/plugins/gdb/gdbwire.c
index d8663f156..a2e223300 100644
--- a/src/plugins/gdb/gdbwire.c
+++ b/src/plugins/gdb/gdbwire.c
@@ -17,6 +17,8 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with GDBWIRE.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 /***** Begin file gdbwire_sys.c **********************************************/
diff --git a/src/plugins/gdb/gdbwire.h b/src/plugins/gdb/gdbwire.h
index f48fe8c6b..36edc8525 100644
--- a/src/plugins/gdb/gdbwire.h
+++ b/src/plugins/gdb/gdbwire.h
@@ -17,6 +17,8 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with GDBWIRE.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 /***** Begin file gdbwire_sys.h **********************************************/
diff --git a/src/plugins/gdb/meson.build b/src/plugins/gdb/meson.build
index 06273593c..804213497 100644
--- a/src/plugins/gdb/meson.build
+++ b/src/plugins/gdb/meson.build
@@ -1,16 +1,17 @@
-if get_option('with_gdb')
+if get_option('plugin_gdb')
 
-gdb_resources = gnome.compile_resources(
+plugins_sources += files([
+  'gbp-gdb-debugger.c',
+  'gdb-plugin.c',
+])
+
+plugin_gdb_resources = gnome.compile_resources(
   'gdb-resources',
   'gdb.gresource.xml',
   c_name: 'gbp_gdb',
 )
 
-gdb_sources = [
-  'gbp-gdb-debugger.c',
-  'gbp-gdb-debugger.h',
-  'gbp-gdb-plugin.c',
-]
+plugins_sources += plugin_gdb_resources[0]
 
 gdbwire = static_library('gdbwire', ['gdbwire.c'],
   c_args: [ '-Wno-redundant-decls',
@@ -20,8 +21,6 @@ gdbwire = static_library('gdbwire', ['gdbwire.c'],
             '-Wno-declaration-after-statement' ],
 )
 
-gnome_builder_plugins_sources += files(gdb_sources)
-gnome_builder_plugins_sources += gdb_resources[0]
-gnome_builder_plugins_link_with += gdbwire
+plugins_link_with += gdbwire
 
 endif
diff --git a/src/plugins/gettext/gettext-plugin.c b/src/plugins/gettext/gettext-plugin.c
index 28593c2ab..2a20dc6f8 100644
--- a/src/plugins/gettext/gettext-plugin.c
+++ b/src/plugins/gettext/gettext-plugin.c
@@ -14,15 +14,19 @@
  *
  * 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
  */
 
+#include "config.h"
+
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-code.h>
 
 #include "ide-gettext-diagnostic-provider.h"
 
-void
-ide_gettext_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_ide_gettext_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_DIAGNOSTIC_PROVIDER,
diff --git a/src/plugins/gettext/gettext.gresource.xml b/src/plugins/gettext/gettext.gresource.xml
index a4326d629..24e3173ba 100644
--- a/src/plugins/gettext/gettext.gresource.xml
+++ b/src/plugins/gettext/gettext.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/gettext">
     <file>gettext.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/gettext/gettext.plugin b/src/plugins/gettext/gettext.plugin
index b84ab8479..9204cf242 100644
--- a/src/plugins/gettext/gettext.plugin
+++ b/src/plugins/gettext/gettext.plugin
@@ -1,10 +1,11 @@
 [Plugin]
-Module=gettext-plugin
-Name=Gettext
-Description=Provides integration with Gettext
 Authors=Daiki Ueno
-Copyright=Copyright © 2016 Daiki Ueno
 Builtin=true
-Embedded=ide_gettext_register_types
-X-Diagnostic-Provider-Languages=c,chdr,cpp,js,python,vala
+Copyright=Copyright © 2016 Daiki Ueno
+Depends=editor;
+Description=Provides integration with Gettext
+Embedded=_ide_gettext_register_types
+Module=gettext
+Name=Gettext
 X-Diagnostic-Provider-Languages-Priority=100
+X-Diagnostic-Provider-Languages=c,chdr,cpp,js,python,vala
diff --git a/src/plugins/gettext/ide-gettext-diagnostic-provider.c 
b/src/plugins/gettext/ide-gettext-diagnostic-provider.c
index ca8348066..373162b69 100644
--- a/src/plugins/gettext/ide-gettext-diagnostic-provider.c
+++ b/src/plugins/gettext/ide-gettext-diagnostic-provider.c
@@ -1,7 +1,7 @@
 /* ide-gettext-diagnostic-provider.c
  *
  * Copyright 2016 Daiki Ueno <dueno src gnome org>
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -15,6 +15,8 @@
  *
  * 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 "ide-gettext-diagnostic-provider"
@@ -78,7 +80,7 @@ ide_gettext_diagnostic_provider_communicate_cb (GObject      *object,
   g_autofree gchar *stderr_buf = NULL;
   g_autofree gchar *stdout_buf = NULL;
   IdeLineReader reader;
-  IdeFile *file;
+  GFile *file;
   gchar *line;
   gsize len;
 
@@ -94,16 +96,16 @@ ide_gettext_diagnostic_provider_communicate_cb (GObject      *object,
 
   file = ide_task_get_task_data (task);
   g_assert (file != NULL);
-  g_assert (IDE_IS_FILE (file));
+  g_assert (G_IS_FILE (file));
 
-  ret = ide_diagnostics_new (NULL);
+  ret = ide_diagnostics_new ();
 
   ide_line_reader_init (&reader, stderr_buf, -1);
 
   while (NULL != (line = ide_line_reader_next (&reader, &len)))
     {
       g_autoptr(IdeDiagnostic) diag = NULL;
-      g_autoptr(IdeSourceLocation) loc = NULL;
+      g_autoptr(IdeLocation) loc = NULL;
       guint64 lineno;
 
       line[len] = '\0';
@@ -130,20 +132,21 @@ ide_gettext_diagnostic_provider_communicate_cb (GObject      *object,
 
       line += strlen (": ");
 
-      loc = ide_source_location_new (file, lineno, 0, 0);
+      loc = ide_location_new (file, lineno, -1);
       diag = ide_diagnostic_new (IDE_DIAGNOSTIC_WARNING, line, loc);
       ide_diagnostics_add (ret, diag);
     }
 
   ide_task_return_pointer (task,
                            g_steal_pointer (&ret),
-                           (GDestroyNotify)ide_diagnostics_unref);
+                           g_object_unref);
 }
 
 static void
 ide_gettext_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
-                                                IdeFile               *file,
-                                                IdeBuffer             *buffer,
+                                                GFile                 *file,
+                                                GBytes                *contents,
+                                                const gchar           *lang_id,
                                                 GCancellable          *cancellable,
                                                 GAsyncReadyCallback    callback,
                                                 gpointer               user_data)
@@ -151,16 +154,13 @@ ide_gettext_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
   IdeGettextDiagnosticProvider *self = (IdeGettextDiagnosticProvider *)provider;
   g_autoptr(IdeSubprocessLauncher) launcher = NULL;
   g_autoptr(IdeSubprocess) subprocess = NULL;
-  g_autoptr(GBytes) contents = NULL;
   g_autoptr(IdeTask) task = NULL;
   g_autoptr(GError) error = NULL;
-  GtkSourceLanguage *language;
-  const gchar *lang_id = NULL;
   const gchar *xgettext_id;
 
   g_assert (IDE_IS_GETTEXT_DIAGNOSTIC_PROVIDER (self));
-  g_assert (IDE_IS_FILE (file));
-  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+  g_assert (contents != NULL);
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
@@ -169,9 +169,7 @@ ide_gettext_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
   ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
 
   /* Figure out what language xgettext should use */
-  if (!(language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer))) ||
-      !(lang_id = gtk_source_language_get_id (language)) ||
-      !(xgettext_id = id_to_xgettext_language (lang_id)))
+  if (!(xgettext_id = id_to_xgettext_language (lang_id)))
     {
       ide_task_return_new_error (task,
                                  G_IO_ERROR,
@@ -182,11 +180,11 @@ ide_gettext_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
     }
 
   /* Return an empty set if we failed to locate any buffer contents */
-  if (!(contents = ide_buffer_get_content (buffer)) || g_bytes_get_size (contents) == 0)
+  if (g_bytes_get_size (contents) == 0)
     {
       ide_task_return_pointer (task,
-                               ide_diagnostics_new (NULL),
-                               (GDestroyNotify)ide_diagnostics_unref);
+                               ide_diagnostics_new (),
+                               g_object_unref);
       return;
     }
 
diff --git a/src/plugins/gettext/ide-gettext-diagnostic-provider.h 
b/src/plugins/gettext/ide-gettext-diagnostic-provider.h
index 1a131c0d5..83321ea0b 100644
--- a/src/plugins/gettext/ide-gettext-diagnostic-provider.h
+++ b/src/plugins/gettext/ide-gettext-diagnostic-provider.h
@@ -1,7 +1,7 @@
 /* ide-gettext-diagnostic-provider.h
  *
  * Copyright 2016 Daiki Ueno <dueno src gnome org>
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/gettext/meson.build b/src/plugins/gettext/meson.build
index 6bcf45ea1..24ccf09b4 100644
--- a/src/plugins/gettext/meson.build
+++ b/src/plugins/gettext/meson.build
@@ -1,18 +1,16 @@
-if get_option('with_gettext')
+if get_option('plugin_gettext')
 
-gettext_resources = gnome.compile_resources(
+plugins_sources += files([
+  'gettext-plugin.c',
+  'ide-gettext-diagnostic-provider.c',
+])
+
+plugin_gettext_resources = gnome.compile_resources(
   'gettext-resources',
   'gettext.gresource.xml',
-  c_name: 'ide_gettext',
+  c_name: 'gbp_gettext',
 )
 
-gettext_sources = [
-  'ide-gettext-diagnostic-provider.c',
-  'ide-gettext-diagnostic-provider.h',
-  'gettext-plugin.c',
-]
-
-gnome_builder_plugins_sources += files(gettext_sources)
-gnome_builder_plugins_sources += gettext_resources[0]
+plugins_sources += plugin_gettext_resources[0]
 
 endif
diff --git a/src/plugins/git/gbp-git-buffer-addin.c b/src/plugins/git/gbp-git-buffer-addin.c
new file mode 100644
index 000000000..7d7038477
--- /dev/null
+++ b/src/plugins/git/gbp-git-buffer-addin.c
@@ -0,0 +1,123 @@
+/* gbp-git-buffer-addin.c
+ *
+ * Copyright 2018-2019 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-git-buffer-addin"
+
+#include "config.h"
+
+#include <libgit2-glib/ggit.h>
+#include <libide-vcs.h>
+
+#include "gbp-git-buffer-addin.h"
+#include "gbp-git-buffer-change-monitor.h"
+#include "gbp-git-vcs.h"
+
+struct _GbpGitBufferAddin
+{
+  GObject                    parent_instance;
+  GbpGitBufferChangeMonitor *monitor;
+};
+
+static void
+gbp_git_buffer_addin_file_laoded (IdeBufferAddin *addin,
+                                  IdeBuffer      *buffer,
+                                  GFile          *file)
+{
+  GbpGitBufferAddin *self = (GbpGitBufferAddin *)addin;
+  g_autoptr(GbpGitBufferChangeMonitor) monitor = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  GgitRepository *repository;
+  IdeObjectBox *box;
+  IdeVcs *vcs;
+
+  g_assert (GBP_IS_GIT_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+
+  context = ide_buffer_ref_context (buffer);
+  vcs = ide_context_peek_child_typed (context, IDE_TYPE_VCS);
+  if (!GBP_IS_GIT_VCS (vcs))
+    return;
+
+  if (!(repository = gbp_git_vcs_get_repository (GBP_GIT_VCS (vcs))))
+    return;
+
+  self->monitor = g_object_new (GBP_TYPE_GIT_BUFFER_CHANGE_MONITOR,
+                                "buffer", buffer,
+                                "repository", repository,
+                                NULL);
+
+  box = ide_object_box_from_object (G_OBJECT (buffer));
+  ide_object_append (IDE_OBJECT (box), IDE_OBJECT (self->monitor));
+
+  ide_buffer_set_change_monitor (buffer, IDE_BUFFER_CHANGE_MONITOR (self->monitor));
+}
+
+static void
+gbp_git_buffer_addin_file_saved (IdeBufferAddin *addin,
+                                 IdeBuffer      *buffer,
+                                 GFile          *file)
+{
+  GbpGitBufferAddin *self = (GbpGitBufferAddin *)addin;
+
+  g_assert (GBP_IS_GIT_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+
+  if (self->monitor != NULL)
+    ide_buffer_change_monitor_reload (IDE_BUFFER_CHANGE_MONITOR (self->monitor));
+}
+
+static void
+gbp_git_buffer_addin_unload (IdeBufferAddin *addin,
+                             IdeBuffer      *buffer)
+{
+  GbpGitBufferAddin *self = (GbpGitBufferAddin *)addin;
+
+  g_assert (GBP_IS_GIT_BUFFER_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (self->monitor != NULL)
+    {
+      ide_buffer_set_change_monitor (buffer, NULL);
+      ide_clear_and_destroy_object (&self->monitor);
+    }
+}
+
+static void
+buffer_addin_iface_init (IdeBufferAddinInterface *iface)
+{
+  iface->file_loaded = gbp_git_buffer_addin_file_laoded;
+  iface->file_saved = gbp_git_buffer_addin_file_saved;
+  iface->unload = gbp_git_buffer_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGitBufferAddin, gbp_git_buffer_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUFFER_ADDIN, buffer_addin_iface_init))
+
+static void
+gbp_git_buffer_addin_class_init (GbpGitBufferAddinClass *klass)
+{
+}
+
+static void
+gbp_git_buffer_addin_init (GbpGitBufferAddin *self)
+{
+}
diff --git a/src/plugins/git/gbp-git-buffer-addin.h b/src/plugins/git/gbp-git-buffer-addin.h
new file mode 100644
index 000000000..3ede066e2
--- /dev/null
+++ b/src/plugins/git/gbp-git-buffer-addin.h
@@ -0,0 +1,31 @@
+/* gbp-git-buffer-addin.h
+ *
+ * Copyright 2018-2019 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_GIT_BUFFER_ADDIN (gbp_git_buffer_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitBufferAddin, gbp_git_buffer_addin, GBP, GIT_BUFFER_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-buffer-change-monitor.c b/src/plugins/git/gbp-git-buffer-change-monitor.c
new file mode 100644
index 000000000..6f73901b6
--- /dev/null
+++ b/src/plugins/git/gbp-git-buffer-change-monitor.c
@@ -0,0 +1,984 @@
+/* gbp-git-buffer-change-monitor.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-git-buffer-change-monitor"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libgit2-glib/ggit.h>
+#include <stdlib.h>
+
+#include "gbp-git-buffer-change-monitor.h"
+#include "gbp-git-vcs.h"
+
+#define DELAY_CHANGED_SEC 1
+
+/**
+ * SECTION:gbp-git-buffer-change-monitor
+ *
+ * This module provides line change monitoring when used in conjunction with an
+ * GbpGitVcs.  The changes are generated by comparing the buffer contents to
+ * the version found inside of the git repository.
+ *
+ * To enable us to avoid blocking the main loop, the actual diff is performed
+ * in a background thread. To avoid threading issues with the rest of LibGBP,
+ * this module creates a copy of the loaded repository. A single thread will be
+ * dispatched for the context and all reload tasks will be performed from that
+ * thread.
+ *
+ * Upon completion of the diff, the results will be passed back to the primary
+ * thread and the state updated for use by line change renderer in the source
+ * view.
+ *
+ * Since: 3.32
+ */
+
+struct _GbpGitBufferChangeMonitor
+{
+  IdeBufferChangeMonitor  parent_instance;
+
+  DzlSignalGroup         *signal_group;
+
+  GgitRepository         *repository;
+  GArray                 *lines;
+
+  GgitBlob               *cached_blob;
+
+  guint                   changed_timeout;
+
+  guint                   state_dirty : 1;
+  guint                   in_calculation : 1;
+  guint                   delete_range_requires_recalculation : 1;
+  guint                   is_child_of_workdir : 1;
+  guint                   in_failed_state : 1;
+};
+
+typedef struct
+{
+  GgitRepository *repository;
+  GArray         *lines;
+  GFile          *file;
+  GBytes         *content;
+  GgitBlob       *blob;
+  IdeObject      *lock_object;
+  guint           is_child_of_workdir : 1;
+} DiffTask;
+
+typedef struct
+{
+  gint                line;
+  IdeBufferLineChange change : 3;
+  guint               really_delete : 1;
+} DiffLine;
+
+typedef struct
+{
+  /*
+   * An array of DiffLine that contains information about the lines that
+   * have changed. This is sorted and used to bsearch() when the buffer
+   * requests the line flags.
+   */
+  GArray *lines;
+
+  /*
+   * We need to keep track of additions/removals as we process our way
+   * through the diff so that we can adjust lines for the deleted case.
+   */
+  gint hunk_add_count;
+  gint hunk_del_count;
+} DiffCallbackData;
+
+G_DEFINE_TYPE (GbpGitBufferChangeMonitor, gbp_git_buffer_change_monitor, IDE_TYPE_BUFFER_CHANGE_MONITOR)
+
+DZL_DEFINE_COUNTER (instances, "GbpGitBufferChangeMonitor", "Instances", "The number of git buffer change 
monitor instances.");
+
+enum {
+  PROP_0,
+  PROP_REPOSITORY,
+  LAST_PROP
+};
+
+static GParamSpec  *properties [LAST_PROP];
+static GAsyncQueue *work_queue;
+static GThread     *work_thread;
+
+static void
+diff_task_free (gpointer data)
+{
+  DiffTask *diff = data;
+
+  if (diff)
+    {
+      g_clear_object (&diff->file);
+      g_clear_object (&diff->blob);
+      g_clear_object (&diff->repository);
+      g_clear_object (&diff->lock_object);
+      g_clear_pointer (&diff->lines, g_array_unref);
+      g_clear_pointer (&diff->content, g_bytes_unref);
+      g_slice_free (DiffTask, diff);
+    }
+}
+
+static gint
+diff_line_compare (const DiffLine *left,
+                   const DiffLine *right)
+{
+  return left->line - right->line;
+}
+
+static GArray *
+gbp_git_buffer_change_monitor_calculate_finish (GbpGitBufferChangeMonitor  *self,
+                                                GAsyncResult               *result,
+                                                GError                    **error)
+{
+  IdeTask *task = (IdeTask *)result;
+  DiffTask *diff;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+  g_assert (IDE_IS_TASK (result));
+
+  if (ide_object_set_error_if_destroyed (IDE_OBJECT (self), error))
+    return NULL;
+
+  diff = ide_task_get_task_data (task);
+
+  if (diff != NULL)
+    {
+      g_assert (GGIT_IS_REPOSITORY (diff->repository));
+      g_assert (G_IS_FILE (diff->file));
+      g_assert (diff->content != NULL);
+      g_assert (GBP_IS_GIT_VCS (diff->lock_object));
+
+      /* Keep the blob around for future use */
+      if (diff->blob != self->cached_blob)
+        g_set_object (&self->cached_blob, diff->blob);
+
+      /* If the file is a child of the working directory, we need to know */
+      self->is_child_of_workdir = diff->is_child_of_workdir;
+    }
+
+  return ide_task_propagate_pointer (task, error);
+}
+
+static void
+gbp_git_buffer_change_monitor_calculate_async (GbpGitBufferChangeMonitor *self,
+                                               GCancellable              *cancellable,
+                                               GAsyncReadyCallback        callback,
+                                               gpointer                   user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  GbpGitVcs *vcs;
+  IdeBuffer *buffer;
+  DiffTask *diff;
+  GFile *file;
+
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (self->repository != NULL);
+
+  self->state_dirty = FALSE;
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_git_buffer_change_monitor_calculate_async);
+
+  buffer = ide_buffer_change_monitor_get_buffer (IDE_BUFFER_CHANGE_MONITOR (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  file = ide_buffer_get_file (buffer);
+  g_assert (G_IS_FILE (file));
+
+  context = ide_object_ref_context (IDE_OBJECT (self));
+  vcs = ide_context_peek_child_typed (context, GBP_TYPE_GIT_VCS);
+
+  if (!GBP_IS_GIT_VCS (vcs))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_CANCELLED,
+                                 "Cannot provide changes, not connected to GbpGitVcs.");
+      return;
+    }
+
+  diff = g_slice_new0 (DiffTask);
+  diff->file = g_object_ref (file);
+  diff->repository = g_object_ref (self->repository);
+  diff->lines = g_array_sized_new (FALSE, FALSE, sizeof (DiffLine), 32);
+  diff->content = ide_buffer_dup_content (buffer);
+  diff->blob = self->cached_blob ? g_object_ref (self->cached_blob) : NULL;
+  diff->lock_object = g_object_ref (IDE_OBJECT (vcs));
+
+  ide_task_set_task_data (task, diff, diff_task_free);
+
+  self->in_calculation = TRUE;
+
+  g_async_queue_push (work_queue, g_steal_pointer (&task));
+}
+
+static void
+gbp_git_buffer_change_monitor_foreach_change (IdeBufferChangeMonitor            *monitor,
+                                              guint                              begin_line,
+                                              guint                              end_line,
+                                              IdeBufferChangeMonitorForeachFunc  callback,
+                                              gpointer                           user_data)
+{
+  GbpGitBufferChangeMonitor *self = (GbpGitBufferChangeMonitor *)monitor;
+
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+  g_assert (callback != NULL);
+
+  if (end_line == G_MAXUINT)
+    end_line--;
+
+  if (self->lines == NULL || self->lines->data == NULL)
+    {
+      /* If within working directory, synthesize line addition. */
+      if (self->is_child_of_workdir)
+        {
+          for (guint i = begin_line; i < end_line; i++)
+            callback (i, IDE_BUFFER_LINE_CHANGE_ADDED, user_data);
+        }
+      return;
+    }
+
+  /* TODO: We could bsearch for the nearest start line */
+
+  for (guint i = 0; i < self->lines->len; i++)
+    {
+      DiffLine *line = &g_array_index (self->lines, DiffLine, i);
+      guint lineno = line->line - 1;
+
+      if (lineno < begin_line)
+        continue;
+
+      if (lineno > end_line)
+        break;
+
+      /* git is 1-based lines */
+      callback (lineno, line->change, user_data);
+    }
+}
+
+static IdeBufferLineChange
+gbp_git_buffer_change_monitor_get_change (IdeBufferChangeMonitor *monitor,
+                                          guint                   line)
+{
+  GbpGitBufferChangeMonitor *self = (GbpGitBufferChangeMonitor *)monitor;
+  DiffLine key = { line + 1, 0 }; /* Git is 1-based */
+  DiffLine *ret;
+
+  /* Don't imply changes we don't know are real, in the case that
+   * we failed to communicate with git properly about the blob diff.
+   */
+  if (self->in_failed_state)
+    return IDE_BUFFER_LINE_CHANGE_NONE;
+
+  if (self->lines == NULL || self->lines->data == NULL)
+    {
+      /* If within working directory, synthesize line addition. */
+      if (self->is_child_of_workdir)
+        return IDE_BUFFER_LINE_CHANGE_ADDED;
+      return IDE_BUFFER_LINE_CHANGE_NONE;
+    }
+
+  ret = bsearch (&key, (gconstpointer)self->lines->data,
+                 self->lines->len, sizeof (DiffLine),
+                 (GCompareFunc)diff_line_compare);
+
+  return ret != NULL ? ret->change : 0;
+}
+
+void
+gbp_git_buffer_change_monitor_set_repository (GbpGitBufferChangeMonitor *self,
+                                              GgitRepository            *repository)
+{
+  gboolean do_reload;
+
+  g_return_if_fail (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+  g_return_if_fail (GGIT_IS_REPOSITORY (repository));
+
+  do_reload = self->repository != NULL && repository != NULL;
+
+  if (g_set_object (&self->repository, repository))
+    {
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REPOSITORY]);
+
+      if (do_reload)
+        ide_buffer_change_monitor_reload (IDE_BUFFER_CHANGE_MONITOR (self));
+    }
+}
+
+static void
+gbp_git_buffer_change_monitor__calculate_cb (GObject      *object,
+                                             GAsyncResult *result,
+                                             gpointer      user_data_unused)
+{
+  GbpGitBufferChangeMonitor *self = (GbpGitBufferChangeMonitor *)object;
+  g_autoptr(GArray) lines = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+  g_assert (user_data_unused == NULL);
+
+  self->in_calculation = FALSE;
+
+  lines = gbp_git_buffer_change_monitor_calculate_finish (self, result, &error);
+
+  if (lines == NULL)
+    {
+      if (!self->in_failed_state && !g_error_matches (error, GGIT_ERROR, GGIT_ERROR_NOTFOUND))
+        {
+          ide_object_warning (self,
+                              /* translators: %s is replaced with the error string from git */
+                              _("There was a failure while calculating line changes from git. The exact 
error was: %s"),
+                              error->message);
+          self->in_failed_state = TRUE;
+        }
+    }
+  else
+    {
+      g_clear_pointer (&self->lines, g_array_unref);
+      self->lines = g_steal_pointer (&lines);
+      self->in_failed_state = FALSE;
+    }
+
+  ide_buffer_change_monitor_emit_changed (IDE_BUFFER_CHANGE_MONITOR (self));
+
+  /* Recalculate if the buffer has changed since last request. */
+  if (self->state_dirty)
+    gbp_git_buffer_change_monitor_calculate_async (self,
+                                                   NULL,
+                                                   gbp_git_buffer_change_monitor__calculate_cb,
+                                                   NULL);
+}
+
+static void
+gbp_git_buffer_change_monitor_recalculate (GbpGitBufferChangeMonitor *self)
+{
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+
+  self->state_dirty = TRUE;
+
+  if (!self->in_calculation)
+    gbp_git_buffer_change_monitor_calculate_async (self,
+                                                   NULL,
+                                                   gbp_git_buffer_change_monitor__calculate_cb,
+                                                   NULL);
+}
+
+static void
+gbp_git_buffer_change_monitor__buffer_delete_range_after_cb (GbpGitBufferChangeMonitor *self,
+                                                             GtkTextIter               *begin,
+                                                             GtkTextIter               *end,
+                                                             IdeBuffer                 *buffer)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+  g_assert (begin);
+  g_assert (end);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (self->delete_range_requires_recalculation)
+    {
+      self->delete_range_requires_recalculation = FALSE;
+      gbp_git_buffer_change_monitor_recalculate (self);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+gbp_git_buffer_change_monitor__buffer_delete_range_cb (GbpGitBufferChangeMonitor *self,
+                                                       GtkTextIter               *begin,
+                                                       GtkTextIter               *end,
+                                                       IdeBuffer                 *buffer)
+{
+  IdeBufferLineChange change;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /*
+   * We need to recalculate the diff when text is deleted if:
+   *
+   * 1) The range includes a newline.
+   * 2) The current line change is set to NONE.
+   *
+   * Technically we need to do it on every change to be more correct, but that wastes a lot of
+   * power. So instead, we'll be a bit lazy about it here and pick up the other changes on a much
+   * more conservative timeout, generated by gbp_git_buffer_change_monitor__buffer_changed_cb().
+   */
+
+  if (gtk_text_iter_get_line (begin) != gtk_text_iter_get_line (end))
+    IDE_GOTO (recalculate);
+
+  change = gbp_git_buffer_change_monitor_get_change (IDE_BUFFER_CHANGE_MONITOR (self),
+                                                     gtk_text_iter_get_line (begin));
+  if (change == IDE_BUFFER_LINE_CHANGE_NONE)
+    IDE_GOTO (recalculate);
+
+  IDE_EXIT;
+
+recalculate:
+  /*
+   * We need to wait for the delete to occur, so mark it as necessary and let
+   * gbp_git_buffer_change_monitor__buffer_delete_range_after_cb perform the operation.
+   */
+  self->delete_range_requires_recalculation = TRUE;
+
+  IDE_EXIT;
+}
+
+static void
+gbp_git_buffer_change_monitor__buffer_insert_text_after_cb (GbpGitBufferChangeMonitor *self,
+                                                            GtkTextIter               *location,
+                                                            gchar                     *text,
+                                                            gint                       len,
+                                                            IdeBuffer                 *buffer)
+{
+  IdeBufferLineChange change;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+  g_assert (location);
+  g_assert (text);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /*
+   * We need to recalculate the diff when text is inserted if:
+   *
+   * 1) A newline is included in the text.
+   * 2) The line currently has flags of NONE.
+   *
+   * Technically we need to do it on every change to be more correct, but that wastes a lot of
+   * power. So instead, we'll be a bit lazy about it here and pick up the other changes on a much
+   * more conservative timeout, generated by gbp_git_buffer_change_monitor__buffer_changed_cb().
+   */
+
+  if (NULL != memmem (text, len, "\n", 1))
+    IDE_GOTO (recalculate);
+
+  change = gbp_git_buffer_change_monitor_get_change (IDE_BUFFER_CHANGE_MONITOR (self),
+                                                     gtk_text_iter_get_line (location));
+  if (change == IDE_BUFFER_LINE_CHANGE_NONE)
+    IDE_GOTO (recalculate);
+
+  IDE_EXIT;
+
+recalculate:
+  gbp_git_buffer_change_monitor_recalculate (self);
+
+  IDE_EXIT;
+}
+
+static gboolean
+gbp_git_buffer_change_monitor__changed_timeout_cb (gpointer user_data)
+{
+  GbpGitBufferChangeMonitor *self = user_data;
+
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+
+  self->changed_timeout = 0;
+  gbp_git_buffer_change_monitor_recalculate (self);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_git_buffer_change_monitor__buffer_changed_after_cb (GbpGitBufferChangeMonitor *self,
+                                                        IdeBuffer                 *buffer)
+{
+  g_assert (IDE_IS_BUFFER_CHANGE_MONITOR (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  self->state_dirty = TRUE;
+
+  if (self->in_calculation)
+    return;
+
+  dzl_clear_source (&self->changed_timeout);
+  self->changed_timeout = g_timeout_add_seconds (DELAY_CHANGED_SEC,
+                                                 gbp_git_buffer_change_monitor__changed_timeout_cb,
+                                                 self);
+}
+
+static void
+gbp_git_buffer_change_monitor_reload (IdeBufferChangeMonitor *monitor)
+{
+  GbpGitBufferChangeMonitor *self = (GbpGitBufferChangeMonitor *)monitor;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+
+  g_clear_object (&self->cached_blob);
+  gbp_git_buffer_change_monitor_recalculate (self);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_git_buffer_change_monitor_load (IdeBufferChangeMonitor *monitor,
+                                    IdeBuffer              *buffer)
+{
+  GbpGitBufferChangeMonitor *self = (GbpGitBufferChangeMonitor *)monitor;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+  g_return_if_fail (IDE_IS_BUFFER (buffer));
+
+  dzl_signal_group_set_target (self->signal_group, buffer);
+
+  IDE_EXIT;
+}
+
+static DiffLine *
+find_or_add_line (GArray *array,
+                  gint    line)
+{
+  DiffLine key = { line, 0 };
+  DiffLine *ret;
+
+  g_assert (array != NULL);
+  g_assert (array->data != NULL);
+  g_assert (line >= 0);
+
+  ret = bsearch (&key, (gconstpointer)array->data,
+                 array->len, sizeof (DiffLine),
+                 (GCompareFunc)diff_line_compare);
+
+  if (ret == NULL)
+    {
+      DiffLine *prev;
+
+      g_array_append_val (array, key);
+
+      if (array->len == 1)
+        return &g_array_index (array, DiffLine, 0);
+
+      g_assert (array->len > 1);
+
+      prev = &g_array_index (array, DiffLine, array->len - 2);
+      if (prev->line < line)
+        return &g_array_index (array, DiffLine, array->len - 1);
+
+      g_array_sort (array, (GCompareFunc)diff_line_compare);
+
+      ret = bsearch (&key, (gconstpointer)array->data,
+                     array->len, sizeof (DiffLine),
+                     (GCompareFunc)diff_line_compare);
+    }
+
+  g_assert (ret != NULL);
+
+  return ret;
+}
+
+static gint
+diff_line_cb (GgitDiffDelta *delta,
+              GgitDiffHunk  *hunk,
+              GgitDiffLine  *line,
+              gpointer       user_data)
+{
+  DiffCallbackData *info = user_data;
+  GgitDiffLineType type;
+  DiffLine *diff_line;
+  gint new_hunk_start;
+  gint old_hunk_start;
+  gint new_lineno;
+  gint old_lineno;
+
+  g_assert (delta != NULL);
+  g_assert (hunk != NULL);
+  g_assert (line != NULL);
+  g_assert (info != NULL);
+  g_assert (info->lines != NULL);
+
+  type = ggit_diff_line_get_origin (line);
+
+  new_lineno = ggit_diff_line_get_new_lineno (line);
+  old_lineno = ggit_diff_line_get_old_lineno (line);
+
+  /*
+   * The callbacks here are are somewhat cryptic and have been little
+   * tweak, one after another.
+   *
+   * What I glean, thus far, is that things happen like this (after
+   * we've accouned for the line number drift). If something looks off,
+   * it probably is!
+   *
+   * Scenario 1
+   * - Delete N
+   * - Delete N
+   * - Added  N
+   *   This means that N is both a change and the previous line(s)
+   *   where deleted.
+   *
+   * Scenario 2
+   * - Delete N
+   *   This means the line(s) previous to N were deleted.
+   *
+   * Scenario 3
+   * - Delete N
+   * - Added  N
+   *   This means N was changed.
+   *
+   * Scenario 4
+   *
+   * - Added N
+   *   This means N was added.
+   */
+
+  switch (type)
+    {
+    case GGIT_DIFF_LINE_ADDITION:
+      diff_line = find_or_add_line (info->lines, new_lineno);
+      if (diff_line->change == IDE_BUFFER_LINE_CHANGE_DELETED)
+        diff_line->change = IDE_BUFFER_LINE_CHANGE_CHANGED;
+      else
+        diff_line->change = IDE_BUFFER_LINE_CHANGE_ADDED;
+
+      if (diff_line->really_delete)
+        diff_line->change |= IDE_BUFFER_LINE_CHANGE_DELETED;
+
+      info->hunk_add_count++;
+
+      break;
+
+    case GGIT_DIFF_LINE_DELETION:
+      new_hunk_start = ggit_diff_hunk_get_new_start (hunk);
+      old_hunk_start = ggit_diff_hunk_get_old_start (hunk);
+
+      old_lineno += new_hunk_start - old_hunk_start;
+      old_lineno += info->hunk_add_count - info->hunk_del_count;
+
+      diff_line = find_or_add_line (info->lines, old_lineno);
+      if (diff_line->change & IDE_BUFFER_LINE_CHANGE_DELETED)
+        diff_line->really_delete = TRUE;
+      diff_line->change = IDE_BUFFER_LINE_CHANGE_DELETED;
+
+      info->hunk_del_count++;
+
+      break;
+
+    case GGIT_DIFF_LINE_DEL_EOFNL:
+      /* TODO: Handle trailing newline differences */
+      break;
+
+    case GGIT_DIFF_LINE_CONTEXT:
+    case GGIT_DIFF_LINE_CONTEXT_EOFNL:
+    case GGIT_DIFF_LINE_ADD_EOFNL:
+    case GGIT_DIFF_LINE_FILE_HDR:
+    case GGIT_DIFF_LINE_HUNK_HDR:
+    case GGIT_DIFF_LINE_BINARY:
+    default:
+      return 0;
+    }
+
+
+  return 0;
+}
+
+static gboolean
+gbp_git_buffer_change_monitor_calculate_threaded (GbpGitBufferChangeMonitor  *self,
+                                                  DiffTask                   *diff,
+                                                  GError                    **error)
+{
+  g_autofree gchar *relative_path = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  DiffCallbackData cb_data = {0};
+  const guint8 *data;
+  gsize data_len = 0;
+
+  g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+  g_assert (diff != NULL);
+  g_assert (G_IS_FILE (diff->file));
+  g_assert (diff->lines != NULL);
+  g_assert (GGIT_IS_REPOSITORY (diff->repository));
+  g_assert (diff->content != NULL);
+  g_assert (!diff->blob || GGIT_IS_BLOB (diff->blob));
+  g_assert (error != NULL);
+  g_assert (*error == NULL);
+
+  workdir = ggit_repository_get_workdir (diff->repository);
+
+  if (!workdir)
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_FILENAME,
+                   _("Repository does not have a working directory."));
+      return FALSE;
+    }
+
+  relative_path = g_file_get_relative_path (workdir, diff->file);
+
+  if (!relative_path)
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_INVALID_FILENAME,
+                   _("File is not under control of git working directory."));
+      return FALSE;
+    }
+
+  diff->is_child_of_workdir = TRUE;
+
+  /*
+   * Find the blob if necessary. This will be cached by the main thread for
+   * us on the way out of the async operation.
+   */
+  if (diff->blob == NULL)
+    {
+      GgitOId *entry_oid = NULL;
+      GgitOId *oid = NULL;
+      GgitObject *blob = NULL;
+      GgitObject *commit = NULL;
+      GgitRef *head = NULL;
+      GgitTree *tree = NULL;
+      GgitTreeEntry *entry = NULL;
+
+      head = ggit_repository_get_head (diff->repository, error);
+      if (!head)
+        goto cleanup;
+
+      oid = ggit_ref_get_target (head);
+      if (!oid)
+        goto cleanup;
+
+      commit = ggit_repository_lookup (diff->repository, oid, GGIT_TYPE_COMMIT, error);
+      if (!commit)
+        goto cleanup;
+
+      tree = ggit_commit_get_tree (GGIT_COMMIT (commit));
+      if (!tree)
+        goto cleanup;
+
+      entry = ggit_tree_get_by_path (tree, relative_path, error);
+      if (!entry)
+        goto cleanup;
+
+      entry_oid = ggit_tree_entry_get_id (entry);
+      if (!entry_oid)
+        goto cleanup;
+
+      blob = ggit_repository_lookup (diff->repository, entry_oid, GGIT_TYPE_BLOB, error);
+      if (!blob)
+        goto cleanup;
+
+      diff->blob = g_object_ref (GGIT_BLOB (blob));
+
+    cleanup:
+      g_clear_object (&blob);
+      g_clear_pointer (&entry_oid, ggit_oid_free);
+      g_clear_pointer (&entry, ggit_tree_entry_unref);
+      g_clear_object (&tree);
+      g_clear_object (&commit);
+      g_clear_pointer (&oid, ggit_oid_free);
+      g_clear_object (&head);
+    }
+
+  if (diff->blob == NULL)
+    {
+      if (*error == NULL)
+        g_set_error (error,
+                     G_IO_ERROR,
+                     G_IO_ERROR_NOT_FOUND,
+                     _("The requested file does not exist within the git index."));
+      return FALSE;
+    }
+
+  data = g_bytes_get_data (diff->content, &data_len);
+
+  cb_data.lines = diff->lines;
+  cb_data.hunk_add_count = 0;
+  cb_data.hunk_del_count = 0;
+
+  ggit_diff_blob_to_buffer (diff->blob, relative_path, data, data_len, relative_path,
+                            NULL, NULL, NULL, NULL,
+                            diff_line_cb, &cb_data, error);
+
+  return *error == NULL;
+}
+
+static gpointer
+gbp_git_buffer_change_monitor_worker (gpointer data)
+{
+  GAsyncQueue *queue = data;
+  gpointer taskptr;
+
+  g_assert (queue != NULL);
+
+  /*
+   * This is a single thread worker that dispatches the particular
+   * change to the given change monitor. We require a single thread
+   * so that we can mantain the invariant that only a single thread
+   * can access a GgitRepository at a time (and change monitors all
+   * share the same GgitRepository amongst themselves).
+   */
+
+  while (NULL != (taskptr = g_async_queue_pop (queue)))
+    {
+      GbpGitBufferChangeMonitor *self;
+      g_autoptr(GError) error = NULL;
+      g_autoptr(IdeTask) task = taskptr;
+      DiffTask *diff;
+      gboolean ret;
+
+      self = ide_task_get_source_object (task);
+      g_assert (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (self));
+
+      diff = ide_task_get_task_data (task);
+      g_assert (diff != NULL);
+
+      /* Acquire the lock for the parent to ensure we have access to repository */
+      ide_object_lock (diff->lock_object);
+      ret = gbp_git_buffer_change_monitor_calculate_threaded (self, diff, &error);
+      ide_object_unlock (diff->lock_object);
+
+      if (!ret)
+        ide_task_return_error (task, g_steal_pointer (&error));
+      else
+        ide_task_return_pointer (task,
+                                 g_steal_pointer (&diff->lines),
+                                 (GDestroyNotify)g_array_unref);
+    }
+
+  return NULL;
+}
+
+static void
+gbp_git_buffer_change_monitor_destroy (IdeObject *object)
+{
+  GbpGitBufferChangeMonitor *self = (GbpGitBufferChangeMonitor *)object;
+
+  dzl_clear_source (&self->changed_timeout);
+
+  if (self->signal_group)
+    {
+      dzl_signal_group_set_target (self->signal_group, NULL);
+      g_clear_object (&self->signal_group);
+    }
+
+  g_clear_object (&self->cached_blob);
+  g_clear_object (&self->repository);
+
+  IDE_OBJECT_CLASS (gbp_git_buffer_change_monitor_parent_class)->destroy (object);
+}
+
+static void
+gbp_git_buffer_change_monitor_finalize (GObject *object)
+{
+  G_OBJECT_CLASS (gbp_git_buffer_change_monitor_parent_class)->finalize (object);
+
+  DZL_COUNTER_DEC (instances);
+}
+
+static void
+gbp_git_buffer_change_monitor_set_property (GObject      *object,
+                                            guint         prop_id,
+                                            const GValue *value,
+                                            GParamSpec   *pspec)
+{
+  GbpGitBufferChangeMonitor *self = GBP_GIT_BUFFER_CHANGE_MONITOR (object);
+
+  switch (prop_id)
+    {
+    case PROP_REPOSITORY:
+      gbp_git_buffer_change_monitor_set_repository (self, g_value_get_object (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_git_buffer_change_monitor_class_init (GbpGitBufferChangeMonitorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+  IdeBufferChangeMonitorClass *parent_class = IDE_BUFFER_CHANGE_MONITOR_CLASS (klass);
+
+  object_class->finalize = gbp_git_buffer_change_monitor_finalize;
+  object_class->set_property = gbp_git_buffer_change_monitor_set_property;
+
+  i_object_class->destroy = gbp_git_buffer_change_monitor_destroy;
+
+  parent_class->load = gbp_git_buffer_change_monitor_load;
+  parent_class->get_change = gbp_git_buffer_change_monitor_get_change;
+  parent_class->reload = gbp_git_buffer_change_monitor_reload;
+  parent_class->foreach_change = gbp_git_buffer_change_monitor_foreach_change;
+
+  properties [PROP_REPOSITORY] =
+    g_param_spec_object ("repository",
+                         "Repository",
+                         "The repository to use for calculating diffs.",
+                         GGIT_TYPE_REPOSITORY,
+                         (G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+
+  /* Note: We use a single worker thread so that we can maintain the
+   *       invariant that only a single thread is touching the GgitRepository
+   *       at a time. (Also, you can only type in one editor at a time, so
+   *       on worker thread for interactive blob changes is fine.
+   */
+  work_queue = g_async_queue_new ();
+  work_thread = g_thread_new ("GbpGitBufferChangeMonitorWorker",
+                              gbp_git_buffer_change_monitor_worker,
+                              work_queue);
+}
+
+static void
+gbp_git_buffer_change_monitor_init (GbpGitBufferChangeMonitor *self)
+{
+  DZL_COUNTER_INC (instances);
+
+  self->signal_group = dzl_signal_group_new (IDE_TYPE_BUFFER);
+  dzl_signal_group_connect_object (self->signal_group,
+                                   "insert-text",
+                                   G_CALLBACK (gbp_git_buffer_change_monitor__buffer_insert_text_after_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+  dzl_signal_group_connect_object (self->signal_group,
+                                   "delete-range",
+                                   G_CALLBACK (gbp_git_buffer_change_monitor__buffer_delete_range_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+  dzl_signal_group_connect_object (self->signal_group,
+                                   "delete-range",
+                                   G_CALLBACK (gbp_git_buffer_change_monitor__buffer_delete_range_after_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+  dzl_signal_group_connect_object (self->signal_group,
+                                   "changed",
+                                   G_CALLBACK (gbp_git_buffer_change_monitor__buffer_changed_after_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+}
diff --git a/src/plugins/git/gbp-git-buffer-change-monitor.h b/src/plugins/git/gbp-git-buffer-change-monitor.h
new file mode 100644
index 000000000..9c29d940d
--- /dev/null
+++ b/src/plugins/git/gbp-git-buffer-change-monitor.h
@@ -0,0 +1,35 @@
+/* gbp-git-buffer-change-monitor.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <libgit2-glib/ggit.h>
+#include <libide-code.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GIT_BUFFER_CHANGE_MONITOR (gbp_git_buffer_change_monitor_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitBufferChangeMonitor, gbp_git_buffer_change_monitor, GBP, 
GIT_BUFFER_CHANGE_MONITOR, IdeBufferChangeMonitor)
+
+void gbp_git_buffer_change_monitor_set_repository (GbpGitBufferChangeMonitor *self,
+                                                   GgitRepository            *repository);
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-dependency-updater.c b/src/plugins/git/gbp-git-dependency-updater.c
new file mode 100644
index 000000000..33ccecd02
--- /dev/null
+++ b/src/plugins/git/gbp-git-dependency-updater.c
@@ -0,0 +1,167 @@
+/* gbp-git-dependency-updater.c
+ *
+ * Copyright 2018-2019 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-git-dependency-updater"
+
+#include "config.h"
+
+#include "gbp-git-dependency-updater.h"
+#include "gbp-git-submodule-stage.h"
+
+struct _GbpGitDependencyUpdater
+{
+  IdeObject parent_instance;
+};
+
+static void
+find_submodule_stage_cb (gpointer data,
+                         gpointer user_data)
+{
+  GbpGitSubmoduleStage **stage = user_data;
+
+  g_assert (IDE_IS_BUILD_STAGE (data));
+  g_assert (stage != NULL);
+  g_assert (*stage == NULL || IDE_IS_BUILD_STAGE (*stage));
+
+  if (GBP_IS_GIT_SUBMODULE_STAGE (data))
+    *stage = GBP_GIT_SUBMODULE_STAGE (data);
+}
+
+static void
+gbp_git_dependency_updater_update_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  IdeBuildManager *manager = (IdeBuildManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_BUILD_MANAGER (manager));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_build_manager_rebuild_finish (manager, result, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_git_dependency_updater_update_async (IdeDependencyUpdater *self,
+                                         GCancellable         *cancellable,
+                                         GAsyncReadyCallback   callback,
+                                         gpointer              user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+  GbpGitSubmoduleStage *stage = NULL;
+  IdeBuildPipeline *pipeline;
+  IdeBuildManager *manager;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GIT_DEPENDENCY_UPDATER (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_git_dependency_updater_update_async);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  manager = ide_build_manager_from_context (context);
+  pipeline = ide_build_manager_get_pipeline (manager);
+
+  g_assert (!pipeline || IDE_IS_BUILD_PIPELINE (pipeline));
+
+  if (pipeline == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "Cannot update git submodules until build pipeline is initialized");
+      IDE_EXIT;
+    }
+
+  /* Find the submodule stage and tell it to download updates one time */
+  ide_build_pipeline_foreach_stage (pipeline, find_submodule_stage_cb, &stage);
+
+  if (stage == NULL)
+    {
+      /* Synthesize success if there is no submodule stage */
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  gbp_git_submodule_stage_force_update (stage);
+
+  /* Ensure downloads and everything past it is invalidated */
+  ide_build_pipeline_invalidate_phase (pipeline, IDE_BUILD_PHASE_DOWNLOADS);
+
+  /* Start building all the way up to the project configure so that
+   * the user knows if the updates broke their configuration or anything.
+   *
+   * TODO: This should probably be done by the calling API so that we don't
+   *       race with other updaters.
+   */
+  ide_build_manager_rebuild_async (manager,
+                                   IDE_BUILD_PHASE_CONFIGURE,
+                                   NULL,
+                                   NULL,
+                                   gbp_git_dependency_updater_update_cb,
+                                   g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static gboolean
+gbp_git_dependency_updater_update_finish (IdeDependencyUpdater  *self,
+                                          GAsyncResult          *result,
+                                          GError               **error)
+{
+  g_assert (GBP_IS_GIT_DEPENDENCY_UPDATER (self));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+dependency_updater_iface_init (IdeDependencyUpdaterInterface *iface)
+{
+  iface->update_async = gbp_git_dependency_updater_update_async;
+  iface->update_finish = gbp_git_dependency_updater_update_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGitDependencyUpdater, gbp_git_dependency_updater, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_DEPENDENCY_UPDATER,
+                                                dependency_updater_iface_init))
+
+static void
+gbp_git_dependency_updater_class_init (GbpGitDependencyUpdaterClass *klass)
+{
+}
+
+static void
+gbp_git_dependency_updater_init (GbpGitDependencyUpdater *self)
+{
+}
diff --git a/src/plugins/git/gbp-git-dependency-updater.h b/src/plugins/git/gbp-git-dependency-updater.h
new file mode 100644
index 000000000..39b07db18
--- /dev/null
+++ b/src/plugins/git/gbp-git-dependency-updater.h
@@ -0,0 +1,31 @@
+/* gbp-git-dependency-updater.h
+ *
+ * Copyright 2018-2019 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-foundry.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GIT_DEPENDENCY_UPDATER (gbp_git_dependency_updater_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitDependencyUpdater, gbp_git_dependency_updater, GBP, GIT_DEPENDENCY_UPDATER, 
IdeObject)
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-index-monitor.c b/src/plugins/git/gbp-git-index-monitor.c
new file mode 100644
index 000000000..6765f13aa
--- /dev/null
+++ b/src/plugins/git/gbp-git-index-monitor.c
@@ -0,0 +1,140 @@
+/* gbp-git-index-monitor.c
+ *
+ * Copyright 2018-2019 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-git-index-monitor"
+
+#include "config.h"
+
+#include <libide-core.h>
+
+#include "gbp-git-index-monitor.h"
+
+struct _GbpGitIndexMonitor
+{
+  GObject       parent_instance;
+  GFile        *repository_dir;
+  GFileMonitor *monitor;
+};
+
+G_DEFINE_TYPE (GbpGitIndexMonitor, gbp_git_index_monitor, G_TYPE_OBJECT)
+
+enum {
+  CHANGED,
+  N_SIGNALS
+};
+
+static guint signals [N_SIGNALS];
+
+static void
+gbp_git_index_monitor_dispose (GObject *object)
+{
+  GbpGitIndexMonitor *self = (GbpGitIndexMonitor *)object;
+
+  g_clear_object (&self->repository_dir);
+
+  if (self->monitor != NULL)
+    {
+      g_file_monitor_cancel (self->monitor);
+      g_clear_object (&self->monitor);
+    }
+
+  G_OBJECT_CLASS (gbp_git_index_monitor_parent_class)->dispose (object);
+}
+
+static void
+gbp_git_index_monitor_class_init (GbpGitIndexMonitorClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gbp_git_index_monitor_dispose;
+
+  signals [CHANGED] =
+    g_signal_new ("changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL,
+                  g_cclosure_marshal_VOID__VOID,
+                  G_TYPE_NONE, 0);
+  g_signal_set_va_marshaller (signals [CHANGED],
+                              G_TYPE_FROM_CLASS (klass),
+                              g_cclosure_marshal_VOID__VOIDv);
+}
+
+static void
+gbp_git_index_monitor_init (GbpGitIndexMonitor *self)
+{
+}
+
+static void
+gbp_git_index_monitor_changed_cb (GbpGitIndexMonitor *self,
+                                  GFile              *file,
+                                  GFile              *other_file,
+                                  GFileMonitorEvent   event,
+                                  GFileMonitor       *monitor)
+{
+  g_autofree gchar *name = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GIT_INDEX_MONITOR (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!other_file || G_IS_FILE (other_file));
+  g_assert (G_IS_FILE_MONITOR (monitor));
+
+  if (event != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT)
+    IDE_EXIT;
+
+  name = g_file_get_basename (file);
+
+  if (ide_str_equal0 (name, "index"))
+    g_signal_emit (self, signals [CHANGED], 0);
+
+  IDE_EXIT;
+}
+
+GbpGitIndexMonitor *
+gbp_git_index_monitor_new (GFile *repository_dir)
+{
+  GbpGitIndexMonitor *self;
+  g_autoptr(GError) error = NULL;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (G_IS_FILE (repository_dir), NULL);
+
+  self = g_object_new (GBP_TYPE_GIT_INDEX_MONITOR, NULL);
+  self->repository_dir = g_object_ref (repository_dir);
+  self->monitor = g_file_monitor_directory (repository_dir,
+                                            G_FILE_MONITOR_NONE,
+                                            NULL,
+                                            &error);
+
+  if (error != NULL)
+    g_critical ("Failed to monitor git repository, no changes will be detected: %s",
+                error->message);
+  else
+    g_signal_connect_object (self->monitor,
+                             "changed",
+                             G_CALLBACK (gbp_git_index_monitor_changed_cb),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+  return g_steal_pointer (&self);
+}
diff --git a/src/plugins/git/gbp-git-index-monitor.h b/src/plugins/git/gbp-git-index-monitor.h
new file mode 100644
index 000000000..155e0d89e
--- /dev/null
+++ b/src/plugins/git/gbp-git-index-monitor.h
@@ -0,0 +1,33 @@
+/* gbp-git-index-monitor.h
+ *
+ * Copyright 2018-2019 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_GIT_INDEX_MONITOR (gbp_git_index_monitor_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitIndexMonitor, gbp_git_index_monitor, GBP, GIT_INDEX_MONITOR, GObject)
+
+GbpGitIndexMonitor *gbp_git_index_monitor_new (GFile *repository_dir);
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-pipeline-addin.c b/src/plugins/git/gbp-git-pipeline-addin.c
new file mode 100644
index 000000000..0e2e683b0
--- /dev/null
+++ b/src/plugins/git/gbp-git-pipeline-addin.c
@@ -0,0 +1,82 @@
+/* gbp-git-pipeline-addin.c
+ *
+ * Copyright 2018-2019 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-git-pipeline-addin"
+
+#include "config.h"
+
+#include <libide-foundry.h>
+#include <glib/gi18n.h>
+
+#include "gbp-git-pipeline-addin.h"
+#include "gbp-git-submodule-stage.h"
+#include "gbp-git-vcs.h"
+
+struct _GbpGitPipelineAddin
+{
+  IdeObject parent_instance;
+};
+
+static void
+gbp_git_pipeline_addin_load (IdeBuildPipelineAddin *addin,
+                             IdeBuildPipeline      *pipeline)
+{
+  g_autoptr(GbpGitSubmoduleStage) submodule = NULL;
+  IdeContext *context;
+  IdeVcs *vcs;
+  guint stage_id;
+
+  g_assert (GBP_IS_GIT_PIPELINE_ADDIN (addin));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+
+  context = ide_object_get_context (IDE_OBJECT (addin));
+  vcs = ide_vcs_from_context (context);
+
+  /* Ignore everything if this isn't a git-based repository */
+  if (!GBP_IS_GIT_VCS (vcs))
+    return;
+
+  submodule = gbp_git_submodule_stage_new (context);
+  stage_id = ide_build_pipeline_attach (pipeline,
+                                        IDE_BUILD_PHASE_DOWNLOADS,
+                                        100,
+                                        IDE_BUILD_STAGE (submodule));
+  ide_build_pipeline_addin_track (addin, stage_id);
+}
+
+static void
+build_pipeline_addin_iface_init (IdeBuildPipelineAddinInterface *iface)
+{
+  iface->load = gbp_git_pipeline_addin_load;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGitPipelineAddin, gbp_git_pipeline_addin, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                                build_pipeline_addin_iface_init))
+
+static void
+gbp_git_pipeline_addin_class_init (GbpGitPipelineAddinClass *klass)
+{
+}
+
+static void
+gbp_git_pipeline_addin_init (GbpGitPipelineAddin *self)
+{
+}
diff --git a/src/plugins/git/gbp-git-pipeline-addin.h b/src/plugins/git/gbp-git-pipeline-addin.h
new file mode 100644
index 000000000..19a8d5be5
--- /dev/null
+++ b/src/plugins/git/gbp-git-pipeline-addin.h
@@ -0,0 +1,31 @@
+/* gbp-git-pipeline-addin.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GIT_PIPELINE_ADDIN (gbp_git_pipeline_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitPipelineAddin, gbp_git_pipeline_addin, GBP, GIT_PIPELINE_ADDIN, IdeObject)
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-remote-callbacks.c b/src/plugins/git/gbp-git-remote-callbacks.c
new file mode 100644
index 000000000..ce5dbec79
--- /dev/null
+++ b/src/plugins/git/gbp-git-remote-callbacks.c
@@ -0,0 +1,265 @@
+/* gbp-git-remote-callbacks.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-git-remote-callbacks"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "gbp-git-remote-callbacks.h"
+
+#define ANIMATION_DURATION_MSEC 250
+
+struct _GbpGitRemoteCallbacks
+{
+  GgitRemoteCallbacks  parent_instance;
+
+  IdeNotification     *progress;
+  GString             *body;
+  GgitCredtype         tried;
+  guint                cancelled : 1;
+};
+
+G_DEFINE_TYPE (GbpGitRemoteCallbacks, gbp_git_remote_callbacks, GGIT_TYPE_REMOTE_CALLBACKS)
+
+enum {
+  PROP_0,
+  PROP_PROGRESS,
+  LAST_PROP
+};
+
+static GParamSpec *properties [LAST_PROP];
+
+GgitRemoteCallbacks *
+gbp_git_remote_callbacks_new (IdeNotification *progress)
+{
+  g_return_val_if_fail (IDE_IS_NOTIFICATION (progress), NULL);
+
+  return g_object_new (GBP_TYPE_GIT_REMOTE_CALLBACKS,
+                       "progress", progress,
+                       NULL);
+}
+
+/**
+ * gbp_git_remote_callbacks_get_progress:
+ *
+ * Gets the #IdeNotification for the operation.
+ *
+ * Returns: (transfer none): An #IdeNotification.
+ *
+ * Since: 3.32
+ */
+IdeNotification *
+gbp_git_remote_callbacks_get_progress (GbpGitRemoteCallbacks *self)
+{
+  g_return_val_if_fail (GBP_IS_GIT_REMOTE_CALLBACKS (self), NULL);
+
+  return self->progress;
+}
+
+static void
+gbp_git_remote_callbacks_real_progress (GgitRemoteCallbacks *callbacks,
+                                        const gchar         *message)
+{
+  GbpGitRemoteCallbacks *self = (GbpGitRemoteCallbacks *)callbacks;
+
+  g_assert (GBP_IS_GIT_REMOTE_CALLBACKS (self));
+
+  if (self->body == NULL)
+    self->body = g_string_new (message);
+  else
+    g_string_append (self->body, message);
+
+  ide_notification_set_body (self->progress, self->body->str);
+}
+
+static void
+gbp_git_remote_callbacks_real_transfer_progress (GgitRemoteCallbacks  *callbacks,
+                                                 GgitTransferProgress *stats)
+{
+  GbpGitRemoteCallbacks *self = (GbpGitRemoteCallbacks *)callbacks;
+  guint total;
+  guint received;
+
+  g_assert (GBP_IS_GIT_REMOTE_CALLBACKS (self));
+  g_assert (stats != NULL);
+
+  if (self->cancelled)
+    return;
+
+  total = ggit_transfer_progress_get_total_objects (stats);
+  received = ggit_transfer_progress_get_received_objects (stats);
+  if (total == 0)
+    return;
+
+  ide_notification_set_progress (self->progress, (gdouble)received / (gdouble)total);
+}
+
+static GgitCred *
+gbp_git_remote_callbacks_real_credentials (GgitRemoteCallbacks  *callbacks,
+                                           const gchar          *url,
+                                           const gchar          *username_from_url,
+                                           GgitCredtype          allowed_types,
+                                           GError              **error)
+{
+  GbpGitRemoteCallbacks *self = (GbpGitRemoteCallbacks *)callbacks;
+  GgitCred *ret = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GIT_REMOTE_CALLBACKS (self));
+  g_assert (url != NULL);
+
+  IDE_TRACE_MSG ("username=%s url=%s", username_from_url ?: "", url);
+
+  if (self->cancelled)
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_CANCELLED,
+                   "The operation has been canceled");
+      IDE_RETURN (NULL);
+    }
+
+  allowed_types &= ~self->tried;
+
+  if ((allowed_types & GGIT_CREDTYPE_SSH_KEY) != 0)
+    {
+      GgitCredSshKeyFromAgent *cred;
+
+      cred = ggit_cred_ssh_key_from_agent_new (username_from_url, error);
+      ret = GGIT_CRED (cred);
+      self->tried |= GGIT_CREDTYPE_SSH_KEY;
+    }
+
+  if ((allowed_types & GGIT_CREDTYPE_SSH_INTERACTIVE) != 0)
+    {
+      GgitCredSshInteractive *cred;
+
+      cred = ggit_cred_ssh_interactive_new (username_from_url, error);
+      ret = GGIT_CRED (cred);
+      self->tried |= GGIT_CREDTYPE_SSH_INTERACTIVE;
+    }
+
+  if (ret == NULL)
+    g_set_error (error,
+                 G_IO_ERROR,
+                 G_IO_ERROR_NOT_SUPPORTED,
+                 _("Builder failed to provide appropriate credentials when cloning repository."));
+
+  IDE_RETURN (ret);
+}
+
+static void
+gbp_git_remote_callbacks_finalize (GObject *object)
+{
+  GbpGitRemoteCallbacks *self = (GbpGitRemoteCallbacks *)object;
+
+  g_clear_object (&self->progress);
+
+  g_string_free (self->body, TRUE);
+  self->body = NULL;
+
+  G_OBJECT_CLASS (gbp_git_remote_callbacks_parent_class)->finalize (object);
+}
+
+static void
+gbp_git_remote_callbacks_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  GbpGitRemoteCallbacks *self = GBP_GIT_REMOTE_CALLBACKS (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROGRESS:
+      g_value_set_object (value, gbp_git_remote_callbacks_get_progress (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_git_remote_callbacks_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  GbpGitRemoteCallbacks *self = GBP_GIT_REMOTE_CALLBACKS (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROGRESS:
+      g_clear_object (&self->progress);
+      self->progress = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_git_remote_callbacks_class_init (GbpGitRemoteCallbacksClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GgitRemoteCallbacksClass *callbacks_class = GGIT_REMOTE_CALLBACKS_CLASS (klass);
+
+  object_class->finalize = gbp_git_remote_callbacks_finalize;
+  object_class->get_property = gbp_git_remote_callbacks_get_property;
+  object_class->set_property = gbp_git_remote_callbacks_set_property;
+
+  callbacks_class->transfer_progress = gbp_git_remote_callbacks_real_transfer_progress;
+  callbacks_class->progress = gbp_git_remote_callbacks_real_progress;
+  callbacks_class->credentials = gbp_git_remote_callbacks_real_credentials;
+
+  properties [PROP_PROGRESS] =
+    g_param_spec_object ("progress",
+                         "Progress",
+                         "An IdeNotification instance containing the operation progress.",
+                         IDE_TYPE_NOTIFICATION,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, LAST_PROP, properties);
+}
+
+static void
+gbp_git_remote_callbacks_init (GbpGitRemoteCallbacks *self)
+{
+}
+
+/**
+ * gbp_git_remote_callbacks_cancel:
+ *
+ * This function should be called when a clone was canceled so that we can
+ * avoid dispatching more events.
+ *
+ * Since: 3.32
+ */
+void
+gbp_git_remote_callbacks_cancel (GbpGitRemoteCallbacks *self)
+{
+  g_return_if_fail (GBP_IS_GIT_REMOTE_CALLBACKS (self));
+
+  self->cancelled  = TRUE;
+}
diff --git a/src/plugins/git/gbp-git-remote-callbacks.h b/src/plugins/git/gbp-git-remote-callbacks.h
new file mode 100644
index 000000000..185fb597c
--- /dev/null
+++ b/src/plugins/git/gbp-git-remote-callbacks.h
@@ -0,0 +1,37 @@
+/* gbp-git-remote-callbacks.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <libgit2-glib/ggit.h>
+#include <libide-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GIT_REMOTE_CALLBACKS (gbp_git_remote_callbacks_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitRemoteCallbacks, gbp_git_remote_callbacks, GBP, GIT_REMOTE_CALLBACKS, 
GgitRemoteCallbacks)
+
+GgitRemoteCallbacks *gbp_git_remote_callbacks_new          (IdeNotification       *progress);
+gdouble              gbp_git_remote_callbacks_get_fraction (GbpGitRemoteCallbacks *self);
+IdeNotification     *gbp_git_remote_callbacks_get_progress (GbpGitRemoteCallbacks *self);
+void                 gbp_git_remote_callbacks_cancel       (GbpGitRemoteCallbacks *self);
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-submodule-stage.c b/src/plugins/git/gbp-git-submodule-stage.c
new file mode 100644
index 000000000..e8dc4ae90
--- /dev/null
+++ b/src/plugins/git/gbp-git-submodule-stage.c
@@ -0,0 +1,218 @@
+/* gbp-git-submodule-stage.c
+ *
+ * Copyright 2018-2019 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-git-submodule-stage"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-code.h>
+#include <libide-gui.h>
+#include <libide-vcs.h>
+
+#include "gbp-git-submodule-stage.h"
+
+struct _GbpGitSubmoduleStage
+{
+  IdeBuildStageLauncher parent_instance;
+
+  guint has_run : 1;
+  guint force_update : 1;
+};
+
+G_DEFINE_TYPE (GbpGitSubmoduleStage, gbp_git_submodule_stage, IDE_TYPE_BUILD_STAGE_LAUNCHER)
+
+GbpGitSubmoduleStage *
+gbp_git_submodule_stage_new (IdeContext *context)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(GbpGitSubmoduleStage) self = NULL;
+  g_autoptr(GFile) workdir = NULL;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  workdir = ide_context_ref_workdir (context);
+
+  self = g_object_new (GBP_TYPE_GIT_SUBMODULE_STAGE, NULL);
+
+  launcher = ide_subprocess_launcher_new (0);
+  ide_subprocess_launcher_set_cwd (launcher, g_file_peek_path (workdir));
+  ide_subprocess_launcher_set_clear_env (launcher, FALSE);
+  ide_subprocess_launcher_push_argv (launcher, "sh");
+  ide_subprocess_launcher_push_argv (launcher, "-c");
+  ide_subprocess_launcher_push_argv (launcher, "git submodule init && git submodule update");
+
+  ide_build_stage_launcher_set_launcher (IDE_BUILD_STAGE_LAUNCHER (self), launcher);
+
+  return g_steal_pointer (&self);
+}
+
+static void
+gbp_git_submodule_stage_query_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  IdeSubprocess *subprocess = (IdeSubprocess *)object;
+  g_autoptr(GbpGitSubmoduleStage) self = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *stdout_buf = NULL;
+  IdeLineReader reader;
+  const gchar *line;
+  gsize line_len;
+
+  g_assert (IDE_IS_SUBPROCESS (subprocess));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GBP_IS_GIT_SUBMODULE_STAGE (self));
+
+  if (!ide_subprocess_communicate_utf8_finish (subprocess, result, &stdout_buf, NULL, &error))
+    {
+      ide_build_stage_log (IDE_BUILD_STAGE (self),
+                           IDE_BUILD_LOG_STDERR,
+                           error->message,
+                           -1);
+      goto failure;
+    }
+
+  ide_line_reader_init (&reader, stdout_buf, -1);
+  while ((line = ide_line_reader_next (&reader, &line_len)))
+    {
+      /* If we find a line starting with -, it isn't initialized
+       * and needs a submodule-init/update.
+       */
+      if (stdout_buf[0] == '-')
+        {
+          ide_build_stage_set_completed (IDE_BUILD_STAGE (self), FALSE);
+          goto unpause;
+        }
+    }
+
+failure:
+  ide_build_stage_set_completed (IDE_BUILD_STAGE (self), TRUE);
+
+unpause:
+  ide_build_stage_unpause (IDE_BUILD_STAGE (self));
+}
+
+static void
+gbp_git_submodule_stage_query (IdeBuildStage    *stage,
+                               IdeBuildPipeline *pipeline,
+                               GPtrArray        *targets,
+                               GCancellable     *cancellable)
+{
+  GbpGitSubmoduleStage *self = (GbpGitSubmoduleStage *)stage;
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GIT_SUBMODULE_STAGE (self));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!ide_application_has_network (IDE_APPLICATION_DEFAULT))
+    {
+      ide_build_stage_log (stage,
+                           IDE_BUILD_LOG_STDERR,
+                           _("Network is not available, skipping submodule update"),
+                           -1);
+      ide_build_stage_set_completed (stage, TRUE);
+      IDE_EXIT;
+    }
+
+  if (self->force_update)
+    {
+      self->force_update = FALSE;
+      self->has_run = TRUE;
+      ide_build_stage_set_completed (stage, FALSE);
+      IDE_EXIT;
+    }
+
+  if (self->has_run)
+    {
+      ide_build_stage_set_completed (stage, TRUE);
+      IDE_EXIT;
+    }
+
+  self->has_run = TRUE;
+
+  /* We need to run "git submodule status" to see if there are any
+   * lines that are prefixed with - (meaning they have not yet been
+   * initialized).
+   *
+   * We only do a git submodule init/update if that is the case, otherwise
+   * dependencies are updated with the dependency updater.
+   */
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  workdir = ide_context_ref_workdir (context);
+
+  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE);
+  ide_subprocess_launcher_push_argv (launcher, "git");
+  ide_subprocess_launcher_push_argv (launcher, "submodule");
+  ide_subprocess_launcher_push_argv (launcher, "status");
+  ide_subprocess_launcher_set_cwd (launcher, g_file_peek_path (workdir));
+  ide_subprocess_launcher_set_clear_env (launcher, FALSE);
+
+  if (!(subprocess = ide_subprocess_launcher_spawn (launcher, cancellable, &error)))
+    {
+      ide_build_stage_log (IDE_BUILD_STAGE (stage),
+                           IDE_BUILD_LOG_STDERR,
+                           error->message,
+                           -1);
+      ide_build_stage_set_completed (IDE_BUILD_STAGE (stage), TRUE);
+      IDE_EXIT;
+    }
+
+  ide_build_stage_pause (IDE_BUILD_STAGE (stage));
+
+  ide_subprocess_communicate_utf8_async (subprocess,
+                                         NULL,
+                                         cancellable,
+                                         gbp_git_submodule_stage_query_cb,
+                                         g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_git_submodule_stage_class_init (GbpGitSubmoduleStageClass *klass)
+{
+  IdeBuildStageClass *stage_class = IDE_BUILD_STAGE_CLASS (klass);
+
+  stage_class->query = gbp_git_submodule_stage_query;
+}
+
+static void
+gbp_git_submodule_stage_init (GbpGitSubmoduleStage *self)
+{
+  ide_build_stage_set_name (IDE_BUILD_STAGE (self), _("Initialize git submodules"));
+  ide_build_stage_launcher_set_ignore_exit_status (IDE_BUILD_STAGE_LAUNCHER (self), TRUE);
+}
+
+void
+gbp_git_submodule_stage_force_update (GbpGitSubmoduleStage *self)
+{
+  g_return_if_fail (GBP_IS_GIT_SUBMODULE_STAGE (self));
+
+  self->force_update = TRUE;
+}
diff --git a/src/plugins/git/gbp-git-submodule-stage.h b/src/plugins/git/gbp-git-submodule-stage.h
new file mode 100644
index 000000000..3b23c033c
--- /dev/null
+++ b/src/plugins/git/gbp-git-submodule-stage.h
@@ -0,0 +1,34 @@
+/* gbp-git-submodule-stage.h
+ *
+ * Copyright 2018-2019 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-foundry.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GIT_SUBMODULE_STAGE (gbp_git_submodule_stage_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitSubmoduleStage, gbp_git_submodule_stage, GBP, GIT_SUBMODULE_STAGE, 
IdeBuildStageLauncher)
+
+GbpGitSubmoduleStage *gbp_git_submodule_stage_new          (IdeContext           *context);
+void                  gbp_git_submodule_stage_force_update (GbpGitSubmoduleStage *self);
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-vcs-cloner.c b/src/plugins/git/gbp-git-vcs-cloner.c
new file mode 100644
index 000000000..2a0b9958a
--- /dev/null
+++ b/src/plugins/git/gbp-git-vcs-cloner.c
@@ -0,0 +1,317 @@
+/* gbp-git-vcs-cloner.c
+ *
+ * Copyright 2018-2019 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-git-vcs-cloner"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-threading.h>
+
+#include "gbp-git-remote-callbacks.h"
+#include "gbp-git-vcs-cloner.h"
+
+struct _GbpGitVcsCloner
+{
+  GObject parent_instance;
+};
+
+typedef struct
+{
+  IdeNotification *notif;
+  IdeVcsUri       *uri;
+  gchar           *branch;
+  GFile           *location;
+  GFile           *project_file;
+  gchar           *author_name;
+  gchar           *author_email;
+} CloneRequest;
+
+static void vcs_cloner_iface_init (IdeVcsClonerInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GbpGitVcsCloner, gbp_git_vcs_cloner, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_VCS_CLONER,
+                                                vcs_cloner_iface_init))
+
+static void
+clone_request_free (gpointer data)
+{
+  CloneRequest *req = data;
+
+  if (req != NULL)
+    {
+      g_clear_pointer (&req->uri, ide_vcs_uri_unref);
+      g_clear_pointer (&req->branch, g_free);
+      g_clear_object (&req->notif);
+      g_clear_object (&req->location);
+      g_clear_object (&req->project_file);
+      g_slice_free (CloneRequest, req);
+    }
+}
+
+static CloneRequest *
+clone_request_new (IdeVcsUri       *uri,
+                   const gchar     *branch,
+                   GFile           *location,
+                   IdeNotification *notif)
+{
+  CloneRequest *req;
+
+  g_assert (uri);
+  g_assert (location);
+  g_assert (notif);
+
+  req = g_slice_new0 (CloneRequest);
+  req->uri = ide_vcs_uri_ref (uri);
+  req->branch = g_strdup (branch);
+  req->location = g_object_ref (location);
+  req->project_file = NULL;
+  req->notif = g_object_ref (notif);
+
+  return req;
+}
+
+static void
+gbp_git_vcs_cloner_class_init (GbpGitVcsClonerClass *klass)
+{
+}
+
+static void
+gbp_git_vcs_cloner_init (GbpGitVcsCloner *self)
+{
+}
+
+static gchar *
+gbp_git_vcs_cloner_get_title (IdeVcsCloner *cloner)
+{
+  return g_strdup ("Git");
+}
+
+static gboolean
+gbp_git_vcs_cloner_validate_uri (IdeVcsCloner  *cloner,
+                                 const gchar   *uri,
+                                 gchar        **errmsg)
+{
+  g_autoptr(IdeVcsUri) vcs_uri = NULL;
+
+  g_assert (IDE_IS_VCS_CLONER (cloner));
+  g_assert (uri != NULL);
+
+  vcs_uri = ide_vcs_uri_new (uri);
+
+  if (vcs_uri != NULL)
+    {
+      const gchar *scheme = ide_vcs_uri_get_scheme (vcs_uri);
+      const gchar *path = ide_vcs_uri_get_path (vcs_uri);
+
+      if (ide_str_equal0 (scheme, "file"))
+        {
+          g_autoptr(GFile) file = g_file_new_for_path (path);
+
+          if (!g_file_query_exists (file, NULL))
+            {
+              if (errmsg != NULL)
+                *errmsg = g_strdup_printf ("A resository could not be found at “%s”.", path);
+              return FALSE;
+            }
+
+          return TRUE;
+        }
+
+      /* We can only support certain schemes */
+      if (ide_str_equal0 (scheme, "http") ||
+          ide_str_equal0 (scheme, "https") ||
+          ide_str_equal0 (scheme, "git") ||
+          ide_str_equal0 (scheme, "rsync") ||
+          ide_str_equal0 (scheme, "ssh"))
+        return TRUE;
+
+      if (errmsg != NULL)
+        *errmsg = g_strdup_printf (_("The protocol “%s” is not supported."), scheme);
+    }
+
+  return FALSE;
+}
+
+static void
+gbp_git_vcs_cloner_worker (IdeTask      *task,
+                           gpointer      source_object,
+                           gpointer      task_data,
+                           GCancellable *cancellable)
+{
+  GbpGitVcsCloner *self = source_object;
+  g_autoptr(GgitConfig) config = NULL;
+  g_autoptr(GFile) config_file = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *uristr = NULL;
+  GgitRepository *repository;
+  GgitCloneOptions *clone_options;
+  GgitFetchOptions *fetch_options;
+  GgitRemoteCallbacks *callbacks;
+  CloneRequest *req = task_data;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_GIT_VCS_CLONER (self));
+  g_assert (req != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  callbacks = gbp_git_remote_callbacks_new (req->notif);
+
+  g_signal_connect_object (cancellable,
+                           "cancelled",
+                           G_CALLBACK (gbp_git_remote_callbacks_cancel),
+                           callbacks,
+                           G_CONNECT_SWAPPED);
+
+  fetch_options = ggit_fetch_options_new ();
+  ggit_fetch_options_set_remote_callbacks (fetch_options, callbacks);
+
+  clone_options = ggit_clone_options_new ();
+  ggit_clone_options_set_is_bare (clone_options, FALSE);
+  ggit_clone_options_set_checkout_branch (clone_options, req->branch);
+  ggit_clone_options_set_fetch_options (clone_options, fetch_options);
+  g_clear_pointer (&fetch_options, ggit_fetch_options_free);
+
+  uristr = ide_vcs_uri_to_string (req->uri);
+
+  repository = ggit_repository_clone (uristr, req->location, clone_options, &error);
+
+  g_clear_object (&callbacks);
+  g_clear_object (&clone_options);
+
+  if (repository == NULL)
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  if (ide_task_return_error_if_cancelled (task))
+    return;
+
+  config_file = g_file_get_child (req->location, ".git/config");
+
+  if ((config = ggit_config_new_from_file (config_file, &error)))
+    {
+      if (req->author_name)
+        ggit_config_set_string (config, "user.name", req->author_name, &error);
+      if (req->author_email)
+        ggit_config_set_string (config, "user.email", req->author_email, &error);
+    }
+
+  req->project_file = ggit_repository_get_workdir (repository);
+
+  ide_task_return_boolean (task, TRUE);
+
+  g_clear_object (&repository);
+}
+
+static void
+gbp_git_vcs_cloner_clone_async (IdeVcsCloner        *cloner,
+                                const gchar         *uri,
+                                const gchar         *destination,
+                                GVariantDict        *options,
+                                GCancellable        *cancellable,
+                                IdeNotification    **notif,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  GbpGitVcsCloner *self = (GbpGitVcsCloner *)cloner;
+  g_autoptr(IdeNotification) notif_local = NULL;
+  g_autoptr(IdeVcsUri) vcs_uri = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) location = NULL;
+  g_autofree gchar *uristr = NULL;
+  CloneRequest *req;
+  const gchar *branch;
+
+  g_assert (GBP_IS_GIT_VCS_CLONER (cloner));
+  g_assert (uri != NULL);
+  g_assert (destination != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_git_vcs_cloner_clone_async);
+
+  notif_local = ide_notification_new ();
+  if (notif != NULL)
+    *notif = g_object_ref (notif_local);
+
+  if (!g_variant_dict_lookup (options, "branch", "&s", &branch))
+    branch = "master";
+
+  /*
+   * ggit_repository_clone() will block and we don't have a good way to
+   * cancel it. So we need to return immediately (even though the clone
+   * will continue in the background for now).
+   *
+   * FIXME: Find Ggit API to cancel clone. We might need access to the
+   *    GgitRemote so we can ggit_remote_disconnect().
+   */
+  ide_task_set_return_on_cancel (task, TRUE);
+
+  uristr = g_strstrip (g_strdup (uri));
+  location = g_file_new_for_path (destination);
+
+  vcs_uri = ide_vcs_uri_new (uristr);
+
+  if (vcs_uri == NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVAL,
+                                 _("A valid Git URL is required"));
+      return;
+    }
+
+  if (g_strcmp0 ("ssh", ide_vcs_uri_get_scheme (vcs_uri)) == 0)
+    {
+      if (ide_vcs_uri_get_user (vcs_uri) == NULL)
+        ide_vcs_uri_set_user (vcs_uri, g_get_user_name ());
+    }
+
+  req = clone_request_new (vcs_uri, branch, location, notif_local);
+
+  g_variant_dict_lookup (options, "author-name", "s", &req->author_name);
+  g_variant_dict_lookup (options, "author-email", "s", &req->author_email);
+
+  ide_task_set_task_data (task, req, clone_request_free);
+  ide_task_run_in_thread (task, gbp_git_vcs_cloner_worker);
+}
+
+static gboolean
+gbp_git_vcs_cloner_clone_finish (IdeVcsCloner  *cloner,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  g_assert (GBP_IS_GIT_VCS_CLONER (cloner));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+vcs_cloner_iface_init (IdeVcsClonerInterface *iface)
+{
+  iface->get_title = gbp_git_vcs_cloner_get_title;
+  iface->validate_uri = gbp_git_vcs_cloner_validate_uri;
+  iface->clone_async = gbp_git_vcs_cloner_clone_async;
+  iface->clone_finish = gbp_git_vcs_cloner_clone_finish;
+}
diff --git a/src/plugins/git/gbp-git-vcs-cloner.h b/src/plugins/git/gbp-git-vcs-cloner.h
new file mode 100644
index 000000000..d634242e9
--- /dev/null
+++ b/src/plugins/git/gbp-git-vcs-cloner.h
@@ -0,0 +1,31 @@
+/* gbp-git-vcs-cloner.h
+ *
+ * Copyright 2018-2019 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-vcs.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GIT_VCS_CLONER (gbp_git_vcs_cloner_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitVcsCloner, gbp_git_vcs_cloner, GBP, GIT_VCS_CLONER, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-vcs-config.c b/src/plugins/git/gbp-git-vcs-config.c
new file mode 100644
index 000000000..cf8a20153
--- /dev/null
+++ b/src/plugins/git/gbp-git-vcs-config.c
@@ -0,0 +1,187 @@
+/* gbp-git-vcs-config.c
+ *
+ * Copyright 2016 Akshaya Kakkilaya <akshaya kakkilaya gmail 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-git-vcs-config"
+
+#include "config.h"
+
+#include <libgit2-glib/ggit.h>
+#include <libide-vcs.h>
+
+#include "gbp-git-vcs-config.h"
+
+struct _GbpGitVcsConfig
+{
+  GObject     parent_instance;
+
+  GgitConfig *config;
+};
+
+static void vcs_config_init (IdeVcsConfigInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (GbpGitVcsConfig, gbp_git_vcs_config, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_VCS_CONFIG, vcs_config_init))
+
+GbpGitVcsConfig *
+gbp_git_vcs_config_new (void)
+{
+  return g_object_new (GBP_TYPE_GIT_VCS_CONFIG, NULL);
+}
+
+static void
+gbp_git_vcs_config_get_string (GgitConfig  *config,
+                               const gchar *key,
+                               GValue      *value,
+                               GError     **error)
+{
+  const gchar *str;
+
+  g_assert (GGIT_IS_CONFIG (config));
+  g_assert (key != NULL);
+
+  str = ggit_config_get_string (config, key, error);
+
+  g_value_set_string (value, str);
+}
+
+static void
+gbp_git_vcs_config_set_string (GgitConfig   *config,
+                               const gchar  *key,
+                               const GValue *value,
+                               GError      **error)
+{
+  const gchar *str;
+
+  g_assert (GGIT_IS_CONFIG (config));
+  g_assert (key != NULL);
+
+  str = g_value_get_string (value);
+
+  if (str != NULL)
+    ggit_config_set_string (config, key, str, error);
+}
+
+static void
+gbp_git_vcs_config_get_config (IdeVcsConfig    *self,
+                               IdeVcsConfigType type,
+                               GValue          *value)
+{
+  g_autoptr(GgitConfig) config = NULL;
+  GgitConfig *orig_config;
+
+  g_return_if_fail (GBP_IS_GIT_VCS_CONFIG (self));
+
+  orig_config = GBP_GIT_VCS_CONFIG (self)->config;
+  config = ggit_config_snapshot (orig_config, NULL);
+
+  if(config == NULL)
+    return;
+
+  switch (type)
+    {
+    case IDE_VCS_CONFIG_FULL_NAME:
+      gbp_git_vcs_config_get_string (config, "user.name", value, NULL);
+      break;
+
+    case IDE_VCS_CONFIG_EMAIL:
+      gbp_git_vcs_config_get_string (config, "user.email", value, NULL);
+      break;
+
+    default:
+      break;
+    }
+}
+
+static void
+gbp_git_vcs_config_set_config (IdeVcsConfig    *self,
+                               IdeVcsConfigType type,
+                               const GValue    *value)
+{
+  GgitConfig *config;
+
+  g_return_if_fail (GBP_IS_GIT_VCS_CONFIG (self));
+
+  config = GBP_GIT_VCS_CONFIG (self)->config;
+
+  switch (type)
+    {
+    case IDE_VCS_CONFIG_FULL_NAME:
+      gbp_git_vcs_config_set_string (config, "user.name", value, NULL);
+      break;
+
+    case IDE_VCS_CONFIG_EMAIL:
+      gbp_git_vcs_config_set_string (config, "user.email", value, NULL);
+      break;
+
+    default:
+      break;
+    }
+}
+
+static void
+gbp_git_vcs_config_constructed (GObject *object)
+{
+  GbpGitVcsConfig *self = GBP_GIT_VCS_CONFIG (object);
+
+  g_autoptr(GFile) global_file = NULL;
+
+  if (!(global_file = ggit_config_find_global ()))
+    {
+      g_autofree gchar *path = NULL;
+
+      path = g_build_filename (g_get_home_dir (), ".gitconfig", NULL);
+      global_file = g_file_new_for_path (path);
+    }
+
+  self->config = ggit_config_new_from_file (global_file, NULL);
+
+  G_OBJECT_CLASS (gbp_git_vcs_config_parent_class)->constructed (object);
+}
+
+static void
+gbp_git_vcs_config_finalize (GObject *object)
+{
+  GbpGitVcsConfig *self = GBP_GIT_VCS_CONFIG (object);
+
+  g_object_unref (self->config);
+
+  G_OBJECT_CLASS (gbp_git_vcs_config_parent_class)->finalize (object);
+}
+
+static void
+vcs_config_init (IdeVcsConfigInterface *iface)
+{
+  iface->get_config = gbp_git_vcs_config_get_config;
+  iface->set_config = gbp_git_vcs_config_set_config;
+}
+
+static void
+gbp_git_vcs_config_class_init (GbpGitVcsConfigClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = gbp_git_vcs_config_constructed;
+  object_class->finalize = gbp_git_vcs_config_finalize;
+}
+
+static void
+gbp_git_vcs_config_init (GbpGitVcsConfig *self)
+{
+}
diff --git a/src/plugins/git/gbp-git-vcs-config.h b/src/plugins/git/gbp-git-vcs-config.h
new file mode 100644
index 000000000..acc416d03
--- /dev/null
+++ b/src/plugins/git/gbp-git-vcs-config.h
@@ -0,0 +1,33 @@
+/* gbp-git-vcs-config.h
+ *
+ * Copyright 2016 Akshaya Kakkilaya <akshaya kakkilaya gmail 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-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GIT_VCS_CONFIG (gbp_git_vcs_config_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitVcsConfig, gbp_git_vcs_config, GBP, GIT_VCS_CONFIG, GObject)
+
+GbpGitVcsConfig *gbp_git_vcs_config_new (void);
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-vcs-initializer.c b/src/plugins/git/gbp-git-vcs-initializer.c
new file mode 100644
index 000000000..d05b3690d
--- /dev/null
+++ b/src/plugins/git/gbp-git-vcs-initializer.c
@@ -0,0 +1,114 @@
+/* gbp-git-vcs-initializer.c
+ *
+ * Copyright 2016-2019 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-git-vcs-initializer"
+
+#include "config.h"
+
+#include <libgit2-glib/ggit.h>
+#include <libide-threading.h>
+
+#include "gbp-git-vcs-initializer.h"
+
+struct _GbpGitVcsInitializer
+{
+  GObject parent_instance;
+};
+
+static void vcs_initializer_init (IdeVcsInitializerInterface *iface);
+
+G_DEFINE_TYPE_EXTENDED (GbpGitVcsInitializer, gbp_git_vcs_initializer, G_TYPE_OBJECT, 0,
+                        G_IMPLEMENT_INTERFACE (IDE_TYPE_VCS_INITIALIZER, vcs_initializer_init))
+
+static void
+gbp_git_vcs_initializer_class_init (GbpGitVcsInitializerClass *klass)
+{
+}
+
+static void
+gbp_git_vcs_initializer_init (GbpGitVcsInitializer *self)
+{
+}
+
+static void
+gbp_git_vcs_initializer_initialize_worker (IdeTask      *task,
+                                           gpointer      source_object,
+                                           gpointer      task_data,
+                                           GCancellable *cancellable)
+{
+  g_autoptr(GgitRepository) repository = NULL;
+  g_autoptr(GError) error = NULL;
+  GFile *file = task_data;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_GIT_VCS_INITIALIZER (source_object));
+  g_assert (G_IS_FILE (file));
+
+  repository = ggit_repository_init_repository (file, FALSE, &error);
+
+  if (repository == NULL)
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_git_vcs_initializer_initialize_async (IdeVcsInitializer   *initializer,
+                                          GFile               *file,
+                                          GCancellable        *cancellable,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
+{
+  GbpGitVcsInitializer *self = (GbpGitVcsInitializer *)initializer;
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (GBP_IS_GIT_VCS_INITIALIZER (self));
+  g_return_if_fail (G_IS_FILE (file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
+  ide_task_run_in_thread (task, gbp_git_vcs_initializer_initialize_worker);
+}
+
+static gboolean
+gbp_git_vcs_initializer_initialize_finish (IdeVcsInitializer  *initializer,
+                                           GAsyncResult       *result,
+                                           GError            **error)
+{
+  g_return_val_if_fail (GBP_IS_GIT_VCS_INITIALIZER (initializer), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static gchar *
+gbp_git_vcs_initializer_get_title (IdeVcsInitializer *initilizer)
+{
+  return g_strdup ("Git");
+}
+
+static void
+vcs_initializer_init (IdeVcsInitializerInterface *iface)
+{
+  iface->get_title = gbp_git_vcs_initializer_get_title;
+  iface->initialize_async = gbp_git_vcs_initializer_initialize_async;
+  iface->initialize_finish = gbp_git_vcs_initializer_initialize_finish;
+}
diff --git a/src/plugins/git/gbp-git-vcs-initializer.h b/src/plugins/git/gbp-git-vcs-initializer.h
new file mode 100644
index 000000000..16493bf88
--- /dev/null
+++ b/src/plugins/git/gbp-git-vcs-initializer.h
@@ -0,0 +1,31 @@
+/* gbp-git-vcs-initializer.h
+ *
+ * Copyright 2016-2019 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-vcs.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GIT_VCS_INITIALIZER (gbp_git_vcs_initializer_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitVcsInitializer, gbp_git_vcs_initializer, GBP, GIT_VCS_INITIALIZER, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-vcs.c b/src/plugins/git/gbp-git-vcs.c
new file mode 100644
index 000000000..c00a73fc4
--- /dev/null
+++ b/src/plugins/git/gbp-git-vcs.c
@@ -0,0 +1,543 @@
+/* gbp-git-vcs.c
+ *
+ * Copyright 2018-2019 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-git-vcs"
+
+#include "config.h"
+
+#include "gbp-git-vcs.h"
+#include "gbp-git-vcs-config.h"
+
+struct _GbpGitVcs
+{
+  IdeObject       parent_instance;
+  GgitRepository *repository;
+  GFile          *location;
+  GFile          *workdir;
+  gchar          *branch;
+};
+
+enum {
+  PROP_0,
+  PROP_BRANCH_NAME,
+  PROP_LOCATION,
+  PROP_REPOSITORY,
+  PROP_WORKDIR,
+  N_PROPS
+};
+
+static void vcs_iface_init (IdeVcsInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GbpGitVcs, gbp_git_vcs, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_VCS, vcs_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_git_vcs_finalize (GObject *object)
+{
+  GbpGitVcs *self = (GbpGitVcs *)object;
+
+  g_clear_object (&self->repository);
+  g_clear_object (&self->location);
+  g_clear_object (&self->workdir);
+  g_clear_pointer (&self->branch, g_free);
+
+  G_OBJECT_CLASS (gbp_git_vcs_parent_class)->finalize (object);
+}
+
+static void
+gbp_git_vcs_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  GbpGitVcs *self = GBP_GIT_VCS (object);
+
+  switch (prop_id)
+    {
+    case PROP_BRANCH_NAME:
+      g_value_set_string (value, self->branch);
+      break;
+
+    case PROP_LOCATION:
+      g_value_set_object (value, self->location);
+      break;
+
+    case PROP_REPOSITORY:
+      g_value_set_object (value, self->repository);
+      break;
+
+    case PROP_WORKDIR:
+      g_value_set_object (value, self->workdir);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_git_vcs_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  GbpGitVcs *self = GBP_GIT_VCS (object);
+
+  switch (prop_id)
+    {
+    case PROP_BRANCH_NAME:
+      self->branch = g_value_dup_string (value);
+      break;
+
+    case PROP_LOCATION:
+      self->location = g_value_dup_object (value);
+      break;
+
+    case PROP_REPOSITORY:
+      self->repository = g_value_dup_object (value);
+      break;
+
+    case PROP_WORKDIR:
+      self->workdir = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_git_vcs_class_init (GbpGitVcsClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_git_vcs_finalize;
+  object_class->get_property = gbp_git_vcs_get_property;
+  object_class->set_property = gbp_git_vcs_set_property;
+
+  properties [PROP_BRANCH_NAME] =
+    g_param_spec_string ("branch-name",
+                         "Branch Name",
+                         "The name of the branch",
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_LOCATION] =
+    g_param_spec_object ("location",
+                         "Location",
+                         "The location for the repository",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_REPOSITORY] =
+    g_param_spec_object ("repository",
+                         "Repository",
+                         "The underlying repository object",
+                         GGIT_TYPE_REPOSITORY,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_WORKDIR] =
+    g_param_spec_object ("workdir",
+                         "Workdir",
+                         "Working directory of the repository",
+                         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_git_vcs_init (GbpGitVcs *self)
+{
+}
+
+GFile *
+gbp_git_vcs_get_location (GbpGitVcs *self)
+{
+  g_return_val_if_fail (GBP_IS_GIT_VCS (self), NULL);
+  return self->location;
+}
+
+GgitRepository *
+gbp_git_vcs_get_repository (GbpGitVcs *self)
+{
+  g_return_val_if_fail (GBP_IS_GIT_VCS (self), NULL);
+  return self->repository;
+}
+
+static GFile *
+gbp_git_vcs_get_workdir (IdeVcs *vcs)
+{
+  return GBP_GIT_VCS (vcs)->workdir;
+}
+
+static gchar *
+gbp_git_vcs_get_branch_name (IdeVcs *vcs)
+{
+  gchar *ret;
+
+  g_return_val_if_fail (GBP_IS_GIT_VCS (vcs), NULL);
+
+  ide_object_lock (IDE_OBJECT (vcs));
+  ret = g_strdup (GBP_GIT_VCS (vcs)->branch);
+  ide_object_unlock (IDE_OBJECT (vcs));
+
+  return g_steal_pointer (&ret);
+}
+
+static IdeVcsConfig *
+gbp_git_vcs_get_config (IdeVcs *vcs)
+{
+  return g_object_new (GBP_TYPE_GIT_VCS_CONFIG, NULL);
+}
+static gboolean
+gbp_git_vcs_is_ignored (IdeVcs  *vcs,
+                        GFile   *file,
+                        GError **error)
+{
+  g_autofree gchar *name = NULL;
+  GbpGitVcs *self = (GbpGitVcs *)vcs;
+  gboolean ret = FALSE;
+
+  g_assert (GBP_IS_GIT_VCS (self));
+  g_assert (G_IS_FILE (file));
+
+  /* Note: this function is required to be thread-safe so that workers
+   *       can check if files are ignored from a thread without
+   *       round-tripping to the main thread.
+   */
+
+  /* self->workdir is not changed after creation, so safe
+   * to access it from a thread.
+   */
+  name = g_file_get_relative_path (self->workdir, file);
+  if (g_strcmp0 (name, ".git") == 0)
+    return TRUE;
+
+  /*
+   * If we have a valid name to work with, we want to query the
+   * repository. But this could be called from a thread, so ensure
+   * we are the only thread accessing self->repository right now.
+   */
+  if (name != NULL)
+    {
+      ide_object_lock (IDE_OBJECT (self));
+      ret = ggit_repository_path_is_ignored (self->repository, name, error);
+      ide_object_unlock (IDE_OBJECT (self));
+    }
+
+  return ret;
+}
+
+typedef struct
+{
+  GFile      *repository_location;
+  GFile      *directory_or_file;
+  GFile      *workdir;
+  GListStore *store;
+  guint       recursive : 1;
+} ListStatus;
+
+static void
+list_status_free (gpointer data)
+{
+  ListStatus *ls = data;
+
+  g_clear_object (&ls->repository_location);
+  g_clear_object (&ls->directory_or_file);
+  g_clear_object (&ls->workdir);
+  g_clear_object (&ls->store);
+  g_slice_free (ListStatus, ls);
+}
+
+static gint
+gbp_git_vcs_list_status_cb (const gchar     *path,
+                            GgitStatusFlags  flags,
+                            gpointer         user_data)
+{
+  ListStatus *state = user_data;
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(IdeVcsFileInfo) info = NULL;
+  IdeVcsFileStatus status = 0;
+
+  g_assert (path != NULL);
+  g_assert (state != NULL);
+  g_assert (G_IS_LIST_STORE (state->store));
+  g_assert (G_IS_FILE (state->workdir));
+
+  file = g_file_get_child (state->workdir, path);
+
+  switch (flags)
+    {
+    case GGIT_STATUS_INDEX_DELETED:
+    case GGIT_STATUS_WORKING_TREE_DELETED:
+      status = IDE_VCS_FILE_STATUS_DELETED;
+      break;
+
+    case GGIT_STATUS_INDEX_RENAMED:
+      status = IDE_VCS_FILE_STATUS_RENAMED;
+      break;
+
+    case GGIT_STATUS_INDEX_NEW:
+    case GGIT_STATUS_WORKING_TREE_NEW:
+      status = IDE_VCS_FILE_STATUS_ADDED;
+      break;
+
+    case GGIT_STATUS_INDEX_MODIFIED:
+    case GGIT_STATUS_INDEX_TYPECHANGE:
+    case GGIT_STATUS_WORKING_TREE_MODIFIED:
+    case GGIT_STATUS_WORKING_TREE_TYPECHANGE:
+      status = IDE_VCS_FILE_STATUS_CHANGED;
+      break;
+
+    case GGIT_STATUS_IGNORED:
+      status = IDE_VCS_FILE_STATUS_IGNORED;
+      break;
+
+    case GGIT_STATUS_CURRENT:
+      status = IDE_VCS_FILE_STATUS_UNCHANGED;
+      break;
+
+    default:
+      status = IDE_VCS_FILE_STATUS_UNTRACKED;
+      break;
+    }
+
+  info = g_object_new (IDE_TYPE_VCS_FILE_INFO,
+                       "file", file,
+                       "status", status,
+                       NULL);
+
+  g_list_store_append (state->store, info);
+
+  return 0;
+}
+
+static void
+gbp_git_vcs_list_status_worker (IdeTask      *task,
+                                gpointer      source_object,
+                                gpointer      task_data,
+                                GCancellable *cancellable)
+{
+  ListStatus *state = task_data;
+  g_autoptr(GListStore) store = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GgitRepository) repository = NULL;
+  g_autoptr(GgitStatusOptions) options = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *relative = NULL;
+  gchar *strv[] = { NULL, NULL };
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_GIT_VCS (source_object));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (state != NULL);
+  g_assert (G_IS_FILE (state->repository_location));
+
+  if (!(repository = ggit_repository_open (state->repository_location, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  if (!(workdir = ggit_repository_get_workdir (repository)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "Failed to locate working directory");
+      return;
+    }
+
+  g_set_object (&state->workdir, workdir);
+
+  if (state->directory_or_file != NULL)
+    relative = g_file_get_relative_path (workdir, state->directory_or_file);
+
+  strv[0] = relative;
+  options = ggit_status_options_new (GGIT_STATUS_OPTION_DEFAULT,
+                                     GGIT_STATUS_SHOW_INDEX_AND_WORKDIR,
+                                     (const gchar **)strv);
+
+  store = g_list_store_new (IDE_TYPE_VCS_FILE_INFO);
+  g_set_object (&state->store, store);
+
+  if (!ggit_repository_file_status_foreach (repository,
+                                            options,
+                                            gbp_git_vcs_list_status_cb,
+                                            state,
+                                            &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task, g_steal_pointer (&store), g_object_unref);
+}
+
+static void
+gbp_git_vcs_list_status_async (IdeVcs              *vcs,
+                               GFile               *directory_or_file,
+                               gboolean             include_descendants,
+                               gint                 io_priority,
+                               GCancellable        *cancellable,
+                               GAsyncReadyCallback  callback,
+                               gpointer             user_data)
+{
+  GbpGitVcs *self = (GbpGitVcs *)vcs;
+  g_autoptr(IdeTask) task = NULL;
+  ListStatus *state;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (GBP_IS_GIT_VCS (self));
+  g_return_if_fail (!directory_or_file || G_IS_FILE (directory_or_file));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  ide_object_lock (IDE_OBJECT (self));
+  state = g_slice_new0 (ListStatus);
+  state->directory_or_file = g_object_ref (directory_or_file);
+  state->repository_location = ggit_repository_get_location (self->repository);
+  state->recursive = !!include_descendants;
+  ide_object_unlock (IDE_OBJECT (self));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_git_vcs_list_status_async);
+  ide_task_set_priority (task, io_priority);
+  ide_task_set_return_on_cancel (task, TRUE);
+  ide_task_set_task_data (task, state, list_status_free);
+
+  if (state->repository_location == NULL)
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "No repository loaded");
+  else
+    ide_task_run_in_thread (task, gbp_git_vcs_list_status_worker);
+
+  IDE_EXIT;
+}
+
+static GListModel *
+gbp_git_vcs_list_status_finish (IdeVcs        *vcs,
+                                GAsyncResult  *result,
+                                GError       **error)
+{
+  g_return_val_if_fail (GBP_IS_GIT_VCS (vcs), NULL);
+  g_return_val_if_fail (IDE_IS_TASK (result), NULL);
+
+  return ide_task_propagate_pointer (IDE_TASK (result), error);
+}
+
+static void
+vcs_iface_init (IdeVcsInterface *iface)
+{
+  iface->get_workdir = gbp_git_vcs_get_workdir;
+  iface->get_branch_name = gbp_git_vcs_get_branch_name;
+  iface->get_config = gbp_git_vcs_get_config;
+  iface->is_ignored = gbp_git_vcs_is_ignored;
+  iface->list_status_async = gbp_git_vcs_list_status_async;
+  iface->list_status_finish = gbp_git_vcs_list_status_finish;
+}
+
+static void
+gbp_git_vcs_reload_worker (IdeTask      *task,
+                           gpointer      source_object,
+                           gpointer      task_data,
+                           GCancellable *cancellable)
+{
+  g_autoptr(GgitRepository) repository = NULL;
+  g_autoptr(GError) error = NULL;
+  GFile *location = task_data;
+
+  IDE_ENTRY;
+
+  g_assert (!IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_GIT_VCS (source_object));
+  g_assert (G_IS_FILE (location));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  if (!(repository = ggit_repository_open (location, &error)))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task, g_steal_pointer (&repository), g_object_unref);
+
+  IDE_EXIT;
+}
+
+void
+gbp_git_vcs_reload_async (GbpGitVcs           *self,
+                          GCancellable        *cancellable,
+                          GAsyncReadyCallback  callback,
+                          gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GIT_VCS (self));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+  ide_task_set_source_tag (task, gbp_git_vcs_reload_async);
+  ide_task_set_task_data (task, g_object_ref (self->location), g_object_unref);
+  ide_task_run_in_thread (task, gbp_git_vcs_reload_worker);
+
+  IDE_EXIT;
+}
+
+gboolean
+gbp_git_vcs_reload_finish (GbpGitVcs     *self,
+                           GAsyncResult  *result,
+                           GError       **error)
+{
+  g_autoptr(GgitRepository) repository = NULL;
+  g_autoptr(GgitRef) ref = NULL;
+
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), FALSE);
+  g_return_val_if_fail (GBP_IS_GIT_VCS (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  ide_object_lock (IDE_OBJECT (self));
+
+  if (!(repository = ide_task_propagate_pointer (IDE_TASK (result), error)))
+    goto failure;
+
+  if ((ref = ggit_repository_get_head (repository, NULL)))
+    {
+      const gchar *name = ggit_ref_get_shorthand (ref);
+
+      if (name != NULL)
+        {
+          g_free (self->branch);
+          self->branch = g_strdup (name);
+          g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BRANCH_NAME]);
+        }
+    }
+
+  if (g_set_object (&self->repository, repository))
+    g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_REPOSITORY]);
+
+failure:
+  ide_object_unlock (IDE_OBJECT (self));
+
+  return repository != NULL;
+}
diff --git a/src/plugins/git/gbp-git-vcs.h b/src/plugins/git/gbp-git-vcs.h
new file mode 100644
index 000000000..cb337921a
--- /dev/null
+++ b/src/plugins/git/gbp-git-vcs.h
@@ -0,0 +1,42 @@
+/* gbp-git-vcs.h
+ *
+ * Copyright 2018-2019 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 <libgit2-glib/ggit.h>
+#include <libide-vcs.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GIT_VCS (gbp_git_vcs_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitVcs, gbp_git_vcs, GBP, GIT_VCS, IdeObject)
+
+GFile          *gbp_git_vcs_get_location   (GbpGitVcs            *self);
+GgitRepository *gbp_git_vcs_get_repository (GbpGitVcs            *self);
+void            gbp_git_vcs_reload_async   (GbpGitVcs            *self,
+                                            GCancellable         *cancellable,
+                                            GAsyncReadyCallback   callback,
+                                            gpointer              user_data);
+gboolean        gbp_git_vcs_reload_finish  (GbpGitVcs            *self,
+                                            GAsyncResult         *result,
+                                            GError              **error);
+
+G_END_DECLS
diff --git a/src/plugins/git/gbp-git-workbench-addin.c b/src/plugins/git/gbp-git-workbench-addin.c
new file mode 100644
index 000000000..678895804
--- /dev/null
+++ b/src/plugins/git/gbp-git-workbench-addin.c
@@ -0,0 +1,386 @@
+/* gbp-git-workbench-addin.c
+ *
+ * Copyright 2018-2019 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-git-workbench-addin"
+
+#include "config.h"
+
+#include <libgit2-glib/ggit.h>
+#include <libide-editor.h>
+#include <libide-io.h>
+#include <libide-threading.h>
+
+#include "gbp-git-buffer-change-monitor.h"
+#include "gbp-git-index-monitor.h"
+#include "gbp-git-vcs.h"
+#include "gbp-git-workbench-addin.h"
+
+struct _GbpGitWorkbenchAddin
+{
+  GObject             parent_instance;
+  IdeWorkbench       *workbench;
+  GbpGitIndexMonitor *monitor;
+  guint               has_loaded : 1;
+};
+
+static void
+gbp_git_workbench_addin_load_project_worker (IdeTask      *task,
+                                             gpointer      source_object,
+                                             gpointer      task_data,
+                                             GCancellable *cancellable)
+{
+  GbpGitWorkbenchAddin *self = source_object;
+  g_autoptr(GgitRepository) repository = NULL;
+  g_autoptr(GbpGitVcs) vcs = NULL;
+  g_autoptr(GFile) location = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *worktree_branch = NULL;
+  GFile *directory = task_data;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_GIT_WORKBENCH_ADDIN (self));
+  g_assert (G_IS_FILE (directory));
+
+  /* Short-circuit if we don't .git */
+  if (!(location = ggit_repository_discover_full (directory, TRUE, NULL, &error)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Failed to locate git repository location");
+      return;
+    }
+
+  g_debug ("Located .git at %s", g_file_peek_path (location));
+
+  /* If @location is a regular file, we might have a git-worktree link */
+  if (g_file_query_file_type (location, 0, NULL) == G_FILE_TYPE_REGULAR)
+    {
+      g_autofree gchar *contents = NULL;
+      gsize len;
+
+      if (g_file_load_contents (location, NULL, &contents, &len, NULL, NULL))
+        {
+          IdeLineReader reader;
+          gchar *line;
+          gsize line_len;
+
+          ide_line_reader_init (&reader, contents, len);
+
+          while ((line = ide_line_reader_next (&reader, &line_len)))
+            {
+              line[line_len] = 0;
+
+              if (g_str_has_prefix (line, "gitdir: "))
+                {
+                  g_autoptr(GFile) location_parent = g_file_get_parent (location);
+                  const gchar *path = line + strlen ("gitdir: ");
+                  const gchar *branch;
+
+                  g_clear_object (&location);
+
+                  if (g_path_is_absolute (path))
+                    location = g_file_new_for_path (path);
+                  else
+                    location = g_file_resolve_relative_path (location_parent, path);
+
+                  /*
+                   * Worktrees only have a single branch, and it is the name
+                   * of the suffix of .git/worktrees/<name>
+                   */
+                  if ((branch = strrchr (line, G_DIR_SEPARATOR)))
+                    worktree_branch = g_strdup (branch + 1);
+
+                  break;
+                }
+            }
+        }
+    }
+
+  if (!(repository = ggit_repository_open (location, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  workdir = ggit_repository_get_workdir (repository);
+
+  g_assert (G_IS_FILE (location));
+  g_assert (G_IS_FILE (workdir));
+  g_assert (GGIT_IS_REPOSITORY (repository));
+
+  if (worktree_branch == NULL)
+    {
+      g_autoptr(GgitRef) ref = NULL;
+
+      if ((ref = ggit_repository_get_head (repository, NULL)))
+        worktree_branch = g_strdup (ggit_ref_get_shorthand (ref));
+
+      if (worktree_branch == NULL)
+        worktree_branch = g_strdup ("master");
+    }
+
+  vcs = g_object_new (GBP_TYPE_GIT_VCS,
+                      "branch-name", worktree_branch,
+                      "location", location,
+                      "repository", repository,
+                      "workdir", workdir,
+                      NULL);
+
+  ide_task_return_pointer (task, g_steal_pointer (&vcs), g_object_unref);
+}
+
+static void
+gbp_git_workbench_addin_load_project_async (IdeWorkbenchAddin   *addin,
+                                            IdeProjectInfo      *project_info,
+                                            GCancellable        *cancellable,
+                                            GAsyncReadyCallback  callback,
+                                            gpointer             user_data)
+{
+  GbpGitWorkbenchAddin *self = (GbpGitWorkbenchAddin *)addin;
+  g_autoptr(IdeTask) task = NULL;
+  GFile *directory;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GIT_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  self->has_loaded = TRUE;
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_git_workbench_addin_load_project_async);
+
+  if (!(directory = ide_project_info_get_directory (project_info)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Missing directory from project info");
+      return;
+    }
+
+  /* Try to discover the git repository from a worker thread. If we find
+   * it, we'll set the VCS on the workbench for various components to use.
+   */
+  ide_task_set_task_data (task, g_object_ref (directory), g_object_unref);
+  ide_task_run_in_thread (task, gbp_git_workbench_addin_load_project_worker);
+}
+
+static void
+gbp_git_workbench_addin_foreach_buffer_cb (IdeBuffer *buffer,
+                                           gpointer   user_data)
+{
+  GgitRepository *repository = user_data;
+  IdeBufferChangeMonitor *monitor;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (GGIT_IS_REPOSITORY (repository));
+
+  monitor = ide_buffer_get_change_monitor (buffer);
+
+  if (GBP_IS_GIT_BUFFER_CHANGE_MONITOR (monitor))
+    gbp_git_buffer_change_monitor_set_repository (GBP_GIT_BUFFER_CHANGE_MONITOR (monitor),
+                                                  repository);
+}
+
+static void
+gbp_git_workbench_addin_reload_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  GbpGitVcs *vcs = (GbpGitVcs *)object;
+  g_autoptr(GbpGitWorkbenchAddin) self = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeBufferManager *buffer_manager;
+  GgitRepository *repository;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GIT_VCS (vcs));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GBP_IS_GIT_WORKBENCH_ADDIN (self));
+
+  if (!gbp_git_vcs_reload_finish (vcs, result, &error))
+    return;
+
+  if (self->workbench == NULL)
+    return;
+
+  repository = gbp_git_vcs_get_repository (vcs);
+  context = ide_workbench_get_context (self->workbench);
+  buffer_manager = ide_buffer_manager_from_context (context);
+
+  ide_buffer_manager_foreach (buffer_manager,
+                              gbp_git_workbench_addin_foreach_buffer_cb,
+                              repository);
+}
+
+static void
+gbp_git_workbench_addin_monitor_changed_cb (GbpGitWorkbenchAddin *self,
+                                            GbpGitIndexMonitor   *monitor)
+{
+  IdeContext *context;
+  IdeVcs *vcs;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GIT_WORKBENCH_ADDIN (self));
+  g_assert (GBP_IS_GIT_INDEX_MONITOR (monitor));
+
+  context = ide_workbench_get_context (self->workbench);
+  vcs = ide_vcs_from_context (context);
+
+  if (!GBP_IS_GIT_VCS (vcs))
+    IDE_EXIT;
+
+  gbp_git_vcs_reload_async (GBP_GIT_VCS (vcs),
+                            NULL,
+                            gbp_git_workbench_addin_reload_cb,
+                            g_object_ref (self));
+
+  IDE_EXIT;
+}
+
+static gboolean
+gbp_git_workbench_addin_load_project_finish (IdeWorkbenchAddin  *addin,
+                                             GAsyncResult       *result,
+                                             GError            **error)
+{
+  GbpGitWorkbenchAddin *self = (GbpGitWorkbenchAddin *)addin;
+  g_autoptr(GbpGitVcs) vcs = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GIT_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_TASK (result));
+
+  if ((vcs = ide_task_propagate_pointer (IDE_TASK (result), error)))
+    {
+      if (IDE_IS_WORKBENCH (self->workbench))
+        {
+          /* Set the vcs for the workbench */
+          ide_workbench_set_vcs (self->workbench, IDE_VCS (vcs));
+
+          if (self->monitor == NULL)
+            {
+              GFile *location = gbp_git_vcs_get_location (vcs);
+
+              self->monitor = gbp_git_index_monitor_new (location);
+
+              g_signal_connect_object (self->monitor,
+                                       "changed",
+                                       G_CALLBACK (gbp_git_workbench_addin_monitor_changed_cb),
+                                       self,
+                                       G_CONNECT_SWAPPED);
+            }
+        }
+    }
+
+  return vcs != NULL;
+}
+
+static void
+gbp_git_workbench_addin_load (IdeWorkbenchAddin *addin,
+                              IdeWorkbench      *workbench)
+{
+  GBP_GIT_WORKBENCH_ADDIN (addin)->workbench = workbench;
+}
+
+static void
+gbp_git_workbench_addin_unload (IdeWorkbenchAddin *addin,
+                                IdeWorkbench      *workbench)
+{
+  GbpGitWorkbenchAddin *self = (GbpGitWorkbenchAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GIT_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (workbench));
+
+  g_clear_object (&self->monitor);
+
+  self->workbench = NULL;
+}
+
+static void
+load_git_for_editor_cb (GObject      *object,
+                        GAsyncResult *result,
+                        gpointer      user_data)
+{
+  gbp_git_workbench_addin_load_project_finish (IDE_WORKBENCH_ADDIN (object), result, NULL);
+}
+
+static void
+gbp_git_workbench_addin_workspace_added (IdeWorkbenchAddin *addin,
+                                         IdeWorkspace      *workspace)
+{
+  GbpGitWorkbenchAddin *self = (GbpGitWorkbenchAddin *)addin;
+
+  g_assert (GBP_IS_GIT_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  if (!self->has_loaded)
+    {
+      /* If we see a new IdeEditorWorkspace without having loaded a project,
+       * that means that we are in a non-project scenario (dedicated editor
+       * window). We can try our best to load a git repository based on
+       * the files that are loaded.
+       */
+      if (IDE_IS_EDITOR_WORKSPACE (workspace))
+        {
+          IdeContext *context = ide_workbench_get_context (self->workbench);
+          g_autoptr(GFile) workdir = ide_context_ref_workdir (context);
+          g_autoptr(IdeTask) task = NULL;
+
+          self->has_loaded = TRUE;
+
+          task = ide_task_new (self, NULL, load_git_for_editor_cb, NULL);
+          ide_task_set_source_tag (task, gbp_git_workbench_addin_workspace_added);
+          ide_task_set_task_data (task, g_object_ref (workdir), g_object_unref);
+          ide_task_run_in_thread (task, gbp_git_workbench_addin_load_project_worker);
+        }
+    }
+}
+
+static void
+workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load = gbp_git_workbench_addin_load;
+  iface->unload = gbp_git_workbench_addin_unload;
+  iface->load_project_async = gbp_git_workbench_addin_load_project_async;
+  iface->load_project_finish = gbp_git_workbench_addin_load_project_finish;
+  iface->workspace_added = gbp_git_workbench_addin_workspace_added;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGitWorkbenchAddin, gbp_git_workbench_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKBENCH_ADDIN,
+                                                workbench_addin_iface_init))
+
+static void
+gbp_git_workbench_addin_class_init (GbpGitWorkbenchAddinClass *klass)
+{
+}
+
+static void
+gbp_git_workbench_addin_init (GbpGitWorkbenchAddin *self)
+{
+}
diff --git a/src/plugins/git/gbp-git-workbench-addin.h b/src/plugins/git/gbp-git-workbench-addin.h
new file mode 100644
index 000000000..218b02004
--- /dev/null
+++ b/src/plugins/git/gbp-git-workbench-addin.h
@@ -0,0 +1,31 @@
+/* gbp-git-workbench-addin.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GIT_WORKBENCH_ADDIN (gbp_git_workbench_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGitWorkbenchAddin, gbp_git_workbench_addin, GBP, GIT_WORKBENCH_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/git/git-plugin.c b/src/plugins/git/git-plugin.c
new file mode 100644
index 000000000..4d4d016cc
--- /dev/null
+++ b/src/plugins/git/git-plugin.c
@@ -0,0 +1,96 @@
+/* git-plugin.c
+ *
+ * Copyright 2018-2019 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 "git-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libgit2-glib/ggit.h>
+#include <libide-editor.h>
+#include <libide-foundry.h>
+#include <libide-vcs.h>
+
+#include "gbp-git-buffer-addin.h"
+#include "gbp-git-dependency-updater.h"
+#include "gbp-git-pipeline-addin.h"
+#include "gbp-git-remote-callbacks.h"
+#include "gbp-git-vcs-cloner.h"
+#include "gbp-git-vcs-config.h"
+#include "gbp-git-vcs-initializer.h"
+#include "gbp-git-workbench-addin.h"
+
+static gboolean
+register_ggit (void)
+{
+  GgitFeatureFlags ggit_flags;
+
+  ggit_init ();
+
+  ggit_flags = ggit_get_features ();
+
+  if ((ggit_flags & GGIT_FEATURE_THREADS) == 0)
+    {
+      g_printerr ("Builder requires libgit2-glib with threading support.");
+      return FALSE;
+    }
+
+  if ((ggit_flags & GGIT_FEATURE_SSH) == 0)
+    {
+      g_printerr ("Builder requires libgit2-glib with SSH support.");
+      return FALSE;
+    }
+
+  return TRUE;
+}
+
+
+_IDE_EXTERN void
+_gbp_git_register_types (PeasObjectModule *module)
+{
+  if (register_ggit ())
+    {
+      ide_g_file_add_ignored_pattern (".git");
+
+      peas_object_module_register_extension_type (module,
+                                                  IDE_TYPE_BUFFER_ADDIN,
+                                                  GBP_TYPE_GIT_BUFFER_ADDIN);
+      peas_object_module_register_extension_type (module,
+                                                  IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                                  GBP_TYPE_GIT_PIPELINE_ADDIN);
+      peas_object_module_register_extension_type (module,
+                                                  IDE_TYPE_DEPENDENCY_UPDATER,
+                                                  GBP_TYPE_GIT_DEPENDENCY_UPDATER);
+      peas_object_module_register_extension_type (module,
+                                                  IDE_TYPE_VCS_CONFIG,
+                                                  GBP_TYPE_GIT_VCS_CONFIG);
+      peas_object_module_register_extension_type (module,
+                                                  IDE_TYPE_VCS_CLONER,
+                                                  GBP_TYPE_GIT_VCS_CLONER);
+      peas_object_module_register_extension_type (module,
+                                                  IDE_TYPE_VCS_INITIALIZER,
+                                                  GBP_TYPE_GIT_VCS_INITIALIZER);
+      peas_object_module_register_extension_type (module,
+                                                  IDE_TYPE_WORKBENCH_ADDIN,
+                                                  GBP_TYPE_GIT_WORKBENCH_ADDIN);
+
+      g_type_ensure (GBP_TYPE_GIT_REMOTE_CALLBACKS);
+    }
+}
diff --git a/src/plugins/git/git.gresource.xml b/src/plugins/git/git.gresource.xml
index c5c3a8dc1..73415c06f 100644
--- a/src/plugins/git/git.gresource.xml
+++ b/src/plugins/git/git.gresource.xml
@@ -1,10 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/git">
     <file>git.plugin</file>
   </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/git-plugin">
-    <file>ide-git-clone-widget.ui</file>
-    <file>themes/shared.css</file>
-  </gresource>
 </gresources>
diff --git a/src/plugins/git/git.plugin b/src/plugins/git/git.plugin
index 040eca3a4..e190366cc 100644
--- a/src/plugins/git/git.plugin
+++ b/src/plugins/git/git.plugin
@@ -1,8 +1,8 @@
 [Plugin]
-Module=git-plugin
-Name=Git
-Description=Provides support for the Git version control system
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
-Embedded=ide_git_register_types
+Copyright=Copyright © 2015-2018 Christian Hergert
+Description=Support for the Git version control system
+Embedded=_gbp_git_register_types
+Module=git
+Name=Git
diff --git a/src/plugins/git/meson.build b/src/plugins/git/meson.build
index 69ae084cc..29d471a14 100644
--- a/src/plugins/git/meson.build
+++ b/src/plugins/git/meson.build
@@ -1,40 +1,27 @@
-if get_option('with_git')
+if get_option('plugin_git')
 
-git_resources = gnome.compile_resources(
+plugins_sources += files([
+  'git-plugin.c',
+  'gbp-git-buffer-addin.c',
+  'gbp-git-buffer-change-monitor.c',
+  'gbp-git-dependency-updater.c',
+  'gbp-git-index-monitor.c',
+  'gbp-git-pipeline-addin.c',
+  'gbp-git-remote-callbacks.c',
+  'gbp-git-submodule-stage.c',
+  'gbp-git-vcs.c',
+  'gbp-git-vcs-cloner.c',
+  'gbp-git-vcs-config.c',
+  'gbp-git-vcs-initializer.c',
+  'gbp-git-workbench-addin.c',
+])
+
+plugin_git_resources = gnome.compile_resources(
   'git-resources',
   'git.gresource.xml',
-  c_name: 'ide_git',
+  c_name: 'gbp_git',
 )
 
-git_sources = [
-  'ide-git-buffer-change-monitor.c',
-  'ide-git-buffer-change-monitor.h',
-  'ide-git-clone-widget.c',
-  'ide-git-clone-widget.h',
-  'ide-git-dependency-updater.c',
-  'ide-git-dependency-updater.h',
-  'ide-git-genesis-addin.c',
-  'ide-git-genesis-addin.h',
-  'ide-git-pipeline-addin.c',
-  'ide-git-pipeline-addin.h',
-  'ide-git-plugin.c',
-  'ide-git-remote-callbacks.c',
-  'ide-git-remote-callbacks.h',
-  'ide-git-submodule-stage.c',
-  'ide-git-submodule-stage.h',
-  'ide-git-vcs.c',
-  'ide-git-vcs.h',
-  'ide-git-vcs-config.c',
-  'ide-git-vcs-config.h',
-  'ide-git-vcs-initializer.c',
-  'ide-git-vcs-initializer.h',
-]
-
-gnome_builder_plugins_deps += [
-  libgit_dep,
-]
-
-gnome_builder_plugins_sources += files(git_sources)
-gnome_builder_plugins_sources += git_resources[0]
+plugins_sources += plugin_git_resources[0]
 
 endif
diff --git a/src/plugins/gjs-symbols/gjs_symbols.plugin b/src/plugins/gjs-symbols/gjs_symbols.plugin
index 7384d57ed..c715af17c 100644
--- a/src/plugins/gjs-symbols/gjs_symbols.plugin
+++ b/src/plugins/gjs-symbols/gjs_symbols.plugin
@@ -1,12 +1,13 @@
 [Plugin]
-Module=gjs_symbols
-Loader=python3
-Name=GJS Symbol Resolver
-Description=Provides a symbol resolver for JavaScript using GJS.
 Authors=Patrick Griffis <tingping tingping se>
-Copyright=Copyright © 2017 Patrick Griffis
 Builtin=true
-X-Symbol-Resolver-Languages=js
-X-Symbol-Resolver-Languages-Priority=0
-X-Code-Indexer-Languages=js
+Copyright=Copyright © 2017 Patrick Griffis
+Description=Provides a symbol resolver for JavaScript using GJS.
+Loader=python3
+Module=gjs_symbols
+Name=GJS Symbol Resolver
 X-Code-Indexer-Languages-Priority=0
+X-Code-Indexer-Languages=js
+X-Symbol-Resolver-Languages-Priority=0
+X-Symbol-Resolver-Languages=js
+X-Builder-ABI=@PACKAGE_ABI@
diff --git a/src/plugins/gjs-symbols/gjs_symbols.py b/src/plugins/gjs-symbols/gjs_symbols.py
index 0112bf2c8..345df5ddd 100644
--- a/src/plugins/gjs-symbols/gjs_symbols.py
+++ b/src/plugins/gjs-symbols/gjs_symbols.py
@@ -24,10 +24,6 @@ import gi
 import json
 import threading
 
-gi.require_versions({
-    'Ide': '1.0',
-})
-
 from gi.repository import GLib
 from gi.repository import GObject
 from gi.repository import Gio
@@ -37,7 +33,7 @@ SYMBOL_PARAM_FLAGS=flags = GObject.ParamFlags.CONSTRUCT_ONLY | GObject.ParamFlag
 
 
 class JsSymbolNode(Ide.SymbolNode):
-    file = GObject.Property(type=Ide.File, flags=SYMBOL_PARAM_FLAGS)
+    file = GObject.Property(type=Gio.File, flags=SYMBOL_PARAM_FLAGS)
     line = GObject.Property(type=int, flags=SYMBOL_PARAM_FLAGS)
     col = GObject.Property(type=int, flags=SYMBOL_PARAM_FLAGS)
 
@@ -52,7 +48,7 @@ class JsSymbolNode(Ide.SymbolNode):
 
     def do_get_location_finish(self, result):
         if result.propagate_boolean():
-            return Ide.SourceLocation.new(self.file, self.line, self.col, 0)
+            return Ide.Location.new(self.file, self.line, self.col)
 
     def __len__(self):
         return len(self.children)
@@ -264,16 +260,22 @@ class GjsSymbolProvider(Ide.Object, Ide.SymbolResolver):
     def _get_launcher(context, file_):
         file_path = file_.get_path()
         script = JS_SCRIPT % file_path
-        unsaved_file = context.get_unsaved_files().get_unsaved_file(file_)
+        unsaved_file = Ide.UnsavedFiles.from_context(context).get_unsaved_file(file_)
+
+        if context.has_project():
+            runtime = Ide.ConfigurationManager.from_context(context).get_current().get_runtime()
+            launcher = runtime.create_launcher()
+        else:
+            launcher = Ide.SubprocessLauncher.new(0)
 
-        runtime = context.get_configuration_manager().get_current().get_runtime()
-        launcher = runtime.create_launcher()
         launcher.set_flags(Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_SILENCE)
         launcher.push_args(('gjs', '-c', script))
+
         if unsaved_file is not None:
             launcher.push_argv(unsaved_file.get_content().get_data().decode('utf-8'))
         else:
             launcher.push_args(('--file', file_path))
+
         return launcher
 
     def do_lookup_symbol_async(self, location, cancellable, callback, user_data=None):
@@ -300,8 +302,7 @@ class GjsSymbolProvider(Ide.Object, Ide.SymbolResolver):
                 task.return_error(GLib.Error('Failed to run gjs'))
                 return
 
-            ide_file = Ide.File(file=file_, context=self.get_context())
-            task.symbol_tree = JsSymbolTree(json.loads(stdout), ide_file)
+            task.symbol_tree = JsSymbolTree(json.loads(stdout), file_)
         except GLib.Error as err:
             task.return_error(err)
         except (json.JSONDecodeError, UnicodeDecodeError) as e:
@@ -382,9 +383,8 @@ class GjsCodeIndexer(Ide.Object, Ide.CodeIndexer):
         try:
             _, stdout, stderr = subprocess.communicate_utf8_finish(result)
 
-            ide_file = Ide.File(file=task.file, context=self.get_context())
             try:
-                root_node = JsSymbolTree._node_from_dict(json.loads(stdout), ide_file)
+                root_node = JsSymbolTree._node_from_dict(json.loads(stdout), task.file)
             except (json.JSONDecodeError, UnicodeDecodeError) as e:
                 raise GLib.Error('Failed to decode gjs json: {}'.format(e))
             except (IndexError, KeyError) as e:
diff --git a/src/plugins/gjs-symbols/meson.build b/src/plugins/gjs-symbols/meson.build
index 189da66cf..43ccc9724 100644
--- a/src/plugins/gjs-symbols/meson.build
+++ b/src/plugins/gjs-symbols/meson.build
@@ -1,11 +1,11 @@
-if get_option('with_gjs_symbols')
+if get_option('plugin_gjs_symbols')
 
 install_data('gjs_symbols.py', install_dir: plugindir)
 
 configure_file(
           input: 'gjs_symbols.plugin',
          output: 'gjs_symbols.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/glade/gbp-glade-editor-addin.c b/src/plugins/glade/gbp-glade-editor-addin.c
index 44f710e06..dfbeaade7 100644
--- a/src/plugins/glade/gbp-glade-editor-addin.c
+++ b/src/plugins/glade/gbp-glade-editor-addin.c
@@ -1,6 +1,6 @@
 /* gbp-glade-editor-addin.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -18,23 +18,24 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-glade-editor-addin"
 
+#include "config.h"
+
 #include <glib/gi18n.h>
+#include <libide-editor.h>
 
 #include "gbp-glade-editor-addin.h"
 #include "gbp-glade-private.h"
 #include "gbp-glade-properties.h"
-#include "gbp-glade-view.h"
+#include "gbp-glade-page.h"
 
 struct _GbpGladeEditorAddin
 {
   GObject               parent_instance;
 
   /* Widgets */
-  IdeEditorPerspective *editor;
+  IdeEditorSurface     *editor;
   GbpGladeProperties   *properties;
   GladeSignalEditor    *signals;
   DzlDockWidget        *signals_dock;
@@ -50,6 +51,46 @@ static void editor_addin_iface_init (IdeEditorAddinInterface *iface);
 G_DEFINE_TYPE_WITH_CODE (GbpGladeEditorAddin, gbp_glade_editor_addin, G_TYPE_OBJECT,
                          G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_ADDIN, editor_addin_iface_init))
 
+static void
+gbp_glade_editor_addin_ensure_properties (GbpGladeEditorAddin *self)
+{
+  IdeTransientSidebar *transient;
+  GtkWidget *utils;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GLADE_EDITOR_ADDIN (self));
+
+  if (self->properties)
+    return;
+
+  transient = ide_editor_surface_get_transient_sidebar (self->editor);
+  utils = ide_editor_surface_get_utilities (self->editor);
+
+  self->properties = g_object_new (GBP_TYPE_GLADE_PROPERTIES,
+                                   "visible", TRUE,
+                                   NULL);
+  g_signal_connect (self->properties,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->properties);
+  gtk_container_add (GTK_CONTAINER (transient), GTK_WIDGET (self->properties));
+
+  self->signals_dock = g_object_new (DZL_TYPE_DOCK_WIDGET,
+                                     "title", _("Signals"),
+                                     "icon-name", "glade-symbolic",
+                                     "visible", TRUE,
+                                     NULL);
+  gtk_container_add (GTK_CONTAINER (utils), GTK_WIDGET (self->signals_dock));
+
+  self->signals = g_object_new (GLADE_TYPE_SIGNAL_EDITOR,
+                                "visible", TRUE,
+                                NULL);
+  gtk_container_add (GTK_CONTAINER (self->signals_dock), GTK_WIDGET (self->signals));
+
+  /* Wire up the shortcuts to the panel too */
+  _gbp_glade_page_init_shortcuts (GTK_WIDGET (self->properties));
+}
+
 static void
 gbp_glade_editor_addin_dispose (GObject *object)
 {
@@ -86,15 +127,21 @@ gbp_glade_editor_addin_selection_changed_cb (GbpGladeEditorAddin *self,
       GtkWidget *widget = selection->data;
       GladeWidget *glade = glade_widget_get_from_gobject (widget);
 
+      gbp_glade_editor_addin_ensure_properties (self);
+
       gbp_glade_properties_set_widget (self->properties, glade);
       glade_signal_editor_load_widget (self->signals, glade);
       gtk_widget_show (GTK_WIDGET (self->signals_dock));
     }
   else
     {
-      gbp_glade_properties_set_widget (self->properties, NULL);
+      if (self->properties)
+        gbp_glade_properties_set_widget (self->properties, NULL);
+
       glade_signal_editor_load_widget (self->signals, NULL);
-      gtk_widget_hide (GTK_WIDGET (self->signals_dock));
+
+      if (self->signals_dock)
+        gtk_widget_hide (GTK_WIDGET (self->signals_dock));
     }
 }
 
@@ -139,104 +186,87 @@ gbp_glade_editor_addin_set_project (GbpGladeEditorAddin *self,
 }
 
 static void
-gbp_glade_editor_addin_view_set (IdeEditorAddin *addin,
-                                 IdeLayoutView  *view)
+gbp_glade_editor_addin_page_set (IdeEditorAddin *addin,
+                                 IdePage        *view)
 {
   GbpGladeEditorAddin *self = (GbpGladeEditorAddin *)addin;
-  IdeLayoutTransientSidebar *transient;
+  IdeTransientSidebar *transient;
   GladeProject *project = NULL;
 
   g_assert (GBP_IS_GLADE_EDITOR_ADDIN (self));
-  g_assert (!view || IDE_IS_LAYOUT_VIEW (view));
+  g_assert (!view || IDE_IS_PAGE (view));
 
-  transient = ide_editor_perspective_get_transient_sidebar (self->editor);
+  transient = ide_editor_surface_get_transient_sidebar (self->editor);
 
   if (self->has_hold)
     {
-      ide_layout_transient_sidebar_unlock (transient);
+      ide_transient_sidebar_unlock (transient);
       self->has_hold = FALSE;
     }
 
-  if (GBP_IS_GLADE_VIEW (view))
+  if (GBP_IS_GLADE_PAGE (view))
     {
-      project = gbp_glade_view_get_project (GBP_GLADE_VIEW (view));
-      ide_layout_transient_sidebar_set_view (transient, view);
-      ide_layout_transient_sidebar_lock (transient);
+      gbp_glade_editor_addin_ensure_properties (self);
+
+      project = gbp_glade_page_get_project (GBP_GLADE_PAGE (view));
+      ide_transient_sidebar_set_page (transient, view);
+      ide_transient_sidebar_lock (transient);
       gtk_widget_show (GTK_WIDGET (transient));
       dzl_dock_item_present (DZL_DOCK_ITEM (self->properties));
       self->has_hold = TRUE;
       dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self->properties),
                                         GTK_WIDGET (view),
-                                        "GBP_GLADE_VIEW");
+                                        "GBP_GLADE_PAGE");
     }
   else
     {
-      gtk_widget_hide (GTK_WIDGET (self->signals_dock));
-      dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self->properties),
-                                        NULL,
-                                        "GBP_GLADE_VIEW");
+      if (self->signals_dock)
+        gtk_widget_hide (GTK_WIDGET (self->signals_dock));
+
+      if (self->properties)
+        dzl_gtk_widget_mux_action_groups (GTK_WIDGET (self->properties),
+                                          NULL,
+                                          "GBP_GLADE_PAGE");
     }
 
   gbp_glade_editor_addin_set_project (self, project);
 }
 
 static void
-gbp_glade_editor_addin_load (IdeEditorAddin       *addin,
-                             IdeEditorPerspective *editor)
+gbp_glade_editor_addin_load (IdeEditorAddin   *addin,
+                             IdeEditorSurface *editor)
 {
   GbpGladeEditorAddin *self = (GbpGladeEditorAddin *)addin;
-  IdeLayoutTransientSidebar *transient;
-  GtkWidget *utils;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (GBP_IS_GLADE_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
 
   self->editor = editor;
-
-  transient = ide_editor_perspective_get_transient_sidebar (self->editor);
-  utils = ide_editor_perspective_get_utilities (self->editor);
-
-  self->properties = g_object_new (GBP_TYPE_GLADE_PROPERTIES,
-                                   "visible", TRUE,
-                                   NULL);
-  gtk_container_add (GTK_CONTAINER (transient), GTK_WIDGET (self->properties));
-
-  self->signals_dock = g_object_new (DZL_TYPE_DOCK_WIDGET,
-                                     "title", _("Signals"),
-                                     "icon-name", "glade-symbolic",
-                                     "visible", TRUE,
-                                     NULL);
-  gtk_container_add (GTK_CONTAINER (utils), GTK_WIDGET (self->signals_dock));
-
-  self->signals = g_object_new (GLADE_TYPE_SIGNAL_EDITOR,
-                                "visible", TRUE,
-                                NULL);
-  gtk_container_add (GTK_CONTAINER (self->signals_dock), GTK_WIDGET (self->signals));
-
-  /* Wire up the shortcuts to the panel too */
-  _gbp_glade_view_init_shortcuts (GTK_WIDGET (self->properties));
 }
 
 static void
 gbp_glade_editor_addin_unload (IdeEditorAddin       *addin,
-                               IdeEditorPerspective *editor)
+                               IdeEditorSurface *editor)
 {
   GbpGladeEditorAddin *self = (GbpGladeEditorAddin *)addin;
-  IdeLayoutTransientSidebar *transient;
+  IdeTransientSidebar *transient;
 
   g_assert (GBP_IS_GLADE_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
 
-  transient = ide_editor_perspective_get_transient_sidebar (self->editor);
+  transient = ide_editor_surface_get_transient_sidebar (self->editor);
 
   if (self->has_hold)
     {
-      ide_layout_transient_sidebar_unlock (transient);
+      ide_transient_sidebar_unlock (transient);
       self->has_hold = FALSE;
     }
 
   gtk_widget_insert_action_group (GTK_WIDGET (editor), "glade", NULL);
-  gtk_widget_destroy (GTK_WIDGET (self->properties));
+
+  if (self->properties)
+    gtk_widget_destroy (GTK_WIDGET (self->properties));
 
   self->editor = NULL;
 }
@@ -246,5 +276,5 @@ editor_addin_iface_init (IdeEditorAddinInterface *iface)
 {
   iface->load = gbp_glade_editor_addin_load;
   iface->unload = gbp_glade_editor_addin_unload;
-  iface->view_set = gbp_glade_editor_addin_view_set;
+  iface->page_set = gbp_glade_editor_addin_page_set;
 }
diff --git a/src/plugins/glade/gbp-glade-editor-addin.h b/src/plugins/glade/gbp-glade-editor-addin.h
index 0124f88ff..df0765642 100644
--- a/src/plugins/glade/gbp-glade-editor-addin.h
+++ b/src/plugins/glade/gbp-glade-editor-addin.h
@@ -1,6 +1,6 @@
 /* gbp-glade-editor-addin.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -22,7 +22,7 @@
 #pragma once
 
 #include <gladeui/glade.h>
-#include <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/glade/gbp-glade-frame-addin.c b/src/plugins/glade/gbp-glade-frame-addin.c
new file mode 100644
index 000000000..e7349399f
--- /dev/null
+++ b/src/plugins/glade/gbp-glade-frame-addin.c
@@ -0,0 +1,409 @@
+/* gbp-glade-frame-addin.c
+ *
+ * Copyright 2018-2019 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-glade-frame-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gladeui/glade.h>
+#include <libide-editor.h>
+
+#include "gbp-glade-frame-addin.h"
+#include "gbp-glade-page.h"
+
+struct _GbpGladeFrameAddin
+{
+  GObject         parent_instance;
+  GtkMenuButton  *button;
+  GtkLabel       *label;
+  GtkImage       *image;
+  GtkButton      *toggle_source;
+  GladeInspector *inspector;
+  DzlSignalGroup *project_signals;
+  IdePage        *view;
+};
+
+static void frame_addin_iface_init (IdeFrameAddinInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GbpGladeFrameAddin, gbp_glade_frame_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_FRAME_ADDIN,
+                                                frame_addin_iface_init))
+
+static void
+gbp_glade_frame_addin_selection_changed_cb (GbpGladeFrameAddin *self,
+                                                   GladeProject             *project)
+{
+  GList *selection = NULL;
+
+  g_assert (GBP_IS_GLADE_FRAME_ADDIN (self));
+  g_assert (!project || GLADE_IS_PROJECT (project));
+
+  if (project != NULL)
+    selection = glade_project_selection_get (project);
+
+  if (selection != NULL && selection->next == NULL)
+    {
+      GtkWidget *widget = selection->data;
+      GladeWidget *glade = glade_widget_get_from_gobject (widget);
+      GladeWidgetAdaptor *adapter = glade_widget_get_adaptor (glade);
+      g_autofree gchar *format = NULL;
+      const gchar *display_name;
+      const gchar *name;
+      const gchar *icon_name;
+
+      name = glade_widget_get_name (glade);
+      display_name = glade_widget_get_display_name (glade);
+      icon_name = glade_widget_adaptor_get_icon_name (adapter);
+
+      if (display_name != NULL &&
+          display_name[0] != '(' &&
+          name != NULL &&
+          !g_str_equal (display_name, name))
+        name = format = g_strdup_printf ("%s — %s", display_name, name);
+
+      gtk_label_set_label (GTK_LABEL (self->label), name);
+      g_object_set (self->image,
+                    "icon-name", icon_name,
+                    "visible", icon_name != NULL,
+                    NULL);
+
+      return;
+    }
+
+  gtk_label_set_label (GTK_LABEL (self->label), _("Select Widget…"));
+  gtk_widget_hide (GTK_WIDGET (self->image));
+}
+
+static void
+gbp_glade_frame_addin_dispose (GObject *object)
+{
+  GbpGladeFrameAddin *self = (GbpGladeFrameAddin *)object;
+
+  if (self->project_signals != NULL)
+    {
+      dzl_signal_group_set_target (self->project_signals, NULL);
+      g_clear_object (&self->project_signals);
+    }
+
+  G_OBJECT_CLASS (gbp_glade_frame_addin_parent_class)->dispose (object);
+}
+
+static void
+gbp_glade_frame_addin_class_init (GbpGladeFrameAddinClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gbp_glade_frame_addin_dispose;
+}
+
+static void
+gbp_glade_frame_addin_init (GbpGladeFrameAddin *self)
+{
+  self->project_signals = dzl_signal_group_new (GLADE_TYPE_PROJECT);
+
+  dzl_signal_group_connect_object (self->project_signals,
+                                   "selection-changed",
+                                   G_CALLBACK (gbp_glade_frame_addin_selection_changed_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+}
+
+static void
+on_popover_show_cb (GtkPopover *popover,
+                    gpointer    user_data)
+{
+  GtkTreeView *tree;
+
+  g_assert (GTK_IS_POPOVER (popover));
+
+  tree = dzl_gtk_widget_find_child_typed (GTK_WIDGET (popover), GTK_TYPE_TREE_VIEW);
+  gtk_tree_view_expand_all (tree);
+}
+
+static void
+find_view_cb (GtkWidget *widget,
+              gpointer   user_data)
+{
+  struct {
+    GFile         *file;
+    GType          type;
+    IdePage *view;
+  } *lookup = user_data;
+  GFile *file;
+
+  if (lookup->view != NULL)
+    return;
+
+  if (g_type_is_a (G_OBJECT_TYPE (widget), lookup->type))
+    {
+      if (IDE_IS_EDITOR_PAGE (widget))
+        {
+          IdeBuffer *buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (widget));
+          file = ide_buffer_get_file (buffer);
+        }
+      else if (GBP_IS_GLADE_PAGE (widget))
+        {
+          file = gbp_glade_page_get_file (GBP_GLADE_PAGE (widget));
+        }
+      else
+        {
+          g_return_if_reached ();
+        }
+
+      if (g_file_equal (lookup->file, file))
+        lookup->view = IDE_PAGE (widget);
+    }
+}
+
+static IdePage *
+find_view_by_file_and_type (IdeWorkbench *workbench,
+                            GFile        *file,
+                            GType         type)
+{
+  struct {
+    GFile         *file;
+    GType          type;
+    IdePage *view;
+  } lookup = { file, type, NULL };
+
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (G_IS_FILE (file));
+  g_assert (type == IDE_TYPE_EDITOR_PAGE || type == GBP_TYPE_GLADE_PAGE);
+
+  ide_workbench_foreach_page (workbench, find_view_cb, &lookup);
+
+  return lookup.view;
+}
+
+static void
+on_toggle_source_clicked_cb (GbpGladeFrameAddin *self,
+                             GtkButton                *toggle_source)
+{
+  IdeWorkbench *workbench;
+  IdePage *other;
+  const gchar *hint;
+  GFile *gfile;
+  GType type;
+
+  g_assert (GBP_IS_GLADE_FRAME_ADDIN (self));
+  g_assert (GTK_IS_BUTTON (toggle_source));
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (toggle_source));
+
+  if (IDE_IS_EDITOR_PAGE (self->view))
+    {
+      IdeBuffer *buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (self->view));
+
+      gfile = ide_buffer_get_file (buffer);
+      type = GBP_TYPE_GLADE_PAGE;
+      hint = "glade";
+    }
+  else if (GBP_IS_GLADE_PAGE (self->view))
+    {
+      gfile = gbp_glade_page_get_file (GBP_GLADE_PAGE (self->view));
+      type = IDE_TYPE_EDITOR_PAGE;
+      hint = "editor";
+    }
+  else
+    {
+      g_return_if_reached ();
+    }
+
+  if (!(other = find_view_by_file_and_type (workbench, gfile, type)))
+    {
+      ide_workbench_open_async (workbench,
+                                gfile,
+                                hint,
+                                IDE_BUFFER_OPEN_FLAGS_NONE,
+                                NULL, NULL, NULL);
+    }
+  else
+    {
+      gtk_widget_grab_focus (GTK_WIDGET (other));
+    }
+}
+
+static void
+gbp_glade_frame_addin_load (IdeFrameAddin *addin,
+                                   IdeFrame      *stack)
+{
+  GbpGladeFrameAddin *self = (GbpGladeFrameAddin *)addin;
+  GtkPopover *popover;
+  GtkWidget *header;
+  GtkBox *box;
+
+  g_assert (GBP_IS_GLADE_FRAME_ADDIN (self));
+  g_assert (IDE_IS_FRAME (stack));
+
+  header = ide_frame_get_titlebar (stack);
+
+  popover = g_object_new (GTK_TYPE_POPOVER,
+                          "width-request", 400,
+                          "height-request", 400,
+                          "position", GTK_POS_BOTTOM,
+                          NULL);
+  g_signal_connect (popover,
+                    "show",
+                    G_CALLBACK (on_popover_show_cb),
+                    NULL);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (popover), "glade-stack-header");
+
+  self->button = g_object_new (GTK_TYPE_MENU_BUTTON,
+                               "popover", popover,
+                               "visible", FALSE,
+                               NULL);
+  g_signal_connect (self->button,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->button);
+  ide_frame_header_add_custom_title (IDE_FRAME_HEADER (header),
+                                            GTK_WIDGET (self->button),
+                                            200);
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "halign", GTK_ALIGN_CENTER,
+                      "orientation", GTK_ORIENTATION_HORIZONTAL,
+                      "spacing", 6,
+                      "visible", TRUE,
+                      NULL);
+  gtk_container_add (GTK_CONTAINER (self->button), GTK_WIDGET (box));
+
+  self->image = g_object_new (GTK_TYPE_IMAGE,
+                              "icon-size", GTK_ICON_SIZE_MENU,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (self->image));
+
+  self->label = g_object_new (GTK_TYPE_LABEL,
+                              "label", _("Select Widget…"),
+                              "visible", TRUE,
+                              NULL);
+  gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (self->label));
+
+  self->inspector = g_object_new (GLADE_TYPE_INSPECTOR,
+                                  "visible", TRUE,
+                                  NULL);
+  gtk_container_add (GTK_CONTAINER (popover), GTK_WIDGET (self->inspector));
+
+  /*
+   * This button allows for toggling between the designer and the
+   * source document. It makes it look like we're switching between
+   * documents in the same frame, but its really two separate views.
+   */
+  self->toggle_source = g_object_new (GTK_TYPE_BUTTON,
+                                      "has-tooltip", TRUE,
+                                      "hexpand", FALSE,
+                                      "visible", FALSE,
+                                      NULL);
+  g_signal_connect (self->toggle_source,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->toggle_source);
+  g_signal_connect_object (self->toggle_source,
+                           "clicked",
+                           G_CALLBACK (on_toggle_source_clicked_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  gtk_container_add_with_properties (GTK_CONTAINER (header), GTK_WIDGET (self->toggle_source),
+                                     "pack-type", GTK_PACK_END,
+                                     "priority", 200,
+                                     NULL);
+}
+
+static void
+gbp_glade_frame_addin_unload (IdeFrameAddin *addin,
+                                     IdeFrame      *stack)
+{
+  GbpGladeFrameAddin *self = (GbpGladeFrameAddin *)addin;
+
+  g_assert (GBP_IS_GLADE_FRAME_ADDIN (self));
+  g_assert (IDE_IS_FRAME (stack));
+
+  self->view = NULL;
+
+  if (self->button != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->button));
+
+  if (self->toggle_source != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->toggle_source));
+}
+
+static void
+gbp_glade_frame_addin_set_view (IdeFrameAddin *addin,
+                                       IdePage       *view)
+{
+  GbpGladeFrameAddin *self = (GbpGladeFrameAddin *)addin;
+  GladeProject *project = NULL;
+
+  g_assert (GBP_IS_GLADE_FRAME_ADDIN (self));
+  g_assert (!view || IDE_IS_PAGE (view));
+
+  self->view = view;
+
+  /*
+   * Update related widgetry from view change.
+   */
+
+  if (GBP_IS_GLADE_PAGE (view))
+    project = gbp_glade_page_get_project (GBP_GLADE_PAGE (view));
+
+  glade_inspector_set_project (self->inspector, project);
+  gtk_widget_set_visible (GTK_WIDGET (self->button), project != NULL);
+
+  dzl_signal_group_set_target (self->project_signals, project);
+  gbp_glade_frame_addin_selection_changed_cb (self, project);
+
+  /*
+   * If this is an editor view and a UI file, we can allow the user
+   * to change to the designer.
+   */
+
+  gtk_widget_hide (GTK_WIDGET (self->toggle_source));
+
+  if (IDE_IS_EDITOR_PAGE (view))
+    {
+      IdeBuffer *buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (view));
+      GFile *file = ide_buffer_get_file (buffer);
+      g_autofree gchar *name = g_file_get_basename (file);
+
+      if (g_str_has_suffix (name, ".ui"))
+        {
+          gtk_button_set_label (self->toggle_source, _("View Design"));
+          gtk_widget_set_tooltip_text (GTK_WIDGET (self->toggle_source),
+                                       _("Switch to UI designer"));
+          gtk_widget_show (GTK_WIDGET (self->toggle_source));
+        }
+    }
+  else if (GBP_IS_GLADE_PAGE (view))
+    {
+      gtk_button_set_label (self->toggle_source, _("View Source"));
+      gtk_widget_set_tooltip_text (GTK_WIDGET (self->toggle_source),
+                                   _("Switch to source code editor"));
+      gtk_widget_show (GTK_WIDGET (self->toggle_source));
+    }
+}
+
+static void
+frame_addin_iface_init (IdeFrameAddinInterface *iface)
+{
+  iface->load = gbp_glade_frame_addin_load;
+  iface->unload = gbp_glade_frame_addin_unload;
+  iface->set_page = gbp_glade_frame_addin_set_view;
+}
diff --git a/src/plugins/glade/gbp-glade-frame-addin.h b/src/plugins/glade/gbp-glade-frame-addin.h
new file mode 100644
index 000000000..a2c7aaba2
--- /dev/null
+++ b/src/plugins/glade/gbp-glade-frame-addin.h
@@ -0,0 +1,31 @@
+/* gbp-glade-frame-addin.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GLADE_FRAME_ADDIN (gbp_glade_frame_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGladeFrameAddin, gbp_glade_frame_addin, GBP, GLADE_FRAME_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/glade/gbp-glade-page-actions.c b/src/plugins/glade/gbp-glade-page-actions.c
new file mode 100644
index 000000000..f82d6d828
--- /dev/null
+++ b/src/plugins/glade/gbp-glade-page-actions.c
@@ -0,0 +1,189 @@
+/* gbp-glade-page-actions.c
+ *
+ * Copyright 2018-2019 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-glade-page-actions"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gbp-glade-private.h"
+
+static void
+gbp_glade_page_action_save (GSimpleAction *action,
+                            GVariant      *param,
+                            gpointer       user_data)
+{
+  GbpGladePage *self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_GLADE_PAGE (self));
+
+  if (!_gbp_glade_page_save (self, &error))
+    /* translators: %s is replaced with the specific error message */
+    g_warning (_("Failed to save glade document: %s"), error->message);
+}
+
+static void
+gbp_glade_page_action_preview (GSimpleAction *action,
+                               GVariant      *param,
+                               gpointer       user_data)
+{
+  GbpGladePage *self = user_data;
+  GladeProject *project;
+  GList *toplevels;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_GLADE_PAGE (self));
+
+  project = gbp_glade_page_get_project (self);
+  toplevels = glade_project_toplevels (project);
+
+  /* Just preview the first toplevel. To preview others, they need to
+   * right-click to get the context menu.
+   */
+  if (toplevels != NULL)
+    {
+      GtkWidget *widget = toplevels->data;
+      GladeWidget *glade;
+
+      g_assert (GTK_IS_WIDGET (widget));
+      glade = glade_widget_get_from_gobject (widget);
+      g_assert (GLADE_IS_WIDGET (glade));
+
+      glade_project_preview (project, glade);
+    }
+}
+
+static void
+gbp_glade_page_action_pointer_mode (GSimpleAction *action,
+                                    GVariant      *param,
+                                    gpointer       user_data)
+{
+  GbpGladePage *self = user_data;
+  g_autoptr(GEnumClass) klass = NULL;
+  GladeProject *project;
+  const gchar *nick;
+  GEnumValue *value;
+  GType type;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (param != NULL);
+  g_assert (g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+  g_assert (GBP_IS_GLADE_PAGE (self));
+
+  project = gbp_glade_page_get_project (self);
+  nick = g_variant_get_string (param, NULL);
+
+  /* No GType to lookup from public API yet */
+  type = g_type_from_name ("GladePointerMode");
+  klass = g_type_class_ref (type);
+  value = g_enum_get_value_by_nick (klass, nick);
+
+  if (value != NULL)
+    glade_project_set_pointer_mode (project, value->value);
+}
+
+static void
+gbp_glade_page_action_paste (GSimpleAction *action,
+                             GVariant      *param,
+                             gpointer       user_data)
+{
+  GbpGladePage *self = user_data;
+  GtkWidget *placeholder;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_GLADE_PAGE (self));
+
+  placeholder = glade_util_get_placeholder_from_pointer (GTK_CONTAINER (self));
+  glade_project_command_paste (self->project, placeholder ? GLADE_PLACEHOLDER (placeholder) : NULL);
+}
+
+#define WRAP_PROJECT_ACTION(name, func)                 \
+static void                                             \
+gbp_glade_page_action_##name (GSimpleAction *action,    \
+                              GVariant      *param,     \
+                              gpointer       user_data) \
+{                                                       \
+  GbpGladePage *self = user_data;                       \
+                                                        \
+  g_assert (G_IS_SIMPLE_ACTION (action));               \
+  g_assert (GBP_IS_GLADE_PAGE (self));                  \
+                                                        \
+  glade_project_##func (self->project);                 \
+}
+
+WRAP_PROJECT_ACTION (cut, command_cut)
+WRAP_PROJECT_ACTION (copy, copy_selection)
+WRAP_PROJECT_ACTION (delete, command_delete)
+WRAP_PROJECT_ACTION (redo, redo)
+WRAP_PROJECT_ACTION (undo, undo)
+
+static GActionEntry actions[] = {
+  { "cut", gbp_glade_page_action_cut },
+  { "copy", gbp_glade_page_action_copy },
+  { "paste", gbp_glade_page_action_paste },
+  { "delete", gbp_glade_page_action_delete },
+  { "redo", gbp_glade_page_action_redo },
+  { "undo", gbp_glade_page_action_undo },
+  { "save", gbp_glade_page_action_save },
+  { "preview", gbp_glade_page_action_preview },
+  { "pointer-mode", gbp_glade_page_action_pointer_mode, "s" },
+};
+
+void
+_gbp_glade_page_update_actions (GbpGladePage *self)
+{
+  GladeCommand *redo;
+  GladeCommand *undo;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (GLADE_IS_PROJECT (self->project));
+
+  redo = glade_project_next_redo_item (self->project);
+  undo = glade_project_next_undo_item (self->project);
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "glade-view", "undo",
+                             "enabled", undo != NULL,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self), "glade-view", "redo",
+                             "enabled", redo != NULL,
+                             NULL);
+}
+
+void
+_gbp_glade_page_init_actions (GbpGladePage *self)
+{
+  g_autoptr(GSimpleActionGroup) group = NULL;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+
+  group = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (group),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self),
+                                  "glade-view",
+                                  G_ACTION_GROUP (group));
+
+  _gbp_glade_page_update_actions (self);
+}
diff --git a/src/plugins/glade/gbp-glade-page-shortcuts.c b/src/plugins/glade/gbp-glade-page-shortcuts.c
new file mode 100644
index 000000000..9f4eaf6dc
--- /dev/null
+++ b/src/plugins/glade/gbp-glade-page-shortcuts.c
@@ -0,0 +1,120 @@
+/* gbp-glade-page-shortcuts.c
+ *
+ * Copyright 2018-2019 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-glade-page-shortcuts"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+
+#include "gbp-glade-private.h"
+#include "gbp-glade-page.h"
+
+#define I_(s) (g_intern_static_string(s))
+
+static DzlShortcutEntry glade_view_shortcuts[] = {
+  { "org.gnome.builder.glade-view.save",
+    0, NULL,
+    NC_("shortcut window", "Glade shortcuts"),
+    NC_("shortcut window", "Designer"),
+    NC_("shortcut window", "Save the interface design") },
+
+  { "org.gnome.builder.glade-view.preview",
+    0, NULL,
+    NC_("shortcut window", "Glade shortcuts"),
+    NC_("shortcut window", "Designer"),
+    NC_("shortcut window", "Preview the interface design") },
+
+  { "org.gnome.builder.glade-view.undo",
+    0, NULL,
+    NC_("shortcut window", "Glade shortcuts"),
+    NC_("shortcut window", "Designer"),
+    NC_("shortcut window", "Undo the last command") },
+
+  { "org.gnome.builder.glade-view.redo",
+    0, NULL,
+    NC_("shortcut window", "Glade shortcuts"),
+    NC_("shortcut window", "Designer"),
+    NC_("shortcut window", "Redo the next command") },
+};
+
+void
+_gbp_glade_page_init_shortcuts (GtkWidget *widget)
+{
+  DzlShortcutController *controller;
+
+  g_return_if_fail (GTK_IS_WIDGET (widget));
+
+  controller = dzl_shortcut_controller_find (widget);
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.glade-view.save"),
+                                              "<Primary>s",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("glade-view.save"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.glade-view.preview"),
+                                              "<Control><Alt>p",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("glade-view.preview"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.glade-view.undo"),
+                                              "<Control>z",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("glade-view.undo"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.glade-view.redo"),
+                                              "<Control><Shift>z",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("glade-view.redo"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.glade-view.copy"),
+                                              "<Primary>c",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("glade-view.copy"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.glade-view.cut"),
+                                              "<Primary>x",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("glade-view.cut"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.glade-view.paste"),
+                                              "<Primary>v",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("glade-view.paste"));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.glade-view.delete"),
+                                              "Delete",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("glade-view.delete"));
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             glade_view_shortcuts,
+                                             G_N_ELEMENTS (glade_view_shortcuts),
+                                             GETTEXT_PACKAGE);
+}
diff --git a/src/plugins/glade/gbp-glade-page.c b/src/plugins/glade/gbp-glade-page.c
new file mode 100644
index 000000000..73a8c21f6
--- /dev/null
+++ b/src/plugins/glade/gbp-glade-page.c
@@ -0,0 +1,756 @@
+/* gbp-glade-page.c
+ *
+ * Copyright 2018-2019 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-glade-page"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gbp-glade-page.h"
+#include "gbp-glade-private.h"
+
+G_DEFINE_TYPE (GbpGladePage, gbp_glade_page, IDE_TYPE_PAGE)
+
+enum {
+  PROP_0,
+  PROP_PROJECT,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+/**
+ * gbp_glade_page_new:
+ *
+ * Create a new #GbpGladePage.
+ *
+ * Returns: (transfer full): a newly created #GbpGladePage
+ */
+GbpGladePage *
+gbp_glade_page_new (void)
+{
+  return g_object_new (GBP_TYPE_GLADE_PAGE, NULL);
+}
+
+static void
+gbp_glade_page_notify_modified_cb (GbpGladePage *self,
+                                   GParamSpec   *pspec,
+                                   GladeProject *project)
+{
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (GLADE_IS_PROJECT (project));
+
+  ide_page_set_modified (IDE_PAGE (self),
+                                glade_project_get_modified (project));
+}
+
+static void
+gbp_glade_page_changed_cb (GbpGladePage *self,
+                           GladeCommand *command,
+                           gboolean      execute,
+                           GladeProject *project)
+{
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (!command || GLADE_IS_COMMAND (command));
+  g_assert (GLADE_IS_PROJECT (project));
+
+  if (project != self->project)
+    return;
+
+  _gbp_glade_page_update_actions (self);
+}
+
+static void
+gbp_glade_page_set_project (GbpGladePage *self,
+                            GladeProject *project)
+{
+  GladeProject *old_project = NULL;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (GLADE_IS_PROJECT (project));
+
+  if (project == self->project)
+    return;
+
+  if (gtk_widget_in_destruction (GTK_WIDGET (self)))
+    return;
+
+  if (self->project != NULL)
+    {
+      old_project = g_object_ref (self->project);
+      glade_app_remove_project (self->project);
+      dzl_signal_group_set_target (self->project_signals, NULL);
+      g_clear_object (&self->project);
+    }
+
+  if (project != NULL)
+    {
+      self->project = g_object_ref (project);
+      glade_app_add_project (self->project);
+      dzl_signal_group_set_target (self->project_signals, project);
+    }
+
+  if (self->designer != NULL)
+    {
+      gtk_widget_destroy (GTK_WIDGET (self->designer));
+      g_assert (self->designer == NULL);
+
+      if (self->project != NULL)
+        {
+          self->designer = g_object_new (GLADE_TYPE_DESIGN_VIEW,
+                                         "project", self->project,
+                                         "vexpand", TRUE,
+                                         "visible", TRUE,
+                                         NULL);
+          g_signal_connect (self->designer,
+                            "destroy",
+                            G_CALLBACK (gtk_widget_destroyed),
+                            &self->designer);
+          dzl_gtk_widget_add_style_class (GTK_WIDGET (self->designer), "glade-designer");
+          gtk_container_add_with_properties (GTK_CONTAINER (self->main_box), GTK_WIDGET (self->designer),
+                                             "pack-type", GTK_PACK_START,
+                                             "position", 0,
+                                             NULL);
+        }
+    }
+
+  if (self->chooser != NULL)
+    glade_adaptor_chooser_set_project (self->chooser, self->project);
+
+  ide_page_set_modified (IDE_PAGE (self),
+                                self->project != NULL && glade_project_get_modified (self->project));
+
+  g_clear_object (&old_project);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_PROJECT]);
+}
+
+gboolean
+_gbp_glade_page_reload (GbpGladePage *self)
+{
+  GladeProject *project;
+
+  g_return_val_if_fail (GBP_IS_GLADE_PAGE (self), FALSE);
+  g_return_val_if_fail (GLADE_IS_PROJECT (self->project), FALSE);
+
+  /*
+   * Switch to a new GladeProject object, which is rather tricky
+   * because we need to update everything that connected to it.
+   * Sadly we can't reuse existing GladeProject objects.
+   */
+  project = glade_project_new ();
+  gbp_glade_page_set_project (self, project);
+  gbp_glade_page_load_file_async (self, self->file, NULL, NULL, NULL);
+  g_clear_object (&project);
+
+  /*
+   * This is sort of a hack, buf if we want everything to adapt to our
+   * new project, we need to signal that the view changed so that it
+   * grabs the new version of our GladeProject.
+   */
+  if (gtk_widget_get_visible (GTK_WIDGET (self)) &&
+      gtk_widget_get_child_visible (GTK_WIDGET (self)))
+    {
+      gtk_widget_hide (GTK_WIDGET (self));
+      gtk_widget_show (GTK_WIDGET (self));
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+gboolean
+_gbp_glade_page_save (GbpGladePage  *self,
+                      GError       **error)
+{
+  const gchar *path;
+
+  g_return_val_if_fail (GBP_IS_GLADE_PAGE (self), FALSE);
+  g_return_val_if_fail (GLADE_IS_PROJECT (self->project), FALSE);
+
+  if (self->file == NULL || !(path = g_file_peek_path (self->file)))
+    {
+      /* Implausible path */
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_NOT_FOUND,
+                   "No file has been set for the view");
+      return FALSE;
+    }
+
+  if (glade_project_save (self->project, path, error))
+    {
+      IdeBufferManager *bufmgr;
+      IdeContext *context;
+      IdeBuffer *buffer;
+
+      context = ide_widget_get_context (GTK_WIDGET (self));
+      bufmgr = ide_buffer_manager_from_context (context);
+
+      /* We successfully wrote the file, so trigger a full reload of the
+       * IdeBuffer if there is one already currently open.
+       */
+
+      if ((buffer = ide_buffer_manager_find_buffer (bufmgr, self->file)))
+        {
+          ide_buffer_manager_load_file_async (bufmgr,
+                                              ide_buffer_get_file (buffer),
+                                              IDE_BUFFER_OPEN_FLAGS_NO_VIEW | 
IDE_BUFFER_OPEN_FLAGS_FORCE_RELOAD,
+                                              NULL, NULL, NULL, NULL);
+        }
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+gbp_glade_page_agree_to_close_async (IdePage       *view,
+                                     GCancellable        *cancellable,
+                                     GAsyncReadyCallback  callback,
+                                     gpointer             user_data)
+{
+  GbpGladePage *self = (GbpGladePage *)view;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_glade_page_agree_to_close_async);
+
+  if (ide_page_get_modified (view))
+    {
+      if (!_gbp_glade_page_save (self, &error))
+        {
+          ide_task_return_error (task, g_steal_pointer (&error));
+          return;
+        }
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_glade_page_agree_to_close_finish (IdePage  *view,
+                                      GAsyncResult   *result,
+                                      GError        **error)
+{
+  g_assert (GBP_IS_GLADE_PAGE (view));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+viewport_style_changed_cb (GbpGladePage    *self,
+                           GtkStyleContext *style_context)
+{
+  GdkRGBA bg, fg;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (GTK_IS_STYLE_CONTEXT (style_context));
+
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+  gtk_style_context_get_color (style_context, GTK_STATE_FLAG_NORMAL, &fg);
+  gtk_style_context_get_background_color (style_context, GTK_STATE_FLAG_NORMAL, &bg);
+  G_GNUC_END_IGNORE_DEPRECATIONS;
+
+  ide_page_set_primary_color_bg (IDE_PAGE (self), &bg);
+  ide_page_set_primary_color_fg (IDE_PAGE (self), &fg);
+}
+
+static void
+gbp_glade_page_buffer_saved_cb (GbpGladePage     *self,
+                                IdeBuffer        *buffer,
+                                IdeBufferManager *bufmgr)
+{
+  GFile *file;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+
+  if (self->file == NULL)
+    return;
+
+  file = ide_buffer_get_file (buffer);
+
+  if (g_file_equal (file, self->file))
+    _gbp_glade_page_reload (self);
+}
+
+static void
+gbp_glade_page_context_set (GtkWidget  *widget,
+                            IdeContext *context)
+{
+  GbpGladePage *self = (GbpGladePage *)widget;
+  IdeBufferManager *bufmgr;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (!context || IDE_IS_CONTEXT (context));
+
+  if (context == NULL)
+    return;
+
+  /* Track when buffers are saved so that we can reload the view */
+  bufmgr = ide_buffer_manager_from_context (context);
+  g_signal_connect_object (bufmgr,
+                           "buffer-saved",
+                           G_CALLBACK (gbp_glade_page_buffer_saved_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+gbp_glade_page_add_signal_handler_cb (GbpGladePage      *self,
+                                      GladeWidget       *widget,
+                                      const GladeSignal *gsignal,
+                                      GladeProject      *project)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (GLADE_IS_WIDGET (widget));
+  g_assert (GLADE_IS_SIGNAL (gsignal));
+  g_assert (GLADE_IS_PROJECT (project));
+
+  g_print ("add signal handler: %s\n",
+           glade_signal_get_handler (gsignal));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_glade_page_remove_signal_handler_cb (GbpGladePage      *self,
+                                         GladeWidget       *widget,
+                                         const GladeSignal *gsignal,
+                                         GladeProject      *project)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (GLADE_IS_WIDGET (widget));
+  g_assert (GLADE_IS_SIGNAL (gsignal));
+  g_assert (GLADE_IS_PROJECT (project));
+
+  g_print ("remove signal handler: %s\n",
+           glade_signal_get_handler (gsignal));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_glade_page_change_signal_handler_cb (GbpGladePage      *self,
+                                         GladeWidget       *widget,
+                                         const GladeSignal *old_gsignal,
+                                         const GladeSignal *new_gsignal,
+                                         GladeProject      *project)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (GLADE_IS_WIDGET (widget));
+  g_assert (GLADE_IS_SIGNAL (old_gsignal));
+  g_assert (GLADE_IS_SIGNAL (new_gsignal));
+  g_assert (GLADE_IS_PROJECT (project));
+
+  g_print ("change signal handler: %s => %s\n",
+           glade_signal_get_handler (old_gsignal),
+           glade_signal_get_handler (new_gsignal));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_glade_page_activate_signal_handler_cb (GbpGladePage      *self,
+                                           GladeWidget       *widget,
+                                           const GladeSignal *gsignal,
+                                           GladeProject      *project)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_GLADE_PAGE (self));
+  g_assert (GLADE_IS_WIDGET (widget));
+  g_assert (GLADE_IS_SIGNAL (gsignal));
+  g_assert (GLADE_IS_PROJECT (project));
+
+  g_print ("activate signal handler: %s\n",
+           glade_signal_get_handler (gsignal));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_glade_page_dispose (GObject *object)
+{
+  GbpGladePage *self = (GbpGladePage *)object;
+
+  g_clear_object (&self->file);
+  g_clear_object (&self->project);
+
+  if (self->project_signals != NULL)
+    {
+      dzl_signal_group_set_target (self->project_signals, NULL);
+      g_clear_object (&self->project_signals);
+    }
+
+  G_OBJECT_CLASS (gbp_glade_page_parent_class)->dispose (object);
+}
+
+static void
+gbp_glade_page_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  GbpGladePage *self = GBP_GLADE_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_PROJECT:
+      g_value_set_object (value, gbp_glade_page_get_project (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_glade_page_class_init (GbpGladePageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  IdePageClass *view_class = IDE_PAGE_CLASS (klass);
+
+  object_class->dispose = gbp_glade_page_dispose;
+  object_class->get_property = gbp_glade_page_get_property;
+
+  view_class->agree_to_close_async = gbp_glade_page_agree_to_close_async;
+  view_class->agree_to_close_finish = gbp_glade_page_agree_to_close_finish;
+
+  properties [PROP_PROJECT] =
+    g_param_spec_object ("project",
+                         "Project",
+                         "The project for the view",
+                         GLADE_TYPE_PROJECT,
+                         (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_css_name (widget_class, "gbpgladeview");
+}
+
+static void
+gbp_glade_page_init (GbpGladePage *self)
+{
+  GtkBox *box;
+  GtkViewport *viewport;
+  GtkStyleContext *style_context;
+  GladeProject *project = NULL;
+  static const struct {
+    const gchar *action_target;
+    const gchar *icon_name;
+    const gchar *tooltip;
+  } pointers[] = {
+    { "select", "pointer-mode-select-symbolic", N_("Switch to selection mode") },
+    { "drag-resize", "pointer-mode-drag-symbolic", N_("Switch to drag-resize mode") },
+    { "margin-edit", "pointer-mode-resize-symbolic", N_("Switch to margin editor") },
+    { "align-edit", "pointer-mode-pin-symbolic", N_("Switch to alignment editor") },
+  };
+
+  ide_page_set_can_split (IDE_PAGE (self), FALSE);
+  ide_page_set_menu_id (IDE_PAGE (self), "gbp-glade-page-menu");
+  ide_page_set_title (IDE_PAGE (self), _("Unnamed Glade project"));
+  ide_page_set_icon_name (IDE_PAGE (self), "glade-symbolic");
+  ide_page_set_menu_id (IDE_PAGE (self), "gbp-glade-page-document-menu");
+
+  self->project_signals = dzl_signal_group_new (GLADE_TYPE_PROJECT);
+
+  dzl_signal_group_connect_object (self->project_signals,
+                                   "notify::modified",
+                                   G_CALLBACK (gbp_glade_page_notify_modified_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->project_signals,
+                                   "changed",
+                                   G_CALLBACK (gbp_glade_page_changed_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->project_signals,
+                                   "add-signal-handler",
+                                   G_CALLBACK (gbp_glade_page_add_signal_handler_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->project_signals,
+                                   "remove-signal-handler",
+                                   G_CALLBACK (gbp_glade_page_remove_signal_handler_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->project_signals,
+                                   "change-signal-handler",
+                                   G_CALLBACK (gbp_glade_page_change_signal_handler_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  dzl_signal_group_connect_object (self->project_signals,
+                                   "activate-signal-handler",
+                                   G_CALLBACK (gbp_glade_page_activate_signal_handler_cb),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+
+  ide_widget_set_context_handler (self, gbp_glade_page_context_set);
+
+  project = glade_project_new ();
+  gbp_glade_page_set_project (self, project);
+  g_clear_object (&project);
+
+  self->main_box = g_object_new (GTK_TYPE_BOX,
+                                 "orientation", GTK_ORIENTATION_VERTICAL,
+                                 "visible", TRUE,
+                                 NULL);
+  gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->main_box));
+
+  self->chooser = g_object_new (GLADE_TYPE_ADAPTOR_CHOOSER,
+                                "project", self->project,
+                                "visible", TRUE,
+                                NULL);
+  g_signal_connect (self->chooser,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->chooser);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->chooser), "glade-chooser");
+  gtk_container_add_with_properties (GTK_CONTAINER (self->main_box), GTK_WIDGET (self->chooser),
+                                     "pack-type", GTK_PACK_END,
+                                     NULL);
+
+  self->designer = g_object_new (GLADE_TYPE_DESIGN_VIEW,
+                                 "project", self->project,
+                                 "vexpand", TRUE,
+                                 "visible", TRUE,
+                                 NULL);
+  g_signal_connect (self->designer,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->designer);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->designer), "glade-designer");
+  gtk_container_add (GTK_CONTAINER (self->main_box), GTK_WIDGET (self->designer));
+
+  /* Discover viewport so that we can track the background color changes
+   * from CSS. That is used to set our primary color.
+   */
+  viewport = dzl_gtk_widget_find_child_typed (GTK_WIDGET (self->designer), GTK_TYPE_VIEWPORT);
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (viewport));
+  g_signal_connect_object (style_context,
+                           "changed",
+                           G_CALLBACK (viewport_style_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  viewport_style_changed_cb (self, style_context);
+
+  /* Setup pointer-mode controls */
+
+  box = g_object_new (GTK_TYPE_BOX,
+                      "visible", TRUE,
+                      NULL);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (box), "linked");
+  gtk_container_add (GTK_CONTAINER (self->chooser), GTK_WIDGET (box));
+
+  for (guint i = 0; i < G_N_ELEMENTS (pointers); i++)
+    {
+      g_autoptr(GVariant) param = NULL;
+      GtkButton *button;
+      GtkImage *image;
+
+      param = g_variant_take_ref (g_variant_new_string (pointers[i].action_target));
+
+      image = g_object_new (GTK_TYPE_IMAGE,
+                            "icon-name", pointers[i].icon_name,
+                            "pixel-size", 16,
+                            "visible", TRUE,
+                            NULL);
+      button = g_object_new (GTK_TYPE_BUTTON,
+                             "action-name", "glade-view.pointer-mode",
+                             "action-target", param,
+                             "child", image,
+                             "has-tooltip", TRUE,
+                             "tooltip-text", pointers[i].tooltip,
+                             "visible", TRUE,
+                             NULL);
+      dzl_gtk_widget_add_style_class (GTK_WIDGET (button), "image-button");
+      gtk_container_add (GTK_CONTAINER (box), GTK_WIDGET (button));
+    }
+
+  /* Setup action state and shortcuts */
+
+  _gbp_glade_page_init_actions (self);
+  _gbp_glade_page_init_shortcuts (GTK_WIDGET (self));
+}
+
+/**
+ * gbp_glade_page_get_project:
+ *
+ * Returns: (transfer none): A #GladeProject or %NULL
+ */
+GladeProject *
+gbp_glade_page_get_project (GbpGladePage *self)
+{
+  g_return_val_if_fail (GBP_IS_GLADE_PAGE (self), NULL);
+
+  return self->project;
+}
+
+static gboolean
+file_missing_or_empty (GFile *file)
+{
+  g_autoptr(GFileInfo) info = NULL;
+
+  g_assert (G_IS_FILE (file));
+
+  info = g_file_query_info (file,
+                            G_FILE_ATTRIBUTE_STANDARD_SIZE,
+                            G_FILE_QUERY_INFO_NONE,
+                            NULL,
+                            NULL);
+
+  return info == NULL || g_file_info_get_size (info) == 0;
+}
+
+static void
+gbp_glade_page_load_file_map_cb (GladeDesignView *designer,
+                                 IdeTask         *task)
+{
+  g_autofree gchar *name = NULL;
+  GbpGladePage *self;
+  const gchar *path;
+  GFile *file;
+
+  g_assert (GLADE_IS_DESIGN_VIEW (designer));
+  g_assert (IDE_IS_TASK (task));
+  g_assert (gtk_widget_get_mapped (GTK_WIDGET (designer)));
+
+  self = ide_task_get_source_object (task);
+  file = ide_task_get_task_data (task);
+
+  g_signal_handlers_disconnect_by_func (self->designer,
+                                        G_CALLBACK (gbp_glade_page_load_file_map_cb),
+                                        task);
+
+  if (!g_file_is_native (file))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_INVALID_FILENAME,
+                                 "File must be a local file");
+      return;
+    }
+
+  /*
+   * If the file is empty, nothing to load for now. Just go
+   * ahead and wait until we save to overwrite it.
+   */
+  if (file_missing_or_empty (file))
+    {
+      name = g_file_get_basename (file);
+      ide_page_set_title (IDE_PAGE (self), name);
+      ide_task_return_boolean (task, TRUE);
+      return;
+    }
+
+  path = g_file_peek_path (file);
+
+  if (!glade_project_load_from_file (self->project, path))
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_FAILED,
+                               "Failed to load glade project");
+  else
+    ide_task_return_boolean (task, TRUE);
+
+  name = glade_project_get_name (self->project);
+  ide_page_set_title (IDE_PAGE (self), name);
+}
+
+void
+gbp_glade_page_load_file_async (GbpGladePage        *self,
+                                GFile               *file,
+                                GCancellable        *cancellable,
+                                GAsyncReadyCallback  callback,
+                                gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (GBP_IS_GLADE_PAGE (self));
+  g_return_if_fail (G_IS_FILE (file));
+  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_glade_page_load_file_async);
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
+
+  g_set_object (&self->file, file);
+
+  /* We can't load the file until we have been mapped or else we see an issue
+   * where toplevels cannot be parented properly. If we come across that, then
+   * delay until the widget is mapped.
+   */
+  if (!gtk_widget_get_mapped (GTK_WIDGET (self->designer)))
+    g_signal_connect_data (self->designer,
+                           "map",
+                           G_CALLBACK (gbp_glade_page_load_file_map_cb),
+                           g_steal_pointer (&task),
+                           (GClosureNotify)g_object_unref,
+                           0);
+  else
+    gbp_glade_page_load_file_map_cb (self->designer, task);
+}
+
+gboolean
+gbp_glade_page_load_file_finish (GbpGladePage  *self,
+                                 GAsyncResult  *result,
+                                 GError       **error)
+{
+  g_return_val_if_fail (GBP_IS_GLADE_PAGE (self), FALSE);
+  g_return_val_if_fail (IDE_IS_TASK (result), FALSE);
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+/**
+ * gbp_glade_page_get_file:
+ *
+ * Returns: (nullable) (transfer none): a #GFile or %NULL
+ */
+GFile *
+gbp_glade_page_get_file (GbpGladePage *self)
+{
+  g_return_val_if_fail (GBP_IS_GLADE_PAGE (self), NULL);
+
+  return self->file;
+}
diff --git a/src/plugins/glade/gbp-glade-page.h b/src/plugins/glade/gbp-glade-page.h
new file mode 100644
index 000000000..7bfb1f14e
--- /dev/null
+++ b/src/plugins/glade/gbp-glade-page.h
@@ -0,0 +1,44 @@
+/* gbp-glade-page.h
+ *
+ * Copyright 2018-2019 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 <gladeui/glade.h>
+#include <libide-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GLADE_PAGE (gbp_glade_page_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGladePage, gbp_glade_page, GBP, GLADE_PAGE, IdePage)
+
+GbpGladePage *gbp_glade_page_new              (void);
+GFile        *gbp_glade_page_get_file         (GbpGladePage         *self);
+void          gbp_glade_page_load_file_async  (GbpGladePage         *self,
+                                               GFile                *file,
+                                               GCancellable         *cancellable,
+                                               GAsyncReadyCallback   callback,
+                                               gpointer              user_data);
+gboolean      gbp_glade_page_load_file_finish (GbpGladePage         *self,
+                                               GAsyncResult         *result,
+                                               GError              **error);
+GladeProject *gbp_glade_page_get_project      (GbpGladePage         *self);
+
+G_END_DECLS
diff --git a/src/plugins/glade/gbp-glade-private.h b/src/plugins/glade/gbp-glade-private.h
index 539af0e62..87169fec4 100644
--- a/src/plugins/glade/gbp-glade-private.h
+++ b/src/plugins/glade/gbp-glade-private.h
@@ -1,6 +1,6 @@
 /* gbp-glade-private.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -20,17 +20,17 @@
 
 #pragma once
 
-#include <ide.h>
+#include <libide-gui.h>
 #include <gladeui/glade.h>
 #include <gladeui/glade-adaptor-chooser.h>
 
-#include "gbp-glade-view.h"
+#include "gbp-glade-page.h"
 
 G_BEGIN_DECLS
 
-struct _GbpGladeView
+struct _GbpGladePage
 {
-  IdeLayoutView        parent_instance;
+  IdePage              parent_instance;
 
   GFile               *file;
   GladeProject        *project;
@@ -41,11 +41,11 @@ struct _GbpGladeView
   GtkBox              *main_box;
 };
 
-void     _gbp_glade_view_init_actions   (GbpGladeView  *self);
-void     _gbp_glade_view_init_shortcuts (GtkWidget     *widget);
-void     _gbp_glade_view_update_actions (GbpGladeView  *self);
-gboolean _gbp_glade_view_reload         (GbpGladeView  *self);
-gboolean _gbp_glade_view_save           (GbpGladeView  *self,
+void     _gbp_glade_page_init_actions   (GbpGladePage  *self);
+void     _gbp_glade_page_init_shortcuts (GtkWidget     *widget);
+void     _gbp_glade_page_update_actions (GbpGladePage  *self);
+gboolean _gbp_glade_page_reload         (GbpGladePage  *self);
+gboolean _gbp_glade_page_save           (GbpGladePage  *self,
                                          GError       **error);
 
 G_END_DECLS
diff --git a/src/plugins/glade/gbp-glade-properties.c b/src/plugins/glade/gbp-glade-properties.c
index 3b5b0e53a..2c6ba142e 100644
--- a/src/plugins/glade/gbp-glade-properties.c
+++ b/src/plugins/glade/gbp-glade-properties.c
@@ -1,6 +1,6 @@
 /* gbp-glade-properties.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -18,10 +18,10 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-glade-properties"
 
+#include "config.h"
+
 #include <glib/gi18n.h>
 
 #include "gbp-glade-properties.h"
@@ -51,7 +51,7 @@ gbp_glade_properties_class_init (GbpGladePropertiesClass *klass)
 {
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/glade-plugin/gbp-glade-properties.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/glade/gbp-glade-properties.ui");
   gtk_widget_class_set_css_name (widget_class, "gbpgladeproperties");
   gtk_widget_class_bind_template_child (widget_class, GbpGladeProperties, stack);
   gtk_widget_class_bind_template_child (widget_class, GbpGladeProperties, stack_switcher);
diff --git a/src/plugins/glade/gbp-glade-properties.h b/src/plugins/glade/gbp-glade-properties.h
index 352b150b2..aff86e58d 100644
--- a/src/plugins/glade/gbp-glade-properties.h
+++ b/src/plugins/glade/gbp-glade-properties.h
@@ -1,6 +1,6 @@
 /* gbp-glade-properties.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -21,7 +21,7 @@
 #pragma once
 
 #include <gladeui/glade.h>
-#include <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/glade/gbp-glade-workbench-addin.c b/src/plugins/glade/gbp-glade-workbench-addin.c
index 286290a6e..0d499e837 100644
--- a/src/plugins/glade/gbp-glade-workbench-addin.c
+++ b/src/plugins/glade/gbp-glade-workbench-addin.c
@@ -1,6 +1,6 @@
 /* gbp-glade-workbench-addin.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -18,44 +18,76 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
+#define G_LOG_DOMAIN "gbp-glade-workbench-addin"
+
 #include "config.h"
 
-#define G_LOG_DOMAIN "gbp-glade-workbench-addin"
+#include <libide-gui.h>
 
-#include "gbp-glade-view.h"
+#include "gbp-glade-page.h"
 #include "gbp-glade-workbench-addin.h"
 
 struct _GbpGladeWorkbenchAddin
 {
-  GObject       parent_instance;
-  IdeWorkbench *workbench;
-  GHashTable   *catalog_paths;
+  GObject          parent_instance;
+  IdeWorkbench    *workbench;
+  IdeBuildManager *build_manager;
+  GHashTable      *catalog_paths;
 };
 
 typedef struct
 {
   GFile        *file;
-  GbpGladeView *view;
-} LocateView;
+  GbpGladePage *view;
+} LocatePage;
 
-static gchar *
-gbp_glade_workbench_addin_get_id (IdeWorkbenchAddin *addin)
+static void
+find_most_recent_editor_cb (GtkWidget *widget,
+                            gpointer   user_data)
 {
-  return g_strdup ("glade");
+  IdeSurface **surface = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKSPACE (widget));
+
+  if (*surface == NULL)
+    *surface = ide_workspace_get_surface_by_name (IDE_WORKSPACE (widget), "editor");
+}
+
+static IdeSurface *
+find_most_recent_editor (GbpGladeWorkbenchAddin *self)
+{
+  IdeSurface *surface = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_WORKBENCH (self->workbench));
+
+  ide_workbench_foreach_workspace (self->workbench,
+                                   find_most_recent_editor_cb,
+                                   &surface);
+
+  return surface;
 }
 
 static gboolean
 gbp_glade_workbench_addin_can_open (IdeWorkbenchAddin *addin,
-                                    IdeUri            *uri,
+                                    GFile             *file,
                                     const gchar       *content_type,
                                     gint              *priority)
 {
+  GbpGladeWorkbenchAddin *self = (GbpGladeWorkbenchAddin *)addin;
   const gchar *path;
 
-  g_assert (GBP_IS_GLADE_WORKBENCH_ADDIN (addin));
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_FILE (file));
+  g_assert (GBP_IS_GLADE_WORKBENCH_ADDIN (self));
   g_assert (priority != NULL);
 
-  path = ide_uri_get_path (uri);
+  /* Ignore all open requests unless we have a surface */
+  if (!find_most_recent_editor (self))
+    return FALSE;
+
+  path = g_file_peek_path (file);
 
   if (g_strcmp0 (content_type, "application/x-gtk-builder") == 0 ||
       g_strcmp0 (content_type, "application/x-designer") == 0 ||
@@ -73,23 +105,23 @@ gbp_glade_workbench_addin_open_cb (GObject      *object,
                                    GAsyncResult *result,
                                    gpointer      user_data)
 {
-  GbpGladeView *view = (GbpGladeView *)object;
+  GbpGladePage *view = (GbpGladePage *)object;
   g_autoptr(GError) error = NULL;
   g_autoptr(IdeTask) task = user_data;
   GladeProject *project;
   GList *toplevels;
 
-  g_assert (GBP_IS_GLADE_VIEW (view));
+  g_assert (GBP_IS_GLADE_PAGE (view));
   g_assert (G_IS_ASYNC_RESULT (result));
   g_assert (IDE_IS_TASK (task));
 
-  if (!gbp_glade_view_load_file_finish (view, result, &error))
+  if (!gbp_glade_page_load_file_finish (view, result, &error))
     {
       ide_task_return_error (task, g_steal_pointer (&error));
       return;
     }
 
-  project = gbp_glade_view_get_project (view);
+  project = gbp_glade_page_get_project (view);
   toplevels = glade_project_toplevels (project);
 
   /* Select the first toplevel so that we don't start with a non-existant
@@ -102,74 +134,81 @@ gbp_glade_workbench_addin_open_cb (GObject      *object,
 }
 
 static void
-locate_view (GtkWidget *view,
+locate_page (GtkWidget *view,
              gpointer   user_data)
 {
-  LocateView *locate = user_data;
+  LocatePage *locate = user_data;
   GFile *file;
 
-  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_PAGE (view));
   g_assert (locate != NULL);
 
   if (locate->view != NULL)
     return;
 
-  if (!GBP_IS_GLADE_VIEW (view))
+  if (!GBP_IS_GLADE_PAGE (view))
     return;
 
-  file = gbp_glade_view_get_file (GBP_GLADE_VIEW (view));
+  file = gbp_glade_page_get_file (GBP_GLADE_PAGE (view));
   if (g_file_equal (file, locate->file))
-    locate->view = GBP_GLADE_VIEW (view);
+    locate->view = GBP_GLADE_PAGE (view);
 }
 
 static void
-gbp_glade_workbench_addin_open_async (IdeWorkbenchAddin     *addin,
-                                      IdeUri                *uri,
-                                      const gchar           *content_type,
-                                      IdeWorkbenchOpenFlags  flags,
-                                      GCancellable          *cancellable,
-                                      GAsyncReadyCallback    callback,
-                                      gpointer               user_data)
+gbp_glade_workbench_addin_open_async (IdeWorkbenchAddin   *addin,
+                                      GFile               *file,
+                                      const gchar         *content_type,
+                                      IdeBufferOpenFlags   flags,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
 {
   GbpGladeWorkbenchAddin *self = (GbpGladeWorkbenchAddin *)addin;
   g_autoptr(IdeTask) task = NULL;
-  g_autoptr(GFile) file = NULL;
-  IdePerspective *editor;
-  GbpGladeView *view;
-  LocateView locate = { 0 };
+  GbpGladePage *view;
+  IdeSurface *editor;
+  LocatePage locate = { 0 };
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (GBP_IS_GLADE_WORKBENCH_ADDIN (self));
   g_assert (IDE_IS_WORKBENCH (self->workbench));
-  g_assert (uri != NULL);
+  g_assert (G_IS_FILE (file));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, gbp_glade_workbench_addin_open_async);
 
-  editor = ide_workbench_get_perspective_by_name (self->workbench, "editor");
-  file = ide_uri_to_file (uri);
+  if (!(editor = find_most_recent_editor (self)))
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_NOT_SUPPORTED,
+                                 "Cannot open, not in project mode");
+      return;
+    }
 
   /* First try to find an existing view for the file */
   locate.file = file;
-  ide_workbench_views_foreach (self->workbench, locate_view, &locate);
+  ide_workbench_foreach_page (self->workbench, locate_page, &locate);
   if (locate.view != NULL)
     {
-      ide_workbench_focus (self->workbench, GTK_WIDGET (locate.view));
+      ide_widget_reveal_and_grab (GTK_WIDGET (locate.view));
       ide_task_return_boolean (task, TRUE);
       return;
     }
 
-  view = gbp_glade_view_new ();
+  view = gbp_glade_page_new ();
   gtk_container_add (GTK_CONTAINER (editor), GTK_WIDGET (view));
   gtk_widget_show (GTK_WIDGET (view));
 
-  gbp_glade_view_load_file_async (view,
+  gbp_glade_page_load_file_async (view,
                                   file,
                                   cancellable,
                                   gbp_glade_workbench_addin_open_cb,
                                   g_steal_pointer (&task));
 
-  ide_workbench_focus (self->workbench, GTK_WIDGET (view));
+  ide_widget_reveal_and_grab (GTK_WIDGET (view));
 }
 
 static gboolean
@@ -257,14 +296,26 @@ gbp_glade_workbench_addin_load (IdeWorkbenchAddin *addin,
                                 IdeWorkbench      *workbench)
 {
   GbpGladeWorkbenchAddin *self = (GbpGladeWorkbenchAddin *)addin;
-  IdeBuildManager *build_manager;
-  IdeContext *context;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (GBP_IS_GLADE_WORKBENCH_ADDIN (self));
   g_assert (IDE_IS_WORKBENCH (workbench));
 
   self->workbench = workbench;
   self->catalog_paths = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+}
+
+static void
+gbp_glade_workbench_addin_project_loaded (IdeWorkbenchAddin *addin,
+                                          IdeProjectInfo    *project_info)
+{
+  GbpGladeWorkbenchAddin *self = (GbpGladeWorkbenchAddin *)addin;
+  IdeBuildManager *build_manager;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GLADE_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
 
   /*
    * We want to watch the build pipeline for changes to the current
@@ -278,10 +329,11 @@ gbp_glade_workbench_addin_load (IdeWorkbenchAddin *addin,
    * the runtime is a foreign mount.
    */
 
-  context = ide_workbench_get_context (workbench);
-  build_manager = ide_context_get_build_manager (context);
+  context = ide_workbench_get_context (self->workbench);
+  build_manager = ide_build_manager_from_context (context);
 
-  g_signal_connect_object (build_manager,
+  self->build_manager = g_object_ref (build_manager);
+  g_signal_connect_object (self->build_manager,
                            "notify::pipeline",
                            G_CALLBACK (on_build_pipeline_changed_cb),
                            self,
@@ -296,20 +348,19 @@ gbp_glade_workbench_addin_unload (IdeWorkbenchAddin *addin,
                                   IdeWorkbench      *workbench)
 {
   GbpGladeWorkbenchAddin *self = (GbpGladeWorkbenchAddin *)addin;
-  IdeBuildManager *build_manager;
-  IdeContext *context;
   const gchar *path;
   GHashTableIter iter;
 
   g_assert (GBP_IS_GLADE_WORKBENCH_ADDIN (self));
   g_assert (IDE_IS_WORKBENCH (workbench));
 
-  context = ide_workbench_get_context (workbench);
-  build_manager = ide_context_get_build_manager (context);
-
-  g_signal_handlers_disconnect_by_func (build_manager,
-                                        G_CALLBACK (on_build_pipeline_changed_cb),
-                                        self);
+  if (self->build_manager != NULL)
+    {
+      g_signal_handlers_disconnect_by_func (self->build_manager,
+                                            G_CALLBACK (on_build_pipeline_changed_cb),
+                                            self);
+      g_clear_object (&self->build_manager);
+    }
 
   g_hash_table_iter_init (&iter, self->catalog_paths);
   while (g_hash_table_iter_next (&iter, (gpointer *)&path, NULL))
@@ -327,7 +378,7 @@ gbp_glade_workbench_addin_unload (IdeWorkbenchAddin *addin,
 static void
 workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
 {
-  iface->get_id = gbp_glade_workbench_addin_get_id;
+  iface->project_loaded = gbp_glade_workbench_addin_project_loaded;
   iface->load = gbp_glade_workbench_addin_load;
   iface->unload = gbp_glade_workbench_addin_unload;
   iface->can_open = gbp_glade_workbench_addin_can_open;
diff --git a/src/plugins/glade/gbp-glade-workbench-addin.h b/src/plugins/glade/gbp-glade-workbench-addin.h
index 1310d7178..b2a9be9c1 100644
--- a/src/plugins/glade/gbp-glade-workbench-addin.h
+++ b/src/plugins/glade/gbp-glade-workbench-addin.h
@@ -1,6 +1,6 @@
 /* gbp-glade-workbench-addin.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -20,7 +20,7 @@
 
 #pragma once
 
-#include <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/glade/glade-plugin.c b/src/plugins/glade/glade-plugin.c
new file mode 100644
index 000000000..da384cc96
--- /dev/null
+++ b/src/plugins/glade/glade-plugin.c
@@ -0,0 +1,48 @@
+/* glade-plugin.c
+ *
+ * Copyright 2018-2019 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-glade-plugin"
+
+#include "config.h"
+
+#include <gladeui/glade.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <libpeas/peas.h>
+
+#include "gbp-glade-editor-addin.h"
+#include "gbp-glade-frame-addin.h"
+#include "gbp-glade-workbench-addin.h"
+
+_IDE_EXTERN void
+_gbp_glade_register_types (PeasObjectModule *module)
+{
+  glade_init ();
+
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_ADDIN,
+                                              GBP_TYPE_GLADE_EDITOR_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_FRAME_ADDIN,
+                                              GBP_TYPE_GLADE_FRAME_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GBP_TYPE_GLADE_WORKBENCH_ADDIN);
+}
diff --git a/src/plugins/glade/glade.gresource.xml b/src/plugins/glade/glade.gresource.xml
index fc9b5e994..50caa9678 100644
--- a/src/plugins/glade/glade.gresource.xml
+++ b/src/plugins/glade/glade.gresource.xml
@@ -1,13 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/glade">
     <file>glade.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/glade-plugin">
-    <file>gtk/menus.ui</file>
-
-    <file>gbp-glade-properties.ui</file>
-
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+    <file preprocess="xml-stripblanks">gbp-glade-properties.ui</file>
     <file>themes/shared.css</file>
     <file>themes/Adwaita.css</file>
     <file>themes/Adwaita-dark.css</file>
diff --git a/src/plugins/glade/glade.plugin b/src/plugins/glade/glade.plugin
index d818163e1..05e5074bd 100644
--- a/src/plugins/glade/glade.plugin
+++ b/src/plugins/glade/glade.plugin
@@ -1,9 +1,9 @@
 [Plugin]
-Module=glade-plugin
-Name=Glade
-Description=Integration with Glade UI designer for Gtk
 Authors=Christian Hergert <christian hergert me>
+Builtin=true
 Copyright=Copyright © 2018 Christian Hergert
 Depends=editor;
-Builtin=true
-Embedded=gbp_glade_register_types
+Description=Integration with Glade UI designer for Gtk
+Embedded=_gbp_glade_register_types
+Module=glade
+Name=Glade
diff --git a/src/plugins/glade/meson.build b/src/plugins/glade/meson.build
index 7b26ce989..c57630612 100644
--- a/src/plugins/glade/meson.build
+++ b/src/plugins/glade/meson.build
@@ -1,27 +1,26 @@
-if get_option('with_glade')
+if get_option('plugin_glade')
 
-glade_resources = gnome.compile_resources(
+plugins_sources += files([
+  'gbp-glade-editor-addin.c',
+  'gbp-glade-frame-addin.c',
+  'gbp-glade-properties.c',
+  'gbp-glade-page.c',
+  'gbp-glade-page-actions.c',
+  'gbp-glade-page-shortcuts.c',
+  'glade-plugin.c',
+  'gbp-glade-workbench-addin.c',
+])
+
+plugin_glade_resources = gnome.compile_resources(
   'glade-resources',
   'glade.gresource.xml',
   c_name: 'gbp_glade',
 )
 
-glade_sources = [
-  'gbp-glade-editor-addin.c',
-  'gbp-glade-layout-stack-addin.c',
-  'gbp-glade-plugin.c',
-  'gbp-glade-properties.c',
-  'gbp-glade-view.c',
-  'gbp-glade-view-actions.c',
-  'gbp-glade-view-shortcuts.c',
-  'gbp-glade-workbench-addin.c',
-]
-
-gnome_builder_plugins_deps += [
+plugins_deps += [
   dependency('gladeui-2.0', version: '>=3.22.0'),
 ]
 
-gnome_builder_plugins_sources += files(glade_sources)
-gnome_builder_plugins_sources += glade_resources[0]
+plugins_sources += plugin_glade_resources[0]
 
 endif
diff --git a/src/plugins/glade/themes/Adwaita-dark.css b/src/plugins/glade/themes/Adwaita-dark.css
index 87f288f85..434ad6dd5 100644
--- a/src/plugins/glade/themes/Adwaita-dark.css
+++ b/src/plugins/glade/themes/Adwaita-dark.css
@@ -1,12 +1,12 @@
-@import url("resource:///org/gnome/builder/plugins/glade-plugin/themes/Adwaita-shared.css");
+@import url("resource:///plugins/glade/themes/Adwaita-shared.css");
 
 /* Draw our grid over the glade background pattern */
 gbpgladeview viewport {
-  background-color: #1c1f20;
+  background-color: #201f21;
   background-size: 8px 8px;
-  background-image: repeating-linear-gradient(0deg, #212527, #212527 1px, transparent 1px, transparent 8px),
-                    repeating-linear-gradient(-90deg, #212527, #212527 1px, transparent 1px, transparent 
8px);
+  background-image: repeating-linear-gradient(0deg, #232224, #232224 1px, transparent 1px, transparent 8px),
+                    repeating-linear-gradient(-90deg, #232224, #232224 1px, transparent 1px, transparent 
8px);
 }
 .glade-chooser {
-  background-color: #1c1f20;
+  background-color: #201f21;
 }
diff --git a/src/plugins/glade/themes/Adwaita-shared.css b/src/plugins/glade/themes/Adwaita-shared.css
index 0b07468ee..2b73fb3bb 100644
--- a/src/plugins/glade/themes/Adwaita-shared.css
+++ b/src/plugins/glade/themes/Adwaita-shared.css
@@ -1,4 +1,4 @@
-@import url("resource:///org/gnome/builder/plugins/glade-plugin/themes/shared.css");
+@import url("resource:///plugins/glade/themes/shared.css");
 
 gbpgladeproperties {
   font-size: 0.83333em;
@@ -27,8 +27,8 @@ gbpgladeproperties spinbutton entry {
 }
 
 gbpgladeproperties switch slider {
-  min-height: 18px;
-  min-width: 32px;
+  min-height: 16px;
+  min-width: 18px;
 }
 
 gbpgladeproperties button.combo {
diff --git a/src/plugins/glade/themes/Adwaita.css b/src/plugins/glade/themes/Adwaita.css
index c34e9a5df..eed31ba86 100644
--- a/src/plugins/glade/themes/Adwaita.css
+++ b/src/plugins/glade/themes/Adwaita.css
@@ -1,4 +1,4 @@
-@import url("resource:///org/gnome/builder/plugins/glade-plugin/themes/Adwaita-shared.css");
+@import url("resource:///plugins/glade/themes/Adwaita-shared.css");
 
 /* Draw our grid over the glade background pattern */
 gbpgladeview viewport {
diff --git a/src/plugins/gnome-code-assistance/gca-diagnostics.c 
b/src/plugins/gnome-code-assistance/gca-diagnostics.c
index c398b20fb..37f11ce84 100644
--- a/src/plugins/gnome-code-assistance/gca-diagnostics.c
+++ b/src/plugins/gnome-code-assistance/gca-diagnostics.c
@@ -2,6 +2,8 @@
  * Generated by gdbus-codegen 2.42.0. DO NOT EDIT.
  *
  * The license of this code is the same as for the source it was derived from.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #ifdef HAVE_CONFIG_H
diff --git a/src/plugins/gnome-code-assistance/gca-diagnostics.h 
b/src/plugins/gnome-code-assistance/gca-diagnostics.h
index 23af79e5b..3528d188c 100644
--- a/src/plugins/gnome-code-assistance/gca-diagnostics.h
+++ b/src/plugins/gnome-code-assistance/gca-diagnostics.h
@@ -2,6 +2,8 @@
  * Generated by gdbus-codegen 2.42.0. DO NOT EDIT.
  *
  * The license of this code is the same as for the source it was derived from.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #ifndef __GCA_DIAGNOSTICS_H__
diff --git a/src/plugins/gnome-code-assistance/gca-plugin.c b/src/plugins/gnome-code-assistance/gca-plugin.c
index bd527df0f..6e883b760 100644
--- a/src/plugins/gnome-code-assistance/gca-plugin.c
+++ b/src/plugins/gnome-code-assistance/gca-plugin.c
@@ -1,6 +1,6 @@
 /* gca-plugin.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,26 +14,23 @@
  *
  * 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
  */
 
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-code.h>
+#include <libide-gui.h>
 
 #include "ide-gca-diagnostic-provider.h"
 #include "ide-gca-preferences-addin.h"
-#include "ide-gca-service.h"
 
-void
-ide_gca_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_ide_gca_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_SERVICE,
-                                              IDE_TYPE_GCA_SERVICE);
-
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_DIAGNOSTIC_PROVIDER,
                                               IDE_TYPE_GCA_DIAGNOSTIC_PROVIDER);
-
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_PREFERENCES_ADDIN,
                                               IDE_TYPE_GCA_PREFERENCES_ADDIN);
diff --git a/src/plugins/gnome-code-assistance/gca-service.c b/src/plugins/gnome-code-assistance/gca-service.c
index a3bbc665f..da06a1c7f 100644
--- a/src/plugins/gnome-code-assistance/gca-service.c
+++ b/src/plugins/gnome-code-assistance/gca-service.c
@@ -2,6 +2,8 @@
  * Generated by gdbus-codegen 2.42.0. DO NOT EDIT.
  *
  * The license of this code is the same as for the source it was derived from.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #ifdef HAVE_CONFIG_H
diff --git a/src/plugins/gnome-code-assistance/gca-service.h b/src/plugins/gnome-code-assistance/gca-service.h
index a9717b785..437d21150 100644
--- a/src/plugins/gnome-code-assistance/gca-service.h
+++ b/src/plugins/gnome-code-assistance/gca-service.h
@@ -2,6 +2,8 @@
  * Generated by gdbus-codegen 2.42.0. DO NOT EDIT.
  *
  * The license of this code is the same as for the source it was derived from.
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #ifndef __GCA_SERVICE_H__
diff --git a/src/plugins/gnome-code-assistance/gca-structs.c b/src/plugins/gnome-code-assistance/gca-structs.c
index c88b2aabe..5eebc546f 100644
--- a/src/plugins/gnome-code-assistance/gca-structs.c
+++ b/src/plugins/gnome-code-assistance/gca-structs.c
@@ -1,6 +1,6 @@
 /* gca-structs.c
  *
- * Copyright 2014 Christian Hergert <christian hergert me>
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "gca-structs.h"
diff --git a/src/plugins/gnome-code-assistance/gca-structs.h b/src/plugins/gnome-code-assistance/gca-structs.h
index 3c88ca9f5..87e18209b 100644
--- a/src/plugins/gnome-code-assistance/gca-structs.h
+++ b/src/plugins/gnome-code-assistance/gca-structs.h
@@ -1,6 +1,6 @@
 /* gca-structs.h
  *
- * Copyright 2014 Christian Hergert <christian hergert me>
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/gnome-code-assistance/gnome-code-assistance.gresource.xml 
b/src/plugins/gnome-code-assistance/gnome-code-assistance.gresource.xml
index bb5165856..5d180eaf2 100644
--- a/src/plugins/gnome-code-assistance/gnome-code-assistance.gresource.xml
+++ b/src/plugins/gnome-code-assistance/gnome-code-assistance.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/gnome-code-assistance">
     <file>gnome-code-assistance.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/gnome-code-assistance/gnome-code-assistance.plugin 
b/src/plugins/gnome-code-assistance/gnome-code-assistance.plugin
index cff1ab921..326b8b55c 100644
--- a/src/plugins/gnome-code-assistance/gnome-code-assistance.plugin
+++ b/src/plugins/gnome-code-assistance/gnome-code-assistance.plugin
@@ -1,9 +1,9 @@
 [Plugin]
-Module=gnome-code-assistance-plugin
-Name=GNOME Code Assistance
-Description=Provides integration with gnome-code-assistance
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
+Copyright=Copyright © 2015 Christian Hergert
+Description=Provides integration with gnome-code-assistance
+Embedded=_ide_gca_register_types
+Module=gnome-code-assistance
+Name=GNOME Code Assistance
 X-Diagnostic-Provider-Languages=css,html,js,json,python,python3,ruby,scss,sh,xml,yaml
-Embedded=ide_gca_register_types
diff --git a/src/plugins/gnome-code-assistance/ide-gca-diagnostic-provider.c 
b/src/plugins/gnome-code-assistance/ide-gca-diagnostic-provider.c
index c49e1dde2..cbfc6cf19 100644
--- a/src/plugins/gnome-code-assistance/ide-gca-diagnostic-provider.c
+++ b/src/plugins/gnome-code-assistance/ide-gca-diagnostic-provider.c
@@ -1,6 +1,6 @@
 /* ide-gca-diagnostic-provider.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-gca-diagnostic-provider"
@@ -36,7 +38,7 @@ typedef struct
 {
   IdeTask        *task; /* Integrity check backpointer */
   IdeUnsavedFile *unsaved_file;
-  IdeFile        *file;
+  GFile          *file;
   gchar          *language_id;
 } DiagnoseState;
 
@@ -105,7 +107,7 @@ variant_to_diagnostics (DiagnoseState *state,
 
   g_assert (variant);
 
-  ar = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_diagnostic_unref);
+  ar = g_ptr_array_new_with_free_func (g_object_unref);
 
   g_variant_iter_init (&iter, variant);
 
@@ -143,10 +145,9 @@ variant_to_diagnostics (DiagnoseState *state,
 
       while (g_variant_iter_next (c, "(x(xx)(xx))", &x1, &x2, &x3, &x4, &x5))
         {
-          IdeSourceRange *range;
-          IdeSourceLocation *begin;
-          IdeSourceLocation *end;
-          IdeFile *file = NULL;
+          g_autoptr(IdeRange) range = NULL;
+          g_autoptr(IdeLocation) begin = NULL;
+          g_autoptr(IdeLocation) end = NULL;
 
           /*
            * FIXME:
@@ -154,22 +155,18 @@ variant_to_diagnostics (DiagnoseState *state,
            * Not always true, but we can cheat for now and claim it is within
            * the file we just parsed.
            */
-          file = state->file;
-
-          begin = ide_source_location_new (file, x2 - 1, x3 - 1, 0);
-          end = ide_source_location_new (file, x4 - 1, x5 - 1, 0);
 
-          range = ide_source_range_new (begin, end);
-          ide_diagnostic_take_range (diag, range);
+          begin = ide_location_new (state->file, x2 - 1, x3 - 1);
+          end = ide_location_new (state->file, x4 - 1, x5 - 1);
 
-          ide_source_location_unref (begin);
-          ide_source_location_unref (end);
+          range = ide_range_new (begin, end);
+          ide_diagnostic_take_range (diag, g_steal_pointer (&range));
         }
 
       g_ptr_array_add (ar, g_steal_pointer (&diag));
     }
 
-  return ide_diagnostics_new (IDE_PTR_ARRAY_STEAL_FULL (&ar));
+  return ide_diagnostics_new_from_array (ar);
 }
 
 static void
@@ -201,8 +198,7 @@ diagnostics_cb (GObject      *object,
 
   diagnostics = variant_to_diagnostics (state, var);
 
-  ide_task_return_pointer (task, diagnostics,
-                           (GDestroyNotify)ide_diagnostics_unref);
+  ide_task_return_pointer (task, diagnostics, g_object_unref);
 
   IDE_EXIT;
 }
@@ -272,8 +268,9 @@ parse_cb (GObject      *object,
     {
       if (g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN))
         {
-          ide_task_return_pointer (task, ide_diagnostics_new (NULL),
-                                   (GDestroyNotify)ide_diagnostics_unref);
+          ide_task_return_pointer (task,
+                                   ide_diagnostics_new (),
+                                   g_object_unref);
         }
       else
         {
@@ -345,7 +342,6 @@ get_proxy_cb (GObject      *object,
   const gchar *temp_path;
   GcaService *proxy;
   GVariant *cursor = NULL;
-  GFile *gfile;
 
   IDE_ENTRY;
 
@@ -363,8 +359,7 @@ get_proxy_cb (GObject      *object,
       IDE_GOTO (cleanup);
     }
 
-  gfile = ide_file_get_file (state->file);
-  temp_path = path = g_file_get_path (gfile);
+  temp_path = path = g_file_get_path (state->file);
 
   if (!path)
     {
@@ -409,8 +404,9 @@ cleanup:
 
 static void
 ide_gca_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
-                                            IdeFile               *file,
-                                            IdeBuffer             *buffer,
+                                            GFile                 *file,
+                                            GBytes                *contents,
+                                            const gchar           *language_id,
                                             GCancellable          *cancellable,
                                             GAsyncReadyCallback    callback,
                                             gpointer               user_data)
@@ -419,22 +415,15 @@ ide_gca_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
   g_autoptr(IdeTask) task = NULL;
   IdeGcaService *service;
   DiagnoseState *state;
-  GtkSourceLanguage *language;
-  IdeContext *context;
   IdeUnsavedFiles *files;
-  const gchar *language_id = NULL;
-  GFile *gfile;
+  IdeContext *context;
 
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_GCA_DIAGNOSTIC_PROVIDER (self));
 
   task = ide_task_new (self, cancellable, callback, user_data);
-
-  language = ide_file_get_language (file);
-
-  if (language != NULL)
-    language_id = gtk_source_language_get_id (language);
+  ide_task_set_source_tag (task, ide_gca_diagnostic_provider_diagnose_async);
 
   if (language_id == NULL)
     {
@@ -446,20 +435,22 @@ ide_gca_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
     }
 
   context = ide_object_get_context (IDE_OBJECT (provider));
-  service = ide_context_get_service_typed (context, IDE_TYPE_GCA_SERVICE);
-  files = ide_context_get_unsaved_files (context);
-  gfile = ide_file_get_file (file);
+  service = ide_gca_service_from_context (context);
+  files = ide_unsaved_files_from_context (context);
 
   state = g_slice_new0 (DiagnoseState);
   state->task = task;
   state->language_id = g_strdup (language_id);
   state->file = g_object_ref (file);
-  state->unsaved_file = ide_unsaved_files_get_unsaved_file (files, gfile);
+  state->unsaved_file = ide_unsaved_files_get_unsaved_file (files, file);
 
   ide_task_set_task_data (task, state, diagnose_state_free);
 
-  ide_gca_service_get_proxy_async (service, language_id, cancellable,
-                                   get_proxy_cb, g_object_ref (task));
+  ide_gca_service_get_proxy_async (service,
+                                   language_id,
+                                   cancellable,
+                                   get_proxy_cb,
+                                   g_steal_pointer (&task));
 
   IDE_EXIT;
 }
diff --git a/src/plugins/gnome-code-assistance/ide-gca-diagnostic-provider.h 
b/src/plugins/gnome-code-assistance/ide-gca-diagnostic-provider.h
index 961d19d5a..13f9b4082 100644
--- a/src/plugins/gnome-code-assistance/ide-gca-diagnostic-provider.h
+++ b/src/plugins/gnome-code-assistance/ide-gca-diagnostic-provider.h
@@ -1,6 +1,6 @@
 /* ide-gca-diagnostic-provider.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/gnome-code-assistance/ide-gca-preferences-addin.c 
b/src/plugins/gnome-code-assistance/ide-gca-preferences-addin.c
index eabba5296..58c97f3df 100644
--- a/src/plugins/gnome-code-assistance/ide-gca-preferences-addin.c
+++ b/src/plugins/gnome-code-assistance/ide-gca-preferences-addin.c
@@ -1,6 +1,6 @@
 /* ide-gca-preferences-addin.c
  *
- * Copyright 2016 Christian Hergert <christian hergert me>
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,10 +14,12 @@
  *
  * 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
  */
 
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-gui.h>
 
 #include "ide-gca-preferences-addin.h"
 
diff --git a/src/plugins/gnome-code-assistance/ide-gca-preferences-addin.h 
b/src/plugins/gnome-code-assistance/ide-gca-preferences-addin.h
index c7e50f08c..d0c6be432 100644
--- a/src/plugins/gnome-code-assistance/ide-gca-preferences-addin.h
+++ b/src/plugins/gnome-code-assistance/ide-gca-preferences-addin.h
@@ -1,6 +1,6 @@
 /* ide-gca-preferences-addin.h
  *
- * Copyright 2016 Christian Hergert <christian hergert me>
+ * Copyright 2016-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/gnome-code-assistance/ide-gca-service.c 
b/src/plugins/gnome-code-assistance/ide-gca-service.c
index 7f631e338..11f8574d5 100644
--- a/src/plugins/gnome-code-assistance/ide-gca-service.c
+++ b/src/plugins/gnome-code-assistance/ide-gca-service.c
@@ -1,6 +1,6 @@
 /* ide-gca-service.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,12 +14,15 @@
  *
  * 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 "ide-gca-service"
 
 #include <dazzle.h>
 #include <glib/gi18n.h>
+#include <libide-threading.h>
 
 #include "ide-gca-service.h"
 
@@ -33,8 +36,7 @@ struct _IdeGcaService
   gulong           bus_closed_handler;
 };
 
-G_DEFINE_TYPE_EXTENDED (IdeGcaService, ide_gca_service, IDE_TYPE_OBJECT, 0,
-                        G_IMPLEMENT_INTERFACE (IDE_TYPE_SERVICE, NULL))
+G_DEFINE_TYPE (IdeGcaService, ide_gca_service, IDE_TYPE_OBJECT)
 
 static void
 on_bus_closed (GDBusConnection *bus,
@@ -260,3 +262,14 @@ ide_gca_service_init (IdeGcaService *self)
   self->proxy_cache = g_hash_table_new_full (g_str_hash, g_str_equal,
                                              g_free, g_object_unref);
 }
+
+IdeGcaService *
+ide_gca_service_from_context (IdeContext *context)
+{
+  g_autoptr(IdeGcaService) self = NULL;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  self = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_GCA_SERVICE);
+  return ide_context_peek_child_typed (context, IDE_TYPE_GCA_SERVICE);
+}
diff --git a/src/plugins/gnome-code-assistance/ide-gca-service.h 
b/src/plugins/gnome-code-assistance/ide-gca-service.h
index 4fa743f31..7ce7ddab6 100644
--- a/src/plugins/gnome-code-assistance/ide-gca-service.h
+++ b/src/plugins/gnome-code-assistance/ide-gca-service.h
@@ -1,6 +1,6 @@
 /* ide-gca-service.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
 
 #include "gca-service.h"
 
@@ -28,13 +30,14 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeGcaService, ide_gca_service, IDE, GCA_SERVICE, IdeObject)
 
-void        ide_gca_service_get_proxy_async  (IdeGcaService        *self,
-                                              const gchar          *language_id,
-                                              GCancellable         *cancellable,
-                                              GAsyncReadyCallback   callback,
-                                              gpointer              user_data);
-GcaService *ide_gca_service_get_proxy_finish (IdeGcaService        *self,
-                                              GAsyncResult         *result,
-                                              GError              **error);
+IdeGcaService *ide_gca_service_from_context     (IdeContext           *context);
+void           ide_gca_service_get_proxy_async  (IdeGcaService        *self,
+                                                 const gchar          *language_id,
+                                                 GCancellable         *cancellable,
+                                                 GAsyncReadyCallback   callback,
+                                                 gpointer              user_data);
+GcaService    *ide_gca_service_get_proxy_finish (IdeGcaService        *self,
+                                                 GAsyncResult         *result,
+                                                 GError              **error);
 
 G_END_DECLS
diff --git a/src/plugins/gnome-code-assistance/meson.build b/src/plugins/gnome-code-assistance/meson.build
index fee9b7a1d..a7d86fa14 100644
--- a/src/plugins/gnome-code-assistance/meson.build
+++ b/src/plugins/gnome-code-assistance/meson.build
@@ -1,31 +1,23 @@
-if get_option('with_gnome_code_assistance')
+if get_option('plugin_gnome_code_assistance')
 
-gca_resources = gnome.compile_resources(
-  'gca-resources',
-  'gnome-code-assistance.gresource.xml',
-  c_name: 'ide_gca',
-)
+install_data('org.gnome.builder.gnome-code-assistance.gschema.xml', install_dir: schema_dir)
 
-gca_sources = [
+plugins_sources += files([
   'gca-diagnostics.c',
-  'gca-diagnostics.h',
   'gca-service.c',
-  'gca-service.h',
   'gca-structs.c',
-  'gca-structs.h',
   'gca-plugin.c',
   'ide-gca-diagnostic-provider.c',
-  'ide-gca-diagnostic-provider.h',
   'ide-gca-preferences-addin.c',
-  'ide-gca-preferences-addin.h',
   'ide-gca-service.c',
-  'ide-gca-service.h',
-]
+])
 
-gnome_builder_plugins_sources += files(gca_sources)
-gnome_builder_plugins_sources += gca_resources[0]
+plugin_gnome_code_assistance_resources = gnome.compile_resources(
+  'gnome-code-assistance-resources',
+  'gnome-code-assistance.gresource.xml',
+  c_name: 'gbp_gnome_code_assistance',
+)
 
-install_data('org.gnome.builder.gnome-code-assistance.gschema.xml',
-  install_dir: schema_dir)
+plugins_sources += plugin_gnome_code_assistance_resources[0]
 
 endif
diff --git a/src/plugins/go-langserv/go-langserv.plugin b/src/plugins/go-langserv/go-langserv.plugin
index e41ce2e7a..941174cde 100644
--- a/src/plugins/go-langserv/go-langserv.plugin
+++ b/src/plugins/go-langserv/go-langserv.plugin
@@ -1,8 +1,10 @@
 [Plugin]
-Module=go_langserver_plugin
+Builtin=true
+Copyright=Copyright © 2018 Henry Finucane
+Description=Provides LSP integration for Go
+Hidden=true
 Loader=python3
+Module=go_langserver_plugin
 Name=Go Language Server Plugin
-Description=Provides LSP integration for Go
-Copyright=Copyright © 2018 Henry Finucane
-Builtin=true
+X-Builder-ABI=@PACKAGE_ABI@
 X-Symbol-Resolver-Languages=go
diff --git a/src/plugins/go-langserv/go_langserver_plugin.py b/src/plugins/go-langserv/go_langserver_plugin.py
index ad31384e0..912b211fb 100644
--- a/src/plugins/go-langserv/go_langserver_plugin.py
+++ b/src/plugins/go-langserv/go_langserver_plugin.py
@@ -4,8 +4,6 @@ import os
 import json
 import gi
 
-gi.require_version('Ide', '1.0')
-
 from gi.repository import GLib
 from gi.repository import Gio
 from gi.repository import GObject
@@ -13,12 +11,16 @@ from gi.repository import Ide
 
 DEV_MODE = os.getenv('DEV_MODE') and True or False
 
-class GoService(Ide.Object, Ide.Service):
+class GoService(Ide.Object):
     _client = None
     _has_started = False
     _supervisor = None
 
-    @GObject.Property(type=Ide.LangservClient)
+    @classmethod
+    def from_context(klass, context):
+        return context.ensure_child_typed(GoService)
+
+    @GObject.Property(type=Ide.LspClient)
     def client(self):
         return self._client
 
@@ -49,7 +51,7 @@ class GoService(Ide.Object, Ide.Service):
             launcher.set_clear_env(False)
 
             # Locate the directory of the project and run go-langserver from there
-            workdir = self.get_context().get_vcs().get_working_directory()
+            workdir = self.get_context().ref_workdir()
             launcher.set_cwd(workdir.get_path())
 
             # Bash will load the host $PATH and $GOPATH (and optionally $GOROOT) for us.
@@ -78,8 +80,10 @@ class GoService(Ide.Object, Ide.Service):
 
         if self._client:
             self._client.stop()
+            self._client.destroy()
 
-        self._client = Ide.LangservClient.new(self.get_context(), io_stream)
+        self._client = Ide.LspClient.new(io_stream)
+        self.append(self._client)
         self._client.add_language('go')
         self._client.start()
         self.notify('client')
@@ -97,26 +101,26 @@ class GoService(Ide.Object, Ide.Service):
     @classmethod
     def bind_client(klass, provider):
         context = provider.get_context()
-        self = context.get_service_typed(GoService)
+        self = GoService.from_context(context)
         self._ensure_started()
         self.bind_property('client', provider, 'client', GObject.BindingFlags.SYNC_CREATE)
 
 # This is the only up-to-date looking list of supported things lsp things:
 # https://github.com/sourcegraph/go-langserver/blob/master/langserver/handler.go#L226
 
-class GoSymbolResolver(Ide.LangservSymbolResolver, Ide.SymbolResolver):
+class GoSymbolResolver(Ide.LspSymbolResolver, Ide.SymbolResolver):
     def do_load(self):
         GoService.bind_client(self)
 
 ## This is supported as of a few weeks ago, but at least for me, it seems
 ## awfully crashy, so I'm going to leave it disabled by default so as to
 ## not give a bad impression
-#class GoCompletionProvider(Ide.LangservCompletionProvider, Ide.CompletionProvider):
+#class GoCompletionProvider(Ide.LspCompletionProvider, Ide.CompletionProvider):
 #    def do_load(self, context):
 #        GoService.bind_client(self)
 
 ## Could not validate that this works, though `go-langserver` says it does.
 ## Calling out to `gofmt` is probably the more canonical route
-#class GoFormatter(Ide.LangservFormatter, Ide.Formatter):
+#class GoFormatter(Ide.LspFormatter, Ide.Formatter):
 #    def do_load(self):
 #        GoService.bind_client(self)
diff --git a/src/plugins/go-langserv/meson.build b/src/plugins/go-langserv/meson.build
index 33f28b3b7..2f8530fe6 100644
--- a/src/plugins/go-langserv/meson.build
+++ b/src/plugins/go-langserv/meson.build
@@ -1,11 +1,11 @@
-if get_option('with_go_langserv')
+if get_option('plugin_go_langserv')
 
 install_data('go_langserver_plugin.py', install_dir: plugindir)
 
 configure_file(
           input: 'go-langserv.plugin',
          output: 'go-langserv.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/gradle/gradle.plugin b/src/plugins/gradle/gradle.plugin
index 2bff79c6a..17ae6807d 100644
--- a/src/plugins/gradle/gradle.plugin
+++ b/src/plugins/gradle/gradle.plugin
@@ -1,10 +1,12 @@
 [Plugin]
-Module=gradle_plugin
-Name=Gradle
-Loader=python3
-Description=Provides integration with the Gradle build tool
 Authors=Alberto Fanjul Alonso <albfan gnome org>
-Copyright=Copyright © 2018 Alberto Fanjul Alonso
 Builtin=true
-X-Project-File-Filter-Pattern=build.gradle
+Copyright=Copyright © 2018 Alberto Fanjul Alonso
+Description=Provides integration with the Gradle build tool
+Hidden=true
+Loader=python3
+Module=gradle_plugin
+Name=Gradle
 X-Project-File-Filter-Name=Gradle (build.gradle)
+X-Project-File-Filter-Pattern=build.gradle
+X-Builder-ABI=@PACKAGE_ABI@
diff --git a/src/plugins/gradle/gradle_plugin.py b/src/plugins/gradle/gradle_plugin.py
index 918bcb7fe..9bf1ada92 100755
--- a/src/plugins/gradle/gradle_plugin.py
+++ b/src/plugins/gradle/gradle_plugin.py
@@ -20,12 +20,9 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-import gi
 import threading
 import os
 
-gi.require_version('Ide', '1.0')
-
 from gi.repository import Gio
 from gi.repository import GLib
 from gi.repository import GObject
@@ -39,6 +36,13 @@ _ATTRIBUTES = ",".join([
     Gio.FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON,
 ])
 
+class GradleBuildSystemDiscovery(Ide.SimpleBuildSystemDiscovery):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.props.glob = 'build.gradle'
+        self.props.hint = 'gradle_plugin'
+        self.props.priority = 2000
+
 class GradleBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
     project_file = GObject.Property(type=Gio.File)
 
@@ -48,32 +52,8 @@ class GradleBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
     def do_get_display_name(self):
         return 'Gradle'
 
-    def do_init_async(self, io_priority, cancellable, callback, data):
-        task = Gio.Task.new(self, cancellable, callback)
-
-        try:
-            # Maybe this is a gradlew
-            if self.props.project_file.get_basename() in ('build.gradle',):
-                task.return_boolean(True)
-                return
-
-            # Maybe this is a directory with a gradlew
-            if self.props.project_file.query_file_type(0) == Gio.FileType.DIRECTORY:
-                child = self.props.project_file.get_child('build.gradle')
-                if child.query_exists(None):
-                    self.props.project_file = child
-                    task.return_boolean(True)
-                    return
-        except Exception as ex:
-            task.return_error(ex)
-
-        task.return_error(Ide.NotSupportedError())
-
-    def do_init_finish(self, task):
-        return task.propagate_boolean()
-
     def do_get_priority(self):
-        return 500
+        return 2000
 
 class GradlePipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
     """
@@ -83,7 +63,7 @@ class GradlePipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
 
     def do_load(self, pipeline):
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != GradleBuildSystem:
             return
@@ -153,7 +133,7 @@ class GradleBuildTargetProvider(Ide.Object, Ide.BuildTargetProvider):
         task.set_priority(GLib.PRIORITY_LOW)
 
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != GradleBuildSystem:
             task.return_error(GLib.Error('Not gradle build system',
@@ -175,7 +155,7 @@ class GradleIdeTestProvider(Ide.TestProvider):
         task.set_priority(GLib.PRIORITY_LOW)
 
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != GradleBuildSystem:
             task.return_error(GLib.Error('Not gradle build system',
@@ -223,13 +203,13 @@ class GradleIdeTestProvider(Ide.TestProvider):
     def do_reload(self):
 
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != GradleBuildSystem:
             return
 
         # find all files in test directory
-        build_manager = context.get_build_manager()
+        build_manager = Ide.BuildManager.from_context(context)
         pipeline = build_manager.get_pipeline()
         srcdir = pipeline.get_srcdir()
         test_suite = Gio.File.new_for_path(os.path.join(srcdir, 'src/test/java'))
@@ -256,8 +236,9 @@ class GradleIdeTestProvider(Ide.TestProvider):
                                                     self.on_enumerator_loaded,
                                                     None)
                 else:
-                    #TODO Ask java through introspection for classes with TestCase and its public void 
methods 
-                    # or Annotation @Test methods
+                    # TODO: Ask java through introspection for classes with
+                    # TestCase and its public void methods or Annotation @Test
+                    # methods
                     result, contents, etag = gfile.load_contents()
                     tests = [x for x in str(contents).split('\\n') if 'public void' in x]
                     tests = [v.replace("()", "").replace("public void","").strip() for v in tests]
diff --git a/src/plugins/gradle/meson.build b/src/plugins/gradle/meson.build
index a9c418adc..3566abb01 100644
--- a/src/plugins/gradle/meson.build
+++ b/src/plugins/gradle/meson.build
@@ -1,11 +1,11 @@
-if get_option('with_gradle')
+if get_option('plugin_gradle')
 
 install_data('gradle_plugin.py', install_dir: plugindir)
 
 configure_file(
           input: 'gradle.plugin',
          output: 'gradle.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/greeter/gbp-greeter-application-addin.c 
b/src/plugins/greeter/gbp-greeter-application-addin.c
new file mode 100644
index 000000000..7fd0a2d93
--- /dev/null
+++ b/src/plugins/greeter/gbp-greeter-application-addin.c
@@ -0,0 +1,229 @@
+/* gbp-greeter-application-addin.c
+ *
+ * Copyright 2018-2019 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-greeter-application-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-greeter.h>
+#include <libide-gui.h>
+
+#include "gbp-greeter-application-addin.h"
+
+struct _GbpGreeterApplicationAddin
+{
+  GObject         parent_instance;
+  IdeApplication *application;
+};
+
+static void
+present_greeter_with_surface (GSimpleAction *action,
+                              GVariant      *param,
+                              gpointer       user_data)
+{
+  GbpGreeterApplicationAddin *self = user_data;
+  g_autoptr(IdeWorkbench) workbench = NULL;
+  IdeGreeterWorkspace *workspace;
+  const gchar *name;
+
+  g_assert (!action || G_IS_SIMPLE_ACTION (action));
+  g_assert (!param || g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+  g_assert (GBP_IS_GREETER_APPLICATION_ADDIN (self));
+  g_assert (IDE_IS_APPLICATION (self->application));
+
+  workbench = ide_workbench_new ();
+  ide_application_add_workbench (self->application, workbench);
+
+  workspace = ide_greeter_workspace_new (self->application);
+  ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+
+  if (param != NULL && (name = g_variant_get_string (param, NULL)))
+    ide_workspace_set_visible_surface_name (IDE_WORKSPACE (workspace), name);
+
+  ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+}
+
+static void
+open_project (GSimpleAction *action,
+              GVariant      *param,
+              gpointer       user_data)
+{
+  GbpGreeterApplicationAddin *self = user_data;
+  g_autoptr(IdeWorkbench) workbench = NULL;
+  IdeGreeterWorkspace *workspace;
+
+  g_assert (!action || G_IS_SIMPLE_ACTION (action));
+  g_assert (!param || g_variant_is_of_type (param, G_VARIANT_TYPE_STRING));
+  g_assert (GBP_IS_GREETER_APPLICATION_ADDIN (self));
+  g_assert (IDE_IS_APPLICATION (self->application));
+
+  workbench = ide_workbench_new ();
+  ide_application_add_workbench (self->application, workbench);
+
+  workspace = ide_greeter_workspace_new (self->application);
+  ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+
+  ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+
+  dzl_gtk_widget_action (GTK_WIDGET (workspace), "win", "open", NULL);
+}
+
+static const GActionEntry actions[] = {
+  { "present-greeter-with-surface", present_greeter_with_surface, "s" },
+  { "open-project", open_project },
+};
+
+static void
+gbp_greeter_application_addin_add_option_entries (IdeApplicationAddin *addin,
+                                                  IdeApplication      *app)
+{
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (G_IS_APPLICATION (app));
+
+  g_application_add_main_option (G_APPLICATION (app),
+                                 "greeter",
+                                 'g',
+                                 G_OPTION_FLAG_IN_MAIN,
+                                 G_OPTION_ARG_NONE,
+                                 _("Display a new greeter window"),
+                                 NULL);
+
+  g_application_add_main_option (G_APPLICATION (app),
+                                 "clone",
+                                 0,
+                                 G_OPTION_FLAG_IN_MAIN,
+                                 G_OPTION_ARG_STRING,
+                                 _("Begin cloning project from URI"),
+                                 "URI");
+}
+
+static void
+gbp_greeter_application_addin_handle_command_line (IdeApplicationAddin     *addin,
+                                                   IdeApplication          *application,
+                                                   GApplicationCommandLine *cmdline)
+{
+  GbpGreeterApplicationAddin *self = (GbpGreeterApplicationAddin *)addin;
+  g_auto(GStrv) argv = NULL;
+  GVariantDict *dict;
+  const gchar *clone_uri = NULL;
+  gint argc;
+
+  g_assert (GBP_IS_GREETER_APPLICATION_ADDIN (self));
+  g_assert (IDE_IS_APPLICATION (application));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  dict = g_application_command_line_get_options_dict (cmdline);
+  argv = ide_application_get_argv (IDE_APPLICATION (application), cmdline);
+  argc = g_strv_length (argv);
+
+  /*
+   * If we are processing the arguments for the startup of the primary
+   * instance, then we want to show the greeter if no arguments are
+   * provided. (That means argc == 1, the programe executable).
+   *
+   * Also, if they provided --greeter or -g we'll show a new greeter.
+   */
+  if ((!g_application_command_line_get_is_remote (cmdline) && argc == 1) ||
+      g_variant_dict_contains (dict, "greeter"))
+    {
+      present_greeter_with_surface (NULL, NULL, addin);
+      return;
+    }
+
+  /*
+   * If the --clone=URI option was provided, switch the greeter to the
+   * clone surface and begin cloning.
+   */
+  if (dict != NULL && g_variant_dict_lookup (dict, "clone", "&s", &clone_uri))
+    {
+      IdeGreeterWorkspace *workspace;
+      IdeWorkbench *workbench;
+      IdeSurface *surface;
+
+      workbench = ide_workbench_new ();
+      ide_application_add_workbench (self->application, workbench);
+
+      workspace = ide_greeter_workspace_new (self->application);
+      ide_workbench_add_workspace (workbench, IDE_WORKSPACE (workspace));
+
+      surface = ide_workspace_get_surface_by_name (IDE_WORKSPACE (workspace), "clone");
+      ide_workspace_set_visible_surface (IDE_WORKSPACE (workspace), surface);
+
+      if (IDE_IS_CLONE_SURFACE (surface))
+        ide_clone_surface_set_uri (IDE_CLONE_SURFACE (surface), clone_uri);
+
+      ide_workbench_focus_workspace (workbench, IDE_WORKSPACE (workspace));
+    }
+}
+
+static void
+gbp_greeter_application_addin_load (IdeApplicationAddin *addin,
+                                    IdeApplication      *application)
+{
+  GbpGreeterApplicationAddin *self = (GbpGreeterApplicationAddin *)addin;
+
+  g_assert (GBP_IS_GREETER_APPLICATION_ADDIN (self));
+  g_assert (IDE_IS_APPLICATION (application));
+
+  self->application = application;
+
+  g_action_map_add_action_entries (G_ACTION_MAP (application),
+                                   actions,
+                                   G_N_ELEMENTS (actions),
+                                   self);
+}
+
+static void
+gbp_greeter_application_addin_unload (IdeApplicationAddin *addin,
+                                      IdeApplication      *application)
+{
+  GbpGreeterApplicationAddin *self = (GbpGreeterApplicationAddin *)addin;
+
+  g_assert (GBP_IS_GREETER_APPLICATION_ADDIN (self));
+  g_assert (IDE_IS_APPLICATION (application));
+
+  for (guint i = 0; i < G_N_ELEMENTS (actions); i++)
+    g_action_map_remove_action (G_ACTION_MAP (application), actions[i].name);
+
+  self->application = NULL;
+}
+
+static void
+application_addin_iface_init (IdeApplicationAddinInterface *iface)
+{
+  iface->load = gbp_greeter_application_addin_load;
+  iface->unload = gbp_greeter_application_addin_unload;
+  iface->add_option_entries = gbp_greeter_application_addin_add_option_entries;
+  iface->handle_command_line = gbp_greeter_application_addin_handle_command_line;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGreeterApplicationAddin, gbp_greeter_application_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_APPLICATION_ADDIN, application_addin_iface_init))
+
+static void
+gbp_greeter_application_addin_class_init (GbpGreeterApplicationAddinClass *klass)
+{
+}
+
+static void
+gbp_greeter_application_addin_init (GbpGreeterApplicationAddin *self)
+{
+}
diff --git a/src/plugins/greeter/gbp-greeter-application-addin.h 
b/src/plugins/greeter/gbp-greeter-application-addin.h
new file mode 100644
index 000000000..eb1f26b86
--- /dev/null
+++ b/src/plugins/greeter/gbp-greeter-application-addin.h
@@ -0,0 +1,31 @@
+/* gbp-greeter-application-addin.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_GREETER_APPLICATION_ADDIN (gbp_greeter_application_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGreeterApplicationAddin, gbp_greeter_application_addin, GBP, 
GREETER_APPLICATION_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/greeter/greeter-plugin.c b/src/plugins/greeter/greeter-plugin.c
new file mode 100644
index 000000000..5d9c55d57
--- /dev/null
+++ b/src/plugins/greeter/greeter-plugin.c
@@ -0,0 +1,36 @@
+/* greeter-plugin.c
+ *
+ * Copyright 2018-2019 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 "greeter-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-greeter.h>
+
+#include "gbp-greeter-application-addin.h"
+
+_IDE_EXTERN void
+_gbp_greeter_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_APPLICATION_ADDIN,
+                                              GBP_TYPE_GREETER_APPLICATION_ADDIN);
+}
diff --git a/src/plugins/greeter/greeter.gresource.xml b/src/plugins/greeter/greeter.gresource.xml
new file mode 100644
index 000000000..13594ee06
--- /dev/null
+++ b/src/plugins/greeter/greeter.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/greeter">
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+    <file>greeter.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/greeter/greeter.plugin b/src/plugins/greeter/greeter.plugin
new file mode 100644
index 000000000..c0114abc4
--- /dev/null
+++ b/src/plugins/greeter/greeter.plugin
@@ -0,0 +1,13 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Description=Builder's greeter window
+Embedded=_gbp_greeter_register_types
+Hidden=true
+Module=greeter
+Name=Greeter
+X-At-Startup=true
+X-Project-File-Filter-Content-Type=inode/directory
+X-Project-File-Filter-Name=Directory
+X-Project-File-Filter-Priority=-100
diff --git a/src/plugins/greeter/gtk/menus.ui b/src/plugins/greeter/gtk/menus.ui
new file mode 100644
index 000000000..ff260345b
--- /dev/null
+++ b/src/plugins/greeter/gtk/menus.ui
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="ide-greeter-workspace-menu">
+    <section id="ide-greeter-workspace-menu-projects">
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-open</attribute>
+        <attribute name="label" translatable="yes">_Open Project</attribute>
+        <attribute name="action">win.open</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-clone</attribute>
+        <attribute name="label" translatable="yes">_Clone Repository</attribute>
+        <attribute name="action">win.surface</attribute>
+        <attribute name="target" type="s">'clone'</attribute>
+      </item>
+    </section>
+    <section id="ide-greeter-workspace-menu-close">
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-close</attribute>
+        <attribute name="label" translatable="yes">Close</attribute>
+        <attribute name="action">win.close</attribute>
+      </item>
+    </section>
+    <section id="ide-greeter-workspace-menu-app">
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-preferences</attribute>
+        <attribute name="label" translatable="yes">Preferences</attribute>
+        <attribute name="action">app.preferences</attribute>
+        <attribute name="accel">&lt;primary&gt;comma</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-shortcuts</attribute>
+        <attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
+        <attribute name="action">app.shortcuts</attribute>
+        <attribute name="accel">&lt;primary&gt;question</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-help</attribute>
+        <attribute name="label" translatable="yes">Help</attribute>
+        <attribute name="action">app.help</attribute>
+        <attribute name="accel">F1</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-about</attribute>
+        <attribute name="label" translatable="yes">About Builder</attribute>
+        <attribute name="action">app.about</attribute>
+      </item>
+    </section>
+    <section id="ide-greeter-workspace-menu-quit-section">
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-quit</attribute>
+        <attribute name="label" translatable="yes">Quit</attribute>
+        <attribute name="action">app.quit</attribute>
+      </item>
+    </section>
+    <!--
+    <section id="ide-greeter-workspace-menu-debug-section">
+      <attribute name="label" translatable="yes">Debugging</attribute>
+      <item>
+        <attribute name="id">ide-greeter-workspace-menu-stats</attribute>
+        <attribute name="label" translatable="yes">Type Statistics</attribute>
+        <attribute name="action">app.about:types</attribute>
+      </item>
+    </section>
+    -->
+  </menu>
+  <menu id="ide-primary-workspace-menu">
+    <section id="ide-primary-workspace-menu-projects-section">
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-open</attribute>
+        <attribute name="label" translatable="yes">_Open Project</attribute>
+        <attribute name="action">app.open-project</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-primary-workspace-menu-clone</attribute>
+        <attribute name="label" translatable="yes">_Clone Repository</attribute>
+        <attribute name="action">app.present-greeter-with-surface</attribute>
+        <attribute name="target" type="s">'clone'</attribute>
+      </item>
+    </section>
+  </menu>
+  <menu id="ide-editor-workspace-menu">
+    <section id="ide-editor-workspace-menu-projects-section">
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-open</attribute>
+        <attribute name="label" translatable="yes">_Open Project</attribute>
+        <attribute name="action">app.open-project</attribute>
+      </item>
+      <item>
+        <attribute name="id">ide-editor-workspace-menu-clone</attribute>
+        <attribute name="label" translatable="yes">_Clone Repository</attribute>
+        <attribute name="action">app.present-greeter-with-surface</attribute>
+        <attribute name="target" type="s">'clone'</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/src/plugins/greeter/meson.build b/src/plugins/greeter/meson.build
new file mode 100644
index 000000000..736f31520
--- /dev/null
+++ b/src/plugins/greeter/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'greeter-plugin.c',
+  'gbp-greeter-application-addin.c',
+])
+
+plugin_greeter_resources = gnome.compile_resources(
+  'gbp-greeter-resources',
+  'greeter.gresource.xml',
+  c_name: 'gbp_greeter',
+)
+
+plugins_sources += plugin_greeter_resources[0]
diff --git a/src/plugins/grep/gbp-grep-model.c b/src/plugins/grep/gbp-grep-model.c
index e7b480133..3848801a5 100644
--- a/src/plugins/grep/gbp-grep-model.c
+++ b/src/plugins/grep/gbp-grep-model.c
@@ -1,6 +1,6 @@
 /* gbp-grep-model.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -18,9 +18,12 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
+#define G_LOG_DOMAIN "gbp-grep-model"
+
 #include "config.h"
 
-#define G_LOG_DOMAIN "gbp-grep-model"
+#include <libide-code.h>
+#include <libide-vcs.h>
 
 #include "gbp-grep-model.h"
 
@@ -32,7 +35,9 @@ typedef struct
 
 struct _GbpGrepModel
 {
-  IdeObject parent_instance;
+  GObject parent_instance;
+
+  IdeContext *context;
 
   /* The root directory to start searching from. */
   GFile *directory;
@@ -44,7 +49,7 @@ struct _GbpGrepModel
 
   /* We need to do client-side processing to extract the exact message
    * locations after grep gives us the matching lines. This allows us to
-   * create IdeProjectEdit source ranges later as well as creating the
+   * create IdeTextEdit source ranges later as well as creating the
    * match positions for highlighting in the treeview cell renderers.
    */
   GRegex *message_regex;
@@ -77,7 +82,7 @@ struct _GbpGrepModel
 
 static void tree_model_iface_init (GtkTreeModelIface *iface);
 
-G_DEFINE_TYPE_WITH_CODE (GbpGrepModel, gbp_grep_model, IDE_TYPE_OBJECT,
+G_DEFINE_TYPE_WITH_CODE (GbpGrepModel, gbp_grep_model, G_TYPE_OBJECT,
                          G_IMPLEMENT_INTERFACE (GTK_TYPE_TREE_MODEL, tree_model_iface_init))
 
 enum {
@@ -159,7 +164,7 @@ gbp_grep_model_line_parse (GbpGrepModelLine *cl,
           cl->line = g_ascii_strtoll (linestr, NULL, 10);
 
           /* Now parse the matches for the line so that we can highlight
-           * them in the treeview and also determine the IdeProjectEdit
+           * them in the treeview and also determine the IdeTextEdit
            * source range when editing files.
            */
 
@@ -205,11 +210,24 @@ gbp_grep_model_line_parse (GbpGrepModelLine *cl,
 GbpGrepModel *
 gbp_grep_model_new (IdeContext *context)
 {
+  GbpGrepModel *self;
+
   g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
 
-  return g_object_new (GBP_TYPE_GREP_MODEL,
-                       "context", context,
-                       NULL);
+  self = g_object_new (GBP_TYPE_GREP_MODEL, NULL);
+  self->context = g_object_ref (context);
+
+  return g_steal_pointer (&self);
+}
+
+static void
+gbp_grep_model_dispose (GObject *object)
+{
+  GbpGrepModel *self = (GbpGrepModel *)object;
+
+  g_clear_object (&self->context);
+
+  G_OBJECT_CLASS (gbp_grep_model_parent_class)->dispose (object);
 }
 
 static void
@@ -219,6 +237,7 @@ gbp_grep_model_finalize (GObject *object)
 
   clear_line (&self->prev_line);
 
+  g_clear_object (&self->context);
   g_clear_object (&self->directory);
   g_clear_pointer (&self->index, index_free);
   g_clear_pointer (&self->query, g_free);
@@ -311,6 +330,7 @@ gbp_grep_model_class_init (GbpGrepModelClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
 
+  object_class->dispose = gbp_grep_model_dispose;
   object_class->finalize = gbp_grep_model_finalize;
   object_class->get_property = gbp_grep_model_get_property;
   object_class->set_property = gbp_grep_model_set_property;
@@ -549,7 +569,6 @@ gbp_grep_model_create_launcher (GbpGrepModel *self)
 {
   g_autoptr(IdeSubprocessLauncher) launcher = NULL;
   const gchar *path;
-  IdeContext *context;
   IdeVcs *vcs;
   GFile *workdir;
   GType git_vcs;
@@ -559,10 +578,9 @@ gbp_grep_model_create_launcher (GbpGrepModel *self)
   g_assert (self->query != NULL);
   g_assert (self->query[0] != '\0');
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  vcs = ide_context_get_vcs (context);
-  workdir = ide_vcs_get_working_directory (vcs);
-  git_vcs = g_type_from_name ("IdeGitVcs");
+  vcs = ide_vcs_from_context (self->context);
+  workdir = ide_vcs_get_workdir (vcs);
+  git_vcs = g_type_from_name ("GbpGitVcs");
 
   if (self->directory != NULL)
     path = g_file_peek_path (self->directory);
@@ -575,7 +593,7 @@ gbp_grep_model_create_launcher (GbpGrepModel *self)
    * Soft runtime check for Git support, so that we can use "git grep"
    * instead of the system "grep".
    */
-  if (git_vcs != G_TYPE_INVALID && g_type_is_a (G_OBJECT_TYPE (vcs), git_vcs))
+  if (git_vcs != G_TYPE_INVALID && G_TYPE_CHECK_INSTANCE_TYPE (vcs, git_vcs))
     use_git_grep = TRUE;
 
   if (use_git_grep)
@@ -766,7 +784,7 @@ gbp_grep_model_scan_async (GbpGrepModel        *self,
       IDE_EXIT;
     }
 
-  if (dzl_str_empty0 (self->query))
+  if (ide_str_empty0 (self->query))
     {
       ide_task_return_new_error (task,
                                  G_IO_ERROR,
@@ -1120,37 +1138,27 @@ create_edits_cb (GbpGrepModel *self,
 
   if (gbp_grep_model_line_parse (&line, row, self->message_regex))
     {
-      g_autoptr(IdeFile) file = NULL;
-      g_autoptr(GFile) gfile = NULL;
-      IdeContext *context;
+      g_autoptr(GFile) file = NULL;
       guint lineno;
 
-      context = ide_object_get_context (IDE_OBJECT (self));
-      g_assert (IDE_IS_CONTEXT (context));
-
-      gfile = gbp_grep_model_get_file (self, line.path);
-      g_assert (G_IS_FILE (gfile));
-
-      file = ide_file_new (context, gfile);
-      g_assert (IDE_IS_FILE (file));
+      file = gbp_grep_model_get_file (self, line.path);
+      g_assert (G_IS_FILE (file));
 
       lineno = line.line ? line.line - 1 : 0;
 
       for (guint i = 0; i < line.matches->len; i++)
         {
           const GbpGrepModelMatch *match = &g_array_index (line.matches, GbpGrepModelMatch, i);
-          g_autoptr(IdeProjectEdit) edit = NULL;
-          g_autoptr(IdeSourceRange) range = NULL;
-          g_autoptr(IdeSourceLocation) begin = NULL;
-          g_autoptr(IdeSourceLocation) end = NULL;
+          g_autoptr(IdeTextEdit) edit = NULL;
+          g_autoptr(IdeRange) range = NULL;
+          g_autoptr(IdeLocation) begin = NULL;
+          g_autoptr(IdeLocation) end = NULL;
 
-          begin = ide_source_location_new (file, lineno, match->match_begin, 0);
-          end = ide_source_location_new (file, lineno, match->match_end, 0);
-          range = ide_source_range_new (begin, end);
+          begin = ide_location_new (file, lineno, match->match_begin);
+          end = ide_location_new (file, lineno, match->match_end);
+          range = ide_range_new (begin, end);
 
-          edit = g_object_new (IDE_TYPE_PROJECT_EDIT,
-                               "range", range,
-                               NULL);
+          edit = ide_text_edit_new (range, NULL);
 
           g_ptr_array_add (edits, g_steal_pointer (&edit));
         }
@@ -1163,7 +1171,7 @@ create_edits_cb (GbpGrepModel *self,
  * gbp_grep_model_create_edits:
  * @self: a #GbpGrepModel
  *
- * Returns: (transfer container): a #GPtrArray of IdeProjectEdit
+ * Returns: (transfer container): a #GPtrArray of IdeTextEdit
  */
 GPtrArray *
 gbp_grep_model_create_edits (GbpGrepModel *self)
diff --git a/src/plugins/grep/gbp-grep-model.h b/src/plugins/grep/gbp-grep-model.h
index de81a8885..863934bf6 100644
--- a/src/plugins/grep/gbp-grep-model.h
+++ b/src/plugins/grep/gbp-grep-model.h
@@ -1,6 +1,6 @@
 /* gbp-grep-model.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -20,7 +20,7 @@
 
 #pragma once
 
-#include <ide.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/grep/gbp-grep-panel.c b/src/plugins/grep/gbp-grep-panel.c
index f5defb6f0..82f6ca243 100644
--- a/src/plugins/grep/gbp-grep-panel.c
+++ b/src/plugins/grep/gbp-grep-panel.c
@@ -1,6 +1,6 @@
 /* gbp-grep-panel.c
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -18,11 +18,14 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-grep-panel"
 
+#include "config.h"
+
 #include <glib/gi18n.h>
+#include <libide-code.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
 
 #include "gbp-grep-panel.h"
 
@@ -220,26 +223,22 @@ gbp_grep_panel_row_activated_cb (GbpGrepPanel      *self,
 
       if G_LIKELY (line != NULL)
         {
-          g_autoptr(IdeSourceLocation) location = NULL;
+          g_autoptr(IdeLocation) location = NULL;
           g_autoptr(GFile) child = NULL;
-          g_autoptr(IdeFile) ichild = NULL;
-          IdePerspective *editor;
-          IdeWorkbench *workbench;
-          IdeContext *context;
+          IdeWorkspace *workspace;
+          IdeSurface *editor;
           guint lineno = line->line;
 
-          workbench = ide_widget_get_workbench (GTK_WIDGET (self));
-          context = ide_workbench_get_context (workbench);
-          editor = ide_workbench_get_perspective_by_name (workbench, "editor");
+          workspace = ide_widget_get_workspace (GTK_WIDGET (self));
+          editor = ide_workspace_get_surface_by_name (workspace, "editor");
 
           if (lineno > 0)
             lineno--;
 
           child = gbp_grep_model_get_file (GBP_GREP_MODEL (model), line->path);
-          ichild = ide_file_new (context, child);
-          location = ide_source_location_new (ichild, lineno, 0, 0);
+          location = ide_location_new (child, lineno, -1);
 
-          ide_editor_perspective_focus_location (IDE_EDITOR_PERSPECTIVE (editor), location);
+          ide_editor_surface_focus_location (IDE_EDITOR_SURFACE (editor), location);
         }
     }
 }
@@ -334,8 +333,8 @@ gbp_grep_panel_replace_clicked_cb (GbpGrepPanel *self,
 
   for (guint i = 0; i < edits->len; i++)
     {
-      IdeProjectEdit *edit = g_ptr_array_index (edits, i);
-      ide_project_edit_set_replacement (edit, text);
+      IdeTextEdit *edit = g_ptr_array_index (edits, i);
+      ide_text_edit_set_text (edit, text);
     }
 
   g_debug ("Replacing %u edit points with %s", edits->len, text);
@@ -347,7 +346,7 @@ gbp_grep_panel_replace_clicked_cb (GbpGrepPanel *self,
   gtk_spinner_start (self->spinner);
 
   context = ide_widget_get_context (GTK_WIDGET (self));
-  bufmgr = ide_context_get_buffer_manager (context);
+  bufmgr = ide_buffer_manager_from_context (context);
 
   ide_buffer_manager_apply_edits_async (bufmgr,
                                         IDE_PTR_ARRAY_STEAL_FULL (&edits),
@@ -411,7 +410,7 @@ gbp_grep_panel_class_init (GbpGrepPanelClass *klass)
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
   gtk_widget_class_set_css_name (widget_class, "gbpgreppanel");
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/grep/gbp-grep-panel.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/grep/gbp-grep-panel.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, close_button);
   gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, replace_button);
   gtk_widget_class_bind_template_child (widget_class, GbpGrepPanel, replace_entry);
@@ -490,8 +489,8 @@ gbp_grep_panel_init (GbpGrepPanel *self)
                        "ellipsize", PANGO_ELLIPSIZE_END,
                        NULL);
   gtk_cell_layout_pack_start (GTK_CELL_LAYOUT (column), cell, TRUE);
-  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, match_data_func, NULL, NULL);
   /* translators: the column header for the matches in the 'find in files' results */
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (column), cell, match_data_func, NULL, NULL);
   gtk_tree_view_column_set_title (column, _("Match"));
   gtk_tree_view_column_set_expand (column, TRUE);
   gtk_tree_view_column_set_resizable (column, TRUE);
diff --git a/src/plugins/grep/gbp-grep-panel.h b/src/plugins/grep/gbp-grep-panel.h
index 04e49c093..8a8cd3521 100644
--- a/src/plugins/grep/gbp-grep-panel.h
+++ b/src/plugins/grep/gbp-grep-panel.h
@@ -1,6 +1,6 @@
 /* gbp-grep-panel.h
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -20,7 +20,7 @@
 
 #pragma once
 
-#include <ide.h>
+#include <dazzle.h>
 
 #include "gbp-grep-model.h"
 
diff --git a/src/plugins/grep/gbp-grep-popover.c b/src/plugins/grep/gbp-grep-popover.c
index 56f61fa65..af2df8e2a 100644
--- a/src/plugins/grep/gbp-grep-popover.c
+++ b/src/plugins/grep/gbp-grep-popover.c
@@ -1,6 +1,6 @@
 /* gbp-grep-popover.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -18,11 +18,13 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-grep-popover"
 
-#include <ide.h>
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-gui.h>
+#include <libide-editor.h>
 
 #include "gbp-grep-model.h"
 #include "gbp-grep-panel.h"
@@ -30,9 +32,9 @@
 
 struct _GbpGrepPopover
 {
-  GtkPopover parent_instance;
+  GtkPopover      parent_instance;
 
-  GFile *file;
+  GFile          *file;
 
   GtkEntry       *entry;
   GtkButton      *button;
@@ -67,7 +69,7 @@ gbp_grep_popover_scan_cb (GObject      *object,
   g_assert (GBP_IS_GREP_PANEL (panel));
 
   if (!gbp_grep_model_scan_finish (model, result, &error))
-    ide_widget_warning (GTK_WIDGET (panel), "Failed to find files: %s", error->message);
+    g_warning ("Failed to find files: %s", error->message);
   else
     gbp_grep_panel_set_model (panel, model);
 
@@ -79,8 +81,8 @@ gbp_grep_popover_button_clicked_cb (GbpGrepPopover *self,
                                     GtkButton      *button)
 {
   g_autoptr(GbpGrepModel) model = NULL;
-  IdePerspective *editor;
-  IdeWorkbench *workbench;
+  IdeSurface *editor;
+  IdeWorkspace *workspace;
   IdeContext *context;
   GtkWidget *panel;
   GtkWidget *utils;
@@ -92,10 +94,10 @@ gbp_grep_popover_button_clicked_cb (GbpGrepPopover *self,
   g_assert (GBP_IS_GREP_POPOVER (self));
   g_assert (GTK_IS_BUTTON (button));
 
-  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
-  editor = ide_workbench_get_perspective_by_name (workbench, "editor");
-  utils = ide_editor_perspective_get_utilities (IDE_EDITOR_PERSPECTIVE (editor));
-  context = ide_workbench_get_context (workbench);
+  workspace = ide_widget_get_workspace (GTK_WIDGET (self));
+  editor = ide_workspace_get_surface_by_name (workspace, "editor");
+  utils = ide_editor_surface_get_utilities (IDE_EDITOR_SURFACE (editor));
+  context = ide_widget_get_context (GTK_WIDGET (workspace));
 
   use_regex = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->regex_button));
   at_word_boundaries = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->whole_button));
@@ -213,7 +215,7 @@ gbp_grep_popover_class_init (GbpGrepPopoverClass *klass)
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/grep/gbp-grep-popover.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/grep/gbp-grep-popover.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpGrepPopover, button);
   gtk_widget_class_bind_template_child (widget_class, GbpGrepPopover, entry);
   gtk_widget_class_bind_template_child (widget_class, GbpGrepPopover, regex_button);
diff --git a/src/plugins/grep/gbp-grep-popover.h b/src/plugins/grep/gbp-grep-popover.h
index 4f569dea1..4d4306f64 100644
--- a/src/plugins/grep/gbp-grep-popover.h
+++ b/src/plugins/grep/gbp-grep-popover.h
@@ -1,6 +1,6 @@
 /* gbp-grep-popover.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
diff --git a/src/plugins/grep/gbp-grep-tree-addin.c b/src/plugins/grep/gbp-grep-tree-addin.c
new file mode 100644
index 000000000..45038c52c
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-tree-addin.c
@@ -0,0 +1,170 @@
+/* gbp-grep-tree-addin.c
+ *
+ * Copyright 2018-2019 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-grep-tree-addin"
+
+#include "config.h"
+
+#include <libide-projects.h>
+#include <libide-tree.h>
+
+#include "gbp-grep-tree-addin.h"
+#include "gbp-grep-popover.h"
+
+struct _GbpGrepTreeAddin
+{
+  GObject  parent_instance;
+
+  IdeTree *tree;
+};
+
+static void
+popover_closed_cb (GtkPopover *popover)
+{
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GTK_IS_POPOVER (popover));
+
+  /*
+   * Clear focus before destroying popover, or we risk some
+   * re-entrancy issues in libdazzle. Needs safer tracking of
+   * focus widgets as gtk is not clearing pointers in destroy.
+   */
+
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (popover));
+  gtk_window_set_focus (GTK_WINDOW (toplevel), NULL);
+  gtk_widget_destroy (GTK_WIDGET (popover));
+}
+
+static void
+find_in_files_action (GSimpleAction *action,
+                      GVariant      *param,
+                      gpointer       user_data)
+{
+  GbpGrepTreeAddin *self = user_data;
+  g_autoptr(GFile) file = NULL;
+  IdeProjectFile *project_file;
+  IdeTreeNode *node;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_GREP_TREE_ADDIN (self));
+  g_assert (self->tree != NULL);
+  g_assert (IDE_IS_TREE (self->tree));
+
+  if ((node = ide_tree_get_selected_node (self->tree)) &&
+      ide_tree_node_holds (node, IDE_TYPE_PROJECT_FILE) &&
+      (project_file = ide_tree_node_get_item (node)) &&
+      (file = ide_project_file_ref_file (project_file)))
+    {
+      gboolean is_dir = ide_project_file_is_directory (project_file);
+      GtkPopover *popover;
+
+      popover = g_object_new (GBP_TYPE_GREP_POPOVER,
+                              "file", file,
+                              "is-directory", is_dir,
+                              "position", GTK_POS_RIGHT,
+                              NULL);
+      g_signal_connect_after (popover,
+                              "closed",
+                              G_CALLBACK (popover_closed_cb),
+                              NULL);
+      ide_tree_show_popover_at_node (self->tree, node, popover);
+    }
+}
+
+static void
+gbp_grep_tree_addin_load (IdeTreeAddin *addin,
+                          IdeTree      *tree,
+                          IdeTreeModel *model)
+{
+  GbpGrepTreeAddin *self = (GbpGrepTreeAddin *)addin;
+  g_autoptr(GActionMap) group = NULL;
+  static const GActionEntry actions[] = {
+    { "find-in-files", find_in_files_action },
+  };
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GREP_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  self->tree = tree;
+
+  group = G_ACTION_MAP (g_simple_action_group_new ());
+  g_action_map_add_action_entries (group, actions, G_N_ELEMENTS (actions), self);
+  gtk_widget_insert_action_group (GTK_WIDGET (tree), "grep", G_ACTION_GROUP (group));
+}
+
+static void
+gbp_grep_tree_addin_unload (IdeTreeAddin *addin,
+                            IdeTree      *tree,
+                            IdeTreeModel *model)
+{
+  GbpGrepTreeAddin *self = (GbpGrepTreeAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GREP_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (tree), "grep", NULL);
+
+  self->tree = NULL;
+}
+
+static void
+gbp_grep_tree_addin_selection_changed (IdeTreeAddin *addin,
+                                       IdeTreeNode  *node)
+{
+  GbpGrepTreeAddin *self = (GbpGrepTreeAddin *)addin;
+  gboolean enabled;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_GREP_TREE_ADDIN (self));
+  g_assert (!node || IDE_IS_TREE_NODE (node));
+
+  enabled = node && ide_tree_node_holds (node, IDE_TYPE_PROJECT_FILE);
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "grep", "find-in-files",
+                             "enabled", enabled,
+                             NULL);
+}
+
+static void
+tree_addin_iface_init (IdeTreeAddinInterface *iface)
+{
+  iface->load = gbp_grep_tree_addin_load;
+  iface->unload = gbp_grep_tree_addin_unload;
+  iface->selection_changed = gbp_grep_tree_addin_selection_changed;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpGrepTreeAddin, gbp_grep_tree_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_TREE_ADDIN, tree_addin_iface_init))
+
+static void
+gbp_grep_tree_addin_class_init (GbpGrepTreeAddinClass *klass)
+{
+}
+
+static void
+gbp_grep_tree_addin_init (GbpGrepTreeAddin *self)
+{
+}
diff --git a/src/plugins/grep/gbp-grep-tree-addin.h b/src/plugins/grep/gbp-grep-tree-addin.h
new file mode 100644
index 000000000..7e90aa768
--- /dev/null
+++ b/src/plugins/grep/gbp-grep-tree-addin.h
@@ -0,0 +1,31 @@
+/* gbp-grep-tree-addin.h
+ *
+ * Copyright 2018-2019 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_GREP_TREE_ADDIN (gbp_grep_tree_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpGrepTreeAddin, gbp_grep_tree_addin, GBP, GREP_TREE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/grep/grep-plugin.c b/src/plugins/grep/grep-plugin.c
new file mode 100644
index 000000000..561dc0b1f
--- /dev/null
+++ b/src/plugins/grep/grep-plugin.c
@@ -0,0 +1,34 @@
+/* gbp-grep-plugin.c
+ *
+ * Copyright 2018-2019 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
+ */
+
+#include "config.h"
+
+#include <libide-tree.h>
+#include <libpeas/peas.h>
+
+#include "gbp-grep-tree-addin.h"
+
+_IDE_EXTERN void
+_gbp_grep_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_TREE_ADDIN,
+                                              GBP_TYPE_GREP_TREE_ADDIN);
+}
diff --git a/src/plugins/grep/grep.gresource.xml b/src/plugins/grep/grep.gresource.xml
index ead09764c..3b3e67fff 100644
--- a/src/plugins/grep/grep.gresource.xml
+++ b/src/plugins/grep/grep.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/grep">
     <file>grep.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/grep">
     <file preprocess="xml-stripblanks">gbp-grep-panel.ui</file>
     <file preprocess="xml-stripblanks">gbp-grep-popover.ui</file>
     <file preprocess="xml-stripblanks">gtk/menus.ui</file>
diff --git a/src/plugins/grep/grep.plugin b/src/plugins/grep/grep.plugin
index 8812d8ab0..d3c3e63c4 100644
--- a/src/plugins/grep/grep.plugin
+++ b/src/plugins/grep/grep.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=grep
-Name=Find in Files
-Description=Search across project files
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2018 Christian Hergert
 Builtin=true
-Depends=editor;project-tree-plugin
-Embedded=gbp_grep_register_types
+Copyright=Copyright © 2018 Christian Hergert
+Depends=editor;project-tree;
+Description=Search across project files
+Embedded=_gbp_grep_register_types
+Module=grep
+Name=Find in Files
+X-Tree-Kind=project-tree;
diff --git a/src/plugins/grep/gtk/menus.ui b/src/plugins/grep/gtk/menus.ui
index aef127545..b088282ec 100644
--- a/src/plugins/grep/gtk/menus.ui
+++ b/src/plugins/grep/gtk/menus.ui
@@ -1,7 +1,7 @@
 <?xml version="1.0"?>
 <interface>
-  <menu id="gb-project-tree-popup-menu">
-    <section id="gb-project-tree-find-section">
+  <menu id="project-tree-menu">
+    <section id="project-tree-menu-placeholder2">
       <item>
         <attribute name="label" translatable="yes">Find in Files</attribute>
         <attribute name="action">grep.find-in-files</attribute>
diff --git a/src/plugins/grep/meson.build b/src/plugins/grep/meson.build
index 8a921fe25..d6e586c7b 100644
--- a/src/plugins/grep/meson.build
+++ b/src/plugins/grep/meson.build
@@ -1,20 +1,19 @@
-if get_option('with_grep')
+if get_option('plugin_grep')
 
-grep_resources = gnome.compile_resources(
+plugins_sources += files([
+  'gbp-grep-model.c',
+  'gbp-grep-panel.c',
+  'gbp-grep-popover.c',
+  'gbp-grep-tree-addin.c',
+  'grep-plugin.c',
+])
+
+plugin_grep_resources = gnome.compile_resources(
   'grep-resources',
   'grep.gresource.xml',
   c_name: 'gbp_grep',
 )
 
-grep_sources = [
-  'gbp-grep-model.c',
-  'gbp-grep-panel.c',
-  'gbp-grep-plugin.c',
-  'gbp-grep-popover.c',
-  'gbp-grep-project-tree-addin.c',
-]
-
-gnome_builder_plugins_sources += files(grep_sources)
-gnome_builder_plugins_sources += grep_resources[0]
+plugins_sources += plugin_grep_resources[0]
 
 endif
diff --git a/src/plugins/grep/themes/Adwaita-dark.css b/src/plugins/grep/themes/Adwaita-dark.css
index 0413f3e93..7341ec7c4 100644
--- a/src/plugins/grep/themes/Adwaita-dark.css
+++ b/src/plugins/grep/themes/Adwaita-dark.css
@@ -1,2 +1,2 @@
-@import url("resource:///org/gnome/builder/plugins/grep/themes/Adwaita-shared.css");
+@import url("resource:///plugins/grep/themes/Adwaita-shared.css");
 
diff --git a/src/plugins/grep/themes/Adwaita.css b/src/plugins/grep/themes/Adwaita.css
index 0413f3e93..7341ec7c4 100644
--- a/src/plugins/grep/themes/Adwaita.css
+++ b/src/plugins/grep/themes/Adwaita.css
@@ -1,2 +1,2 @@
-@import url("resource:///org/gnome/builder/plugins/grep/themes/Adwaita-shared.css");
+@import url("resource:///plugins/grep/themes/Adwaita-shared.css");
 
diff --git a/src/plugins/history/gbp-history-editor-page-addin.c 
b/src/plugins/history/gbp-history-editor-page-addin.c
new file mode 100644
index 000000000..43133ac58
--- /dev/null
+++ b/src/plugins/history/gbp-history-editor-page-addin.c
@@ -0,0 +1,332 @@
+/* gbp-history-editor-page-addin.c
+ *
+ * Copyright 2017-2019 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-history-editor-page-addin"
+
+#include "gbp-history-editor-page-addin.h"
+#include "gbp-history-item.h"
+#include "gbp-history-frame-addin.h"
+
+struct _GbpHistoryEditorPageAddin
+{
+  GObject               parent_instance;
+
+  /* Unowned pointer */
+  IdeEditorPage        *editor;
+
+  /* Weak pointer */
+  GbpHistoryFrameAddin *frame_addin;
+
+  gsize                 last_change_count;
+
+  guint                 queued_edit_line;
+  guint                 queued_edit_source;
+};
+
+static void
+gbp_history_editor_page_addin_frame_set (IdeEditorPageAddin *addin,
+                                         IdeFrame           *stack)
+{
+  GbpHistoryEditorPageAddin *self = (GbpHistoryEditorPageAddin *)addin;
+  IdeFrameAddin *frame_addin;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_FRAME (stack));
+
+  frame_addin = ide_frame_addin_find_by_module_name (stack, "history");
+
+  g_assert (frame_addin != NULL);
+  g_assert (GBP_IS_HISTORY_FRAME_ADDIN (frame_addin));
+
+  g_set_weak_pointer (&self->frame_addin, GBP_HISTORY_FRAME_ADDIN (frame_addin));
+
+  IDE_EXIT;
+}
+
+static void
+gbp_history_editor_page_addin_push (GbpHistoryEditorPageAddin *self,
+                                    const GtkTextIter         *iter)
+{
+  g_autoptr(GbpHistoryItem) item = NULL;
+  GtkTextBuffer *buffer;
+  GtkTextMark *mark;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_PAGE_ADDIN (self));
+  g_assert (iter != NULL);
+  g_assert (self->editor != NULL);
+
+  if (self->frame_addin == NULL)
+    IDE_GOTO (no_stack_loaded);
+
+  /*
+   * Create an unnamed mark for this history item, and push the history
+   * item into the stacks history.
+   */
+  buffer = gtk_text_iter_get_buffer (iter);
+  mark = gtk_text_buffer_create_mark (buffer, NULL, iter, TRUE);
+  item = gbp_history_item_new (mark);
+
+  gbp_history_frame_addin_push (self->frame_addin, item);
+
+no_stack_loaded:
+  IDE_EXIT;
+}
+
+static void
+gbp_history_editor_page_addin_jump (GbpHistoryEditorPageAddin *self,
+                                    const GtkTextIter         *from,
+                                    const GtkTextIter         *to,
+                                    IdeSourceView             *source_view)
+{
+  IdeBuffer *buffer;
+  gsize change_count;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_PAGE_ADDIN (self));
+  g_assert (from != NULL);
+  g_assert (to != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (source_view));
+
+  buffer = IDE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view)));
+  change_count = ide_buffer_get_change_count (buffer);
+
+  /*
+   * If the buffer has changed since the last jump was recorded,
+   * we want to track this as an edit point so that we can come
+   * back to it later.
+   */
+
+#if 0
+  g_print ("Cursor jumped from %u:%u\n",
+           gtk_text_iter_get_line (iter) + 1,
+           gtk_text_iter_get_line_offset (iter) + 1);
+  g_print ("Now=%lu Prev=%lu\n", change_count, self->last_change_count);
+#endif
+
+  //if (change_count != self->last_change_count)
+    {
+      self->last_change_count = change_count;
+      gbp_history_editor_page_addin_push (self, from);
+      gbp_history_editor_page_addin_push (self, to);
+    }
+}
+
+static gboolean
+gbp_history_editor_page_addin_flush_edit (gpointer user_data)
+{
+  GbpHistoryEditorPageAddin *self = user_data;
+  IdeBuffer *buffer;
+  GtkTextIter iter;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_PAGE_ADDIN (self));
+  g_assert (self->editor != NULL);
+
+  self->queued_edit_source = 0;
+
+  buffer = ide_editor_page_get_buffer (self->editor);
+  gtk_text_buffer_get_iter_at_line (GTK_TEXT_BUFFER (buffer), &iter, self->queued_edit_line);
+  gbp_history_editor_page_addin_push (self, &iter);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_history_editor_page_addin_queue (GbpHistoryEditorPageAddin *self,
+                                     guint                      line)
+{
+  g_assert (GBP_IS_HISTORY_EDITOR_PAGE_ADDIN (self));
+
+  /*
+   * If the buffer is modified, we want to keep track of this position in the
+   * history (the layout stack will automatically merge it with the previous
+   * entry if they are close).
+   *
+   * However, the insert-text signal can happen in rapid succession, so we only
+   * want to deal with it after a small timeout to coallesce the entries into a
+   * single push() into the history stack.
+   */
+
+  if (self->queued_edit_source == 0)
+    {
+      self->queued_edit_line = line;
+      self->queued_edit_source = gdk_threads_add_idle_full (G_PRIORITY_LOW,
+                                                            gbp_history_editor_page_addin_flush_edit,
+                                                            g_object_ref (self),
+                                                            g_object_unref);
+    }
+}
+
+static void
+gbp_history_editor_page_addin_insert_text (GbpHistoryEditorPageAddin *self,
+                                           const GtkTextIter         *location,
+                                           const gchar               *text,
+                                           gint                       length,
+                                           IdeBuffer                 *buffer)
+{
+  g_assert (GBP_IS_HISTORY_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (location != NULL);
+  g_assert (text != NULL);
+
+  if (!ide_buffer_get_loading (buffer))
+    gbp_history_editor_page_addin_queue (self, gtk_text_iter_get_line (location));
+}
+
+static void
+gbp_history_editor_page_addin_delete_range (GbpHistoryEditorPageAddin *self,
+                                            const GtkTextIter         *begin,
+                                            const GtkTextIter         *end,
+                                            IdeBuffer                 *buffer)
+{
+  g_assert (GBP_IS_HISTORY_EDITOR_PAGE_ADDIN (self));
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (!ide_buffer_get_loading (buffer))
+    gbp_history_editor_page_addin_queue (self, gtk_text_iter_get_line (begin));
+}
+
+static void
+gbp_history_editor_page_addin_buffer_loaded (GbpHistoryEditorPageAddin *self,
+                                             IdeBuffer                 *buffer)
+{
+  IdeSourceView *source_view;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_PAGE (self->editor));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /*
+   * The cursor should have settled here, push it's location onto the
+   * history stack so that ctrl+i works after jumping backwards.
+   */
+
+  source_view = ide_editor_page_get_view (self->editor);
+
+  if (gtk_widget_has_focus (GTK_WIDGET (source_view)))
+    {
+      GtkTextIter iter;
+
+      ide_buffer_get_selection_bounds (buffer, &iter, NULL);
+      gbp_history_editor_page_addin_queue (self, gtk_text_iter_get_line (&iter));
+    }
+}
+
+static void
+gbp_history_editor_page_addin_load (IdeEditorPageAddin *addin,
+                                    IdeEditorPage      *view)
+{
+  GbpHistoryEditorPageAddin *self = (GbpHistoryEditorPageAddin *)addin;
+  IdeSourceView *source_view;
+  IdeBuffer *buffer;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  self->editor = view;
+
+  buffer = ide_editor_page_get_buffer (view);
+  source_view = ide_editor_page_get_view (view);
+
+  self->last_change_count = ide_buffer_get_change_count (buffer);
+
+  g_signal_connect_swapped (source_view,
+                            "jump",
+                            G_CALLBACK (gbp_history_editor_page_addin_jump),
+                            addin);
+
+  g_signal_connect_swapped (buffer,
+                            "insert-text",
+                            G_CALLBACK (gbp_history_editor_page_addin_insert_text),
+                            self);
+
+  g_signal_connect_swapped (buffer,
+                            "delete-range",
+                            G_CALLBACK (gbp_history_editor_page_addin_delete_range),
+                            self);
+
+  g_signal_connect_swapped (buffer,
+                            "loaded",
+                            G_CALLBACK (gbp_history_editor_page_addin_buffer_loaded),
+                            self);
+}
+
+static void
+gbp_history_editor_page_addin_unload (IdeEditorPageAddin *addin,
+                                      IdeEditorPage      *view)
+{
+  GbpHistoryEditorPageAddin *self = (GbpHistoryEditorPageAddin *)addin;
+  IdeSourceView *source_view;
+  IdeBuffer *buffer;
+
+  g_assert (GBP_IS_HISTORY_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  g_clear_handle_id (&self->queued_edit_source, g_source_remove);
+
+  source_view = ide_editor_page_get_view (view);
+  buffer = ide_editor_page_get_buffer (view);
+
+  g_signal_handlers_disconnect_by_func (source_view,
+                                        G_CALLBACK (gbp_history_editor_page_addin_jump),
+                                        self);
+
+  g_signal_handlers_disconnect_by_func (buffer,
+                                        G_CALLBACK (gbp_history_editor_page_addin_insert_text),
+                                        self);
+
+  g_signal_handlers_disconnect_by_func (buffer,
+                                        G_CALLBACK (gbp_history_editor_page_addin_delete_range),
+                                        self);
+
+  g_signal_handlers_disconnect_by_func (buffer,
+                                        G_CALLBACK (gbp_history_editor_page_addin_buffer_loaded),
+                                        self);
+
+  g_clear_weak_pointer (&self->frame_addin);
+
+  self->editor = NULL;
+}
+
+static void
+editor_view_addin_iface_init (IdeEditorPageAddinInterface *iface)
+{
+  iface->load = gbp_history_editor_page_addin_load;
+  iface->unload = gbp_history_editor_page_addin_unload;
+  iface->frame_set = gbp_history_editor_page_addin_frame_set;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpHistoryEditorPageAddin, gbp_history_editor_page_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                                editor_view_addin_iface_init))
+
+static void
+gbp_history_editor_page_addin_class_init (GbpHistoryEditorPageAddinClass *klass)
+{
+}
+
+static void
+gbp_history_editor_page_addin_init (GbpHistoryEditorPageAddin *self)
+{
+}
diff --git a/src/plugins/history/gbp-history-editor-page-addin.h 
b/src/plugins/history/gbp-history-editor-page-addin.h
new file mode 100644
index 000000000..494febdbb
--- /dev/null
+++ b/src/plugins/history/gbp-history-editor-page-addin.h
@@ -0,0 +1,31 @@
+/* gbp-history-editor-page-addin.h
+ *
+ * Copyright 2017-2019 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-editor.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_HISTORY_EDITOR_PAGE_ADDIN (gbp_history_editor_page_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpHistoryEditorPageAddin, gbp_history_editor_page_addin, GBP, 
HISTORY_EDITOR_PAGE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/history/gbp-history-frame-addin.c b/src/plugins/history/gbp-history-frame-addin.c
new file mode 100644
index 000000000..a0f5fb3cc
--- /dev/null
+++ b/src/plugins/history/gbp-history-frame-addin.c
@@ -0,0 +1,447 @@
+/* gbp-history-frame-addin.c
+ *
+ * Copyright 2017-2019 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-history-frame-addin"
+
+#include "gbp-history-frame-addin.h"
+
+#define MAX_HISTORY_ITEMS   20
+#define NEARBY_LINES_THRESH 10
+
+struct _GbpHistoryFrameAddin
+{
+  GObject         parent_instance;
+
+  GListStore     *back_store;
+  GListStore     *forward_store;
+
+  GtkBox         *controls;
+  GtkButton      *previous_button;
+  GtkButton      *next_button;
+
+  IdeFrame *stack;
+
+  guint           navigating;
+};
+
+static void
+gbp_history_frame_addin_update (GbpHistoryFrameAddin *self)
+{
+  gboolean has_items;
+
+  g_assert (GBP_IS_HISTORY_FRAME_ADDIN (self));
+
+  has_items = g_list_model_get_n_items (G_LIST_MODEL (self->back_store)) > 0;
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->controls),
+                             "history", "move-previous-edit",
+                             "enabled", has_items,
+                             NULL);
+
+  has_items = g_list_model_get_n_items (G_LIST_MODEL (self->forward_store)) > 0;
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->controls),
+                             "history", "move-next-edit",
+                             "enabled", has_items,
+                             NULL);
+
+#if 0
+  g_print ("Backward\n");
+
+  for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->back_store)); i++)
+    {
+      g_autoptr(GbpHistoryItem) item = g_list_model_get_item (G_LIST_MODEL (self->back_store), i);
+
+      g_print ("%s\n", gbp_history_item_get_label (item));
+    }
+
+  g_print ("Forward\n");
+
+  for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->forward_store)); i++)
+    {
+      g_autoptr(GbpHistoryItem) item = g_list_model_get_item (G_LIST_MODEL (self->forward_store), i);
+
+      g_print ("%s\n", gbp_history_item_get_label (item));
+    }
+#endif
+}
+
+static void
+gbp_history_frame_addin_navigate (GbpHistoryFrameAddin *self,
+                                         GbpHistoryItem             *item)
+{
+  g_autoptr(IdeLocation) location = NULL;
+  GtkWidget *editor;
+
+  g_assert (GBP_IS_HISTORY_FRAME_ADDIN (self));
+  g_assert (GBP_IS_HISTORY_ITEM (item));
+
+  location = gbp_history_item_get_location (item);
+  editor = gtk_widget_get_ancestor (GTK_WIDGET (self->controls), IDE_TYPE_EDITOR_SURFACE);
+  ide_editor_surface_focus_location (IDE_EDITOR_SURFACE (editor), location);
+
+  gbp_history_frame_addin_update (self);
+}
+
+static gboolean
+item_is_nearby (IdeEditorPage  *editor,
+                GbpHistoryItem *item)
+{
+  GtkTextIter insert;
+  IdeBuffer *buffer;
+  GFile *buffer_file;
+  GFile *item_file;
+  gint buffer_line;
+  gint item_line;
+
+  g_assert (IDE_IS_EDITOR_PAGE (editor));
+  g_assert (GBP_IS_HISTORY_ITEM (item));
+
+  buffer = ide_editor_page_get_buffer (editor);
+
+  /* Make sure this is the same file */
+  buffer_file = ide_buffer_get_file (buffer);
+  item_file = gbp_history_item_get_file (item);
+  if (!g_file_equal (buffer_file, item_file))
+    return FALSE;
+
+  /* Check if the lines are nearby */
+  ide_buffer_get_selection_bounds (buffer, &insert, NULL);
+  buffer_line = gtk_text_iter_get_line (&insert);
+  item_line = gbp_history_item_get_line (item);
+
+  return ABS (buffer_line - item_line) < NEARBY_LINES_THRESH;
+}
+
+static void
+move_previous_edit_action (GSimpleAction *action,
+                           GVariant      *param,
+                           gpointer       user_data)
+{
+  GbpHistoryFrameAddin *self = user_data;
+  IdePage *current;
+  GListModel *model;
+  guint n_items;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_HISTORY_FRAME_ADDIN (self));
+  g_assert (self->stack != NULL);
+
+  model = G_LIST_MODEL (self->back_store);
+  n_items = g_list_model_get_n_items (model);
+  current = ide_frame_get_visible_child (self->stack);
+
+  /*
+   * The tip of the backward jumplist could be very close to
+   * where we are now. So keep skipping backwards until the
+   * item isn't near our current position.
+   */
+
+  self->navigating++;
+
+  for (guint i = n_items; i > 0; i--)
+    {
+      g_autoptr(GbpHistoryItem) item = g_list_model_get_item (model, i - 1);
+
+      g_list_store_remove (self->back_store, i - 1);
+      g_list_store_insert (self->forward_store, 0, item);
+
+      if (!IDE_IS_EDITOR_PAGE (current) ||
+          !item_is_nearby (IDE_EDITOR_PAGE (current), item))
+        {
+          gbp_history_frame_addin_navigate (self, item);
+          break;
+        }
+    }
+
+  self->navigating--;
+}
+
+static void
+move_next_edit_action (GSimpleAction *action,
+                       GVariant      *param,
+                       gpointer       user_data)
+{
+  GbpHistoryFrameAddin *self = user_data;
+  IdePage *current;
+  GListModel *model;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_HISTORY_FRAME_ADDIN (self));
+
+  model = G_LIST_MODEL (self->forward_store);
+  current = ide_frame_get_visible_child (self->stack);
+
+  self->navigating++;
+
+  while (g_list_model_get_n_items (model) > 0)
+    {
+      g_autoptr(GbpHistoryItem) item = g_list_model_get_item (model, 0);
+
+      g_list_store_remove (self->forward_store, 0);
+      g_list_store_append (self->back_store, item);
+
+      if (!IDE_IS_EDITOR_PAGE (current) ||
+          !item_is_nearby (IDE_EDITOR_PAGE (current), item))
+        {
+          gbp_history_frame_addin_navigate (self, item);
+          break;
+        }
+    }
+
+  self->navigating--;
+}
+
+static const GActionEntry entries[] = {
+  { "move-previous-edit", move_previous_edit_action },
+  { "move-next-edit", move_next_edit_action },
+};
+
+static void
+gbp_history_frame_addin_load (IdeFrameAddin *addin,
+                                     IdeFrame      *stack)
+{
+  GbpHistoryFrameAddin *self = (GbpHistoryFrameAddin *)addin;
+  g_autoptr(GSimpleActionGroup) actions = NULL;
+  GtkWidget *header;
+
+  g_assert (GBP_IS_HISTORY_FRAME_ADDIN (addin));
+  g_assert (IDE_IS_FRAME (stack));
+
+  self->stack = stack;
+
+  actions = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (actions),
+                                   entries,
+                                   G_N_ELEMENTS (entries),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (stack),
+                                  "history",
+                                  G_ACTION_GROUP (actions));
+
+  header = ide_frame_get_titlebar (stack);
+
+  self->controls = g_object_new (GTK_TYPE_BOX,
+                                 "orientation", GTK_ORIENTATION_HORIZONTAL,
+                                 "sensitive", FALSE,
+                                 "visible", TRUE,
+                                 NULL);
+  g_signal_connect (self->controls,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->controls);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->controls), "linked");
+  gtk_container_add_with_properties (GTK_CONTAINER (header), GTK_WIDGET (self->controls),
+                                     "priority", -100,
+                                     NULL);
+
+  self->previous_button = g_object_new (GTK_TYPE_BUTTON,
+                                        "action-name", "history.move-previous-edit",
+                                        "child", g_object_new (GTK_TYPE_IMAGE,
+                                                               "icon-name", "go-previous-symbolic",
+                                                               "visible", TRUE,
+                                                               NULL),
+                                        "visible", TRUE,
+                                        NULL);
+  g_signal_connect (self->previous_button,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->previous_button);
+  gtk_container_add (GTK_CONTAINER (self->controls), GTK_WIDGET (self->previous_button));
+
+  self->next_button = g_object_new (GTK_TYPE_BUTTON,
+                                    "action-name", "history.move-next-edit",
+                                    "child", g_object_new (GTK_TYPE_IMAGE,
+                                                           "icon-name", "go-next-symbolic",
+                                                           "visible", TRUE,
+                                                           NULL),
+                                    "visible", TRUE,
+                                    NULL);
+  g_signal_connect (self->next_button,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->next_button);
+  gtk_container_add (GTK_CONTAINER (self->controls), GTK_WIDGET (self->next_button));
+
+  gbp_history_frame_addin_update (self);
+}
+
+static void
+gbp_history_frame_addin_unload (IdeFrameAddin *addin,
+                                       IdeFrame      *stack)
+{
+  GbpHistoryFrameAddin *self = (GbpHistoryFrameAddin *)addin;
+
+  g_assert (GBP_IS_HISTORY_FRAME_ADDIN (addin));
+  g_assert (IDE_IS_FRAME (stack));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (stack), "history", NULL);
+
+  g_clear_object (&self->back_store);
+  g_clear_object (&self->forward_store);
+
+  if (self->controls != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->controls));
+  if (self->next_button != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->next_button));
+  if (self->previous_button != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->previous_button));
+
+  self->stack = NULL;
+}
+
+static void
+gbp_history_frame_addin_set_view (IdeFrameAddin *addin,
+                                         IdePage       *view)
+{
+  GbpHistoryFrameAddin *self = (GbpHistoryFrameAddin *)addin;
+
+  g_assert (GBP_IS_HISTORY_FRAME_ADDIN (self));
+  g_assert (!view || IDE_IS_PAGE (view));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->controls), IDE_IS_EDITOR_PAGE (view));
+}
+
+static void
+frame_addin_iface_init (IdeFrameAddinInterface *iface)
+{
+  iface->load = gbp_history_frame_addin_load;
+  iface->unload = gbp_history_frame_addin_unload;
+  iface->set_page = gbp_history_frame_addin_set_view;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpHistoryFrameAddin, gbp_history_frame_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_FRAME_ADDIN,
+                                                frame_addin_iface_init))
+
+static void
+gbp_history_frame_addin_class_init (GbpHistoryFrameAddinClass *klass)
+{
+}
+
+static void
+gbp_history_frame_addin_init (GbpHistoryFrameAddin *self)
+{
+  self->back_store = g_list_store_new (GBP_TYPE_HISTORY_ITEM);
+  self->forward_store = g_list_store_new (GBP_TYPE_HISTORY_ITEM);
+}
+
+static void
+move_forward_to_back_store (GbpHistoryFrameAddin *self)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_HISTORY_FRAME_ADDIN (self));
+
+  /* Be certain we're not disposed */
+  if (self->forward_store == NULL || self->back_store == NULL)
+    IDE_EXIT;
+
+  while (g_list_model_get_n_items (G_LIST_MODEL (self->forward_store)))
+    {
+      g_autoptr(GbpHistoryItem) item = NULL;
+
+      item = g_list_model_get_item (G_LIST_MODEL (self->forward_store), 0);
+      g_list_store_remove (self->forward_store, 0);
+      g_list_store_append (self->back_store, item);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+gbp_history_frame_addin_remove_dups (GbpHistoryFrameAddin *self)
+{
+  guint n_items;
+
+  g_assert (GBP_IS_HISTORY_FRAME_ADDIN (self));
+  g_assert (self->forward_store != NULL);
+  g_assert (g_list_model_get_n_items (G_LIST_MODEL (self->forward_store)) == 0);
+
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->back_store));
+
+  /* Start from the oldest history item and work our way to the most
+   * recent item. Try to find any items later in the jump list which
+   * we can coallesce with. If so, remove the entry, preferring the
+   * more recent item.
+   */
+
+  for (guint i = 0; i < n_items; i++)
+    {
+      GbpHistoryItem *item;
+
+    try_again:
+      item = g_list_model_get_item (G_LIST_MODEL (self->back_store), i);
+
+      for (guint j = n_items; (j - 1) > i; j--)
+        {
+          g_autoptr(GbpHistoryItem) recent = NULL;
+
+          recent = g_list_model_get_item (G_LIST_MODEL (self->back_store), j - 1);
+
+          g_assert (recent != item);
+
+          if (gbp_history_item_chain (recent, item))
+            {
+              g_list_store_remove (self->back_store, i);
+              g_object_unref (item);
+              n_items--;
+              goto try_again;
+            }
+        }
+
+      g_object_unref (item);
+    }
+}
+
+void
+gbp_history_frame_addin_push (GbpHistoryFrameAddin *self,
+                                     GbpHistoryItem             *item)
+{
+  guint n_items;
+
+  IDE_ENTRY;
+
+  g_return_if_fail (GBP_IS_HISTORY_FRAME_ADDIN (self));
+  g_return_if_fail (GBP_IS_HISTORY_ITEM (item));
+  g_return_if_fail (self->back_store != NULL);
+  g_return_if_fail (self->forward_store != NULL);
+  g_return_if_fail (self->stack != NULL);
+
+  /* Ignore while we are navigating */
+  if (self->navigating != 0)
+    return;
+
+  /* Move all of our forward marks to the backward list */
+  move_forward_to_back_store (self);
+
+  /* Now add our new item to the list */
+  g_list_store_append (self->back_store, item);
+
+  /* Now remove dups in the list */
+  gbp_history_frame_addin_remove_dups (self);
+
+  /* Truncate from head if necessary */
+  n_items = g_list_model_get_n_items (G_LIST_MODEL (self->back_store));
+  if (n_items >= MAX_HISTORY_ITEMS)
+    g_list_store_remove (self->back_store, 0);
+
+  gbp_history_frame_addin_update (self);
+
+  IDE_EXIT;
+}
diff --git a/src/plugins/history/gbp-history-frame-addin.h b/src/plugins/history/gbp-history-frame-addin.h
new file mode 100644
index 000000000..f81aeedf4
--- /dev/null
+++ b/src/plugins/history/gbp-history-frame-addin.h
@@ -0,0 +1,34 @@
+/* gbp-history-frame-addin.h
+ *
+ * Copyright 2017-2019 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 "gbp-history-item.h"
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_HISTORY_FRAME_ADDIN (gbp_history_frame_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpHistoryFrameAddin, gbp_history_frame_addin, GBP, HISTORY_FRAME_ADDIN, GObject)
+
+void gbp_history_frame_addin_push (GbpHistoryFrameAddin *self,
+                                   GbpHistoryItem       *item);
+
+G_END_DECLS
diff --git a/src/plugins/history/gbp-history-item.c b/src/plugins/history/gbp-history-item.c
index d69f92f84..290bf1bc5 100644
--- a/src/plugins/history/gbp-history-item.c
+++ b/src/plugins/history/gbp-history-item.c
@@ -1,6 +1,6 @@
 /* gbp-history-item.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-history-item"
@@ -72,11 +74,11 @@ gbp_history_item_init (GbpHistoryItem *self)
 GbpHistoryItem *
 gbp_history_item_new (GtkTextMark *mark)
 {
-  GtkTextIter iter;
+  g_autoptr(IdeContext) context = NULL;
   GbpHistoryItem *item;
   GtkTextBuffer *buffer;
-  IdeContext *context;
-  IdeFile *file;
+  GtkTextIter iter;
+  GFile *file;
 
   g_return_val_if_fail (GTK_IS_TEXT_MARK (mark), NULL);
 
@@ -86,16 +88,16 @@ gbp_history_item_new (GtkTextMark *mark)
   item = g_object_new (GBP_TYPE_HISTORY_ITEM, NULL);
   item->mark = g_object_ref (mark);
 
-  context = ide_buffer_get_context (IDE_BUFFER (buffer));
+  context = ide_buffer_ref_context (IDE_BUFFER (buffer));
   g_set_weak_pointer (&item->context, context);
 
   gtk_text_buffer_get_iter_at_mark (buffer, &iter, mark);
   item->line = gtk_text_iter_get_line (&iter);
 
   file = ide_buffer_get_file (IDE_BUFFER (buffer));
-  item->file = g_object_ref (ide_file_get_file (file));
+  item->file = g_object_ref (file);
 
-  return item;
+  return g_steal_pointer (&item);
 }
 
 gboolean
@@ -136,8 +138,8 @@ gbp_history_item_chain (GbpHistoryItem *self,
 gchar *
 gbp_history_item_get_label (GbpHistoryItem *self)
 {
+  g_autofree gchar *title = NULL;
   GtkTextBuffer *buffer;
-  const gchar *title;
   GtkTextIter iter;
   guint line;
 
@@ -151,7 +153,7 @@ gbp_history_item_get_label (GbpHistoryItem *self)
 
   gtk_text_buffer_get_iter_at_mark (buffer, &iter, self->mark);
   line = gtk_text_iter_get_line (&iter) + 1;
-  title = ide_buffer_get_title (IDE_BUFFER (buffer));
+  title = ide_buffer_dup_title (IDE_BUFFER (buffer));
 
   return g_strdup_printf ("%s <span fgcolor='32767'>%u</span>", title, line);
 }
@@ -160,11 +162,13 @@ gbp_history_item_get_label (GbpHistoryItem *self)
  * gbp_history_item_get_location:
  * @self: a #GbpHistoryItem
  *
- * Gets an #IdeSourceLocation represented by this item.
+ * Gets an #IdeLocation represented by this item.
  *
- * Returns: (transfer full): A new #IdeSourceLocation
+ * Returns: (transfer full): A new #IdeLocation
+ *
+ * Since: 3.32
  */
-IdeSourceLocation *
+IdeLocation *
 gbp_history_item_get_location (GbpHistoryItem *self)
 {
   GtkTextBuffer *buffer;
@@ -176,13 +180,8 @@ gbp_history_item_get_location (GbpHistoryItem *self)
   if (self->context == NULL)
     return NULL;
 
-  buffer = gtk_text_mark_get_buffer (self->mark);
-
-  if (buffer == NULL)
-    {
-      g_autoptr(IdeFile) file = ide_file_new (self->context, self->file);
-      return ide_source_location_new (file, self->line, 0, 0);
-    }
+  if (!(buffer = gtk_text_mark_get_buffer (self->mark)))
+    return ide_location_new (self->file, self->line, 0);
 
   g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
   gtk_text_buffer_get_iter_at_mark (buffer, &iter, self->mark);
@@ -194,6 +193,8 @@ gbp_history_item_get_location (GbpHistoryItem *self)
  * gbp_history_item_get_file:
  *
  * Returns: (transfer none): a #GFile.
+ *
+ * Since: 3.32
  */
 GFile *
 gbp_history_item_get_file (GbpHistoryItem *self)
@@ -210,6 +211,8 @@ gbp_history_item_get_file (GbpHistoryItem *self)
  *
  * If the text mark is still valid, it will be used to locate the
  * mark which may have moved.
+ *
+ * Since: 3.32
  */
 guint
 gbp_history_item_get_line (GbpHistoryItem *self)
diff --git a/src/plugins/history/gbp-history-item.h b/src/plugins/history/gbp-history-item.h
index c49be055e..57292edf6 100644
--- a/src/plugins/history/gbp-history-item.h
+++ b/src/plugins/history/gbp-history-item.h
@@ -1,6 +1,6 @@
 /* gbp-history-item.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-editor.h>
 
 G_BEGIN_DECLS
 
@@ -26,12 +28,12 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpHistoryItem, gbp_history_item, GBP, HISTORY_ITEM, GObject)
 
-GbpHistoryItem    *gbp_history_item_new          (GtkTextMark    *mark);
-gchar             *gbp_history_item_get_label    (GbpHistoryItem *self);
-IdeSourceLocation *gbp_history_item_get_location (GbpHistoryItem *self);
-GFile             *gbp_history_item_get_file     (GbpHistoryItem *self);
-guint              gbp_history_item_get_line     (GbpHistoryItem *self);
-gboolean           gbp_history_item_chain        (GbpHistoryItem *self,
-                                                  GbpHistoryItem *other);
+GbpHistoryItem *gbp_history_item_new          (GtkTextMark    *mark);
+gchar          *gbp_history_item_get_label    (GbpHistoryItem *self);
+IdeLocation    *gbp_history_item_get_location (GbpHistoryItem *self);
+GFile          *gbp_history_item_get_file     (GbpHistoryItem *self);
+guint           gbp_history_item_get_line     (GbpHistoryItem *self);
+gboolean        gbp_history_item_chain        (GbpHistoryItem *self,
+                                               GbpHistoryItem *other);
 
 G_END_DECLS
diff --git a/src/plugins/history/history-plugin.c b/src/plugins/history/history-plugin.c
new file mode 100644
index 000000000..fd76bbff6
--- /dev/null
+++ b/src/plugins/history/history-plugin.c
@@ -0,0 +1,37 @@
+/* history-plugin.c
+ *
+ * Copyright 2017-2019 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
+ */
+
+#include <libide-gui.h>
+#include <libide-editor.h>
+#include <libpeas/peas.h>
+
+#include "gbp-history-editor-page-addin.h"
+#include "gbp-history-frame-addin.h"
+
+_IDE_EXTERN void
+_gbp_history_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                              GBP_TYPE_HISTORY_EDITOR_PAGE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_FRAME_ADDIN,
+                                              GBP_TYPE_HISTORY_FRAME_ADDIN);
+}
diff --git a/src/plugins/history/history.gresource.xml b/src/plugins/history/history.gresource.xml
index fdb0a5e24..bb8a986b7 100644
--- a/src/plugins/history/history.gresource.xml
+++ b/src/plugins/history/history.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/history">
     <file>history.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/history/history.plugin b/src/plugins/history/history.plugin
index a547cbf5c..9637ae72d 100644
--- a/src/plugins/history/history.plugin
+++ b/src/plugins/history/history.plugin
@@ -1,9 +1,9 @@
 [Plugin]
-Module=history-plugin
-Name=Edit History
-Description=Tracks edits in buffer to provide navigation history
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2017 Christian Hergert
-Depends=editor
 Builtin=true
-Embedded=gbp_history_register_types
+Copyright=Copyright © 2017 Christian Hergert
+Depends=editor;
+Description=Tracks edits in buffer to provide navigation history
+Embedded=_gbp_history_register_types
+Module=history
+Name=Editor History
diff --git a/src/plugins/history/meson.build b/src/plugins/history/meson.build
index 6c58e54cb..78d5e5cc1 100644
--- a/src/plugins/history/meson.build
+++ b/src/plugins/history/meson.build
@@ -1,22 +1,14 @@
-if get_option('with_history')
+plugins_sources += files([
+  'gbp-history-editor-page-addin.c',
+  'gbp-history-frame-addin.c',
+  'gbp-history-item.c',
+  'history-plugin.c',
+])
 
-history_resources = gnome.compile_resources(
+plugin_history_resources = gnome.compile_resources(
   'history-resources',
   'history.gresource.xml',
   c_name: 'gbp_history',
 )
 
-history_sources = [
-  'gbp-history-layout-stack-addin.c',
-  'gbp-history-layout-stack-addin.h',
-  'gbp-history-editor-view-addin.c',
-  'gbp-history-editor-view-addin.h',
-  'gbp-history-item.c',
-  'gbp-history-item.h',
-  'gbp-history-plugin.c',
-]
-
-gnome_builder_plugins_sources += files(history_sources)
-gnome_builder_plugins_sources += history_resources[0]
-
-endif
+plugins_sources += plugin_history_resources[0]
diff --git a/src/plugins/html-completion/html-completion-plugin.c 
b/src/plugins/html-completion/html-completion-plugin.c
index 57ae0b024..fb2781ed6 100644
--- a/src/plugins/html-completion/html-completion-plugin.c
+++ b/src/plugins/html-completion/html-completion-plugin.c
@@ -1,6 +1,6 @@
 /* html-completion-plugin.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,15 +14,19 @@
  *
  * 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
  */
 
-#include <ide.h>
+#include "config.h"
+
+#include <libide-sourceview.h>
 #include <libpeas/peas.h>
 
 #include "ide-html-completion-provider.h"
 
-void
-ide_html_completion_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_ide_html_completion_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_COMPLETION_PROVIDER,
diff --git a/src/plugins/html-completion/html-completion.gresource.xml 
b/src/plugins/html-completion/html-completion.gresource.xml
index cadc5638f..b6939bdf5 100644
--- a/src/plugins/html-completion/html-completion.gresource.xml
+++ b/src/plugins/html-completion/html-completion.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/html-completion">
     <file>html-completion.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/html-completion/html-completion.plugin 
b/src/plugins/html-completion/html-completion.plugin
index 5fea16855..33c0b4b0c 100644
--- a/src/plugins/html-completion/html-completion.plugin
+++ b/src/plugins/html-completion/html-completion.plugin
@@ -1,9 +1,9 @@
 [Plugin]
-Module=html-completion-plugin
-Name=HTML Auto-Completion
-Description=Provides auto-completion when authoring HTML documents
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
+Copyright=Copyright © 2015 Christian Hergert
+Description=Provides auto-completion when authoring HTML documents
+Embedded=_ide_html_completion_register_types
+Module=html-completion
+Name=HTML Auto-Completion
 X-Completion-Provider-Languages=asp,dtl,html,php,css
-Embedded=ide_html_completion_register_types
diff --git a/src/plugins/html-completion/ide-html-completion-provider.c 
b/src/plugins/html-completion/ide-html-completion-provider.c
index ba00b39c3..462e3f5bd 100644
--- a/src/plugins/html-completion/ide-html-completion-provider.c
+++ b/src/plugins/html-completion/ide-html-completion-provider.c
@@ -1,6 +1,6 @@
 /* ide-html-completion-provider.c
  *
- * Copyright 2014 Christian Hergert <christian hergert me>
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "html-completion"
diff --git a/src/plugins/html-completion/ide-html-completion-provider.h 
b/src/plugins/html-completion/ide-html-completion-provider.h
index 223c09765..f494970c3 100644
--- a/src/plugins/html-completion/ide-html-completion-provider.h
+++ b/src/plugins/html-completion/ide-html-completion-provider.h
@@ -1,6 +1,6 @@
 /* ide-html-completion-provider.h
  *
- * Copyright 2014 Christian Hergert <christian hergert me>
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/html-completion/ide-html-proposal.c b/src/plugins/html-completion/ide-html-proposal.c
index 6e081b6be..24efc3b6b 100644
--- a/src/plugins/html-completion/ide-html-proposal.c
+++ b/src/plugins/html-completion/ide-html-proposal.c
@@ -1,6 +1,6 @@
 /* ide-html-proposal.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,12 +14,14 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-html-proposal"
 
+#include "config.h"
+
 #include "ide-html-proposal.h"
 
 struct _IdeHtmlProposal
diff --git a/src/plugins/html-completion/ide-html-proposal.h b/src/plugins/html-completion/ide-html-proposal.h
index 01fcafb6c..1d50c07cb 100644
--- a/src/plugins/html-completion/ide-html-proposal.h
+++ b/src/plugins/html-completion/ide-html-proposal.h
@@ -1,6 +1,6 @@
 /* ide-html-proposal.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-sourceview.h>
 
 #include "ide-html-proposals.h"
 
diff --git a/src/plugins/html-completion/ide-html-proposals.c 
b/src/plugins/html-completion/ide-html-proposals.c
index 944964502..952590331 100644
--- a/src/plugins/html-completion/ide-html-proposals.c
+++ b/src/plugins/html-completion/ide-html-proposals.c
@@ -1,6 +1,6 @@
 /* ide-html-proposals.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,12 +14,14 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-html-proposals"
 
+#include "config.h"
+
 #include "ide-html-proposal.h"
 #include "ide-html-proposals.h"
 
diff --git a/src/plugins/html-completion/ide-html-proposals.h 
b/src/plugins/html-completion/ide-html-proposals.h
index cf1972ed0..467398312 100644
--- a/src/plugins/html-completion/ide-html-proposals.h
+++ b/src/plugins/html-completion/ide-html-proposals.h
@@ -1,6 +1,6 @@
 /* ide-html-proposals.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/html-completion/meson.build b/src/plugins/html-completion/meson.build
index 1b5b78f83..a44ec62f4 100644
--- a/src/plugins/html-completion/meson.build
+++ b/src/plugins/html-completion/meson.build
@@ -1,19 +1,18 @@
-if get_option('with_html_completion')
+if get_option('plugin_html_completion')
 
-html_completion_resources = gnome.compile_resources(
-  'html-completion-resources',
-  'html-completion.gresource.xml',
-  c_name: 'gbp_html_completion',
-)
-
-html_completion_sources = [
+plugins_sources += files([
   'html-completion-plugin.c',
   'ide-html-completion-provider.c',
   'ide-html-proposal.c',
   'ide-html-proposals.c',
-]
+])
+
+plugin_html_completion_resources = gnome.compile_resources(
+  'gbp-html-completion-resources',
+  'html-completion.gresource.xml',
+  c_name: 'gbp_html_completion',
+)
 
-gnome_builder_plugins_sources += files(html_completion_sources)
-gnome_builder_plugins_sources += html_completion_resources[0]
+plugins_sources += plugin_html_completion_resources[0]
 
 endif
diff --git a/src/plugins/html-preview/gtk/menus.ui b/src/plugins/html-preview/gtk/menus.ui
index ebaa9a36e..729b91e0d 100644
--- a/src/plugins/html-preview/gtk/menus.ui
+++ b/src/plugins/html-preview/gtk/menus.ui
@@ -1,11 +1,11 @@
 <?xml version="1.0"?>
 <interface>
-  <menu id="ide-editor-view-document-menu">
+  <menu id="ide-editor-page-document-menu">
     <section id="editor-document-section">
       <item>
         <attribute name="after">editor-document-open-in-new-frame</attribute>
         <attribute name="label" translatable="yes">Open Preview</attribute>
-        <attribute name="action">editor-view.preview-as-html</attribute>
+        <attribute name="action">editor-page.preview-as-html</attribute>
       </item>
     </section>
   </menu>
diff --git a/src/plugins/html-preview/html-preview.gresource.xml 
b/src/plugins/html-preview/html-preview.gresource.xml
index bcc1fe449..0fb4f242c 100644
--- a/src/plugins/html-preview/html-preview.gresource.xml
+++ b/src/plugins/html-preview/html-preview.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins/html_preview">
+  <gresource prefix="/plugins/html_preview">
     <file>js/markdown-view.js</file>
     <file>js/marked.js</file>
     <file>css/markdown.css</file>
diff --git a/src/plugins/html-preview/html-preview.plugin b/src/plugins/html-preview/html-preview.plugin
index 4b6c7d751..6a1e9c106 100644
--- a/src/plugins/html-preview/html-preview.plugin
+++ b/src/plugins/html-preview/html-preview.plugin
@@ -1,10 +1,12 @@
 [Plugin]
-Module=html_preview
-Loader=python3
-Name=HTML, reStructuredText and Markdown Preview
-Description=Live preview of HTML, reStructuredText and Markdown documents.
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
-Depends=webkit
+Copyright=Copyright © 2015 Christian Hergert
+Depends=webkit;
+Description=Live preview of HTML, reStructuredText and Markdown documents.
+Loader=python3
+Module=html_preview
+Name=HTML, reStructuredText and Markdown Preview
 X-Editor-View-Languages=*
+X-Builder-ABI=@PACKAGE_ABI@
+X-Has-Resources=true
diff --git a/src/plugins/html-preview/html_preview.py b/src/plugins/html-preview/html_preview.py
index 641f053ef..677f917db 100644
--- a/src/plugins/html-preview/html_preview.py
+++ b/src/plugins/html-preview/html_preview.py
@@ -29,10 +29,6 @@ import sys
 import subprocess
 import threading
 
-gi.require_version('Gtk', '3.0')
-gi.require_version('Ide', '1.0')
-gi.require_version('WebKit2', '4.0')
-
 from gi.repository import Dazzle
 from gi.repository import GLib
 from gi.repository import Gio
@@ -124,13 +120,29 @@ class HtmlPreviewData(GObject.Object, Ide.ApplicationAddin):
 
     def get_data(self, name):
         # Hold onto the GBytes to avoid copying the buffer
-        path = os.path.join('/org/gnome/builder/plugins/html_preview', name)
+        path = os.path.join('/plugins/html_preview', name)
         return Gio.resources_lookup_data(path, 0)
 
 class HtmlWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
+    workbench = None
+
     def do_load(self, workbench):
         self.workbench = workbench
 
+    def do_unload(self, workbench):
+        self.workbench = None
+
+    def find_notif_by_id(self, id):
+        notifs = self.workbench.get_context().get_child_typed(Ide.Notifications)
+        return notifs.find_by_id(id)
+
+    def withdraw_notification(self, id):
+        notifs = self.workbench.get_context().get_child_typed(Ide.Notifications)
+        notif = notifs.find_by_id(id)
+        if notif is not None:
+            notif.withdraw()
+
+    def do_workspace_added(self, workspace):
         group = Gio.SimpleActionGroup()
 
         self.install_action = Gio.SimpleAction(name='install-docutils', enabled=True)
@@ -141,21 +153,26 @@ class HtmlWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
         self.install_action.connect('activate', lambda *_: self.install_sphinx())
         group.insert(self.install_action)
 
-        self.workbench.insert_action_group('html-preview', group)
+        workspace.insert_action_group('html-preview', group)
 
-    def do_unload(self, workbench):
-        workbench.insert_action_group('html-preview', None)
-        self.workbench = None
+    def do_workspace_removed(self, workspace):
+        workspace.insert_action_group('html-preview', None)
 
     def install_docutils(self):
         transfer = Ide.PkconTransfer(packages=['python3-docutils'])
-        manager = Gio.Application.get_default().get_transfer_manager()
+        manager = Ide.TransferManager.get_default()
+
+        notif = transfer.create_notification()
+        notif.attach(self.workbench.get_context())
 
         manager.execute_async(transfer, None, self.docutils_installed, None)
 
     def install_sphinx(self):
         transfer = Ide.PkconTransfer(packages=['python3-sphinx'])
-        manager = Gio.Application.get_default().get_transfer_manager()
+        manager = Ide.TransferManager.get_default()
+
+        notif = transfer.create_notification()
+        notif.attach(self.workbench.get_context())
 
         manager.execute_async(transfer, None, self.sphinx_installed, None)
 
@@ -170,7 +187,7 @@ class HtmlWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
             return
 
         can_preview_rst = True
-        self.workbench.pop_message('org.gnome.builder.docutils.install')
+        self.withdraw_notification('org.gnome.builder.html-preview.docutils')
 
     def sphinx_installed(self, object, result, data):
         global can_preview_sphinx
@@ -183,19 +200,19 @@ class HtmlWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
             return
 
         can_preview_sphinx = True
-        self.workbench.pop_message('org.gnome.builder.sphinx.install')
+        self.withdraw_notification('org.gnome.builder.html-preview.docutils')
+        self.withdraw_notification('org.gnome.builder.html-preview.sphinx')
 
-
-class HtmlPreviewAddin(GObject.Object, Ide.EditorViewAddin):
+class HtmlPreviewAddin(GObject.Object, Ide.EditorPageAddin):
     def do_load(self, view):
-        self.workbench = view.get_ancestor(Ide.Workbench)
+        self.context = Ide.widget_get_context(view)
         self.view = view
 
         self.can_preview = False
         self.sphinx_basedir = None
         self.sphinx_builddir = None
 
-        group = view.get_action_group('editor-view')
+        group = view.get_action_group('editor-page')
 
         self.action = Gio.SimpleAction(name='preview-as-html', enabled=True)
         self.activate_handler = self.action.connect('activate', self.preview_activated)
@@ -212,17 +229,17 @@ class HtmlPreviewAddin(GObject.Object, Ide.EditorViewAddin):
         controller.add_command_action('org.gnome.builder.html-preview.preview',
                                       '<Control><Alt>p',
                                       Dazzle.ShortcutPhase.CAPTURE,
-                                      'editor-view.preview-as-html')
+                                      'editor-page.preview-as-html')
 
     def do_unload(self, view):
         self.action.disconnect(self.activate_handler)
 
-        group = view.get_action_group('editor-view')
+        group = view.get_action_group('editor-page')
         group.remove_action('preview-as-html')
 
         self.action = None
         self.view = None
-        self.workbench = None
+        self.context = None
 
     def do_language_changed(self, language_id):
         enabled = (language_id in ('html', 'markdown', 'rst'))
@@ -233,7 +250,7 @@ class HtmlPreviewAddin(GObject.Object, Ide.EditorViewAddin):
         if self.lang_id == 'rst':
             if not self.sphinx_basedir:
                 document = self.view.get_buffer()
-                path = document.get_file().get_file().get_path()
+                path = document.get_file().get_path()
                 self.sphinx_basedir = self.search_sphinx_base_dir(path)
 
             if self.sphinx_basedir:
@@ -273,7 +290,7 @@ class HtmlPreviewAddin(GObject.Object, Ide.EditorViewAddin):
                 return
 
         document = view.get_buffer()
-        web_view = HtmlPreviewView(document,
+        web_view = HtmlPreviewPage(document,
                                    self.sphinx_basedir,
                                    self.sphinx_builddir,
                                    visible=True)
@@ -295,9 +312,7 @@ class HtmlPreviewAddin(GObject.Object, Ide.EditorViewAddin):
         self.action.set_enabled(True)
 
     def search_sphinx_base_dir(self, path):
-        context = self.workbench.get_context()
-        vcs = context.get_vcs()
-        working_dir = vcs.get_working_directory().get_path()
+        working_dir = self.context.ref_workdir()
 
         try:
             if os.path.commonpath([working_dir, path]) != working_dir:
@@ -321,27 +336,26 @@ class HtmlPreviewAddin(GObject.Object, Ide.EditorViewAddin):
             folder = os.path.dirname(folder)
 
     def show_missing_docutils_message(self, view):
-        message = Ide.WorkbenchMessage(
-            id='org.gnome.builder.docutils.install',
+        notif = Ide.Notification(
+            id='org.gnome.builder.html-preview.docutils',
             title=_('Your computer is missing python3-docutils'),
-            show_close_button=True,
-            visible=True)
-
-        message.add_action(_('Install'), 'html-preview.install-docutils')
-        self.workbench.push_message(message)
+            body=_('This package is necessary to provide previews of markup-based documents.'),
+            icon_name='dialog-warning-symbolic',
+            urgent=True)
+        notif.add_button(_('Install Package'), None, 'html-preview.install-docutils')
+        notif.attach(self.context)
 
     def show_missing_sphinx_message(self, view):
-        message = Ide.WorkbenchMessage(
-            id='org.gnome.builder.sphinx.install',
+        notif = Ide.Notification(
+            id='org.gnome.builder.html-preview.sphinx',
             title=_('Your computer is missing python3-sphinx'),
-            show_close_button=True,
-            visible=True)
-
-        message.add_action(_('Install'), 'html-preview.install-sphinx')
-        self.workbench.push_message(message)
+            body=_('This package is necessary to provide previews of markup-based documents.'),
+            icon_name='dialog-warning-symbolic',
+            urgent=True)
+        notif.add_button(_('Install Package'), None, 'html-preview.install-sphinx')
+        notif.attach(self.context)
 
-
-class HtmlPreviewView(Ide.LayoutView):
+class HtmlPreviewPage(Ide.Page):
     markdown = False
     rst = False
 
@@ -352,13 +366,16 @@ class HtmlPreviewView(Ide.LayoutView):
     def __init__(self, document, sphinx_basedir, sphinx_builddir, *args, **kwargs):
         global old_open
 
-        super().__init__(*args, **kwargs)
+        print("Test");
+
+        Ide.Page.__init__(self, *args, **kwargs)
+        #super().__init__(self, *args, **kwargs)
 
         self.sphinx_basedir = sphinx_basedir
         self.sphinx_builddir = sphinx_builddir
         self.document = document
 
-        self.webview = WebKit2.WebView(visible=True, expand=True)
+        self.webview = WebKit2.WebView.new(expand=True, visible=True)
         self.add(self.webview)
 
         settings = self.webview.get_settings()
@@ -429,7 +446,7 @@ class HtmlPreviewView(Ide.LayoutView):
                          name='sphinx-rst-thread').start()
 
     def purge_cache(self, basedir, builddir, document):
-        path = document.get_file().get_file().get_path()
+        path = document.get_file().get_path()
         rel_path = os.path.relpath(path, start=basedir)
         rel_path_doctree = os.path.splitext(rel_path)[0] + '.doctree'
         doctree_path = os.path.join(builddir, '.doctrees', rel_path_doctree)
@@ -483,7 +500,7 @@ class HtmlPreviewView(Ide.LayoutView):
             state.need_build = True
             return
 
-        gfile = self.document.get_file().get_file()
+        gfile = self.document.get_file()
         base_uri = gfile.get_uri()
 
         begin, end = self.document.get_bounds()
diff --git a/src/plugins/html-preview/meson.build b/src/plugins/html-preview/meson.build
index b992490fd..4810516d0 100644
--- a/src/plugins/html-preview/meson.build
+++ b/src/plugins/html-preview/meson.build
@@ -1,4 +1,4 @@
-if get_option('with_html_preview')
+if get_option('plugin_html_preview')
 
 html_preview_resources = gnome.compile_resources(
   'html_preview',
@@ -13,7 +13,7 @@ install_data('html_preview.py', install_dir: plugindir)
 configure_file(
           input: 'html-preview.plugin',
          output: 'html-preview.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/jedi/jedi.plugin b/src/plugins/jedi/jedi.plugin
index b99fc5985..091e361eb 100644
--- a/src/plugins/jedi/jedi.plugin
+++ b/src/plugins/jedi/jedi.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=jedi_plugin
-Loader=python3
-Name=Python Auto-Completion (Jedi)
-Description=Provides autocompletion features for the Python programming language.
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
+Copyright=Copyright © 2015-2019 Christian Hergert
+Description=Provides autocompletion features for the Python programming language.
+Loader=python3
+Module=jedi_plugin
+Name=Python Auto-Completion (Jedi)
+X-Builder-ABI=@PACKAGE_ABI@
 X-Completion-Provider-Languages=python,python3
diff --git a/src/plugins/jedi/jedi_plugin.py b/src/plugins/jedi/jedi_plugin.py
index 985c1f605..119832e88 100644
--- a/src/plugins/jedi/jedi_plugin.py
+++ b/src/plugins/jedi/jedi_plugin.py
@@ -4,7 +4,7 @@
 # jedi_plugin.py
 #
 # Copyright 2015 Elad Alfassa <elad fedoraproject org>
-# Copyright 2015 Christian Hergert <chris dronelabs com>
+# Copyright 2015-2019 Christian Hergert <chris dronelabs 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
@@ -35,11 +35,6 @@ import os.path
 import sqlite3
 import threading
 
-gi.require_version('GIRepository', '2.0')
-gi.require_version('Gtk', '3.0')
-gi.require_version('GtkSource', '4')
-gi.require_version('Ide', '1.0')
-
 from collections import OrderedDict
 
 from gi.importer import DynamicImporter
@@ -484,7 +479,7 @@ class JediCompletionProvider(Ide.Object, Ide.CompletionProvider):
 
         begin, end = buffer.get_bounds()
 
-        task.filename = buffer.get_file().get_file().get_path()
+        task.filename = buffer.get_file().get_path()
         task.line = iter.get_line()
         task.line_offset = iter.get_line_offset()
         #if task.line_offset > 0:
diff --git a/src/plugins/jedi/meson.build b/src/plugins/jedi/meson.build
index 55ce52d6e..ef7a4e113 100644
--- a/src/plugins/jedi/meson.build
+++ b/src/plugins/jedi/meson.build
@@ -1,11 +1,11 @@
-if get_option('with_jedi')
+if get_option('plugin_jedi')
 
 install_data('jedi_plugin.py', install_dir: plugindir)
 
 configure_file(
           input: 'jedi.plugin',
          output: 'jedi.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/jhbuild/jhbuild.plugin b/src/plugins/jhbuild/jhbuild.plugin
index 0a7068d29..5388e6002 100644
--- a/src/plugins/jhbuild/jhbuild.plugin
+++ b/src/plugins/jhbuild/jhbuild.plugin
@@ -1,8 +1,9 @@
 [Plugin]
-Module=jhbuild_plugin
-Name=JHBuild
-Description=Provides support for building with JHBuild
 Authors=Patrick Griffis <tingping tingping se>
+Builtin=true
 Copyright=Copyright © 2016 Patrick Griffis
+Description=Provides support for building with JHBuild
 Loader=python3
-Builtin=true
+Module=jhbuild_plugin
+Name=JHBuild
+X-Builder-ABI=@PACKAGE_ABI@
diff --git a/src/plugins/jhbuild/jhbuild_plugin.py b/src/plugins/jhbuild/jhbuild_plugin.py
index ad4568c8b..5c8271e4c 100644
--- a/src/plugins/jhbuild/jhbuild_plugin.py
+++ b/src/plugins/jhbuild/jhbuild_plugin.py
@@ -16,18 +16,20 @@
 #
 # 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
 
-import gi
 import os
 
-gi.require_version('Ide', '1.0')
-
 from gi.repository import GLib
 from gi.repository import GObject
 from gi.repository import Gio
 from gi.repository import Ide
 
+_ = Ide.gettext
+
 class JhbuildRuntime(Ide.Runtime):
+    __gtype_name__ = 'JhbuildRuntime'
 
     def __init__(self, *args, **kwargs):
         self.jhbuild_path = kwargs.get('executable_path', None)
@@ -86,7 +88,8 @@ class JhbuildRuntime(Ide.Runtime):
         except GLib.Error:
             return False
 
-class JhbuildRuntimeProvider(GObject.Object, Ide.RuntimeProvider):
+class JhbuildRuntimeProvider(Ide.Object, Ide.RuntimeProvider):
+    __gtype_name__ = 'JhbuildRuntimeProvider'
 
     def __init__(self, *args, **kwargs):
         super().__init__(self, *args, **kwargs)
@@ -115,15 +118,16 @@ class JhbuildRuntimeProvider(GObject.Object, Ide.RuntimeProvider):
     def do_load(self, manager):
         jhbuild_path = self._get_jhbuild_path()
         if jhbuild_path is not None:
-            context = manager.get_context()
-            runtime = JhbuildRuntime(context=context,
-                                     id='jhbuild',
+            runtime = JhbuildRuntime(id='jhbuild',
+                                     category=_('Host System'),
                                      display_name='JHBuild',
                                      executable_path=jhbuild_path)
+            self.append(runtime)
             manager.add(runtime)
             self.runtimes.append(runtime)
 
     def do_unload(self, manager):
         for runtime in self.runtimes:
             manager.remove(runtime)
+            runtime.destroy()
         self.runtimes = []
diff --git a/src/plugins/jhbuild/meson.build b/src/plugins/jhbuild/meson.build
index 2c44a355d..35e3f9d8c 100644
--- a/src/plugins/jhbuild/meson.build
+++ b/src/plugins/jhbuild/meson.build
@@ -1,11 +1,11 @@
-if get_option('with_jhbuild')
+if get_option('plugin_jhbuild')
 
 install_data('jhbuild_plugin.py', install_dir: plugindir)
 
 configure_file(
           input: 'jhbuild.plugin',
          output: 'jhbuild.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/ls/gbp-ls-model.c b/src/plugins/ls/gbp-ls-model.c
index 252b0b756..ba81e51ac 100644
--- a/src/plugins/ls/gbp-ls-model.c
+++ b/src/plugins/ls/gbp-ls-model.c
@@ -1,6 +1,6 @@
 /* gbp-ls-model.c
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -18,11 +18,11 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-ls-model"
 
-#include <ide.h>
+#include "config.h"
+
+#include <libide-gui.h>
 
 #include "gbp-ls-model.h"
 
diff --git a/src/plugins/ls/gbp-ls-model.h b/src/plugins/ls/gbp-ls-model.h
index 00993a761..ea4633674 100644
--- a/src/plugins/ls/gbp-ls-model.h
+++ b/src/plugins/ls/gbp-ls-model.h
@@ -1,6 +1,6 @@
 /* gbp-ls-model.h
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
diff --git a/src/plugins/ls/gbp-ls-page.c b/src/plugins/ls/gbp-ls-page.c
new file mode 100644
index 000000000..2b86bc75c
--- /dev/null
+++ b/src/plugins/ls/gbp-ls-page.c
@@ -0,0 +1,349 @@
+/* gbp-ls-page.c
+ *
+ * Copyright 2018-2019 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-ls-page"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gbp-ls-model.h"
+#include "gbp-ls-page.h"
+
+struct _GbpLsPage
+{
+  IdePage            parent_instance;
+
+  GCancellable      *model_cancellable;
+  GbpLsModel        *model;
+
+  GtkScrolledWindow *scroller;
+  GtkTreeView       *tree_view;
+  GtkTreeViewColumn *modified_column;
+  GtkCellRenderer   *modified_cell;
+  GtkTreeViewColumn *size_column;
+  GtkCellRenderer   *size_cell;
+
+  guint              close_on_activate : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_CLOSE_ON_ACTIVATE,
+  PROP_DIRECTORY,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (GbpLsPage, gbp_ls_page, IDE_TYPE_PAGE)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_ls_page_row_activated_cb (GbpLsPage         *self,
+                              GtkTreePath       *path,
+                              GtkTreeViewColumn *column,
+                              GtkTreeView       *tree_view)
+{
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+
+  g_assert (GBP_IS_LS_PAGE (self));
+  g_assert (path != NULL);
+
+  if ((model = gtk_tree_view_get_model (tree_view)) &&
+      gtk_tree_model_get_iter (model, &iter, path))
+    {
+      g_autoptr(GFile) file = NULL;
+      GFileType file_type;
+
+      gtk_tree_model_get (model, &iter,
+                          GBP_LS_MODEL_COLUMN_FILE, &file,
+                          GBP_LS_MODEL_COLUMN_TYPE, &file_type,
+                          -1);
+
+      if (file_type == G_FILE_TYPE_DIRECTORY)
+        gbp_ls_page_set_directory (self, file);
+      else
+        {
+          IdeWorkbench *workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+
+          ide_workbench_open_async (workbench,
+                                    file,
+                                    NULL,
+                                    IDE_BUFFER_OPEN_FLAGS_NONE,
+                                    NULL, NULL, NULL);
+
+          if (self->close_on_activate)
+            dzl_gtk_widget_action (GTK_WIDGET (self), "frame", "close-page", NULL);
+        }
+    }
+}
+
+static void
+modified_cell_data_func (GtkCellLayout   *cell_layout,
+                         GtkCellRenderer *cell,
+                         GtkTreeModel    *tree_model,
+                         GtkTreeIter     *iter,
+                         gpointer         data)
+{
+  g_autofree gchar *format = NULL;
+  g_autoptr(GDateTime) when = NULL;
+
+  gtk_tree_model_get (tree_model, iter,
+                      GBP_LS_MODEL_COLUMN_MODIFIED, &when,
+                      -1);
+  format = dzl_g_date_time_format_for_display (when);
+  g_object_set (cell, "text", format, NULL);
+}
+
+static void
+size_cell_data_func (GtkCellLayout   *cell_layout,
+                     GtkCellRenderer *cell,
+                     GtkTreeModel    *tree_model,
+                     GtkTreeIter     *iter,
+                     gpointer         data)
+{
+  g_autofree gchar *format = NULL;
+  gint64 size = -1;
+
+  gtk_tree_model_get (tree_model, iter,
+                      GBP_LS_MODEL_COLUMN_SIZE, &size,
+                      -1);
+  format = g_format_size (size);
+  g_object_set (cell, "text", format, NULL);
+}
+
+static void
+gbp_ls_page_finalize (GObject *object)
+{
+  GbpLsPage *self = (GbpLsPage *)object;
+
+  g_clear_object (&self->model_cancellable);
+  g_clear_object (&self->model);
+
+  G_OBJECT_CLASS (gbp_ls_page_parent_class)->finalize (object);
+}
+
+static void
+gbp_ls_page_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  GbpLsPage *self = GBP_LS_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      g_value_set_object (value, gbp_ls_page_get_directory (self));
+      break;
+
+    case PROP_CLOSE_ON_ACTIVATE:
+      g_value_set_boolean (value, self->close_on_activate);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_ls_page_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  GbpLsPage *self = GBP_LS_PAGE (object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      gbp_ls_page_set_directory (self, g_value_get_object (value));
+      break;
+
+    case PROP_CLOSE_ON_ACTIVATE:
+      self->close_on_activate = g_value_get_boolean (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_ls_page_class_init (GbpLsPageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->finalize = gbp_ls_page_finalize;
+  object_class->get_property = gbp_ls_page_get_property;
+  object_class->set_property = gbp_ls_page_set_property;
+
+  properties [PROP_DIRECTORY] =
+    g_param_spec_object ("directory",
+                         "Directory",
+                         "The directory to be displayed",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_CLOSE_ON_ACTIVATE] =
+    g_param_spec_boolean ("close-on-activate", NULL, NULL,
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/ls/gbp-ls-page.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpLsPage, modified_cell);
+  gtk_widget_class_bind_template_child (widget_class, GbpLsPage, modified_column);
+  gtk_widget_class_bind_template_child (widget_class, GbpLsPage, size_cell);
+  gtk_widget_class_bind_template_child (widget_class, GbpLsPage, size_column);
+  gtk_widget_class_bind_template_child (widget_class, GbpLsPage, scroller);
+  gtk_widget_class_bind_template_child (widget_class, GbpLsPage, tree_view);
+}
+
+static void
+gbp_ls_page_init (GbpLsPage *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  ide_page_set_icon_name (IDE_PAGE (self), "folder-symbolic");
+
+  g_signal_connect_object (self->tree_view,
+                           "row-activated",
+                           G_CALLBACK (gbp_ls_page_row_activated_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->size_column),
+                                      self->size_cell,
+                                      size_cell_data_func,
+                                      NULL, NULL);
+
+  gtk_cell_layout_set_cell_data_func (GTK_CELL_LAYOUT (self->modified_column),
+                                      self->modified_cell,
+                                      modified_cell_data_func,
+                                      NULL, NULL);
+}
+
+GtkWidget *
+gbp_ls_page_new (void)
+{
+  return g_object_new (GBP_TYPE_LS_PAGE, NULL);
+}
+
+GFile *
+gbp_ls_page_get_directory (GbpLsPage *self)
+{
+  g_return_val_if_fail (GBP_IS_LS_PAGE (self), NULL);
+
+  if (self->model != NULL)
+    return gbp_ls_model_get_directory (GBP_LS_MODEL (self->model));
+
+  return NULL;
+}
+
+static void
+gbp_ls_page_init_model_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  GbpLsModel *model = (GbpLsModel *)object;
+  g_autoptr(GbpLsPage) self = user_data;
+  g_autoptr(GError) error = NULL;
+  GtkTreeIter iter;
+
+  g_assert (GBP_IS_LS_MODEL (model));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GBP_IS_LS_PAGE (self));
+
+  if (model != self->model)
+    return;
+
+  if (!g_async_initable_init_finish (G_ASYNC_INITABLE (model), result, &error))
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+        ide_page_report_error (IDE_PAGE (self),
+                                      _("Failed to load directory: %s"),
+                                      error->message);
+      return;
+    }
+
+  gtk_tree_view_set_model (self->tree_view, GTK_TREE_MODEL (model));
+
+  if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (model), &iter))
+    {
+      GtkTreeSelection *selection;
+
+      selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->tree_view));
+      gtk_tree_selection_select_iter (selection, &iter);
+    }
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->tree_view));
+}
+
+void
+gbp_ls_page_set_directory (GbpLsPage *self,
+                           GFile     *directory)
+{
+  g_autofree gchar *title = NULL;
+  g_autofree gchar *name = NULL;
+  g_autoptr(GFile) local_directory = NULL;
+  GFile *old_directory;
+
+  g_return_if_fail (GBP_IS_LS_PAGE (self));
+  g_return_if_fail (!directory || G_IS_FILE (directory));
+
+  if (directory == NULL)
+    {
+      IdeContext *context = ide_widget_get_context (GTK_WIDGET (self));
+      directory = local_directory = ide_context_ref_workdir (context);
+    }
+
+  g_assert (G_IS_FILE (directory));
+
+  old_directory = gbp_ls_page_get_directory (self);
+
+  if (directory != NULL &&
+      old_directory != NULL &&
+      g_file_equal (directory, old_directory))
+    return;
+
+  g_clear_object (&self->model);
+
+  g_cancellable_cancel (self->model_cancellable);
+  g_clear_object (&self->model_cancellable);
+
+  self->model_cancellable = g_cancellable_new ();
+  self->model = gbp_ls_model_new (directory);
+
+  g_async_initable_init_async (G_ASYNC_INITABLE (self->model),
+                               G_PRIORITY_DEFAULT,
+                               self->model_cancellable,
+                               gbp_ls_page_init_model_cb,
+                               g_object_ref (self));
+
+  name = g_file_get_basename (directory);
+  title = g_strdup_printf (_("%s — Directory"), name);
+  ide_page_set_title (IDE_PAGE (self), title);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DIRECTORY]);
+}
diff --git a/src/plugins/ls/gbp-ls-page.h b/src/plugins/ls/gbp-ls-page.h
new file mode 100644
index 000000000..070db0690
--- /dev/null
+++ b/src/plugins/ls/gbp-ls-page.h
@@ -0,0 +1,36 @@
+/* gbp-ls-page.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_LS_PAGE (gbp_ls_page_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpLsPage, gbp_ls_page, GBP, LS_PAGE, IdePage)
+
+GtkWidget *gbp_ls_page_new           (void);
+GFile     *gbp_ls_page_get_directory (GbpLsPage *self);
+void       gbp_ls_page_set_directory (GbpLsPage *self,
+                                      GFile     *directory);
+
+G_END_DECLS
diff --git a/src/plugins/ls/gbp-ls-page.ui b/src/plugins/ls/gbp-ls-page.ui
new file mode 100644
index 000000000..38a50a7d3
--- /dev/null
+++ b/src/plugins/ls/gbp-ls-page.ui
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpLsPage" parent="IdePage">
+    <child>
+      <object class="GtkScrolledWindow" id="scroller">
+        <property name="vexpand">true</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkTreeView" id="tree_view">
+            <property name="vexpand">true</property>
+            <property name="headers-visible">true</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkTreeViewColumn" id="name_column">
+                <property name="title" translatable="yes">Name</property>
+                <property name="expand">true</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkCellRendererPixbuf" id="pixbuf_cell">
+                    <property name="xpad">8</property>
+                    <property name="ypad">6</property>
+                  </object>
+                  <attributes>
+                    <attribute name="gicon">0</attribute>
+                  </attributes>
+                  <cell-packing>
+                    <property name="expand">false</property>
+                  </cell-packing>
+                </child>
+                <child>
+                  <object class="GtkCellRendererText" id="name_cell">
+                    <property name="ellipsize">end</property>
+                  </object>
+                  <attributes>
+                    <attribute name="text">1</attribute>
+                  </attributes>
+                  <cell-packing>
+                    <property name="expand">true</property>
+                  </cell-packing>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn" id="size_column">
+                <property name="title" translatable="yes">Size</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkCellRendererText" id="size_cell">
+                  </object>
+                  <cell-packing>
+                    <property name="expand">true</property>
+                  </cell-packing>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkTreeViewColumn" id="modified_column">
+                <property name="title" translatable="yes">Modified</property>
+                <property name="visible">true</property>
+                <child>
+                  <object class="GtkCellRendererText" id="modified_cell">
+                  </object>
+                  <cell-packing>
+                    <property name="expand">true</property>
+                  </cell-packing>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/ls/gbp-ls-workbench-addin.c b/src/plugins/ls/gbp-ls-workbench-addin.c
index 5786a683b..50fdf59ae 100644
--- a/src/plugins/ls/gbp-ls-workbench-addin.c
+++ b/src/plugins/ls/gbp-ls-workbench-addin.c
@@ -1,6 +1,6 @@
 /* gbp-ls-workbench-addin.c
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -18,12 +18,12 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-ls-workbench-addin"
 
+#include "config.h"
+
 #include "gbp-ls-workbench-addin.h"
-#include "gbp-ls-view.h"
+#include "gbp-ls-page.h"
 
 struct _GbpLsWorkbenchAddin
 {
@@ -34,18 +34,12 @@ struct _GbpLsWorkbenchAddin
 typedef struct
 {
   GFile     *file;
-  GbpLsView *view;
+  GbpLsPage *view;
 } LocateView;
 
-static gchar *
-gbp_ls_workbench_addin_get_id (IdeWorkbenchAddin *addin)
-{
-  return g_strdup ("directory");
-}
-
 static gboolean
 gbp_ls_workbench_addin_can_open (IdeWorkbenchAddin *addin,
-                                 IdeUri            *uri,
+                                 GFile             *file,
                                  const gchar       *content_type,
                                  gint              *priority)
 {
@@ -68,63 +62,64 @@ locate_view (GtkWidget *view,
   LocateView *locate = user_data;
   GFile *file;
 
-  g_assert (IDE_IS_LAYOUT_VIEW (view));
+  g_assert (IDE_IS_PAGE (view));
   g_assert (locate != NULL);
 
   if (locate->view != NULL)
     return;
 
-  if (!GBP_IS_LS_VIEW (view))
+  if (!GBP_IS_LS_PAGE (view))
     return;
 
-  file = gbp_ls_view_get_directory (GBP_LS_VIEW (view));
+  file = gbp_ls_page_get_directory (GBP_LS_PAGE (view));
   if (g_file_equal (file, locate->file))
-    locate->view = GBP_LS_VIEW (view);
+    locate->view = GBP_LS_PAGE (view);
 }
 
 static void
 gbp_ls_workbench_addin_open_async (IdeWorkbenchAddin     *addin,
-                                   IdeUri                *uri,
+                                   GFile                 *file,
                                    const gchar           *content_type,
-                                   IdeWorkbenchOpenFlags  flags,
+                                   IdeBufferOpenFlags     flags,
                                    GCancellable          *cancellable,
                                    GAsyncReadyCallback    callback,
                                    gpointer               user_data)
 {
   GbpLsWorkbenchAddin *self = (GbpLsWorkbenchAddin *)addin;
   g_autoptr(IdeTask) task = NULL;
-  g_autoptr(GFile) file = NULL;
-  IdePerspective *editor;
-  GbpLsView *view;
+  IdeWorkspace *workspace;
+  IdeSurface *surface;
+  GbpLsPage *view;
   LocateView locate = { 0 };
 
   g_assert (GBP_IS_LS_WORKBENCH_ADDIN (self));
   g_assert (IDE_IS_WORKBENCH (self->workbench));
-  g_assert (uri != NULL);
+  g_assert (G_IS_FILE (file));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, gbp_ls_workbench_addin_open_async);
 
-  editor = ide_workbench_get_perspective_by_name (self->workbench, "editor");
-  file = ide_uri_to_file (uri);
+  workspace = ide_workbench_get_current_workspace (self->workbench);
+  if (!(surface = ide_workspace_get_surface_by_name (workspace, "editor")))
+    surface = ide_workspace_get_surface_by_name (workspace, "terminal");
 
   /* First try to find an existing view for the file */
   locate.file = file;
-  ide_workbench_views_foreach (self->workbench, locate_view, &locate);
+  ide_workbench_foreach_page (self->workbench, locate_view, &locate);
   if (locate.view != NULL)
     {
-      ide_workbench_focus (self->workbench, GTK_WIDGET (locate.view));
+      ide_widget_reveal_and_grab (GTK_WIDGET (locate.view));
       ide_task_return_boolean (task, TRUE);
       return;
     }
 
-  view = g_object_new (GBP_TYPE_LS_VIEW,
+  view = g_object_new (GBP_TYPE_LS_PAGE,
                        "close-on-activate", TRUE,
                        "directory", file,
                        "visible", TRUE,
                        NULL);
-  gtk_container_add (GTK_CONTAINER (editor), GTK_WIDGET (view));
+  gtk_container_add (GTK_CONTAINER (surface), GTK_WIDGET (view));
 
   ide_task_return_boolean (task, TRUE);
 }
@@ -157,7 +152,6 @@ gbp_ls_workbench_addin_unload (IdeWorkbenchAddin *addin,
 static void
 workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
 {
-  iface->get_id = gbp_ls_workbench_addin_get_id;
   iface->can_open = gbp_ls_workbench_addin_can_open;
   iface->open_async = gbp_ls_workbench_addin_open_async;
   iface->open_finish = gbp_ls_workbench_addin_open_finish;
diff --git a/src/plugins/ls/gbp-ls-workbench-addin.h b/src/plugins/ls/gbp-ls-workbench-addin.h
index b494e30e5..e08ddc4aa 100644
--- a/src/plugins/ls/gbp-ls-workbench-addin.h
+++ b/src/plugins/ls/gbp-ls-workbench-addin.h
@@ -1,6 +1,6 @@
 /* gbp-ls-workbench-addin.h
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -20,7 +20,7 @@
 
 #pragma once
 
-#include <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/ls/ls-plugin.c b/src/plugins/ls/ls-plugin.c
new file mode 100644
index 000000000..c078b8906
--- /dev/null
+++ b/src/plugins/ls/ls-plugin.c
@@ -0,0 +1,34 @@
+/* ls-plugin.c
+ *
+ * Copyright 2018-2019 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
+ */
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+
+#include "gbp-ls-workbench-addin.h"
+
+_IDE_EXTERN void
+_gbp_ls_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GBP_TYPE_LS_WORKBENCH_ADDIN);
+}
diff --git a/src/plugins/ls/ls.gresource.xml b/src/plugins/ls/ls.gresource.xml
index 26e2dec94..3e9ec28a9 100644
--- a/src/plugins/ls/ls.gresource.xml
+++ b/src/plugins/ls/ls.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/ls">
     <file>ls.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/ls">
-    <file preprocess="xml-stripblanks">gbp-ls-view.ui</file>
+    <file preprocess="xml-stripblanks">gbp-ls-page.ui</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/ls/ls.plugin b/src/plugins/ls/ls.plugin
index 18af04853..c7bf0b0d9 100644
--- a/src/plugins/ls/ls.plugin
+++ b/src/plugins/ls/ls.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=ls
-Name=View Directory Listings
-Description=List files in a directory as a view
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2018 Christian Hergert
 Builtin=true
+Copyright=Copyright © 2018 Christian Hergert
 Depends=editor;
-Embedded=gbp_ls_register_types
+Description=List files in a directory as a view
+Embedded=_gbp_ls_register_types
+Hidden=true
+Module=ls
+Name=View Directory Listings
diff --git a/src/plugins/ls/meson.build b/src/plugins/ls/meson.build
index 1dcea074e..99633be7f 100644
--- a/src/plugins/ls/meson.build
+++ b/src/plugins/ls/meson.build
@@ -1,19 +1,14 @@
-if get_option('with_ls')
+plugins_sources += files([
+  'gbp-ls-model.c',
+  'gbp-ls-page.c',
+  'gbp-ls-workbench-addin.c',
+  'ls-plugin.c',
+])
 
-grep_resources = gnome.compile_resources(
+plugin_ls_resources = gnome.compile_resources(
   'ls-resources',
   'ls.gresource.xml',
   c_name: 'gbp_ls',
 )
 
-grep_sources = [
-  'gbp-ls-model.c',
-  'gbp-ls-plugin.c',
-  'gbp-ls-view.c',
-  'gbp-ls-workbench-addin.c',
-]
-
-gnome_builder_plugins_sources += files(grep_sources)
-gnome_builder_plugins_sources += grep_resources[0]
-
-endif
+plugins_sources += plugin_ls_resources[0]
diff --git a/src/plugins/make/make.gresource.xml b/src/plugins/make/make.gresource.xml
index ef074e9be..d4facf356 100644
--- a/src/plugins/make/make.gresource.xml
+++ b/src/plugins/make/make.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins/make_plugin">
+  <gresource prefix="/plugins/make_plugin">
     <file compressed="true">resources/Makefile</file>
     <file compressed="true">resources/main.c</file>
     <file compressed="true">resources/.gitignore</file>
diff --git a/src/plugins/make/make.plugin b/src/plugins/make/make.plugin
index f8bb3346f..7e7b66e86 100644
--- a/src/plugins/make/make.plugin
+++ b/src/plugins/make/make.plugin
@@ -1,10 +1,14 @@
 [Plugin]
-Module=make_plugin
-Loader=python3
-Name=Make
-Description=Provides support for Makefile projects without autotools
 Authors=Matthew Leeds <mleeds redhat com>
-Copyright=Copyright © 2017 Matthew Leeds
 Builtin=true
-X-Project-File-Filter-Pattern=Makefile
+Copyright=Copyright © 2017 Matthew Leeds
+Depends=editor;buildui;
+Description=Provides support for Makefile projects without autotools
+Hidden=true
+Loader=python3
+Module=make_plugin
+Name=Make
+X-Has-Resources=true
 X-Project-File-Filter-Name=Makefile Project
+X-Project-File-Filter-Pattern=Makefile
+X-Builder-ABI=@PACKAGE_ABI@
diff --git a/src/plugins/make/make_plugin.py b/src/plugins/make/make_plugin.py
index 3154105fa..4e5e46f8a 100644
--- a/src/plugins/make/make_plugin.py
+++ b/src/plugins/make/make_plugin.py
@@ -15,13 +15,9 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-import gi
 import os
 from os import path
 
-gi.require_version('Ide', '1.0')
-gi.require_version('Template', '1.0')
-
 from gi.repository import GObject
 from gi.repository import Gio
 from gi.repository import GLib
@@ -31,56 +27,47 @@ from gi.repository import Template
 
 _ = Ide.gettext
 
-class MakeBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
+class MakeBuildSystemDiscovery(Ide.SimpleBuildSystemDiscovery):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.props.glob = 'Makefile'
+        self.props.hint = 'make_plugin'
+        self.props.priority = 1000
+
+class MakeBuildSystem(Ide.Object, Ide.BuildSystem):
     project_file = GObject.Property(type=Gio.File)
     make_dir = GObject.Property(type=Gio.File)
     run_args = None
 
+    def do_parent_set(self, parent):
+        if self.project_file.get_basename() == 'Makefile':
+            self.make_dir = project_file.get_parent()
+        elif self.project_file.query_file_type(0, None) == Gio.FileType.DIRECTORY:
+            self.make_dir = self.project_file
+
     def do_get_id(self):
         return 'make'
 
     def do_get_display_name(self):
         return 'Make'
 
-    def do_init_async(self, priority, cancel, callback, data=None):
-        task = Gio.Task.new(self, cancel, callback)
-        task.set_priority(priority)
-
-        # TODO: Be async here also
-        project_file = self.get_context().get_project_file()
-        if project_file.get_basename() == 'Makefile':
-            self.props.make_dir = project_file.get_parent()
-            task.return_boolean(True)
-        else:
-            child = project_file.get_child('Makefile')
-            exists = child.query_exists(cancel)
-            if exists:
-                self.props.make_dir = project_file
-                self.props.project_file = child
-            task.return_boolean(exists)
-
-    def do_init_finish(self, result):
-        return result.propagate_boolean()
-
     def do_get_priority(self):
         return 0
 
     def do_get_builddir(self, pipeline):
-        context = self.get_context()
-        return context.get_vcs().get_working_directory().get_path()
+        return self.get_context().ref_workdir().get_path()
 
-    def do_get_build_flags_async(self, ifile, cancellable, callback, data=None):
+    def do_get_build_flags_async(self, file, cancellable, callback, data=None):
         task = Gio.Task.new(self, cancellable, callback)
-        task.ifile = ifile
+        task.file = file
         task.build_flags = []
         task.return_boolean(True)
 
     def do_get_build_flags_finish(self, result):
-        if result.propagate_boolean():
-            return result.build_flags
+        return result.build_flags
 
     def get_make_dir(self):
-        return self.props.make_dir
+        return self.make_dir
 
 class MakePipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
     """
@@ -90,7 +77,7 @@ class MakePipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
 
     def do_load(self, pipeline):
         context = pipeline.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         # Only register stages if we are a makefile project
         if type(build_system) != MakeBuildSystem:
@@ -102,7 +89,7 @@ class MakePipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
         # If the configuration has set $MAKE, then use it.
         make = config.getenv('MAKE') or "make"
 
-        srcdir = context.get_vcs().get_working_directory().get_path()
+        srcdir = context.ref_workdir().get_path()
         builddir = pipeline.get_builddir()
 
         # Register the build launcher which will perform the incremental
@@ -123,7 +110,7 @@ class MakePipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
         build_stage.set_name(_("Build project"))
         build_stage.set_clean_launcher(clean_launcher)
         build_stage.connect('query', self._query)
-        self.track(pipeline.connect(Ide.BuildPhase.BUILD, 0, build_stage))
+        self.track(pipeline.attach(Ide.BuildPhase.BUILD, 0, build_stage))
 
         # Register the install launcher which will perform our
         # "make install" when the Ide.BuildPhase.INSTALL phase
@@ -135,7 +122,7 @@ class MakePipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
 
         install_stage = Ide.BuildStageLauncher.new(context, install_launcher)
         install_stage.set_name(_("Install project"))
-        self.track(pipeline.connect(Ide.BuildPhase.INSTALL, 0, install_stage))
+        self.track(pipeline.attach(Ide.BuildPhase.INSTALL, 0, install_stage))
 
         # Determine what it will take to "make run" for this pipeline
         # and stash it on the build_system for use by the build target.
@@ -143,7 +130,7 @@ class MakePipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
         # has a "run" target.
         build_system.run_args = [make, '-C', build_system.get_make_dir().get_path(), 'run']
 
-    def _query(self, stage, pipeline, cancellable):
+    def _query(self, stage, pipeline, targets, cancellable):
         stage.set_completed(False)
 
 class MakeBuildTarget(Ide.Object, Ide.BuildTarget):
@@ -160,7 +147,7 @@ class MakeBuildTarget(Ide.Object, Ide.BuildTarget):
 
     def do_get_argv(self):
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
         assert type(build_system) == MakeBuildSystem
         return build_system.run_args
 
@@ -180,7 +167,7 @@ class MakeBuildTargetProvider(Ide.Object, Ide.BuildTargetProvider):
         task.set_priority(GLib.PRIORITY_LOW)
 
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != MakeBuildSystem:
             task.return_error(GLib.Error('Not a make build system',
@@ -188,7 +175,7 @@ class MakeBuildTargetProvider(Ide.Object, Ide.BuildTargetProvider):
                                          code=Gio.IOErrorEnum.NOT_SUPPORTED))
             return
 
-        task.targets = [MakeBuildTarget(context=self.get_context())]
+        task.targets = [MakeBuildTarget()]
         task.return_boolean(True)
 
     def do_get_targets_finish(self, result):
@@ -334,7 +321,7 @@ class MakeTemplateBase(Ide.TemplateBase, Ide.ProjectTemplate):
             if src.startswith('resource://'):
                 self.add_resource(src[11:], destination, scope, modes.get(src, 0))
             else:
-                path = os.path.join('/org/gnome/builder/plugins/make_plugin', src)
+                path = os.path.join('/plugins/make_plugin', src)
                 self.add_resource(path, destination, scope, modes.get(src, 0))
 
         self.expand_all_async(cancellable, self.expand_all_cb, task)
diff --git a/src/plugins/make/meson.build b/src/plugins/make/meson.build
index 89c5e19f0..f52cf32d7 100644
--- a/src/plugins/make/meson.build
+++ b/src/plugins/make/meson.build
@@ -1,4 +1,4 @@
-if get_option('with_make')
+if get_option('plugin_make')
 
 make_resources = gnome.compile_resources(
   'make_plugin',
@@ -13,7 +13,7 @@ install_data('make_plugin.py', install_dir: plugindir)
 configure_file(
           input: 'make.plugin',
          output: 'make.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/maven/maven.plugin b/src/plugins/maven/maven.plugin
index afc3953f3..a306895b4 100644
--- a/src/plugins/maven/maven.plugin
+++ b/src/plugins/maven/maven.plugin
@@ -1,10 +1,13 @@
 [Plugin]
-Module=maven_plugin
-Name=Maven
-Loader=python3
-Description=Provides integration with the Maven build tool
 Authors=Alberto Fanjul Alonso <albfan gnome org>
-Copyright=Copyright © 2018 Alberto Fanjul Alonso
 Builtin=true
-X-Project-File-Filter-Pattern=pom.xml
+Copyright=Copyright © 2018 Alberto Fanjul Alonso
+Description=Provides integration with the Maven build tool
+Depends=editor;buildui;
+Hidden=true
+Loader=python3
+Module=maven_plugin
+Name=Maven
+X-Builder-ABI=@PACKAGE_ABI@
 X-Project-File-Filter-Name=Maven (pom.xml)
+X-Project-File-Filter-Pattern=pom.xml
diff --git a/src/plugins/maven/maven_plugin.py b/src/plugins/maven/maven_plugin.py
index 546ca3e25..c0c7db4b8 100755
--- a/src/plugins/maven/maven_plugin.py
+++ b/src/plugins/maven/maven_plugin.py
@@ -19,12 +19,9 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-import gi
 import threading
 import os
 
-gi.require_version('Ide', '1.0')
-
 from gi.repository import Gio
 from gi.repository import GLib
 from gi.repository import GObject
@@ -38,7 +35,14 @@ _ATTRIBUTES = ",".join([
     Gio.FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON,
 ])
 
-class MavenBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
+class MavenBuildSystemDiscovery(Ide.SimpleBuildSystemDiscovery):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.props.glob = 'pom.xml'
+        self.props.hint = 'maven_plugin'
+        self.props.priority = 2000
+
+class MavenBuildSystem(Ide.Object, Ide.BuildSystem):
     project_file = GObject.Property(type=Gio.File)
 
     def do_get_id(self):
@@ -47,32 +51,8 @@ class MavenBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
     def do_get_display_name(self):
         return 'Maven'
 
-    def do_init_async(self, io_priority, cancellable, callback, data):
-        task = Ide.Task.new(self, cancellable, callback)
-
-        try:
-            # Maybe this is a pom.xml
-            if self.props.project_file.get_basename() in ('pom.xml',):
-                task.return_boolean(True)
-                return
-
-            # Maybe this is a directory with a pom.xml
-            if self.props.project_file.query_file_type(0) == Gio.FileType.DIRECTORY:
-                child = self.props.project_file.get_child('pom.xml')
-                if child.query_exists(None):
-                    self.props.project_file = child
-                    task.return_boolean(True)
-                    return
-        except Exception as ex:
-            task.return_error(ex)
-
-        task.return_error(Ide.NotSupportedError())
-
-    def do_init_finish(self, task):
-        return task.propagate_boolean()
-
     def do_get_priority(self):
-        return 400
+        return 2000
 
 class MavenPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
     """
@@ -82,7 +62,7 @@ class MavenPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
 
     def do_load(self, pipeline):
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != MavenBuildSystem:
             return
@@ -156,7 +136,7 @@ class MavenBuildTargetProvider(Ide.Object, Ide.BuildTargetProvider):
         task.set_priority(GLib.PRIORITY_LOW)
 
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != MavenBuildSystem:
             task.return_error(GLib.Error('Not maven build system',
@@ -178,7 +158,7 @@ class MavenIdeTestProvider(Ide.TestProvider):
         task.set_priority(GLib.PRIORITY_LOW)
 
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != MavenBuildSystem:
             task.return_error(GLib.Error('Not maven build system',
@@ -226,14 +206,14 @@ class MavenIdeTestProvider(Ide.TestProvider):
     def do_reload(self):
 
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != MavenBuildSystem:
             return
 
         # find all files in test directory
         # http://maven.apache.org/surefire/maven-surefire-plugin/examples/inclusion-exclusion.html
-        build_manager = context.get_build_manager()
+        build_manager = Ide.BuildManager.from_context(context)
         pipeline = build_manager.get_pipeline()
         srcdir = pipeline.get_srcdir()
         test_suite = Gio.File.new_for_path(os.path.join(srcdir, 'src/test/java'))
@@ -260,8 +240,9 @@ class MavenIdeTestProvider(Ide.TestProvider):
                                                     self.on_enumerator_loaded,
                                                     None)
                 else:
-                    #TODO Ask java through introspection for classes with TestCase and its public void 
methods 
-                    # or Annotation @Test methods
+                    # TODO: Ask java through introspection for classes with
+                    # TestCase and its public void methods or Annotation @Test
+                    # methods
                     result, contents, etag = gfile.load_contents()
                     tests = [x for x in str(contents).split('\\n') if 'public void' in x]
                     tests = [v.replace("()", "").replace("public void","").strip() for v in tests]
diff --git a/src/plugins/maven/meson.build b/src/plugins/maven/meson.build
index f174eb853..fc1a059d8 100644
--- a/src/plugins/maven/meson.build
+++ b/src/plugins/maven/meson.build
@@ -1,11 +1,11 @@
-if get_option('with_maven')
+if get_option('plugin_maven')
 
 install_data('maven_plugin.py', install_dir: plugindir)
 
 configure_file(
           input: 'maven.plugin',
          output: 'maven.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/meson-templates/icons/scalable/actions/pattern-browse.svg 
b/src/plugins/meson-templates/icons/scalable/actions/pattern-browse.svg
new file mode 100644
index 000000000..efbe8a017
--- /dev/null
+++ b/src/plugins/meson-templates/icons/scalable/actions/pattern-browse.svg
@@ -0,0 +1,44 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg xmlns:cc='http://creativecommons.org/ns#' xmlns:dc='http://purl.org/dc/elements/1.1/' 
sodipodi:docname='pattern-browse.svg' height='98' id='svg7384' 
xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' 
xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' 
xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:svg='http://www.w3.org/2000/svg' 
version='1.1' inkscape:version='0.91 r13725' width='98' xmlns='http://www.w3.org/2000/svg'>
+  <metadata id='metadata90'>
+    <rdf:RDF>
+      <cc:Work rdf:about=''>
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/>
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview inkscape:bbox-nodes='true' inkscape:bbox-paths='true' bordercolor='#666666' 
borderopacity='1' inkscape:current-layer='layer3' inkscape:cx='231.92972' inkscape:cy='67.863133' 
gridtolerance='10' inkscape:guide-bbox='true' guidetolerance='10' id='namedview88' 
inkscape:object-nodes='false' inkscape:object-paths='false' objecttolerance='10' pagecolor='#555753' 
inkscape:pageopacity='1' inkscape:pageshadow='2' showborder='false' showgrid='false' showguides='true' 
inkscape:snap-bbox='true' inkscape:snap-bbox-midpoints='false' inkscape:snap-global='true' 
inkscape:snap-grids='true' inkscape:snap-nodes='false' inkscape:snap-others='false' 
inkscape:snap-to-guides='true' inkscape:window-height='1403' inkscape:window-maximized='1' 
inkscape:window-width='2560' inkscape:window-x='2560' inkscape:window-y='0' inkscape:zoom='1'>
+    <inkscape:grid empspacing='2' enabled='true' id='grid4866' originx='198' originy='192' 
snapvisiblegridlinesonly='true' spacingx='1px' spacingy='1px' type='xygrid' visible='true'/>
+  </sodipodi:namedview>
+  <title id='title9167'>Gnome Symbolic Icon Theme</title>
+  <defs id='defs7386'/>
+  <g inkscape:groupmode='layer' id='layer3' inkscape:label='patterns' transform='translate(198,-110)'>
+    <rect height='95' id='rect23109' rx='3.8039246' ry='3.8039246' 
style='color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:new'
 width='95' x='-196.5' y='111.5'/>
+    <path inkscape:connector-curvature='0' d='m -195.27503,123.51384 92.50606,0' id='path23541' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:0.99999988;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <path inkscape:connector-curvature='0' d='m -106.51099,116.49687 -4.04376,3.99957' id='path23545' 
sodipodi:nodetypes='cc' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <rect height='0.98332036' id='rect23547' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='-111.04089' y='116.01071'/>
+    <rect height='0.98332036' id='rect23549' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='-107.01923' y='116.01071'/>
+    <rect height='0.98332036' id='rect23551' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='-107.01923' y='120.01028'/>
+    <rect height='0.98332036' id='rect23553' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='-111.04089' y='120.01028'/>
+    <path inkscape:connector-curvature='0' d='m -110.55475,116.49687 4.04376,3.99957' id='path23555' 
sodipodi:nodetypes='cc' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <path inkscape:connector-curvature='0' d='m -195.27503,135.51384 92.50606,0' id='path23652' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:0.99999988;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <rect height='5.922019' id='rect23654' rx='3.0052037' ry='3.0052037' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='45.873554' x='-171.42294' y='126.58289'/>
+    <rect height='15.026019' id='rect23557' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-186.1268' y='141.97461'/>
+    <rect height='15.026019' id='rect23559' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-166.1268' y='141.97461'/>
+    <rect height='15.026019' id='rect23561' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-146.1268' y='141.97461'/>
+    <rect height='15.026019' id='rect23563' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-126.1268' y='141.97461'/>
+    <rect height='15.026019' id='rect23565' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-186.1268' y='161.97461'/>
+    <rect height='15.026019' id='rect23567' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-166.1268' y='161.97461'/>
+    <rect height='15.026019' id='rect23569' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-146.1268' y='161.97461'/>
+    <rect height='15.026019' id='rect23571' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-126.1268' y='161.97461'/>
+    <rect height='15.026019' id='rect23573' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-186.1268' y='181.97461'/>
+    <rect height='15.026019' id='rect23575' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-166.1268' y='181.97461'/>
+    <rect height='15.026019' id='rect23577' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-146.1268' y='181.97461'/>
+    <rect height='15.026019' id='rect23579' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-126.1268' y='181.97461'/>
+    <rect height='98' id='rect7477' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate'
 width='98' x='-198' y='110'/>
+  </g>
+</svg>
diff --git a/src/plugins/meson-templates/icons/scalable/actions/pattern-cli.svg 
b/src/plugins/meson-templates/icons/scalable/actions/pattern-cli.svg
new file mode 100644
index 000000000..e796ff0a9
--- /dev/null
+++ b/src/plugins/meson-templates/icons/scalable/actions/pattern-cli.svg
@@ -0,0 +1,32 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg xmlns:cc='http://creativecommons.org/ns#' xmlns:dc='http://purl.org/dc/elements/1.1/' 
sodipodi:docname='pattern-cli.svg' height='98' id='svg7384' 
xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' 
xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' 
xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:svg='http://www.w3.org/2000/svg' 
version='1.1' inkscape:version='0.91 r13725' width='98' xmlns='http://www.w3.org/2000/svg'>
+  <metadata id='metadata90'>
+    <rdf:RDF>
+      <cc:Work rdf:about=''>
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/>
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview inkscape:bbox-nodes='true' inkscape:bbox-paths='true' bordercolor='#666666' 
borderopacity='1' inkscape:current-layer='layer3' inkscape:cx='31.929722' inkscape:cy='67.863133' 
gridtolerance='10' inkscape:guide-bbox='true' guidetolerance='10' id='namedview88' 
inkscape:object-nodes='false' inkscape:object-paths='false' objecttolerance='10' pagecolor='#555753' 
inkscape:pageopacity='1' inkscape:pageshadow='2' showborder='false' showgrid='false' showguides='true' 
inkscape:snap-bbox='true' inkscape:snap-bbox-midpoints='false' inkscape:snap-global='true' 
inkscape:snap-grids='true' inkscape:snap-nodes='false' inkscape:snap-others='false' 
inkscape:snap-to-guides='true' inkscape:window-height='1403' inkscape:window-maximized='1' 
inkscape:window-width='2560' inkscape:window-x='2560' inkscape:window-y='0' inkscape:zoom='1'>
+    <inkscape:grid empspacing='2' enabled='true' id='grid4866' originx='-2.0000029' originy='192' 
snapvisiblegridlinesonly='true' spacingx='1px' spacingy='1px' type='xygrid' visible='true'/>
+  </sodipodi:namedview>
+  <title id='title9167'>Gnome Symbolic Icon Theme</title>
+  <defs id='defs7386'/>
+  <g inkscape:groupmode='layer' id='layer3' inkscape:label='patterns' transform='translate(-2.0000029,-110)'>
+    <rect height='95' id='rect7548' rx='3.8039246' ry='3.8039246' 
style='color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:new'
 width='95' x='3.5000029' y='111.5'/>
+    <path inkscape:connector-curvature='0' d='m 4.72497,123.51384 92.50606,0' id='path7552' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:0.99999988;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <path inkscape:connector-curvature='0' d='m 93.48901,116.49687 -4.04376,3.99957' id='path7556' 
sodipodi:nodetypes='cc' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <rect height='0.98332036' id='rect7558' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='88.959106' y='116.01071'/>
+    <rect height='0.98332036' id='rect7560' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='92.980774' y='116.01071'/>
+    <rect height='0.98332036' id='rect7562' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='92.980774' y='120.01028'/>
+    <rect height='0.98332036' id='rect7564' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='88.959106' y='120.01028'/>
+    <path inkscape:connector-curvature='0' d='m 89.44525,116.49687 4.04376,3.99957' id='path7566' 
sodipodi:nodetypes='cc' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <rect height='98' id='rect7592' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate'
 width='98' x='2.0000029' y='110'/>
+    <rect height='12' id='rect8382' rx='0' ry='0' 
style='color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;enable-background:accumulate'
 transform='matrix(0,1,-1,0,0,0)' width='2' x='139.96692' y='-32.978367'/>
+    <path inkscape:connector-curvature='0' d='m 19.4375,136 -5.71875,5.71875 C 13.52288,141.91462 
13.25562,142 13,142 l -1,0 0,-1 c 0,-0.25562 0.0854,-0.52288 0.28125,-0.71875 L 16.5625,136 
12.28125,131.71875 C 12.08538,131.52288 12,131.25562 12,131 l 0,-1 1,0 c 0.25562,0 0.52288,0.0854 
0.71875,0.28125 z' id='rect6014-1' sodipodi:nodetypes='ccscsccsscscc' 
style='display:inline;fill:#729fcf;fill-opacity:1;stroke:none'/>
+  </g>
+</svg>
diff --git a/src/plugins/meson-templates/icons/scalable/actions/pattern-gnome.svg 
b/src/plugins/meson-templates/icons/scalable/actions/pattern-gnome.svg
new file mode 100644
index 000000000..eaa20e32f
--- /dev/null
+++ b/src/plugins/meson-templates/icons/scalable/actions/pattern-gnome.svg
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd";
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape";
+   sodipodi:docname="pattern-gnome.svg"
+   height="98.464043"
+   id="svg7384"
+   version="1.1"
+   inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
+   width="98">
+  <metadata
+     id="metadata90">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview
+     inkscape:bbox-nodes="true"
+     inkscape:bbox-paths="true"
+     bordercolor="#666666"
+     borderlayer="true"
+     borderopacity="1"
+     inkscape:current-layer="layer3"
+     inkscape:cx="-103.5869"
+     inkscape:cy="84.019343"
+     gridtolerance="10"
+     inkscape:guide-bbox="true"
+     guidetolerance="10"
+     id="namedview88"
+     inkscape:object-nodes="false"
+     inkscape:object-paths="false"
+     objecttolerance="10"
+     pagecolor="#8ce0f0"
+     inkscape:pageopacity="1"
+     inkscape:pageshadow="2"
+     showborder="false"
+     showgrid="false"
+     showguides="true"
+     inkscape:showpageshadow="false"
+     inkscape:snap-bbox="true"
+     inkscape:snap-bbox-midpoints="false"
+     inkscape:snap-global="true"
+     inkscape:snap-grids="true"
+     inkscape:snap-nodes="false"
+     inkscape:snap-others="false"
+     inkscape:snap-to-guides="true"
+     inkscape:window-height="1376"
+     inkscape:window-maximized="1"
+     inkscape:window-width="3440"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
+     inkscape:zoom="2.8284272">
+    <inkscape:grid
+       empspacing="2"
+       enabled="true"
+       id="grid4866"
+       originx="-500.00437"
+       originy="0.041632664"
+       snapvisiblegridlinesonly="true"
+       spacingx="1"
+       spacingy="1"
+       type="xygrid"
+       visible="true" />
+  </sodipodi:namedview>
+  <title
+     id="title9167">Gnome Symbolic Icon Theme</title>
+  <defs
+     id="defs7386" />
+  <g
+     inkscape:groupmode="layer"
+     id="layer3"
+     inkscape:label="patterns"
+     transform="translate(-500.00437,82.42241)">
+    <rect
+       height="95"
+       id="rect7610-2"
+       rx="3.8039246"
+       ry="3.8039246"
+       
style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:new"
+       width="95"
+       x="501.50436"
+       y="-80.458366" />
+    <rect
+       height="98"
+       id="rect7630-9"
+       
style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+       width="98"
+       x="500.00436"
+       y="-82.422409" />
+    <rect
+       height="32"
+       id="rect42957-1"
+       inkscape:label="a"
+       
style="color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:1;marker:none"
+       width="32"
+       x="536.43658"
+       y="-48.734909" />
+    <g
+       id="g3771"
+       style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-miterlimit:4"
+       transform="matrix(0.42486067,0,0,0.42486067,528.31832,-66.422416)">
+      <g
+         id="g3773"
+         style="fill:#000000;fill-opacity:1">
+        <path
+           inkscape:connector-curvature="0"
+           d="M 86.068,0 C 61.466,0 56.851,35.041 70.691,35.041 84.529,35.041 110.671,0 86.068,0 Z"
+           id="path3775"
+           style="fill:#000000;fill-opacity:1" />
+        <path
+           inkscape:connector-curvature="0"
+           d="M 45.217,30.699 C 52.586,31.149 60.671,2.577 46.821,4.374 32.976,6.171 37.845,30.249 
45.217,30.699 Z"
+           id="path3777"
+           style="fill:#000000;fill-opacity:1" />
+        <path
+           inkscape:connector-curvature="0"
+           d="M 11.445,48.453 C 16.686,46.146 12.12,23.581 3.208,29.735 -5.7,35.89 6.204,50.759 
11.445,48.453 Z"
+           id="path3779"
+           style="fill:#000000;fill-opacity:1" />
+        <path
+           inkscape:connector-curvature="0"
+           d="M 26.212,36.642 C 32.451,35.37 32.793,9.778 21.667,14.369 10.539,18.961 19.978,37.916 
26.212,36.642 Z"
+           id="path3781"
+           style="fill:#000000;fill-opacity:1" />
+        <path
+           inkscape:connector-curvature="0"
+           d="m 58.791,93.913 c 1.107,8.454 -6.202,12.629 -13.36,7.179 C 22.644,83.743 83.16,75.088 
79.171,51.386 75.86,31.712 15.495,37.769 8.621,68.553 3.968,89.374 27.774,118.26 52.614,118.26 c 12.22,0 
26.315,-11.034 28.952,-25.012 C 83.58,82.589 57.867,86.86 58.791,93.913 Z"
+           id="path3783"
+           style="fill:#000000;fill-opacity:1" />
+      </g>
+    </g>
+    <g
+       id="g3956"
+       transform="matrix(0.16008135,0,0,0.16008135,638.89545,-92.450831)">
+      <path
+         inkscape:connector-curvature="0"
+         d="m -565.99523,509.46063 c -8.08731,0.21792 -14.47394,3.12448 -19.17071,8.69866 -4.86385,5.80101 
-7.31024,13.81651 -7.31024,24.03862 0,10.19394 2.44651,18.18745 7.31024,23.98846 4.88761,5.801 
11.59815,8.69866 20.15764,8.69866 8.5831,0 15.3105,-2.89766 20.17436,-8.69866 4.86373,-5.80101 
7.29358,-13.79452 7.29353,-23.98846 -5e-5,-10.22211 -2.4298,-18.23761 -7.29353,-24.03862 -4.86386,-5.80075 
-11.59131,-8.69866 -20.17436,-8.69866 -0.33434,0 -0.6582,-0.009 -0.98693,0 z m 0.60221,11.77669 c 
0.12927,-0.003 0.25357,0 0.38472,0 4.21998,0 7.48996,1.8261 9.8028,5.48697 2.31266,3.66086 3.47944,8.82788 
3.47949,15.47362 0,6.61757 -1.16692,11.74604 -3.47949,15.40691 -2.31274,3.66086 -5.58286,5.50352 
-9.8028,5.50352 -4.19632,0 -7.43983,-1.84266 -9.75257,-5.50352 -2.31274,-3.66087 -3.47944,-8.78934 
-3.47949,-15.40691 0,-6.64574 1.16684,-11.81276 3.47949,-15.47362 2.24035,-3.54647 5.35963,-5.37604 
9.36785,-5.48697 z"
+         id="path3787"
+         
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:148.699646px;line-height:125%;font-family:'Bitstream
 Vera 
Sans';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000003pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
 />
+      <path
+         inkscape:connector-curvature="0"
+         d="m -657.35432,568.97161 c -7.12362,5.98235 -17.72219,5.91366 -22.13752,5.91366 -8.8932,0 
-15.93855,-2.92879 -21.13613,-8.78612 -5.19765,-5.88525 -7.7964,-13.85456 -7.7964,-23.90791 0,-10.16578 
2.64646,-18.16325 7.93945,-23.99241 5.293,-5.82892 12.54098,-8.74363 21.74413,-8.74363 3.55245,0 
6.94991,0.39433 10.19254,1.18273 3.26638,0.78841 6.34203,1.95706 9.22697,3.50595 l -3.70487,10.9527 c 
-1.62185,-0.88773 -3.4788,-1.76286 -5.20022,-2.37807 -2.93262,-0.98557 -5.87712,-1.47823 -8.83351,-1.47823 
-5.48379,0 -9.71581,1.81623 -12.69601,5.44892 -2.95649,3.60454 -4.4347,8.7718 -4.4347,15.50204 0,6.67415 
1.4305,11.82733 4.29167,15.46003 2.86099,3.6327 7.16068,5.44892 12.19522,5.44892 5.11476,0 8.28269,-1.28922 
9.97226,-2.64762 v -10.91144 h -11.08087 v -10.89809 h 21.45799"
+         id="path3789"
+         
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:148.699646px;line-height:125%;font-family:'Bitstream
 Vera 
Sans';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000003pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
 />
+      <path
+         inkscape:connector-curvature="0"
+         d="m -528.50252,510.59568 h 17.5241 l 12.15952,39.37066 12.23105,-39.37066 h 14.81181 l 
6.69132,63.06461 h -13.01787 l -4.0148,-39.4349 -12.30257,39.62391 h -8.72628 l -12.30263,-40.9623 
-4.01479,40.77329 h -13.05365 l 6.69132,-63.06461"
+         id="path3793"
+         
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:148.699646px;line-height:125%;font-family:'Bitstream
 Vera 
Sans';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000003pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
 />
+      <path
+         inkscape:connector-curvature="0"
+         d="m -455.68828,510.59568 h 37.15811 v 12.29183 h -23.38928 v 13.08097 h 17.97969 v 10.95369 h 
-17.97969 v 14.44629 h 24.17608 v 12.29183 h -37.94491 v -63.06461"
+         id="path3795"
+         
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:148.699646px;line-height:125%;font-family:'Bitstream
 Vera 
Sans';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000003pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
 />
+      <path
+         inkscape:connector-curvature="0"
+         d="m -647.94283,510.59568 h 8.6869 l 27.44915,37.90083 v -37.90083 h 11.71533 v 63.06461 h -8.6869 
l -27.4491,-37.90083 v 37.90083 h -11.71538 v -63.06461"
+         id="path3791"
+         
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:148.699646px;line-height:125%;font-family:'Bitstream
 Vera 
Sans';text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000003pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
 />
+    </g>
+    <text
+       id="text3797"
+       xml:space="preserve"
+       
style="font-style:normal;font-weight:normal;font-size:3.77335668px;line-height:0%;font-family:'Bitstream Vera 
Sans';fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000003pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="573.65802"
+       y="-8.2047768"><tspan
+         id="tspan3799"
+         style="letter-spacing:1.36017227"
+         x="573.65802"
+         y="-8.2047768"><tspan
+           id="tspan3801"
+           style="letter-spacing:0.0310309">TM</tspan></tspan></text>
+  </g>
+</svg>
diff --git a/src/plugins/meson-templates/icons/scalable/actions/pattern-grid.svg 
b/src/plugins/meson-templates/icons/scalable/actions/pattern-grid.svg
new file mode 100644
index 000000000..874080957
--- /dev/null
+++ b/src/plugins/meson-templates/icons/scalable/actions/pattern-grid.svg
@@ -0,0 +1,42 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg xmlns:cc='http://creativecommons.org/ns#' xmlns:dc='http://purl.org/dc/elements/1.1/' 
sodipodi:docname='pattern-grid.svg' height='98' id='svg7384' 
xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' 
xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' 
xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:svg='http://www.w3.org/2000/svg' 
version='1.1' inkscape:version='0.91 r13725' width='98' xmlns='http://www.w3.org/2000/svg'>
+  <metadata id='metadata90'>
+    <rdf:RDF>
+      <cc:Work rdf:about=''>
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/>
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview inkscape:bbox-nodes='true' inkscape:bbox-paths='true' bordercolor='#666666' 
borderopacity='1' inkscape:current-layer='layer3' inkscape:cx='131.92972' inkscape:cy='67.863133' 
gridtolerance='10' inkscape:guide-bbox='true' guidetolerance='10' id='namedview88' 
inkscape:object-nodes='false' inkscape:object-paths='false' objecttolerance='10' pagecolor='#555753' 
inkscape:pageopacity='1' inkscape:pageshadow='2' showborder='false' showgrid='false' showguides='true' 
inkscape:snap-bbox='true' inkscape:snap-bbox-midpoints='false' inkscape:snap-global='true' 
inkscape:snap-grids='true' inkscape:snap-nodes='false' inkscape:snap-others='false' 
inkscape:snap-to-guides='true' inkscape:window-height='1403' inkscape:window-maximized='1' 
inkscape:window-width='2560' inkscape:window-x='2560' inkscape:window-y='0' inkscape:zoom='1'>
+    <inkscape:grid empspacing='2' enabled='true' id='grid4866' originx='97.999997' originy='192' 
snapvisiblegridlinesonly='true' spacingx='1px' spacingy='1px' type='xygrid' visible='true'/>
+  </sodipodi:namedview>
+  <title id='title9167'>Gnome Symbolic Icon Theme</title>
+  <defs id='defs7386'/>
+  <g inkscape:groupmode='layer' id='layer3' inkscape:label='patterns' transform='translate(97.999997,-110)'>
+    <rect height='95' id='rect7496' rx='3.8039246' ry='3.8039246' 
style='color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:new'
 width='95' x='-96.5' y='111.5'/>
+    <path inkscape:connector-curvature='0' d='m -95.27503,123.51384 92.50606,0' id='path7500' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:0.99999988;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <path inkscape:connector-curvature='0' d='m -6.51099,116.49687 -4.04376,3.99957' id='path7504' 
sodipodi:nodetypes='cc' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <rect height='0.98332036' id='rect7506' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='-11.040891' y='116.01071'/>
+    <rect height='0.98332036' id='rect7508' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='-7.0192232' y='116.01071'/>
+    <rect height='0.98332036' id='rect7510' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='-7.0192232' y='120.01028'/>
+    <rect height='0.98332036' id='rect7512' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='-11.040891' y='120.01028'/>
+    <path inkscape:connector-curvature='0' d='m -10.55475,116.49687 4.04376,3.99957' id='path7514' 
sodipodi:nodetypes='cc' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <rect height='15.026019' id='rect7520' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-86.126801' y='135.97461'/>
+    <rect height='15.026019' id='rect7522' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-66.126801' y='135.97461'/>
+    <rect height='15.026019' id='rect7524' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-46.126797' y='135.97461'/>
+    <rect height='15.026019' id='rect7526' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-26.126797' y='135.97461'/>
+    <rect height='15.026019' id='rect7528' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-86.126801' y='155.97461'/>
+    <rect height='15.026019' id='rect7530' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-66.126801' y='155.97461'/>
+    <rect height='15.026019' id='rect7532' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-46.126797' y='155.97461'/>
+    <rect height='15.026019' id='rect7534' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-26.126797' y='155.97461'/>
+    <rect height='15.026019' id='rect7536' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-86.126801' y='175.97461'/>
+    <rect height='15.026019' id='rect7538' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-66.126801' y='175.97461'/>
+    <rect height='15.026019' id='rect7540' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-46.126797' y='175.97461'/>
+    <rect height='15.026019' id='rect7542' rx='1.2406625' ry='1.2406625' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#bdd2e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='15.026019' x='-26.126797' y='175.97461'/>
+    <rect height='98' id='rect7544' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate'
 width='98' x='-98' y='110'/>
+  </g>
+</svg>
diff --git a/src/plugins/meson-templates/icons/scalable/actions/pattern-legacy.svg 
b/src/plugins/meson-templates/icons/scalable/actions/pattern-legacy.svg
new file mode 100644
index 000000000..21a1a125a
--- /dev/null
+++ b/src/plugins/meson-templates/icons/scalable/actions/pattern-legacy.svg
@@ -0,0 +1,40 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg xmlns:cc='http://creativecommons.org/ns#' xmlns:dc='http://purl.org/dc/elements/1.1/' 
sodipodi:docname='pattern-legacy.svg' height='98' id='svg7384' 
xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' 
xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' 
xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:svg='http://www.w3.org/2000/svg' 
version='1.1' inkscape:version='0.91 r13725' width='98' xmlns='http://www.w3.org/2000/svg'>
+  <metadata id='metadata90'>
+    <rdf:RDF>
+      <cc:Work rdf:about=''>
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/>
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview inkscape:bbox-nodes='true' inkscape:bbox-paths='true' bordercolor='#666666' 
borderopacity='1' inkscape:current-layer='layer3' inkscape:cx='-168.07028' inkscape:cy='67.863133' 
gridtolerance='10' inkscape:guide-bbox='true' guidetolerance='10' id='namedview88' 
inkscape:object-nodes='false' inkscape:object-paths='false' objecttolerance='10' pagecolor='#555753' 
inkscape:pageopacity='1' inkscape:pageshadow='2' showborder='false' showgrid='false' showguides='true' 
inkscape:snap-bbox='true' inkscape:snap-bbox-midpoints='false' inkscape:snap-global='true' 
inkscape:snap-grids='true' inkscape:snap-nodes='false' inkscape:snap-others='false' 
inkscape:snap-to-guides='true' inkscape:window-height='1403' inkscape:window-maximized='1' 
inkscape:window-width='2560' inkscape:window-x='2560' inkscape:window-y='0' inkscape:zoom='1'>
+    <inkscape:grid empspacing='2' enabled='true' id='grid4866' originx='-202' originy='192' 
snapvisiblegridlinesonly='true' spacingx='1px' spacingy='1px' type='xygrid' visible='true'/>
+  </sodipodi:namedview>
+  <title id='title9167'>Gnome Symbolic Icon Theme</title>
+  <defs id='defs7386'/>
+  <g inkscape:groupmode='layer' id='layer3' inkscape:label='patterns' transform='translate(-202,-110)'>
+    <rect height='95' id='rect7638' rx='3.8039246' ry='3.8039246' 
style='color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:new'
 width='95' x='203.5' y='111.5'/>
+    <path inkscape:connector-curvature='0' d='m 204.72497,123.51384 92.50606,0' id='path7642' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:0.99999988;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <path inkscape:connector-curvature='0' d='m 293.48901,116.49687 -4.04376,3.99957' id='path7646' 
sodipodi:nodetypes='cc' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <rect height='0.98332036' id='rect7648' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='288.95911' y='116.01071'/>
+    <rect height='0.98332036' id='rect7650' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='292.98077' y='116.01071'/>
+    <rect height='0.98332036' id='rect7652' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='292.98077' y='120.01028'/>
+    <rect height='0.98332036' id='rect7654' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='1.0054175' x='288.95911' y='120.01028'/>
+    <path inkscape:connector-curvature='0' d='m 289.44525,116.49687 4.04376,3.99957' id='path7656' 
sodipodi:nodetypes='cc' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#eeeeec;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'/>
+    <rect height='98' id='rect7658' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate'
 width='98' x='202' y='110'/>
+    <rect height='10.960155' id='rect23252' rx='0' ry='0' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='29.168156' x='204.90173' y='123.99176'/>
+    <rect height='5.038136' id='rect23254' rx='0' ry='0' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='22.98097' x='236.98669' y='126.99701'/>
+    <rect height='5.038136' id='rect23256' rx='0' ry='0' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='22.98097' x='266.95038' y='126.99701'/>
+    <rect height='5.038136' id='rect23258' rx='0' ry='0' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.39999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='22.98097' x='208.08368' y='126.99701'/>
+    <rect height='46.039845' id='rect23260' rx='0' ry='0' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 transform='scale(1,-1)' width='41.043156' x='204.41559' y='-180.5498'/>
+    <rect height='4.9939413' id='rect23262' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='33.896931' x='207.99536' y='138.00134'/>
+    <rect height='4.9939413' id='rect23264' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='24.969707' x='207.99536' y='146.00049'/>
+    <rect height='4.9939413' id='rect23266' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='28.019106' x='207.99536' y='154.00049'/>
+    
+    <rect height='4.9939413' id='rect23270' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:new'
 width='33.013046' x='207.99536' y='170.00049'/>
+  </g>
+</svg>
diff --git a/src/plugins/meson-templates/icons/scalable/actions/pattern-library.svg 
b/src/plugins/meson-templates/icons/scalable/actions/pattern-library.svg
new file mode 100644
index 000000000..481104a7d
--- /dev/null
+++ b/src/plugins/meson-templates/icons/scalable/actions/pattern-library.svg
@@ -0,0 +1,26 @@
+<?xml version='1.0' encoding='UTF-8' standalone='no'?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg xmlns:cc='http://creativecommons.org/ns#' xmlns:dc='http://purl.org/dc/elements/1.1/' 
sodipodi:docname='pattern-library.svg' height='98' id='svg7384' 
xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape' 
xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' 
xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' xmlns:svg='http://www.w3.org/2000/svg' 
version='1.1' inkscape:version='0.91 r13725' width='98' xmlns='http://www.w3.org/2000/svg'>
+  <metadata id='metadata90'>
+    <rdf:RDF>
+      <cc:Work rdf:about=''>
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type rdf:resource='http://purl.org/dc/dcmitype/StillImage'/>
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <sodipodi:namedview inkscape:bbox-nodes='true' inkscape:bbox-paths='true' bordercolor='#666666' 
borderopacity='1' inkscape:current-layer='layer3' inkscape:cx='-68.070278' inkscape:cy='67.863133' 
gridtolerance='10' inkscape:guide-bbox='true' guidetolerance='10' id='namedview88' 
inkscape:object-nodes='false' inkscape:object-paths='false' objecttolerance='10' pagecolor='#555753' 
inkscape:pageopacity='1' inkscape:pageshadow='2' showborder='false' showgrid='false' showguides='true' 
inkscape:snap-bbox='true' inkscape:snap-bbox-midpoints='false' inkscape:snap-global='true' 
inkscape:snap-grids='true' inkscape:snap-nodes='false' inkscape:snap-others='false' 
inkscape:snap-to-guides='true' inkscape:window-height='1403' inkscape:window-maximized='1' 
inkscape:window-width='2560' inkscape:window-x='2560' inkscape:window-y='0' inkscape:zoom='1'>
+    <inkscape:grid empspacing='2' enabled='true' id='grid4866' originx='-102' originy='192' 
snapvisiblegridlinesonly='true' spacingx='1px' spacingy='1px' type='xygrid' visible='true'/>
+  </sodipodi:namedview>
+  <title id='title9167'>Gnome Symbolic Icon Theme</title>
+  <defs id='defs7386'/>
+  <g inkscape:groupmode='layer' id='layer3' inkscape:label='patterns' transform='translate(-102,-110)'>
+    <rect height='95' id='rect7610' rx='3.8039246' ry='3.8039246' 
style='color:#000000;display:inline;overflow:visible;visibility:visible;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#729fcf;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker:none;enable-background:new'
 width='95' x='103.5' y='111.5'/>
+    <rect height='98' id='rect7630' 
style='color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999976;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate'
 width='98' x='102' y='110'/>
+    <rect height='32' id='rect42957' inkscape:label='a' 
style='color:#bebebe;display:inline;overflow:visible;visibility:visible;fill:none;stroke:none;stroke-width:1;marker:none'
 width='32' x='135.25' y='143.6875'/>
+    <path inkscape:connector-curvature='0' d='m 157.9996,144.1875 c -0.47988,0.8705 -0.93525,2.01154 
-1.4375,2.875 -0.18774,-0.014 -0.37142,-0.0626 -0.5625,-0.0626 -0.66451,0 -1.32018,0.0974 -1.9375,0.25 
-0.61005,-0.7656 -1.26068,-1.79626 -1.875,-2.5625 -0.56919,0.21008 -1.10454,0.45296 -1.625,0.75 
0.19186,0.96524 0.55379,2.1421 0.75,3.125 -0.68601,0.49764 -1.25237,1.064 -1.75,1.75 -0.98291,-0.1962 
-2.15976,-0.55814 -3.125,-0.75 -0.29704,0.52046 -0.53993,1.0558 -0.75,1.625 0.76624,0.61432 1.79689,1.26496 
2.5625,1.875 -0.1527,0.61732 -0.25,1.273 -0.25,1.9375 0,0.191 0.0493,0.37476 0.0625,0.5625 -0.86347,0.50226 
-2.0045,0.95762 -2.875,1.4375 0.10248,0.54952 0.25661,1.10748 0.4375,1.625 0.9828,-0.02 2.19899,-0.1892 
3.1875,-0.1874 0.37425,0.78352 0.84106,1.50762 1.4375,2.125 -0.34408,0.93566 -0.89625,2.0172 -1.25,2.9375 
0.4258,0.3514 0.89899,0.65246 1.375,0.9375 0.73841,-0.64022 1.55084,-1.54294 2.3125,-2.1875 0.75609,0.34512 
1.57724,0.53058 2.4375,0.625 0.32908,0.95352 0.60857,
 2.17616 
 0.9375,3.125 0.60308,-0.004 1.1736,-0.0282 1.75,-0.125 0.1611,-0.98582 0.22074,-2.2371 0.375,-3.25 
0.82017,-0.23368 1.62268,-0.53396 2.3125,-1 0.84866,0.52892 1.79774,1.25778 2.625,1.75 0.44747,-0.38134 
0.86866,-0.80252 1.25,-1.25 -0.49222,-0.82726 -1.22108,-1.77634 -1.75,-2.625 0.46603,-0.68982 
0.76632,-1.49232 1,-2.3125 1.0129,-0.1542 2.26417,-0.2139 3.25,-0.375 0.0967,-0.5764 0.12162,-1.14692 
0.125,-1.75 -0.94885,-0.32894 -2.17148,-0.60842 -3.125,-0.9375 -0.0944,-0.86026 -0.27989,-1.6814 
-0.625,-2.4375 0.64456,-0.76166 1.54728,-1.57408 2.1875,-2.3125 -0.28504,-0.476 -0.5861,-0.9492 
-0.9375,-1.375 -0.9203,0.35376 -2.00185,0.90592 -2.9375,1.25 -0.61738,-0.59644 -1.34147,-1.06324 
-2.125,-1.4375 -0.002,-0.98852 0.16792,-2.2047 0.1875,-3.1875 -0.51752,-0.1808 -1.07547,-0.33502 
-1.625,-0.4375 z m -2,6.8125 c 2.20914,0 4,1.79086 4,4 0,2.20914 -1.79086,4 -4,4 -2.20914,0 -4,-1.79086 -4,-4 
0,-2.20914 1.79086,-4 4,-4 z' id='path42961' style='color:#000000;display:inline;overflow:vis
 ible;vis
 
ibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;enable-background:accumulate'/>
+    <path inkscape:connector-curvature='0' d='m 142.6246,159.1875 c -0.46028,0.0942 -0.88782,0.26192 
-1.3125,0.4375 -0.0334,1.24866 0.17386,2.88354 -0.3125,3.3125 -0.47793,0.42154 -2.07672,0.0686 
-3.3125,-0.0626 -0.26278,0.47298 -0.47052,0.97046 -0.625,1.5 0.95619,0.79172 2.28102,1.67802 2.3125,2.3125 
0.0319,0.64238 -1.25284,1.60978 -2.125,2.5 0.20677,0.51566 0.50029,0.98708 0.8125,1.4375 1.21665,-0.25282 
2.73278,-0.74708 3.25,-0.375 0.52662,0.37884 0.53042,2.0107 0.6875,3.25 0.4991,0.15 1.02502,0.209 1.5625,0.25 
0.5627,-1.10924 1.13483,-2.6396 1.75,-2.8125 0.6315,-0.1774 1.92709,0.91626 3,1.5625 0.43742,-0.3032 
0.82586,-0.67412 1.1875,-1.0625 -0.50768,-1.14464 -1.44217,-2.58384 -1.1875,-3.1875 0.25491,-0.60422 
1.95081,-0.93926 3.125,-1.375 0.009,-0.147 0.0625,-0.28828 0.0625,-0.4375 0,-0.38274 -0.0688,-0.75798 
-0.125,-1.125 -1.21219,-0.32164 -2.93441,-0.4826 -3.25,-1.0625 -0.31347,-0.57602 0.48357,-2.1228 
0.875,-3.3125 -0.40232,-0.35716 -0.83882,-0.67432 -1.3125,-0.9375 -1.
 00179,0.
 75026 -2.16866,1.98872 -2.8125,1.875 -0.63377,-0.112 -1.32578,-1.6391 -2,-2.6875 -0.0804,0.014 
-0.17054,-0.016 -0.25,0 z m 1.25,3.75 c 1.86396,0 3.375,1.51104 3.375,3.375 0,1.86396 -1.51104,3.375 
-3.375,3.375 -1.86396,0 -3.375,-1.51104 -3.375,-3.375 0,-1.86396 1.51104,-3.375 3.375,-3.375 z' 
id='path42972' 
style='color:#000000;display:inline;overflow:visible;visibility:visible;fill:#729fcf;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;enable-background:accumulate'/>
+  </g>
+</svg>
diff --git a/src/plugins/meson-templates/meson-templates.gresource.xml 
b/src/plugins/meson-templates/meson-templates.gresource.xml
index 198b3b545..f71945e91 100644
--- a/src/plugins/meson-templates/meson-templates.gresource.xml
+++ b/src/plugins/meson-templates/meson-templates.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins/meson_templates">
+  <gresource prefix="/plugins/meson_templates">
     <file compressed="true">resources/src/window.ui</file>
     <file compressed="true">resources/src/window.js.tmpl</file>
     <file compressed="true">resources/src/meson-py.build</file>
@@ -45,4 +45,12 @@
     <file compressed="true">resources/po/POTFILES</file>
     <file compressed="true">resources/po/LINGUAS</file>
   </gresource>
+  <gresource prefix="/org/gnome/builder">
+    <file compressed="true">icons/scalable/actions/pattern-legacy.svg</file>
+    <file compressed="true">icons/scalable/actions/pattern-library.svg</file>
+    <file compressed="true">icons/scalable/actions/pattern-grid.svg</file>
+    <file compressed="true">icons/scalable/actions/pattern-browse.svg</file>
+    <file compressed="true">icons/scalable/actions/pattern-cli.svg</file>
+    <file compressed="true">icons/scalable/actions/pattern-gnome.svg</file>
+  </gresource>
 </gresources>
diff --git a/src/plugins/meson-templates/meson-templates.plugin 
b/src/plugins/meson-templates/meson-templates.plugin
index 1bd65f67e..13e2e912c 100644
--- a/src/plugins/meson-templates/meson-templates.plugin
+++ b/src/plugins/meson-templates/meson-templates.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Name=Meson Templates
-Description=Provides templates for creating meson projects
 Authors=Patrick Griffis <tingping tingping se>
 Copyright=Copyright © 2016 Patrick Griffis
-Builtin=true
+Description=Provides templates for creating meson projects
 Hidden=true
 Loader=python3
 Module=meson_templates
+Name=Meson Templates
+X-Builder-ABI=@PACKAGE_ABI@
+X-Has-Resources=true
diff --git a/src/plugins/meson-templates/meson.build b/src/plugins/meson-templates/meson.build
index b24d41a24..bf60ab160 100644
--- a/src/plugins/meson-templates/meson.build
+++ b/src/plugins/meson-templates/meson.build
@@ -1,5 +1,3 @@
-if get_option('with_meson_templates')
-
 meson_templates_resources = gnome.compile_resources(
   'meson_templates',
   'meson-templates.gresource.xml',
@@ -13,9 +11,7 @@ install_data('meson_templates.py', install_dir: plugindir)
 configure_file(
           input: 'meson-templates.plugin',
          output: 'meson-templates.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
-
-endif
diff --git a/src/plugins/meson-templates/meson_templates.py b/src/plugins/meson-templates/meson_templates.py
index 90f192943..0b2764289 100644
--- a/src/plugins/meson-templates/meson_templates.py
+++ b/src/plugins/meson-templates/meson_templates.py
@@ -20,9 +20,6 @@ import gi
 import os
 from os import path
 
-gi.require_version('Ide', '1.0')
-gi.require_version('Template', '1.0')
-
 from gi.repository import (
     Ide,
     Gio,
@@ -41,7 +38,6 @@ class LibraryTemplateProvider(GObject.Object, Ide.TemplateProvider):
                 CLIProjectTemplate(),
                 EmptyProjectTemplate()]
 
-
 class MesonTemplateLocator(Template.TemplateLocator):
     license = None
 
@@ -129,8 +125,10 @@ class MesonTemplate(Ide.TemplateBase, Ide.ProjectTemplate):
         scope.get('name_').assign_string(name_)
         scope.get('NAME').assign_string(name.upper().replace('-','_'))
 
-        # TODO: Support setting app id
-        appid = 'org.gnome.' + name.title()
+        if 'app-id' in params:
+            appid = params['app-id'].get_string()
+        else:
+            appid = 'org.example.App'
         appid_path = '/' + appid.replace('.', '/')
         scope.get('appid').assign_string(appid)
         scope.get('appid_path').assign_string(appid_path)
@@ -225,7 +223,7 @@ class MesonTemplate(Ide.TemplateBase, Ide.ProjectTemplate):
             if src.startswith('resource://'):
                 self.add_resource(src[11:], destination, scope, modes.get(src, 0))
             else:
-                path = os.path.join('/org/gnome/builder/plugins/meson_templates', src)
+                path = os.path.join('/plugins/meson_templates', src)
                 self.add_resource(path, destination, scope, modes.get(src, 0))
 
         self.expand_all_async(cancellable, self.expand_all_cb, task)
diff --git a/src/plugins/meson.build b/src/plugins/meson.build
index 14e7061a1..eeb4af8cf 100644
--- a/src/plugins/meson.build
+++ b/src/plugins/meson.build
@@ -1,42 +1,78 @@
 plugindir = join_paths(get_option('libdir'), 'gnome-builder/plugins')
 plugindatadir = join_paths(get_option('datadir'), 'gnome-builder/plugins')
 
-gnome_builder_plugins_sources = ['gnome-builder-plugins.c']
-gnome_builder_plugins_args = []
-gnome_builder_plugins_deps = [libpeas_dep, libide_plugin_dep, libide_dep]
-gnome_builder_plugins_link_with = []
-gnome_builder_plugins_link_deps = join_paths(meson.current_source_dir(), 'plugins.map')
-gnome_builder_plugins_link_args = [
-  '-Wl,--version-script,' + gnome_builder_plugins_link_deps,
+plugins_sources = []
+plugins_include_directories = []
+plugins_generated_sources = []
+plugins_link_with = []
+
+plugins_deps = [
+  libdazzle_dep,
+  libgtk_dep,
+  libgtksource_dep,
+  libgit_dep,
+  libjsonrpc_glib_dep,
+
+  libide_code_dep,
+  libide_core_dep,
+  libide_debugger_dep,
+  libide_editor_dep,
+  libide_foundry_dep,
+  libide_greeter_dep,
+  libide_gui_dep,
+  libide_io_dep,
+  libide_plugins_dep,
+  libide_projects_dep,
+  libide_search_dep,
+  libide_sourceview_dep,
+  libide_terminal_dep,
+  libide_themes_dep,
+  libide_threading_dep,
+  libide_tree_dep,
+  libide_vcs_dep,
+  libide_webkit_dep,
 ]
 
+subdir('auto-save')
 subdir('autotools')
 subdir('beautifier')
-subdir('c-pack')
+subdir('buildconfig')
+subdir('buildsystem')
+subdir('buildui')
+subdir('buffer-monitor')
 subdir('cargo')
 subdir('clang')
 subdir('cmake')
 subdir('code-index')
+subdir('codeui')
 subdir('color-picker')
 subdir('command-bar')
 subdir('comment-code')
+subdir('c-pack')
 subdir('create-project')
 subdir('ctags')
+subdir('debuggerui')
 subdir('devhelp')
+subdir('deviceui')
 subdir('deviced')
+subdir('doap')
+subdir('editor')
+subdir('editorconfig')
+subdir('emacs')
 subdir('eslint')
+subdir('flatpak')
 subdir('file-search')
 subdir('find-other-file')
-subdir('flatpak')
-subdir('gradle')
 subdir('gcc')
 subdir('gdb')
 subdir('gettext')
 subdir('git')
-subdir('gjs-symbols')
 subdir('glade')
 subdir('gnome-code-assistance')
 subdir('go-langserv')
+subdir('gjs-symbols')
+subdir('gradle')
+subdir('greeter')
 subdir('grep')
 subdir('history')
 subdir('html-completion')
@@ -49,10 +85,12 @@ subdir('maven')
 subdir('meson')
 subdir('meson-templates')
 subdir('messages')
+subdir('modelines')
 subdir('mono')
 subdir('newcomers')
 subdir('notification')
 subdir('npm')
+subdir('omni-gutter')
 subdir('phpize')
 subdir('project-tree')
 subdir('python-gi-imports-completion')
@@ -60,107 +98,87 @@ subdir('python-pack')
 subdir('qemu')
 subdir('quick-highlight')
 subdir('recent')
+subdir('restore-cursor')
 subdir('retab')
-subdir('rust-langserv')
+subdir('rls')
 subdir('rustup')
-subdir('spellcheck')
 subdir('snippets')
+subdir('spellcheck')
+subdir('sublime')
 subdir('support')
 subdir('symbol-tree')
 subdir('sysprof')
 subdir('sysroot')
 subdir('terminal')
+subdir('testui')
 subdir('todo')
-subdir('vala-pack')
+subdir('trim-spaces')
 subdir('valgrind')
+subdir('vcsui')
+subdir('vim')
 subdir('words')
 subdir('xml-pack')
 
-gnome_builder_plugins = shared_library(
-  'gnome-builder-plugins',
-  gnome_builder_plugins_sources,
-
-   dependencies: gnome_builder_plugins_deps,
-   link_depends: 'plugins.map',
-         c_args: gnome_builder_plugins_args + release_args,
-      link_args: gnome_builder_plugins_link_args,
-      link_with: gnome_builder_plugins_link_with,
-        install: true,
-    install_dir: pkglibdir,
-  install_rpath: pkglibdir_abs,
-)
-
-gnome_builder_plugins_dep = declare_dependency(
-   dependencies: libide_deps,
-      link_with: gnome_builder_plugins_link_with + [gnome_builder_plugins],
+plugins = static_library('plugins', plugins_sources,
+         dependencies: plugins_deps,
+               c_args: release_args,
+  include_directories: plugins_include_directories,
+            link_with: plugins_link_with,
 )
 
 status += [
   'Plugins:',
   '',
-  'Autotools ............. : @0@'.format(get_option('with_autotools')),
-  'Beautifier ............ : @0@'.format(get_option('with_beautifier')),
-  'C Language Pack ....... : @0@'.format(get_option('with_c_pack')),
-  'Cargo ................. : @0@'.format(get_option('with_cargo')),
-  'Clang ................. : @0@'.format(get_option('with_clang')),
-  'CMake ................. : @0@'.format(get_option('with_cmake')),
-  'Color Picker .......... : @0@'.format(get_option('with_color_picker')),
-  'Command Bar ........... : @0@'.format(get_option('with_command_bar')),
-  'Comment Code .......... : @0@'.format(get_option('with_comment_code')),
-  'Project Wizard ........ : @0@'.format(get_option('with_create_project')),
-  'CTags ................. : @0@'.format(get_option('with_ctags')),
-  'Devhelp ............... : @0@'.format(get_option('with_devhelp')),
-  'Deviced ............... : @0@'.format(get_option('with_deviced')),
-  'ESLint ................ : @0@'.format(get_option('with_eslint')),
-  'File Search ........... : @0@'.format(get_option('with_file_search')),
-  'Find other file ....... : @0@'.format(get_option('with_find_other_file')),
-  'Flatpak ............... : @0@'.format(get_option('with_flatpak')),
-  'Gradle ................ : @0@'.format(get_option('with_gradle')),
-  'GCC ................... : @0@'.format(get_option('with_gcc')),
-  'GDB ................... : @0@'.format(get_option('with_gdb')),
-  'Gettext ............... : @0@'.format(get_option('with_gettext')),
-  'Git ................... : @0@'.format(get_option('with_git')),
-  'GJS Symbol Resolver ... : @0@'.format(get_option('with_gjs_symbols')),
-  'Glade ................. : @0@'.format(get_option('with_glade')),
-  'GNOME Code Assistance . : @0@'.format(get_option('with_gnome_code_assistance')),
-  'Go Language Server .... : @0@'.format(get_option('with_go_langserv')),
-  'Grep .................. : @0@'.format(get_option('with_grep')),
-  'History ............... : @0@'.format(get_option('with_history')),
-  'HTML Completion ....... : @0@'.format(get_option('with_html_completion')),
-  'HTML Preview .......... : @0@'.format(get_option('with_html_preview')),
-  'Python Jedi ........... : @0@'.format(get_option('with_jedi')),
-  'JHBuild ............... : @0@'.format(get_option('with_jhbuild')),
-  'Directory View ........ : @0@'.format(get_option('with_ls')),
-  'Make .................. : @0@'.format(get_option('with_make')),
-  'Maven.................. : @0@'.format(get_option('with_maven')),
-  'Meson ................. : @0@'.format(get_option('with_meson')),
-  'Mono .................. : @0@'.format(get_option('with_mono')),
-  'Notifications ......... : @0@'.format(get_option('with_notification')),
-  'Node Package Manager .. : @0@'.format(get_option('with_npm')),
-  'PHPize ................ : @0@'.format(get_option('with_phpize')),
-  'Project Tree .......... : @0@'.format(get_option('with_project_tree')),
-  'Python GI Completion .. : @0@'.format(get_option('with_python_gi_imports_completion')),
-  'Python Language Pack .. : @0@'.format(get_option('with_python_pack')),
-  'Qemu .................. : @0@'.format(get_option('with_qemu')),
-  'Quick Highlight ....... : @0@'.format(get_option('with_quick_highlight')),
-  'Retab ................. : @0@'.format(get_option('with_retab')),
-  'Rust Language Server .. : @0@'.format(get_option('with_rust_langserv')),
-  'RustUp ................ : @0@'.format(get_option('with_rustup')),
-  'Snippets .............. : @0@'.format(get_option('with_snippets')),
-  'Spellchecking ......... : @0@'.format(get_option('with_spellcheck')),
-  'Support Tool .......... : @0@'.format(get_option('with_support')),
-  'Symbol Tree ........... : @0@'.format(get_option('with_symbol_tree')),
-  'Sysprof Profiler ...... : @0@'.format(get_option('with_sysprof')),
-  'Sysroot ......          : @0@'.format(get_option('with_sysroot')),
-  'Todo .................. : @0@'.format(get_option('with_todo')),
-  'Vala Language Pack .... : @0@'.format(get_option('with_vala_pack')),
-  'Valgrind .............. : @0@'.format(get_option('with_valgrind')),
-  'Word Completion ....... : @0@'.format(get_option('with_words')),
-  'XML Language Pack ..... : @0@'.format(get_option('with_xml_pack')),
-  '', '',
-
-  'Templates:',
+  'Autotools ............. : @0@'.format(get_option('plugin_autotools')),
+  'Beautifier ............ : @0@'.format(get_option('plugin_beautifier')),
+  'C Pack ................ : @0@'.format(get_option('plugin_c_pack')),
+  'Cargo ................. : @0@'.format(get_option('plugin_cargo')),
+  'Clang ................. : @0@'.format(get_option('plugin_clang')),
+  'CMake ................. : @0@'.format(get_option('plugin_cmake')),
+  'Code Index ............ : @0@'.format(get_option('plugin_code_index')),
+  'Color Pickr ........... : @0@'.format(get_option('plugin_color_picker')),
+  'CTags ................. : @0@'.format(get_option('plugin_ctags')),
+  'Devhelp ............... : @0@'.format(get_option('plugin_devhelp')),
+  'Deviced ............... : @0@'.format(get_option('plugin_deviced')),
+  'Editorconfig .......... : @0@'.format(get_option('plugin_editorconfig')),
+  'ESLint ................ : @0@'.format(get_option('plugin_eslint')),
+  'File Search ........... : @0@'.format(get_option('plugin_file_search')),
+  'Flatpak ............... : @0@'.format(get_option('plugin_flatpak')),
+  'GDB ................... : @0@'.format(get_option('plugin_gdb')),
+  'Gettext ............... : @0@'.format(get_option('plugin_gettext')),
+  'Git ................... : @0@'.format(get_option('plugin_git')),
+  'GJS Symbols ........... : @0@'.format(get_option('plugin_gjs_symbols')),
+  'Glade ................. : @0@'.format(get_option('plugin_glade')),
+  'GNOME Code Assistance . : @0@'.format(get_option('plugin_gnome_code_assistance')),
+  'Go Language Server .... : @0@'.format(get_option('plugin_go_langserv')),
+  'Gradle ................ : @0@'.format(get_option('plugin_gradle')),
+  'Grep .................. : @0@'.format(get_option('plugin_grep')),
+  'HTML Completion ....... : @0@'.format(get_option('plugin_html_completion')),
+  'HTML Preview .......... : @0@'.format(get_option('plugin_html_preview')),
+  'Jedi .................. : @0@'.format(get_option('plugin_jedi')),
+  'JHBuild ............... : @0@'.format(get_option('plugin_jhbuild')),
+  'Make .................. : @0@'.format(get_option('plugin_make')),
+  'Maven ................. : @0@'.format(get_option('plugin_maven')),
+  'Meson ................. : @0@'.format(get_option('plugin_meson')),
+  'Modelines ............. : @0@'.format(get_option('plugin_modelines')),
+  'Mono .................. : @0@'.format(get_option('plugin_mono')),
+  'Newcomers ............. : @0@'.format(get_option('plugin_newcomers')),
+  'Notifications ......... : @0@'.format(get_option('plugin_notification')),
+  'Npm ................... : @0@'.format(get_option('plugin_npm')),
+  'PHPize ................ : @0@'.format(get_option('plugin_phpize')),
+  'Python Pack ........... : @0@'.format(get_option('plugin_python_pack')),
+  'Qemu .................. : @0@'.format(get_option('plugin_qemu')),
+  'Quick Highlight ....... : @0@'.format(get_option('plugin_quick_highlight')),
+  'Retab ................. : @0@'.format(get_option('plugin_retab')),
+  'RLS ................... : @0@'.format(get_option('plugin_rls')),
+  'Rustup ................ : @0@'.format(get_option('plugin_rustup')),
+  'Spellcheck ............ : @0@'.format(get_option('plugin_spellcheck')),
+  'Sysprof ............... : @0@'.format(get_option('plugin_sysprof')),
+  'Sysroot ............... : @0@'.format(get_option('plugin_sysroot')),
+  'Todo .................. : @0@'.format(get_option('plugin_todo')),
+  'Vala Pack ............. : @0@'.format(get_option('plugin_vala')),
+  'Valgrind .............. : @0@'.format(get_option('plugin_valgrind')),
+  'Word Completion ....... : @0@'.format(get_option('plugin_words')),
+  'XML Pack .............. : @0@'.format(get_option('plugin_xml_pack')),
   '',
-  'Meson ................. : @0@'.format(get_option('with_meson_templates')),
-  '', ''
 ]
diff --git a/src/plugins/meson/gbp-meson-build-stage-cross-file.c 
b/src/plugins/meson/gbp-meson-build-stage-cross-file.c
index 2edfbeb89..491e62b6e 100644
--- a/src/plugins/meson/gbp-meson-build-stage-cross-file.c
+++ b/src/plugins/meson/gbp-meson-build-stage-cross-file.c
@@ -16,6 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gbp-meson-build-stage-cross-file"
@@ -47,6 +49,7 @@ add_lang_executable (const gchar *lang,
 static void
 gbp_meson_build_stage_cross_file_query (IdeBuildStage    *stage,
                                         IdeBuildPipeline *pipeline,
+                                        GPtrArray        *targets,
                                         GCancellable     *cancellable)
 {
   GbpMesonBuildStageCrossFile *self = (GbpMesonBuildStageCrossFile *)stage;
@@ -176,7 +179,6 @@ gbp_meson_build_stage_cross_file_class_init (GbpMesonBuildStageCrossFileClass *k
 static void
 gbp_meson_build_stage_cross_file_init (GbpMesonBuildStageCrossFile *self)
 {
-  
 }
 
 GbpMesonBuildStageCrossFile *
diff --git a/src/plugins/meson/gbp-meson-build-stage-cross-file.h 
b/src/plugins/meson/gbp-meson-build-stage-cross-file.h
index 3ae12cf40..4873772c2 100644
--- a/src/plugins/meson/gbp-meson-build-stage-cross-file.h
+++ b/src/plugins/meson/gbp-meson-build-stage-cross-file.h
@@ -16,11 +16,13 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-build-system-discovery.c 
b/src/plugins/meson/gbp-meson-build-system-discovery.c
new file mode 100644
index 000000000..4028af51a
--- /dev/null
+++ b/src/plugins/meson/gbp-meson-build-system-discovery.c
@@ -0,0 +1,91 @@
+/* gbp-meson-build-system-discovery.c
+ *
+ * Copyright 2018-2019 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-meson-build-system-discovery"
+
+#include "config.h"
+
+#include <libide-foundry.h>
+
+#include "gbp-meson-build-system-discovery.h"
+
+struct _GbpMesonBuildSystemDiscovery
+{
+  GObject parent_instance;
+};
+
+static gchar *
+gbp_meson_build_system_discovery_discover (IdeBuildSystemDiscovery  *discovery,
+                                           GFile                    *directory,
+                                           GCancellable             *cancellable,
+                                           gint                     *priority,
+                                           GError                  **error)
+{
+  g_autoptr(GFile) meson_build = NULL;
+  g_autoptr(GFileInfo) info = NULL;
+
+  g_assert (!IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_MESON_BUILD_SYSTEM_DISCOVERY (discovery));
+  g_assert (G_IS_FILE (directory));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (priority != NULL);
+
+  *priority = 0;
+
+  meson_build = g_file_get_child (directory, "meson.build");
+  info = g_file_query_info (meson_build,
+                            G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                            G_FILE_QUERY_INFO_NONE,
+                            cancellable,
+                            NULL);
+
+  if (info == NULL || g_file_info_get_file_type (info) != G_FILE_TYPE_REGULAR)
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_NOT_SUPPORTED,
+                   "Meson is not supported in this project");
+      return NULL;
+    }
+
+  *priority = GBP_MESON_BUILD_SYSTEM_DISCOVERY_PRIORITY;
+
+  return g_strdup ("meson");
+}
+
+static void
+build_system_discovery_iface_init (IdeBuildSystemDiscoveryInterface *iface)
+{
+  iface->discover = gbp_meson_build_system_discovery_discover;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpMesonBuildSystemDiscovery, gbp_meson_build_system_discovery, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUILD_SYSTEM_DISCOVERY,
+                                                build_system_discovery_iface_init))
+
+static void
+gbp_meson_build_system_discovery_class_init (GbpMesonBuildSystemDiscoveryClass *klass)
+{
+}
+
+static void
+gbp_meson_build_system_discovery_init (GbpMesonBuildSystemDiscovery *self)
+{
+}
diff --git a/src/plugins/meson/gbp-meson-build-system-discovery.h 
b/src/plugins/meson/gbp-meson-build-system-discovery.h
new file mode 100644
index 000000000..3596b11bd
--- /dev/null
+++ b/src/plugins/meson/gbp-meson-build-system-discovery.h
@@ -0,0 +1,32 @@
+/* gbp-meson-build-system-discovery.h
+ *
+ * Copyright 2018-2019 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_MESON_BUILD_SYSTEM_DISCOVERY     (gbp_meson_build_system_discovery_get_type())
+#define GBP_MESON_BUILD_SYSTEM_DISCOVERY_PRIORITY (100)
+
+G_DECLARE_FINAL_TYPE (GbpMesonBuildSystemDiscovery, gbp_meson_build_system_discovery, GBP, 
MESON_BUILD_SYSTEM_DISCOVERY, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/meson/gbp-meson-build-system.c b/src/plugins/meson/gbp-meson-build-system.c
index a5349722c..b5b913fbf 100644
--- a/src/plugins/meson/gbp-meson-build-system.c
+++ b/src/plugins/meson/gbp-meson-build-system.c
@@ -1,6 +1,6 @@
 /* gbp-meson-build-system.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-meson-build-system"
@@ -85,10 +87,11 @@ gbp_meson_build_system_ensure_config_async (GbpMesonBuildSystem *self,
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
 
   ide_build_manager_execute_async (build_manager,
                                    IDE_BUILD_PHASE_CONFIGURE,
+                                   NULL,
                                    cancellable,
                                    gbp_meson_build_system_ensure_config_cb,
                                    g_steal_pointer (&task));
@@ -203,7 +206,7 @@ gbp_meson_build_system_load_commands_config_cb (GObject      *object,
     }
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   pipeline = ide_build_manager_get_pipeline (build_manager);
 
   if (pipeline == NULL)
@@ -281,7 +284,7 @@ gbp_meson_build_system_load_commands_async (GbpMesonBuildSystem *self,
    */
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   pipeline = ide_build_manager_get_pipeline (build_manager);
 
   /*
@@ -466,23 +469,22 @@ gbp_meson_build_system_get_build_flags_for_files_cb (GObject      *object,
 
   /* Get non-standard system includes */
   context = ide_object_get_context (IDE_OBJECT (self));
-  config_manager = ide_context_get_configuration_manager (context);
+  config_manager = ide_configuration_manager_from_context (context);
   config = ide_configuration_manager_get_current (config_manager);
   if (NULL != (runtime = ide_configuration_get_runtime (config)))
     system_includes = ide_runtime_get_system_include_dirs (runtime);
 
-  ret = g_hash_table_new_full ((GHashFunc)ide_file_hash,
-                               (GEqualFunc)ide_file_equal,
+  ret = g_hash_table_new_full (g_file_hash,
+                               (GEqualFunc)g_file_equal,
                                g_object_unref,
                                (GDestroyNotify)g_strfreev);
 
   for (guint i = 0; i < files->len; i++)
     {
-      IdeFile *file = g_ptr_array_index (files, i);
-      GFile *gfile = ide_file_get_file (file);
+      GFile *file = g_ptr_array_index (files, i);
       g_auto(GStrv) flags = NULL;
 
-      flags = ide_compile_commands_lookup (compile_commands, gfile,
+      flags = ide_compile_commands_lookup (compile_commands, file,
                                            (const gchar * const *)system_includes,
                                            NULL, NULL);
       g_hash_table_insert (ret, g_object_ref (file), g_steal_pointer (&flags));
@@ -526,7 +528,7 @@ gbp_meson_build_system_get_build_flags_cb (GObject      *object,
 
   /* Get non-standard system includes */
   context = ide_object_get_context (IDE_OBJECT (self));
-  config_manager = ide_context_get_configuration_manager (context);
+  config_manager = ide_configuration_manager_from_context (context);
   config = ide_configuration_manager_get_current (config_manager);
   if (NULL != (runtime = ide_configuration_get_runtime (config)))
     system_includes = ide_runtime_get_system_include_dirs (runtime);
@@ -545,27 +547,24 @@ gbp_meson_build_system_get_build_flags_cb (GObject      *object,
 
 static void
 gbp_meson_build_system_get_build_flags_async (IdeBuildSystem      *build_system,
-                                              IdeFile             *file,
+                                              GFile               *file,
                                               GCancellable        *cancellable,
                                               GAsyncReadyCallback  callback,
                                               gpointer             user_data)
 {
   GbpMesonBuildSystem *self = (GbpMesonBuildSystem *)build_system;
   g_autoptr(IdeTask) task = NULL;
-  GFile *gfile;
 
   IDE_ENTRY;
 
   g_assert (GBP_IS_MESON_BUILD_SYSTEM (self));
-  g_assert (IDE_IS_FILE (file));
+  g_assert (G_IS_FILE (file));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
-  gfile = ide_file_get_file (file);
-
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_priority (task, G_PRIORITY_LOW);
   ide_task_set_source_tag (task, gbp_meson_build_system_get_build_flags_async);
-  ide_task_set_task_data (task, g_object_ref (gfile), g_object_unref);
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
 
   gbp_meson_build_system_load_commands_async (self,
                                               cancellable,
@@ -710,49 +709,6 @@ gbp_meson_build_system_notify_pipeline (GbpMesonBuildSystem *self,
   IDE_EXIT;
 }
 
-static void
-gbp_meson_build_system_init_worker (IdeTask      *task,
-                                    gpointer      source_object,
-                                    gpointer      task_data,
-                                    GCancellable *cancellable)
-{
-  GFile *project_file = task_data;
-  g_autofree gchar *name = NULL;
-
-  IDE_ENTRY;
-
-  g_assert (GBP_IS_MESON_BUILD_SYSTEM (source_object));
-  g_assert (G_IS_FILE (project_file));
-  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
-
-  name = g_file_get_basename (project_file);
-
-  if (dzl_str_equal0 (name, "meson.build"))
-    {
-      ide_task_return_pointer (task, g_object_ref (project_file), g_object_unref);
-      IDE_EXIT;
-    }
-
-  if (g_file_query_file_type (project_file, 0, cancellable) == G_FILE_TYPE_DIRECTORY)
-    {
-      g_autoptr(GFile) meson_build = g_file_get_child (project_file, "meson.build");
-
-      if (g_file_query_exists (meson_build, cancellable))
-        {
-          ide_task_return_pointer (task, g_object_ref (meson_build), g_object_unref);
-          IDE_EXIT;
-        }
-    }
-
-  ide_task_return_new_error (task,
-                             G_IO_ERROR,
-                             G_IO_ERROR_NOT_SUPPORTED,
-                             "%s is not supported by the meson plugin",
-                             name);
-
-  IDE_EXIT;
-}
-
 static void
 gbp_meson_build_system_init_async (GAsyncInitable      *initable,
                                    gint                 io_priority,
@@ -774,7 +730,7 @@ gbp_meson_build_system_init_async (GAsyncInitable      *initable,
   context = ide_object_get_context (IDE_OBJECT (self));
   g_assert (IDE_IS_CONTEXT (context));
 
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
 
   task = ide_task_new (self, cancellable, callback, user_data);
@@ -792,7 +748,7 @@ gbp_meson_build_system_init_async (GAsyncInitable      *initable,
                            self,
                            G_CONNECT_SWAPPED);
 
-  ide_task_run_in_thread (task, gbp_meson_build_system_init_worker);
+  ide_task_return_boolean (task, TRUE);
 
   IDE_EXIT;
 }
diff --git a/src/plugins/meson/gbp-meson-build-system.h b/src/plugins/meson/gbp-meson-build-system.h
index d9e44260d..16a6747d5 100644
--- a/src/plugins/meson/gbp-meson-build-system.h
+++ b/src/plugins/meson/gbp-meson-build-system.h
@@ -1,6 +1,6 @@
 /* gbp-meson-build-system.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-build-target-provider.c 
b/src/plugins/meson/gbp-meson-build-target-provider.c
index 2e0ae2e41..434a16718 100644
--- a/src/plugins/meson/gbp-meson-build-target-provider.c
+++ b/src/plugins/meson/gbp-meson-build-target-provider.c
@@ -1,6 +1,6 @@
 /* gbp-meson-build-target-provider.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-meson-build-target-provider"
@@ -39,7 +41,7 @@ create_launcher (IdeContext  *context,
   g_assert (IDE_IS_CONTEXT (context));
   g_assert (error == NULL || *error == NULL);
 
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   pipeline = ide_build_manager_get_pipeline (build_manager);
 
   if (pipeline == NULL)
@@ -128,7 +130,12 @@ gbp_meson_build_target_provider_communicate_cb2 (GObject      *object,
 
           /* We only need one result */
           ret = g_ptr_array_new_with_free_func (g_object_unref);
-          g_ptr_array_add (ret, gbp_meson_build_target_new (context, gdir, name));
+          g_ptr_array_add (ret,
+                           gbp_meson_build_target_new (context,
+                                                       gdir,
+                                                       name,
+                                                       NULL,
+                                                       IDE_ARTIFACT_KIND_EXECUTABLE));
           ide_task_return_pointer (task, g_steal_pointer (&ret), (GDestroyNotify)g_ptr_array_unref);
 
           return;
@@ -208,6 +215,7 @@ gbp_meson_build_target_provider_communicate_cb (GObject      *object,
     {
       JsonNode *element = json_array_get_element (array, i);
       const gchar *name;
+      const gchar *install_filename;
       const gchar *filename;
       const gchar *type;
       JsonObject *obj;
@@ -221,6 +229,9 @@ gbp_meson_build_target_provider_communicate_cb (GObject      *object,
           NULL != (name = json_node_get_string (member)) &&
           NULL != (member = json_object_get_member (obj, "install_filename")) &&
           JSON_NODE_HOLDS_VALUE (member) &&
+          NULL != (install_filename = json_node_get_string (member)) &&
+          NULL != (member = json_object_get_member (obj, "filename")) &&
+          JSON_NODE_HOLDS_VALUE (member) &&
           NULL != (filename = json_node_get_string (member)) &&
           NULL != (member = json_object_get_member (obj, "type")) &&
           JSON_NODE_HOLDS_VALUE (member) &&
@@ -234,24 +245,32 @@ gbp_meson_build_target_provider_communicate_cb (GObject      *object,
           g_autofree gchar *base = NULL;
           g_autofree gchar *name_of_dir = NULL;
           g_autoptr(GFile) dir = NULL;
+          IdeArtifactKind kind = 0;
 
-          install_dir = g_path_get_dirname (filename);
+          install_dir = g_path_get_dirname (install_filename);
           name_of_dir = g_path_get_basename (install_dir);
 
           g_debug ("Found target %s", name);
 
-          base = g_path_get_basename (filename);
+          base = g_path_get_basename (install_filename);
           dir = g_file_new_for_path (install_dir);
 
-          target = gbp_meson_build_target_new (context, dir, base);
+          if (ide_str_equal0 (type, "executable"))
+            kind = IDE_ARTIFACT_KIND_EXECUTABLE;
+          else if (ide_str_equal0 (type, "static library"))
+            kind = IDE_ARTIFACT_KIND_STATIC_LIBRARY;
+          else if (ide_str_equal0 (type, "shared library"))
+            kind = IDE_ARTIFACT_KIND_SHARED_LIBRARY;
+
+          target = gbp_meson_build_target_new (context, dir, base, filename, kind);
 
-          found_bindir |= dzl_str_equal0 (name_of_dir, "bin");
+          found_bindir |= ide_str_equal0 (name_of_dir, "bin");
 
           /*
            * Until Builder supports selecting a target to run, we need to prefer
            * bindir targets over other targets.
            */
-          if (dzl_str_equal0 (name_of_dir, "bin") && dzl_str_equal0 (type, "executable"))
+          if (ide_str_equal0 (name_of_dir, "bin") && kind == IDE_ARTIFACT_KIND_EXECUTABLE)
             g_ptr_array_insert (ret, 0, g_steal_pointer (&target));
           else
             g_ptr_array_add (ret, g_steal_pointer (&target));
@@ -278,7 +297,7 @@ gbp_meson_build_target_provider_communicate_cb (GObject      *object,
     }
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   pipeline = ide_build_manager_get_pipeline (build_manager);
   cancellable = ide_task_get_cancellable (task);
 
@@ -328,7 +347,7 @@ gbp_meson_build_target_provider_get_targets_async (IdeBuildTargetProvider *provi
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
 
   if (!GBP_IS_MESON_BUILD_SYSTEM (build_system))
     {
@@ -339,7 +358,7 @@ gbp_meson_build_target_provider_get_targets_async (IdeBuildTargetProvider *provi
       IDE_EXIT;
     }
 
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   pipeline = ide_build_manager_get_pipeline (build_manager);
 
   if (pipeline == NULL)
diff --git a/src/plugins/meson/gbp-meson-build-target-provider.h 
b/src/plugins/meson/gbp-meson-build-target-provider.h
index d180a9221..3d1b1d0dc 100644
--- a/src/plugins/meson/gbp-meson-build-target-provider.h
+++ b/src/plugins/meson/gbp-meson-build-target-provider.h
@@ -1,6 +1,6 @@
 /* gbp-meson-build-target-provider.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-build-target.c b/src/plugins/meson/gbp-meson-build-target.c
index 82150271b..43f770daa 100644
--- a/src/plugins/meson/gbp-meson-build-target.c
+++ b/src/plugins/meson/gbp-meson-build-target.c
@@ -1,6 +1,6 @@
 /* gbp-meson-build-target.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-meson-build-target"
@@ -22,16 +24,19 @@
 
 struct _GbpMesonBuildTarget
 {
-  IdeObject  parent_instance;
+  IdeObject        parent_instance;
 
-  GFile     *install_directory;
-  gchar     *name;
+  GFile           *install_directory;
+  gchar           *name;
+  gchar           *filename;
+  IdeArtifactKind  kind;
 };
 
 enum {
   PROP_0,
   PROP_INSTALL_DIRECTORY,
   PROP_NAME,
+  PROP_FILE_NAME,
   N_PROPS
 };
 
@@ -57,11 +62,18 @@ gbp_meson_build_target_get_name (IdeBuildTarget *build_target)
   return self->name ? g_strdup (self->name) : NULL;
 }
 
+static IdeArtifactKind
+gbp_meson_build_target_get_kind (IdeBuildTarget *target)
+{
+  return GBP_MESON_BUILD_TARGET (target)->kind;
+}
+
 static void
 build_target_iface_init (IdeBuildTargetInterface *iface)
 {
   iface->get_install_directory = gbp_meson_build_target_get_install_directory;
   iface->get_name = gbp_meson_build_target_get_name;
+  iface->get_kind = gbp_meson_build_target_get_kind;
 }
 
 G_DEFINE_TYPE_WITH_CODE (GbpMesonBuildTarget, gbp_meson_build_target, IDE_TYPE_OBJECT,
@@ -85,6 +97,10 @@ gbp_meson_build_target_get_property (GObject    *object,
       g_value_set_string (value, self->name);
       break;
 
+    case PROP_FILE_NAME:
+      g_value_set_string (value, self->filename);
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -108,6 +124,10 @@ gbp_meson_build_target_set_property (GObject      *object,
       self->name = g_value_dup_string (value);
       break;
 
+    case PROP_FILE_NAME:
+      self->filename = g_value_dup_string (value);
+      break;
+
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
     }
@@ -147,6 +167,13 @@ gbp_meson_build_target_class_init (GbpMesonBuildTargetClass *klass)
                          NULL,
                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
 
+  properties [PROP_FILE_NAME] =
+    g_param_spec_string ("file-name",
+                         NULL,
+                         NULL,
+                         NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
   g_object_class_install_properties (object_class, N_PROPS, properties);
 }
 
@@ -156,9 +183,11 @@ gbp_meson_build_target_init (GbpMesonBuildTarget *self)
 }
 
 IdeBuildTarget *
-gbp_meson_build_target_new (IdeContext *context,
-                            GFile      *install_directory,
-                            gchar      *name)
+gbp_meson_build_target_new (IdeContext      *context,
+                            GFile           *install_directory,
+                            const gchar     *name,
+                            const gchar     *filename,
+                            IdeArtifactKind  kind)
 {
   GbpMesonBuildTarget *self;
 
@@ -166,11 +195,19 @@ gbp_meson_build_target_new (IdeContext *context,
   g_return_val_if_fail (G_IS_FILE (install_directory), NULL);
   g_return_val_if_fail (name != NULL, NULL);
 
-  self = g_object_new (GBP_TYPE_MESON_BUILD_TARGET,
-                       "context", context,
-                       NULL);
+  self = g_object_new (GBP_TYPE_MESON_BUILD_TARGET, NULL);
   g_set_object (&self->install_directory, install_directory);
   self->name = g_strdup (name);
+  self->filename = g_strdup (filename);
+  self->kind = kind;
 
   return IDE_BUILD_TARGET (self);
 }
+
+const gchar *
+gbp_meson_build_target_get_filename (GbpMesonBuildTarget *self)
+{
+  g_return_val_if_fail (GBP_IS_MESON_BUILD_TARGET (self), NULL);
+
+  return self->filename;
+}
diff --git a/src/plugins/meson/gbp-meson-build-target.h b/src/plugins/meson/gbp-meson-build-target.h
index c33cf6db7..1ef5f5172 100644
--- a/src/plugins/meson/gbp-meson-build-target.h
+++ b/src/plugins/meson/gbp-meson-build-target.h
@@ -1,6 +1,6 @@
 /* gbp-meson-build-target.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
@@ -26,8 +28,11 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpMesonBuildTarget, gbp_meson_build_target, GBP, MESON_BUILD_TARGET, IdeObject)
 
-IdeBuildTarget *gbp_meson_build_target_new (IdeContext *context,
-                                            GFile      *install_directory,
-                                            gchar      *name);
+IdeBuildTarget *gbp_meson_build_target_new          (IdeContext          *context,
+                                                     GFile               *install_directory,
+                                                     const gchar         *name,
+                                                     const gchar         *filename,
+                                                     IdeArtifactKind      kind);
+const gchar    *gbp_meson_build_target_get_filename (GbpMesonBuildTarget *self);
 
 G_END_DECLS
diff --git a/src/plugins/meson/gbp-meson-pipeline-addin.c b/src/plugins/meson/gbp-meson-pipeline-addin.c
index 1f6a978e1..9c638faf2 100644
--- a/src/plugins/meson/gbp-meson-pipeline-addin.c
+++ b/src/plugins/meson/gbp-meson-pipeline-addin.c
@@ -1,6 +1,6 @@
 /* gbp-meson-pipeline-addin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-meson-pipeline-addin"
@@ -23,6 +25,7 @@
 #include "gbp-meson-toolchain.h"
 #include "gbp-meson-build-stage-cross-file.h"
 #include "gbp-meson-build-system.h"
+#include "gbp-meson-build-target.h"
 #include "gbp-meson-pipeline-addin.h"
 
 struct _GbpMesonPipelineAddin
@@ -33,9 +36,60 @@ struct _GbpMesonPipelineAddin
 static const gchar *ninja_names[] = { "ninja", "ninja-build" };
 
 static void
-on_stage_query (IdeBuildStage    *stage,
-                IdeBuildPipeline *pipeline,
-                GCancellable     *cancellable)
+on_build_stage_query (IdeBuildStage    *stage,
+                      IdeBuildPipeline *pipeline,
+                      GPtrArray        *targets,
+                      GCancellable     *cancellable)
+{
+  IdeSubprocessLauncher *launcher;
+  g_autoptr(GPtrArray) replace = NULL;
+  const gchar * const *argv;
+
+  g_assert (IDE_IS_BUILD_STAGE (stage));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* Defer to ninja to determine completed status */
+  ide_build_stage_set_completed (stage, FALSE);
+
+  /* Clear any previous argv items from a possible previous build */
+  launcher = ide_build_stage_launcher_get_launcher (IDE_BUILD_STAGE_LAUNCHER (stage));
+  argv = ide_subprocess_launcher_get_argv (launcher);
+  replace = g_ptr_array_new_with_free_func (g_free);
+  for (guint i = 0; argv[i]; i++)
+    {
+      g_ptr_array_add (replace, g_strdup (argv[i]));
+      if (g_strv_contains (ninja_names, argv[i]))
+        break;
+    }
+  g_ptr_array_add (replace, NULL);
+  ide_subprocess_launcher_set_argv (launcher, (const gchar * const *)replace->pdata);
+
+  /* If we have targets to build, specify them */
+  if (targets != NULL)
+    {
+      for (guint i = 0; i < targets->len; i++)
+        {
+          IdeBuildTarget *target = g_ptr_array_index (targets, i);
+
+          if (GBP_IS_MESON_BUILD_TARGET (target))
+            {
+              const gchar *filename;
+
+              filename = gbp_meson_build_target_get_filename (GBP_MESON_BUILD_TARGET (target));
+
+              if (filename != NULL)
+                ide_subprocess_launcher_push_argv (launcher, filename);
+            }
+        }
+    }
+}
+
+static void
+on_install_stage_query (IdeBuildStage    *stage,
+                        IdeBuildPipeline *pipeline,
+                        GPtrArray        *targets,
+                        GCancellable     *cancellable)
 {
   g_assert (IDE_IS_BUILD_STAGE (stage));
   g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
@@ -80,7 +134,7 @@ gbp_meson_pipeline_addin_load (IdeBuildPipelineAddin *addin,
 
   context = ide_object_get_context (IDE_OBJECT (self));
 
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
   if (!GBP_IS_MESON_BUILD_SYSTEM (build_system))
     IDE_GOTO (failure);
 
@@ -139,7 +193,7 @@ gbp_meson_pipeline_addin_load (IdeBuildPipelineAddin *addin,
       cross_file_stage = gbp_meson_build_stage_cross_file_new (context, toolchain);
       crossbuild_file = gbp_meson_build_stage_cross_file_get_path (cross_file_stage, pipeline);
 
-      id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_PREPARE, 0, IDE_BUILD_STAGE 
(cross_file_stage));
+      id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_PREPARE, 0, IDE_BUILD_STAGE 
(cross_file_stage));
       ide_build_pipeline_addin_track (addin, id);
     }
 
@@ -156,7 +210,7 @@ gbp_meson_pipeline_addin_load (IdeBuildPipelineAddin *addin,
       ide_subprocess_launcher_push_argv (config_launcher, crossbuild_file);
     }
 
-  if (!dzl_str_empty0 (config_opts))
+  if (!ide_str_empty0 (config_opts))
     {
       g_auto(GStrv) argv = NULL;
       gint argc;
@@ -173,7 +227,7 @@ gbp_meson_pipeline_addin_load (IdeBuildPipelineAddin *addin,
   if (g_file_test (build_ninja, G_FILE_TEST_IS_REGULAR))
     ide_build_stage_set_completed (config_stage, TRUE);
 
-  id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_CONFIGURE, 0, config_stage);
+  id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_CONFIGURE, 0, config_stage);
   ide_build_pipeline_addin_track (addin, id);
 
   /*
@@ -198,9 +252,9 @@ gbp_meson_pipeline_addin_load (IdeBuildPipelineAddin *addin,
   ide_build_stage_launcher_set_clean_launcher (IDE_BUILD_STAGE_LAUNCHER (build_stage), clean_launcher);
   ide_build_stage_set_check_stdout (build_stage, TRUE);
   ide_build_stage_set_name (build_stage, _("Building project"));
-  g_signal_connect (build_stage, "query", G_CALLBACK (on_stage_query), NULL);
+  g_signal_connect (build_stage, "query", G_CALLBACK (on_build_stage_query), NULL);
 
-  id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_BUILD, 0, build_stage);
+  id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_BUILD, 0, build_stage);
   ide_build_pipeline_addin_track (addin, id);
 
   /* Setup our install stage */
@@ -208,8 +262,8 @@ gbp_meson_pipeline_addin_load (IdeBuildPipelineAddin *addin,
   ide_subprocess_launcher_push_argv (install_launcher, "install");
   install_stage = ide_build_stage_launcher_new (context, install_launcher);
   ide_build_stage_set_name (install_stage, _("Installing project"));
-  g_signal_connect (install_stage, "query", G_CALLBACK (on_stage_query), NULL);
-  id = ide_build_pipeline_connect (pipeline, IDE_BUILD_PHASE_INSTALL, 0, install_stage);
+  g_signal_connect (install_stage, "query", G_CALLBACK (on_install_stage_query), NULL);
+  id = ide_build_pipeline_attach (pipeline, IDE_BUILD_PHASE_INSTALL, 0, install_stage);
   ide_build_pipeline_addin_track (addin, id);
 
   IDE_EXIT;
diff --git a/src/plugins/meson/gbp-meson-pipeline-addin.h b/src/plugins/meson/gbp-meson-pipeline-addin.h
index 854261cb8..f2b24093e 100644
--- a/src/plugins/meson/gbp-meson-pipeline-addin.h
+++ b/src/plugins/meson/gbp-meson-pipeline-addin.h
@@ -1,6 +1,6 @@
 /* gbp-meson-pipeline-addin.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-test-provider.c b/src/plugins/meson/gbp-meson-test-provider.c
index c0fbe4609..509fcd4a7 100644
--- a/src/plugins/meson/gbp-meson-test-provider.c
+++ b/src/plugins/meson/gbp-meson-test-provider.c
@@ -1,6 +1,6 @@
 /* gbp-meson-test-provider.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,14 @@
  *
  * 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-meson-test-provider"
 
 #include <json-glib/json-glib.h>
+#include <libide-threading.h>
 
 #include "gbp-meson-build-system.h"
 #include "gbp-meson-test.h"
@@ -285,7 +288,7 @@ gbp_meson_test_provider_reload (gpointer user_data)
    * Check that we're working with a meson build system.
    */
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
   if (!GBP_IS_MESON_BUILD_SYSTEM (build_system))
     IDE_RETURN (G_SOURCE_REMOVE);
 
@@ -293,7 +296,7 @@ gbp_meson_test_provider_reload (gpointer user_data)
    * Get access to the pipeline so we can create a launcher to
    * introspect meson from within the build environment.
    */
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   pipeline = ide_build_manager_get_pipeline (build_manager);
   if (pipeline == NULL)
     IDE_RETURN (G_SOURCE_REMOVE);
@@ -374,6 +377,7 @@ gbp_meson_test_provider_run_cb (GObject      *object,
     }
 
   ide_test_set_status (test, IDE_TEST_STATUS_SUCCESS);
+  ide_object_destroy (IDE_OBJECT (runner));
 
   ide_task_return_boolean (task, TRUE);
 }
@@ -520,18 +524,21 @@ gbp_meson_test_provider_run_finish (IdeTestProvider  *provider,
 }
 
 static void
-gbp_meson_test_provider_constructed (GObject *object)
+gbp_meson_test_provider_parent_set (IdeObject *object,
+                                    IdeObject *parent)
 {
   GbpMesonTestProvider *self = (GbpMesonTestProvider *)object;
   IdeBuildManager *build_manager;
   IdeContext *context;
 
   g_assert (GBP_IS_MESON_TEST_PROVIDER (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
 
-  G_OBJECT_CLASS (gbp_meson_test_provider_parent_class)->constructed (object);
+  if (parent == NULL)
+    return;
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
 
   g_signal_connect_object (build_manager,
                            "notify::pipeline",
@@ -558,11 +565,13 @@ static void
 gbp_meson_test_provider_class_init (GbpMesonTestProviderClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
   IdeTestProviderClass *provider_class = IDE_TEST_PROVIDER_CLASS (klass);
 
-  object_class->constructed = gbp_meson_test_provider_constructed;
   object_class->dispose = gbp_meson_test_provider_dispose;
 
+  i_object_class->parent_set = gbp_meson_test_provider_parent_set;
+
   provider_class->run_async = gbp_meson_test_provider_run_async;
   provider_class->run_finish = gbp_meson_test_provider_run_finish;
   provider_class->reload = gbp_meson_test_provider_queue_reload;
diff --git a/src/plugins/meson/gbp-meson-test-provider.h b/src/plugins/meson/gbp-meson-test-provider.h
index 8b9c6f7ec..2ed6efd51 100644
--- a/src/plugins/meson/gbp-meson-test-provider.h
+++ b/src/plugins/meson/gbp-meson-test-provider.h
@@ -1,6 +1,6 @@
 /* gbp-meson-test-provider.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-test.c b/src/plugins/meson/gbp-meson-test.c
index d14f4b9d4..6c8272e60 100644
--- a/src/plugins/meson/gbp-meson-test.c
+++ b/src/plugins/meson/gbp-meson-test.c
@@ -1,6 +1,6 @@
 /* gbp-meson-test.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-meson-test"
diff --git a/src/plugins/meson/gbp-meson-test.h b/src/plugins/meson/gbp-meson-test.h
index 4cc3f90c7..5f35bbde8 100644
--- a/src/plugins/meson/gbp-meson-test.h
+++ b/src/plugins/meson/gbp-meson-test.h
@@ -1,6 +1,6 @@
 /* gbp-meson-test.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-tool-row.c b/src/plugins/meson/gbp-meson-tool-row.c
index c409f3ce8..35976e185 100644
--- a/src/plugins/meson/gbp-meson-tool-row.c
+++ b/src/plugins/meson/gbp-meson-tool-row.c
@@ -1,7 +1,7 @@
 /* gbp-meson-tool-row.c
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,6 +15,8 @@
  *
  * 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-meson-tool-row"
@@ -215,7 +217,7 @@ gbp_meson_tool_row_class_init (GbpMesonToolRowClass *klass)
                                 G_TYPE_NONE,
                                 0);
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/meson-plugin/gbp-meson-tool-row.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/meson/gbp-meson-tool-row.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpMesonToolRow, name_label);
   gtk_widget_class_bind_template_child (widget_class, GbpMesonToolRow, delete_button);
 }
diff --git a/src/plugins/meson/gbp-meson-tool-row.h b/src/plugins/meson/gbp-meson-tool-row.h
index 4c75f444d..42727a00c 100644
--- a/src/plugins/meson/gbp-meson-tool-row.h
+++ b/src/plugins/meson/gbp-meson-tool-row.h
@@ -1,7 +1,7 @@
 /* gbp-meson-tool-row.h
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-toolchain-edition-preferences-addin.c 
b/src/plugins/meson/gbp-meson-toolchain-edition-preferences-addin.c
index eb947d1a5..50560bad6 100644
--- a/src/plugins/meson/gbp-meson-toolchain-edition-preferences-addin.c
+++ b/src/plugins/meson/gbp-meson-toolchain-edition-preferences-addin.c
@@ -1,7 +1,7 @@
 /* gbp-meson-toolchain-edition-preferences.c
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,14 @@
  *
  * 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-meson-toolchain-edition-preferences-addin"
 
 #include <glib/gi18n.h>
+#include <libide-gui.h>
 
 #include "gbp-meson-toolchain-edition-preferences-addin.h"
 #include "gbp-meson-toolchain-edition-preferences-row.h"
@@ -71,7 +74,9 @@ meson_toolchain_edition_preferences_add_new (GbpMesonToolchainEditionPreferences
                            NULL);
 
   file = g_file_new_for_path (new_target);
-  output_stream = g_file_create (file, G_FILE_CREATE_NONE, NULL, &error);
+
+  if ((output_stream = g_file_create (file, G_FILE_CREATE_NONE, NULL, &error)))
+    g_output_stream_close (G_OUTPUT_STREAM (output_stream), NULL, NULL);
 
   id = dzl_preferences_add_custom (self->preferences, "sdk", "toolchain", GTK_WIDGET (pref_row), "", 1);
   g_array_append_val (self->ids, id);
@@ -173,7 +178,7 @@ gbp_meson_toolchain_edition_preferences_addin_load_finish (GObject      *object,
       id = dzl_preferences_add_custom (self->preferences, "sdk", "toolchain", GTK_WIDGET (pref_row), NULL, 
i);
       g_array_append_val (self->ids, id);
     }
-  
+
 }
 
 static void
diff --git a/src/plugins/meson/gbp-meson-toolchain-edition-preferences-addin.h 
b/src/plugins/meson/gbp-meson-toolchain-edition-preferences-addin.h
index 1d0e1e322..b9e5784a2 100644
--- a/src/plugins/meson/gbp-meson-toolchain-edition-preferences-addin.h
+++ b/src/plugins/meson/gbp-meson-toolchain-edition-preferences-addin.h
@@ -1,7 +1,7 @@
 /* gbp-meson-toolchain-edition-preferences.h
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-toolchain-edition-preferences-row.c 
b/src/plugins/meson/gbp-meson-toolchain-edition-preferences-row.c
index 33c602575..75bac586b 100644
--- a/src/plugins/meson/gbp-meson-toolchain-edition-preferences-row.c
+++ b/src/plugins/meson/gbp-meson-toolchain-edition-preferences-row.c
@@ -1,7 +1,7 @@
 /* gbp-meson-toolchain-edition-preferences-row.c
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,6 +15,8 @@
  *
  * 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-meson-toolchain-edition-preferences-row"
@@ -365,6 +367,8 @@ gbp_meson_toolchain_edition_preferences_row_finalize (GObject *object)
  * @self: a #GbpMesonToolchainEditionPreferencesRow
  *
  * Requests the configuration popover the be shown over the widget.
+ *
+ * Since: 3.32
  */
 void
 gbp_meson_toolchain_edition_preferences_row_show_popup (GbpMesonToolchainEditionPreferencesRow *self)
@@ -435,7 +439,7 @@ gbp_meson_toolchain_edition_preferences_row_class_init (GbpMesonToolchainEdition
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/meson-plugin/gbp-meson-toolchain-edition-preferences-row.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/meson/gbp-meson-toolchain-edition-preferences-row.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpMesonToolchainEditionPreferencesRow, display_name);
   gtk_widget_class_bind_template_child (widget_class, GbpMesonToolchainEditionPreferencesRow, popover);
   gtk_widget_class_bind_template_child (widget_class, GbpMesonToolchainEditionPreferencesRow, name_entry);
diff --git a/src/plugins/meson/gbp-meson-toolchain-edition-preferences-row.h 
b/src/plugins/meson/gbp-meson-toolchain-edition-preferences-row.h
index cf52a9a82..2570cc7ef 100644
--- a/src/plugins/meson/gbp-meson-toolchain-edition-preferences-row.h
+++ b/src/plugins/meson/gbp-meson-toolchain-edition-preferences-row.h
@@ -1,7 +1,7 @@
 /* gbp-meson-toolchain-edition-preferences-row.h
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-toolchain-provider.c 
b/src/plugins/meson/gbp-meson-toolchain-provider.c
index 18fbffd44..ba8afd07a 100644
--- a/src/plugins/meson/gbp-meson-toolchain-provider.c
+++ b/src/plugins/meson/gbp-meson-toolchain-provider.c
@@ -16,6 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gbp-meson-toolchain-provider"
@@ -228,7 +230,7 @@ gbp_meson_toolchain_provider_load_async (IdeToolchainProvider     *provider,
   ide_task_set_priority (task, G_PRIORITY_LOW);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  build_system = ide_context_get_build_system (context);
+  build_system = ide_build_system_from_context (context);
 
   if (!GBP_IS_MESON_BUILD_SYSTEM (build_system))
     {
@@ -252,7 +254,7 @@ gbp_meson_toolchain_provider_load_async (IdeToolchainProvider     *provider,
   user_folder_path = g_build_filename (g_get_user_data_dir (), "meson", "cross", NULL);
   folders = g_list_append (folders, g_file_new_for_path (user_folder_path));
 
-  project_folder = g_file_get_parent (ide_context_get_project_file (context));
+  project_folder = ide_context_ref_workdir (context);
   folders = g_list_append (folders, g_steal_pointer (&project_folder));
 
   fs = file_searching_new ();
@@ -340,5 +342,4 @@ gbp_meson_toolchain_provider_class_init (GbpMesonToolchainProviderClass *klass)
 static void
 gbp_meson_toolchain_provider_init (GbpMesonToolchainProvider *self)
 {
-  
 }
diff --git a/src/plugins/meson/gbp-meson-toolchain-provider.h 
b/src/plugins/meson/gbp-meson-toolchain-provider.h
index 15537cbce..c8ef84127 100644
--- a/src/plugins/meson/gbp-meson-toolchain-provider.h
+++ b/src/plugins/meson/gbp-meson-toolchain-provider.h
@@ -16,11 +16,13 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-toolchain.c b/src/plugins/meson/gbp-meson-toolchain.c
index ca050219c..272f44902 100644
--- a/src/plugins/meson/gbp-meson-toolchain.c
+++ b/src/plugins/meson/gbp-meson-toolchain.c
@@ -16,6 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gbp-meson-toolchain"
@@ -44,16 +46,9 @@ static GParamSpec *properties [N_PROPS];
 GbpMesonToolchain *
 gbp_meson_toolchain_new (IdeContext *context)
 {
-  g_autoptr(GbpMesonToolchain) toolchain = NULL;
-
   g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
 
-  toolchain = g_object_new (GBP_TYPE_MESON_TOOLCHAIN,
-                            "context", context,
-                            NULL);
-
-
-  return g_steal_pointer (&toolchain);
+  return g_object_new (GBP_TYPE_MESON_TOOLCHAIN, NULL);
 }
 
 gboolean
@@ -125,7 +120,7 @@ gbp_meson_toolchain_load (GbpMesonToolchain  *self,
  *
  * Returns: (transfer none): the path to the Meson cross-file.
  *
- * Since: 3.30
+ * Since: 3.32
  */
 const gchar *
 gbp_meson_toolchain_get_file_path (GbpMesonToolchain  *self)
@@ -184,5 +179,4 @@ gbp_meson_toolchain_class_init (GbpMesonToolchainClass *klass)
 static void
 gbp_meson_toolchain_init (GbpMesonToolchain *self)
 {
-  
 }
diff --git a/src/plugins/meson/gbp-meson-toolchain.h b/src/plugins/meson/gbp-meson-toolchain.h
index 334ec5dbb..b5229bc44 100644
--- a/src/plugins/meson/gbp-meson-toolchain.h
+++ b/src/plugins/meson/gbp-meson-toolchain.h
@@ -16,11 +16,13 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/gbp-meson-utils.c b/src/plugins/meson/gbp-meson-utils.c
index 192135079..595e9fa2e 100644
--- a/src/plugins/meson/gbp-meson-utils.c
+++ b/src/plugins/meson/gbp-meson-utils.c
@@ -16,6 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gbp-meson-utils"
diff --git a/src/plugins/meson/gbp-meson-utils.h b/src/plugins/meson/gbp-meson-utils.h
index a17e87686..3eff486a2 100644
--- a/src/plugins/meson/gbp-meson-utils.h
+++ b/src/plugins/meson/gbp-meson-utils.h
@@ -16,10 +16,12 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/meson/meson-plugin.c b/src/plugins/meson/meson-plugin.c
index 3af544153..b19008e34 100644
--- a/src/plugins/meson/meson-plugin.c
+++ b/src/plugins/meson/meson-plugin.c
@@ -1,6 +1,6 @@
 /* meson-plugin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,25 +14,44 @@
  *
  * 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
  */
 
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
 
 #include "gbp-meson-build-system.h"
+#include "gbp-meson-build-system-discovery.h"
 #include "gbp-meson-build-target-provider.h"
 #include "gbp-meson-pipeline-addin.h"
 #include "gbp-meson-test-provider.h"
 #include "gbp-meson-toolchain-provider.h"
 #include "gbp-meson-toolchain-edition-preferences-addin.h"
 
-void
-gbp_meson_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_gbp_meson_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_PIPELINE_ADDIN, 
GBP_TYPE_MESON_PIPELINE_ADDIN);
-  peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_SYSTEM, GBP_TYPE_MESON_BUILD_SYSTEM);
-  peas_object_module_register_extension_type (module, IDE_TYPE_BUILD_TARGET_PROVIDER, 
GBP_TYPE_MESON_BUILD_TARGET_PROVIDER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_TEST_PROVIDER, GBP_TYPE_MESON_TEST_PROVIDER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_TOOLCHAIN_PROVIDER, 
GBP_TYPE_MESON_TOOLCHAIN_PROVIDER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_PREFERENCES_ADDIN, 
GBP_TYPE_MESON_TOOLCHAIN_EDITION_PREFERENCES_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                              GBP_TYPE_MESON_PIPELINE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_SYSTEM,
+                                              GBP_TYPE_MESON_BUILD_SYSTEM);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_SYSTEM_DISCOVERY,
+                                              GBP_TYPE_MESON_BUILD_SYSTEM_DISCOVERY);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_TARGET_PROVIDER,
+                                              GBP_TYPE_MESON_BUILD_TARGET_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_TEST_PROVIDER,
+                                              GBP_TYPE_MESON_TEST_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_TOOLCHAIN_PROVIDER,
+                                              GBP_TYPE_MESON_TOOLCHAIN_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_MESON_TOOLCHAIN_EDITION_PREFERENCES_ADDIN);
 }
diff --git a/src/plugins/meson/meson.build b/src/plugins/meson/meson.build
index 90319af7a..a08a119b3 100644
--- a/src/plugins/meson/meson.build
+++ b/src/plugins/meson/meson.build
@@ -1,42 +1,29 @@
-if get_option('with_meson')
+if get_option('plugin_meson')
 
-meson_resources = gnome.compile_resources(    
-  'gbp-meson-resources',                      
-  'meson.gresource.xml',                      
-  c_name: 'gbp_meson',                        
-)                                           
-
-meson_sources = [
+plugins_sources += files([
   'meson-plugin.c',
   'gbp-meson-toolchain-edition-preferences-addin.c',
-  'gbp-meson-toolchain-edition-preferences-addin.h',
   'gbp-meson-toolchain-edition-preferences-row.c',
-  'gbp-meson-toolchain-edition-preferences-row.h',
   'gbp-meson-build-stage-cross-file.c',
-  'gbp-meson-build-stage-cross-file.h',
   'gbp-meson-build-system.c',
-  'gbp-meson-build-system.h',
+  'gbp-meson-build-system-discovery.c',
   'gbp-meson-build-target.c',
-  'gbp-meson-build-target.h',
   'gbp-meson-build-target-provider.c',
-  'gbp-meson-build-target-provider.h',
   'gbp-meson-pipeline-addin.c',
-  'gbp-meson-pipeline-addin.h',
   'gbp-meson-test-provider.c',
-  'gbp-meson-test-provider.h',
   'gbp-meson-test.c',
-  'gbp-meson-test.h',
   'gbp-meson-toolchain.c',
-  'gbp-meson-toolchain.h',
   'gbp-meson-toolchain-provider.c',
-  'gbp-meson-toolchain-provider.h',
   'gbp-meson-tool-row.c',
-  'gbp-meson-tool-row.h',
   'gbp-meson-utils.c',
-  'gbp-meson-utils.h',
-]
+])
+
+plugin_meson_resources = gnome.compile_resources(
+  'gbp-meson-resources',
+  'meson.gresource.xml',
+  c_name: 'gbp_meson',
+)
 
-gnome_builder_plugins_sources += files(meson_sources)       
-gnome_builder_plugins_sources += meson_resources[0]         
+plugins_sources += plugin_meson_resources[0]
 
 endif
diff --git a/src/plugins/meson/meson.gresource.xml b/src/plugins/meson/meson.gresource.xml
index a74ad8587..7aba3f64d 100644
--- a/src/plugins/meson/meson.gresource.xml
+++ b/src/plugins/meson/meson.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/meson">
     <file>meson.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/meson-plugin">
     <file>gbp-meson-toolchain-edition-preferences-row.ui</file>
     <file>gbp-meson-tool-row.ui</file>
   </gresource>
diff --git a/src/plugins/meson/meson.plugin b/src/plugins/meson/meson.plugin
index 04941e4ae..8d9ab2349 100644
--- a/src/plugins/meson/meson.plugin
+++ b/src/plugins/meson/meson.plugin
@@ -1,10 +1,11 @@
 [Plugin]
-Module=meson-plugin
-Name=Meson
-Description=Provides integration with the Meson build system
 Authors=Patrick Griffis <tingping tingping se>
-Copyright=Copyright © 2016 Patrick Griffis
 Builtin=true
-X-Project-File-Filter-Pattern=meson.build
+Copyright=Copyright © 2016 Patrick Griffis
+Description=Provides integration with the Meson build system
+Embedded=_gbp_meson_register_types
+Hidden=true
+Module=meson
+Name=Meson
 X-Project-File-Filter-Name=Meson Project (meson.build)
-Embedded=gbp_meson_register_types
+X-Project-File-Filter-Pattern=meson.build
diff --git a/src/plugins/messages/gbp-messages-editor-addin.c 
b/src/plugins/messages/gbp-messages-editor-addin.c
index 940a5e7d5..1e634a385 100644
--- a/src/plugins/messages/gbp-messages-editor-addin.c
+++ b/src/plugins/messages/gbp-messages-editor-addin.c
@@ -1,6 +1,6 @@
 /* gbp-messages-editor-addin.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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-messages-editor-addin"
 
-#include <ide.h>
+#include <libide-editor.h>
 
 #include "gbp-messages-editor-addin.h"
 #include "gbp-messages-panel.h"
@@ -30,16 +32,16 @@ struct _GbpMessagesEditorAddin
 };
 
 static void
-gbp_messages_editor_addin_load (IdeEditorAddin       *addin,
-                                IdeEditorPerspective *editor)
+gbp_messages_editor_addin_load (IdeEditorAddin   *addin,
+                                IdeEditorSurface *editor)
 {
   GbpMessagesEditorAddin *self = (GbpMessagesEditorAddin *)addin;
   GtkWidget *utilities;
 
   g_assert (GBP_IS_MESSAGES_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
 
-  utilities = ide_editor_perspective_get_utilities (editor);
+  utilities = ide_editor_surface_get_utilities (editor);
 
   /* hidden by default */
   self->panel = g_object_new (GBP_TYPE_MESSAGES_PANEL, NULL);
@@ -52,12 +54,12 @@ gbp_messages_editor_addin_load (IdeEditorAddin       *addin,
 
 static void
 gbp_messages_editor_addin_unload (IdeEditorAddin       *addin,
-                                  IdeEditorPerspective *editor)
+                                  IdeEditorSurface *editor)
 {
   GbpMessagesEditorAddin *self = (GbpMessagesEditorAddin *)addin;
 
   g_assert (GBP_IS_MESSAGES_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
 
   if (self->panel != NULL)
     gtk_widget_destroy (GTK_WIDGET (self->panel));
diff --git a/src/plugins/messages/gbp-messages-editor-addin.h 
b/src/plugins/messages/gbp-messages-editor-addin.h
index 25d8a8933..ae5c7c689 100644
--- a/src/plugins/messages/gbp-messages-editor-addin.h
+++ b/src/plugins/messages/gbp-messages-editor-addin.h
@@ -1,6 +1,6 @@
 /* gbp-messages-editor-addin.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/messages/gbp-messages-panel.c b/src/plugins/messages/gbp-messages-panel.c
index f8856a2d9..a9db8fbf9 100644
--- a/src/plugins/messages/gbp-messages-panel.c
+++ b/src/plugins/messages/gbp-messages-panel.c
@@ -1,6 +1,6 @@
 /* gbp-messages-panel.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,14 @@
  *
  * 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-messages-panel"
 
-#include <ide.h>
+#include <libide-editor.h>
+#include <libide-terminal.h>
 
 #include "gbp-messages-panel.h"
 
@@ -37,6 +40,7 @@ G_DEFINE_TYPE (GbpMessagesPanel, gbp_messages_panel, DZL_TYPE_DOCK_WIDGET)
 static void
 gbp_messages_panel_log_cb (GbpMessagesPanel *self,
                            GLogLevelFlags    log_level,
+                           const gchar      *domain,
                            const gchar      *message,
                            IdeContext       *context)
 {
@@ -92,8 +96,7 @@ gbp_messages_panel_class_init (GbpMessagesPanelClass *klass)
 
   widget_class->destroy = gbp_messages_panel_destroy;
 
-  gtk_widget_class_set_template_from_resource (widget_class,
-                                               
"/org/gnome/builder/plugins/messages-plugin/gbp-messages-panel.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/messages/gbp-messages-panel.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpMessagesPanel, scrollbar);
   gtk_widget_class_bind_template_child (widget_class, GbpMessagesPanel, terminal);
 }
diff --git a/src/plugins/messages/gbp-messages-panel.h b/src/plugins/messages/gbp-messages-panel.h
index 8aa5a120f..babbfd0a6 100644
--- a/src/plugins/messages/gbp-messages-panel.h
+++ b/src/plugins/messages/gbp-messages-panel.h
@@ -1,6 +1,6 @@
 /* gbp-messages-panel.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/messages/meson.build b/src/plugins/messages/meson.build
index 96afadbcf..af8a212fa 100644
--- a/src/plugins/messages/meson.build
+++ b/src/plugins/messages/meson.build
@@ -1,16 +1,13 @@
-messages_resources = gnome.compile_resources(
+plugins_sources += files([
+  'gbp-messages-editor-addin.c',
+  'gbp-messages-panel.c',
+  'messages-plugin.c',
+])
+
+plugin_messages_resources = gnome.compile_resources(
   'messages-resources',
   'messages.gresource.xml',
   c_name: 'gbp_messages',
 )
 
-messages_sources = [
-  'gbp-messages-editor-addin.c',
-  'gbp-messages-editor-addin.h',
-  'gbp-messages-panel.c',
-  'gbp-messages-panel.h',
-  'gbp-messages-plugin.c',
-]
-
-gnome_builder_plugins_sources += files(messages_sources)
-gnome_builder_plugins_sources += messages_resources[0]
+plugins_sources += plugin_messages_resources[0]
diff --git a/src/plugins/messages/messages-plugin.c b/src/plugins/messages/messages-plugin.c
new file mode 100644
index 000000000..2a93f433b
--- /dev/null
+++ b/src/plugins/messages/messages-plugin.c
@@ -0,0 +1,34 @@
+/* messages-plugin.c
+ *
+ * Copyright 2018-2019 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
+ */
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-editor.h>
+
+#include "gbp-messages-editor-addin.h"
+
+_IDE_EXTERN void
+_gbp_messages_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_ADDIN,
+                                              GBP_TYPE_MESSAGES_EDITOR_ADDIN);
+}
diff --git a/src/plugins/messages/messages.gresource.xml b/src/plugins/messages/messages.gresource.xml
index d32baf5e4..e2e923da1 100644
--- a/src/plugins/messages/messages.gresource.xml
+++ b/src/plugins/messages/messages.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/messages">
     <file>messages.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/messages-plugin">
     <file preprocess="xml-stripblanks">gbp-messages-panel.ui</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/messages/messages.plugin b/src/plugins/messages/messages.plugin
index 438ff362a..139ad0c53 100644
--- a/src/plugins/messages/messages.plugin
+++ b/src/plugins/messages/messages.plugin
@@ -1,10 +1,10 @@
 [Plugin]
-Module=messages-plugin
-Name=Internal Logging
-Description=Show internal warning logs
 Authors=Christian Hergert <christian hergert me>
+Builtin=true
 Copyright=Copyright © 2018 Christian Hergert
 Depends=editor;
+Description=Show internal warning logs
+Embedded=_gbp_messages_register_types
 Hidden=true
-Builtin=true
-Embedded=gbp_messages_register_types
+Module=messages
+Name=Internal Logging
diff --git a/src/plugins/modelines/gbp-modelines-file-settings.c 
b/src/plugins/modelines/gbp-modelines-file-settings.c
new file mode 100644
index 000000000..8c75ca757
--- /dev/null
+++ b/src/plugins/modelines/gbp-modelines-file-settings.c
@@ -0,0 +1,121 @@
+/* gbp-modelines-file-settings.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-modelines-file-settings"
+
+#include "config.h"
+
+#include <libide-code.h>
+#include <glib/gi18n.h>
+
+#include "gbp-modelines-file-settings.h"
+#include "modeline-parser.h"
+
+struct _GbpModelinesFileSettings
+{
+  IdeFileSettings parent_instance;
+};
+
+G_DEFINE_TYPE (GbpModelinesFileSettings, gbp_modelines_file_settings, IDE_TYPE_FILE_SETTINGS)
+
+static gboolean
+buffer_file_matches (GbpModelinesFileSettings *self,
+                     IdeBuffer                *buffer)
+{
+  GFile *our_file;
+  GFile *buffer_file;
+
+  g_assert (GBP_IS_MODELINES_FILE_SETTINGS (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  buffer_file = ide_buffer_get_file (buffer);
+  our_file = ide_file_settings_get_file (IDE_FILE_SETTINGS (self));
+
+  return g_file_equal (buffer_file, our_file);
+}
+
+static void
+buffer_loaded_cb (GbpModelinesFileSettings *self,
+                  IdeBuffer                *buffer,
+                  IdeBufferManager         *buffer_manager)
+{
+  g_assert (GBP_IS_MODELINES_FILE_SETTINGS (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+
+  if (buffer_file_matches (self, buffer))
+    modeline_parser_apply_modeline (GTK_TEXT_BUFFER (buffer), IDE_FILE_SETTINGS (self));
+}
+
+static void
+buffer_saved_cb (GbpModelinesFileSettings *self,
+                 IdeBuffer                *buffer,
+                 IdeBufferManager         *buffer_manager)
+{
+  g_assert (GBP_IS_MODELINES_FILE_SETTINGS (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (buffer_manager));
+
+  if (buffer_file_matches (self, buffer))
+    modeline_parser_apply_modeline (GTK_TEXT_BUFFER (buffer), IDE_FILE_SETTINGS (self));
+}
+
+static void
+gbp_modelines_file_settings_parent_set (IdeObject *object,
+                                        IdeObject *parent)
+{
+  GbpModelinesFileSettings *self = (GbpModelinesFileSettings *)object;
+  IdeBufferManager *buffer_manager;
+  IdeContext *context;
+
+  g_assert (IDE_IS_OBJECT (object));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (parent == NULL)
+    return;
+
+  context = ide_object_get_context (IDE_OBJECT (self));
+  buffer_manager = ide_buffer_manager_from_context (context);
+
+  g_signal_connect_object (buffer_manager,
+                           "buffer-loaded",
+                           G_CALLBACK (buffer_loaded_cb),
+                           self,
+                           G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+  g_signal_connect_object (buffer_manager,
+                           "buffer-saved",
+                           G_CALLBACK (buffer_saved_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+gbp_modelines_file_settings_class_init (GbpModelinesFileSettingsClass *klass)
+{
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
+
+  i_object_class->parent_set = gbp_modelines_file_settings_parent_set;
+}
+
+static void
+gbp_modelines_file_settings_init (GbpModelinesFileSettings *self)
+{
+}
diff --git a/src/plugins/modelines/gbp-modelines-file-settings.h 
b/src/plugins/modelines/gbp-modelines-file-settings.h
new file mode 100644
index 000000000..bd15e9804
--- /dev/null
+++ b/src/plugins/modelines/gbp-modelines-file-settings.h
@@ -0,0 +1,31 @@
+/* gbp-modelines-file-settings.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "ide-file-settings.h"
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_MODELINES_FILE_SETTINGS (gbp_modelines_file_settings_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpModelinesFileSettings, gbp_modelines_file_settings, GBP, MODELINES_FILE_SETTINGS, 
IdeFileSettings)
+
+G_END_DECLS
diff --git a/src/libide/modelines/language-mappings b/src/plugins/modelines/language-mappings
similarity index 100%
rename from src/libide/modelines/language-mappings
rename to src/plugins/modelines/language-mappings
diff --git a/src/plugins/modelines/meson.build b/src/plugins/modelines/meson.build
new file mode 100644
index 000000000..7916fc4ae
--- /dev/null
+++ b/src/plugins/modelines/meson.build
@@ -0,0 +1,17 @@
+if get_option('plugin_modelines')
+
+plugins_sources += files([
+  'gbp-modelines-file-settings.c',
+  'modeline-parser.c',
+  'modelines-plugin.c',
+])
+
+plugin_modelines_resources = gnome.compile_resources(
+  'gbp-modelines-resources',
+  'modelines.gresource.xml',
+  c_name: 'gbp_modelines',
+)
+
+plugins_sources += plugin_modelines_resources[0]
+
+endif
diff --git a/src/plugins/modelines/modeline-parser.c b/src/plugins/modelines/modeline-parser.c
new file mode 100644
index 000000000..9e6ea7d0c
--- /dev/null
+++ b/src/plugins/modelines/modeline-parser.c
@@ -0,0 +1,814 @@
+/*
+ * modeline-parser.c
+ * Emacs, Kate and Vim-style modelines support for gedit.
+ *
+ * Copyright 2005-2007 - Steve Frécinaux <code istique net>
+ *
+ * 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 2, 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/>.
+ */
+
+#define G_LOG_DOMAIN "modelines"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <gtksourceview/gtksource.h>
+
+#include "modelines/modeline-parser.h"
+
+#define MODELINES_LANGUAGE_MAPPINGS_FILE "/plugins/modelines/language-mappings"
+#define gedit_debug_message(ignored,fmt,...) g_debug(fmt,__VA_ARGS__)
+
+/* Mappings: language name -> Gedit language ID */
+static GHashTable *vim_languages = NULL;
+static GHashTable *emacs_languages = NULL;
+static GHashTable *kate_languages = NULL;
+
+typedef enum
+{
+       MODELINE_SET_NONE = 0,
+       MODELINE_SET_TAB_WIDTH = 1 << 0,
+       MODELINE_SET_INDENT_WIDTH = 1 << 1,
+       MODELINE_SET_WRAP_MODE = 1 << 2,
+       MODELINE_SET_SHOW_RIGHT_MARGIN = 1 << 3,
+       MODELINE_SET_RIGHT_MARGIN_POSITION = 1 << 4,
+       MODELINE_SET_LANGUAGE = 1 << 5,
+       MODELINE_SET_INSERT_SPACES = 1 << 6
+} ModelineSet;
+
+typedef struct _ModelineOptions
+{
+       gchar           *language_id;
+
+       /* these options are similar to the GtkSourceView properties of the
+        * same names.
+        */
+       gboolean        insert_spaces;
+       guint           tab_width;
+       guint           indent_width;
+       GtkWrapMode     wrap_mode;
+       gboolean        display_right_margin;
+       guint           right_margin_position;
+
+       ModelineSet     set;
+} ModelineOptions;
+
+#define MODELINE_OPTIONS_DATA_KEY "ModelineOptionsDataKey"
+
+static gboolean
+has_option (ModelineOptions *options,
+            ModelineSet      set)
+{
+       return options->set & set;
+}
+
+void
+modeline_parser_init (void)
+{
+}
+
+void
+modeline_parser_shutdown (void)
+{
+       if (vim_languages != NULL)
+               g_hash_table_unref (vim_languages);
+
+       if (emacs_languages != NULL)
+               g_hash_table_unref (emacs_languages);
+
+       if (kate_languages != NULL)
+               g_hash_table_unref (kate_languages);
+
+       vim_languages = NULL;
+       emacs_languages = NULL;
+       kate_languages = NULL;
+}
+
+static GHashTable *
+load_language_mappings_group (GKeyFile *key_file, const gchar *group)
+{
+       GHashTable *table;
+       gchar **keys;
+       gsize length = 0;
+       int i;
+
+       table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+       keys = g_key_file_get_keys (key_file, group, &length, NULL);
+
+       gedit_debug_message (DEBUG_PLUGINS,
+                            "%" G_GSIZE_FORMAT " mappings in group %s",
+                            length, group);
+
+       for (i = 0; i < length; i++)
+       {
+               /* steal the name string */
+               gchar *name = keys[i];
+               gchar *id = g_key_file_get_string (key_file, group, name, NULL);
+               g_hash_table_insert (table, name, id);
+       }
+       g_free (keys);
+
+       return table;
+}
+
+/* lazy loading of language mappings */
+static void
+load_language_mappings (void)
+{
+       g_autoptr(GBytes) bytes = NULL;
+       GKeyFile *mappings;
+       const gchar *data;
+       gsize len = 0;
+       GError *error = NULL;
+
+       bytes = g_resources_lookup_data (MODELINES_LANGUAGE_MAPPINGS_FILE, 0, NULL);
+       g_assert (bytes != NULL);
+
+       data = g_bytes_get_data (bytes, &len);
+       g_assert (data);
+       g_assert (len > 0);
+
+       mappings = g_key_file_new ();
+
+       if (g_key_file_load_from_data (mappings, data, len, 0, &error))
+       {
+               gedit_debug_message (DEBUG_PLUGINS,
+                                    "Loaded language mappings from %s",
+                                    MODELINES_LANGUAGE_MAPPINGS_FILE);
+
+               vim_languages = load_language_mappings_group (mappings, "vim");
+               emacs_languages = load_language_mappings_group (mappings, "emacs");
+               kate_languages = load_language_mappings_group (mappings, "kate");
+       }
+       else
+       {
+               gedit_debug_message (DEBUG_PLUGINS,
+                                    "Failed to loaded language mappings from %s: %s",
+                                    MODELINES_LANGUAGE_MAPPINGS_FILE, error->message);
+
+               g_error_free (error);
+       }
+
+       g_key_file_free (mappings);
+}
+
+static gchar *
+get_language_id (const gchar *language_name, GHashTable *mapping)
+{
+       gchar *name;
+       gchar *language_id = NULL;
+
+       name = g_ascii_strdown (language_name, -1);
+
+       if (mapping != NULL)
+       {
+               language_id = g_hash_table_lookup (mapping, name);
+
+               if (language_id != NULL)
+               {
+                       g_free (name);
+                       return g_strdup (language_id);
+               }
+       }
+
+       /* by default assume that the gtksourcevuew id is the same */
+       return name;
+}
+
+static gchar *
+get_language_id_vim (const gchar *language_name)
+{
+       if (vim_languages == NULL)
+               load_language_mappings ();
+
+       return get_language_id (language_name, vim_languages);
+}
+
+static gchar *
+get_language_id_emacs (const gchar *language_name)
+{
+       if (emacs_languages == NULL)
+               load_language_mappings ();
+
+       return get_language_id (language_name, emacs_languages);
+}
+
+static gchar *
+get_language_id_kate (const gchar *language_name)
+{
+       if (kate_languages == NULL)
+               load_language_mappings ();
+
+       return get_language_id (language_name, kate_languages);
+}
+
+static gboolean
+skip_whitespaces (gchar **s)
+{
+       while (**s != '\0' && g_ascii_isspace (**s))
+               (*s)++;
+       return **s != '\0';
+}
+
+/* Parse vi(m) modelines.
+ * Vi(m) modelines looks like this:
+ *   - first form:   [text]{white}{vi:|vim:|ex:}[white]{options}
+ *   - second form:  [text]{white}{vi:|vim:|ex:}[white]se[t] {options}:[text]
+ * They can happen on the three first or last lines.
+ */
+static gchar *
+parse_vim_modeline (gchar           *s,
+                   ModelineOptions *options)
+{
+       gboolean in_set = FALSE;
+       gboolean neg;
+       guint intval;
+       GString *key, *value;
+
+       key = g_string_sized_new (8);
+       value = g_string_sized_new (8);
+
+       while (*s != '\0' && !(in_set && *s == ':'))
+       {
+               while (*s != '\0' && (*s == ':' || g_ascii_isspace (*s)))
+                       s++;
+
+               if (*s == '\0')
+                       break;
+
+               if (strncmp (s, "set ", 4) == 0 ||
+                   strncmp (s, "se ", 3) == 0)
+               {
+                       s = strchr(s, ' ') + 1;
+                       in_set = TRUE;
+               }
+
+               neg = FALSE;
+               if (strncmp (s, "no", 2) == 0)
+               {
+                       neg = TRUE;
+                       s += 2;
+               }
+
+               g_string_assign (key, "");
+               g_string_assign (value, "");
+
+               while (*s != '\0' && *s != ':' && *s != '=' &&
+                      !g_ascii_isspace (*s))
+               {
+                       g_string_append_c (key, *s);
+                       s++;
+               }
+
+               if (*s == '=')
+               {
+                       s++;
+                       while (*s != '\0' && *s != ':' &&
+                              !g_ascii_isspace (*s))
+                       {
+                               g_string_append_c (value, *s);
+                               s++;
+                       }
+               }
+
+               if (strcmp (key->str, "ft") == 0 ||
+                   strcmp (key->str, "filetype") == 0)
+               {
+                       g_free (options->language_id);
+                       options->language_id = get_language_id_vim (value->str);
+
+                       options->set |= MODELINE_SET_LANGUAGE;
+               }
+               else if (strcmp (key->str, "et") == 0 ||
+                   strcmp (key->str, "expandtab") == 0)
+               {
+                       options->insert_spaces = !neg;
+                       options->set |= MODELINE_SET_INSERT_SPACES;
+               }
+               else if (strcmp (key->str, "ts") == 0 ||
+                        strcmp (key->str, "tabstop") == 0)
+               {
+                       intval = atoi (value->str);
+
+                       if (intval)
+                       {
+                               options->tab_width = intval;
+                               options->set |= MODELINE_SET_TAB_WIDTH;
+                       }
+               }
+               else if (strcmp (key->str, "sw") == 0 ||
+                        strcmp (key->str, "shiftwidth") == 0)
+               {
+                       intval = atoi (value->str);
+
+                       if (intval)
+                       {
+                               options->indent_width = intval;
+                               options->set |= MODELINE_SET_INDENT_WIDTH;
+                       }
+               }
+               else if (strcmp (key->str, "wrap") == 0)
+               {
+                       options->wrap_mode = neg ? GTK_WRAP_NONE : GTK_WRAP_WORD;
+
+                       options->set |= MODELINE_SET_WRAP_MODE;
+               }
+               else if (strcmp (key->str, "textwidth") == 0 ||
+                        strcmp (key->str, "tw") == 0)
+               {
+                       intval = atoi (value->str);
+
+                       if (intval)
+                       {
+                               options->right_margin_position = intval;
+                               options->display_right_margin = TRUE;
+
+                               options->set |= MODELINE_SET_SHOW_RIGHT_MARGIN |
+                                               MODELINE_SET_RIGHT_MARGIN_POSITION;
+
+                       }
+               }
+       }
+
+       g_string_free (key, TRUE);
+       g_string_free (value, TRUE);
+
+       return s;
+}
+
+/* Parse emacs modelines.
+ * Emacs modelines looks like this: "-*- key1: value1; key2: value2 -*-"
+ * They can happen on the first line, or on the second one if the first line is
+ * a shebang (#!)
+ * See http://www.delorie.com/gnu/docs/emacs/emacs_486.html
+ */
+static gchar *
+parse_emacs_modeline (gchar           *s,
+                     ModelineOptions *options)
+{
+       guint intval;
+       GString *key, *value;
+
+       key = g_string_sized_new (8);
+       value = g_string_sized_new (8);
+
+       while (*s != '\0')
+       {
+               while (*s != '\0' && (*s == ';' || g_ascii_isspace (*s)))
+                       s++;
+               if (*s == '\0' || strncmp (s, "-*-", 3) == 0)
+                       break;
+
+               g_string_assign (key, "");
+               g_string_assign (value, "");
+
+               while (*s != '\0' && *s != ':' && *s != ';' &&
+                      !g_ascii_isspace (*s))
+               {
+                       g_string_append_c (key, *s);
+                       s++;
+               }
+
+               if (!skip_whitespaces (&s))
+                       break;
+
+               if (*s != ':')
+                       continue;
+               s++;
+
+               if (!skip_whitespaces (&s))
+                       break;
+
+               while (*s != '\0' && *s != ';' && !g_ascii_isspace (*s))
+               {
+                       g_string_append_c (value, *s);
+                       s++;
+               }
+
+               gedit_debug_message (DEBUG_PLUGINS,
+                                    "Emacs modeline bit: %s = %s",
+                                    key->str, value->str);
+
+               /* "Mode" key is case insenstive */
+               if (g_ascii_strcasecmp (key->str, "Mode") == 0)
+               {
+                       g_free (options->language_id);
+                       options->language_id = get_language_id_emacs (value->str);
+
+                       options->set |= MODELINE_SET_LANGUAGE;
+               }
+               else if (strcmp (key->str, "tab-width") == 0)
+               {
+                       intval = atoi (value->str);
+
+                       if (intval)
+                       {
+                               options->tab_width = intval;
+                               options->set |= MODELINE_SET_TAB_WIDTH;
+                       }
+               }
+               else if (strcmp (key->str, "indent-offset") == 0 ||
+                        strcmp (key->str, "c-basic-offset") == 0 ||
+                        strcmp (key->str, "js-indent-level") == 0)
+               {
+                       intval = atoi (value->str);
+
+                       if (intval)
+                       {
+                               options->indent_width = intval;
+                               options->set |= MODELINE_SET_INDENT_WIDTH;
+                       }
+               }
+               else if (strcmp (key->str, "indent-tabs-mode") == 0)
+               {
+                       intval = strcmp (value->str, "nil") == 0;
+                       options->insert_spaces = intval;
+
+                       options->set |= MODELINE_SET_INSERT_SPACES;
+               }
+               else if (strcmp (key->str, "autowrap") == 0)
+               {
+                       intval = strcmp (value->str, "nil") != 0;
+                       options->wrap_mode = intval ? GTK_WRAP_WORD : GTK_WRAP_NONE;
+
+                       options->set |= MODELINE_SET_WRAP_MODE;
+               }
+       }
+
+       g_string_free (key, TRUE);
+       g_string_free (value, TRUE);
+
+       return *s == '\0' ? s : s + 2;
+}
+
+/*
+ * Parse kate modelines.
+ * Kate modelines are of the form "kate: key1 value1; key2 value2;"
+ * These can happen on the 10 first or 10 last lines of the buffer.
+ * See http://wiki.kate-editor.org/index.php/Modelines
+ */
+static gchar *
+parse_kate_modeline (gchar           *s,
+                    ModelineOptions *options)
+{
+       guint intval;
+       GString *key, *value;
+
+       key = g_string_sized_new (8);
+       value = g_string_sized_new (8);
+
+       while (*s != '\0')
+       {
+               while (*s != '\0' && (*s == ';' || g_ascii_isspace (*s)))
+                       s++;
+               if (*s == '\0')
+                       break;
+
+               g_string_assign (key, "");
+               g_string_assign (value, "");
+
+               while (*s != '\0' && *s != ';' && !g_ascii_isspace (*s))
+               {
+                       g_string_append_c (key, *s);
+                       s++;
+               }
+
+               if (!skip_whitespaces (&s))
+                       break;
+               if (*s == ';')
+                       continue;
+
+               while (*s != '\0' && *s != ';' &&
+                      !g_ascii_isspace (*s))
+               {
+                       g_string_append_c (value, *s);
+                       s++;
+               }
+
+               gedit_debug_message (DEBUG_PLUGINS,
+                                    "Kate modeline bit: %s = %s",
+                                    key->str, value->str);
+
+               if (strcmp (key->str, "hl") == 0 ||
+                   strcmp (key->str, "syntax") == 0)
+               {
+                       g_free (options->language_id);
+                       options->language_id = get_language_id_kate (value->str);
+
+                       options->set |= MODELINE_SET_LANGUAGE;
+               }
+               else if (strcmp (key->str, "tab-width") == 0)
+               {
+                       intval = atoi (value->str);
+
+                       if (intval)
+                       {
+                               options->tab_width = intval;
+                               options->set |= MODELINE_SET_TAB_WIDTH;
+                       }
+               }
+               else if (strcmp (key->str, "indent-width") == 0)
+               {
+                       intval = atoi (value->str);
+                       if (intval) options->indent_width = intval;
+               }
+               else if (strcmp (key->str, "space-indent") == 0)
+               {
+                       intval = strcmp (value->str, "on") == 0 ||
+                                strcmp (value->str, "true") == 0 ||
+                                strcmp (value->str, "1") == 0;
+
+                       options->insert_spaces = intval;
+                       options->set |= MODELINE_SET_INSERT_SPACES;
+               }
+               else if (strcmp (key->str, "word-wrap") == 0)
+               {
+                       intval = strcmp (value->str, "on") == 0 ||
+                                strcmp (value->str, "true") == 0 ||
+                                strcmp (value->str, "1") == 0;
+
+                       options->wrap_mode = intval ? GTK_WRAP_WORD : GTK_WRAP_NONE;
+
+                       options->set |= MODELINE_SET_WRAP_MODE;
+               }
+               else if (strcmp (key->str, "word-wrap-column") == 0)
+               {
+                       intval = atoi (value->str);
+
+                       if (intval)
+                       {
+                               options->right_margin_position = intval;
+                               options->display_right_margin = TRUE;
+
+                               options->set |= MODELINE_SET_RIGHT_MARGIN_POSITION |
+                                               MODELINE_SET_SHOW_RIGHT_MARGIN;
+                       }
+               }
+       }
+
+       g_string_free (key, TRUE);
+       g_string_free (value, TRUE);
+
+       return s;
+}
+
+/* Scan a line for vi(m)/emacs/kate modelines.
+ * Line numbers are counted starting at one.
+ */
+static void
+parse_modeline (gchar           *line,
+               gint             line_number,
+               gint             line_count,
+               ModelineOptions *options)
+{
+       gchar *s = line;
+
+       /* look for the beginning of a modeline */
+       while (s != NULL && *s != '\0')
+       {
+               if (s > line && !g_ascii_isspace (*(s - 1)))
+                {
+                        s++;
+                       continue;
+                }
+
+               if ((line_number <= 3 || line_number > line_count - 3) &&
+                   (strncmp (s, "ex:", 3) == 0 ||
+                    strncmp (s, "vi:", 3) == 0 ||
+                    strncmp (s, "vim:", 4) == 0))
+               {
+                       gedit_debug_message (DEBUG_PLUGINS, "Vim modeline on line %d", line_number);
+
+                       while (*s != ':') s++;
+                       s = parse_vim_modeline (s + 1, options);
+               }
+               else if (line_number <= 2 && strncmp (s, "-*-", 3) == 0)
+               {
+                       gedit_debug_message (DEBUG_PLUGINS, "Emacs modeline on line %d", line_number);
+
+                       s = parse_emacs_modeline (s + 3, options);
+               }
+               else if ((line_number <= 10 || line_number > line_count - 10) &&
+                        strncmp (s, "kate:", 5) == 0)
+               {
+                       gedit_debug_message (DEBUG_PLUGINS, "Kate modeline on line %d", line_number);
+
+                       s = parse_kate_modeline (s + 5, options);
+               }
+               else
+                {
+                        s++;
+                }
+       }
+}
+
+static void
+free_modeline_options (ModelineOptions *options)
+{
+       g_free (options->language_id);
+       g_slice_free (ModelineOptions, options);
+}
+
+void
+modeline_parser_apply_modeline (GtkTextBuffer   *buffer,
+                                IdeFileSettings *file_settings)
+{
+       ModelineOptions options;
+       GtkTextIter iter, liter;
+       gint line_count;
+       ModelineOptions *previous;
+
+       options.language_id = NULL;
+       options.set = MODELINE_SET_NONE;
+
+       gtk_text_buffer_get_start_iter (buffer, &iter);
+
+       line_count = gtk_text_buffer_get_line_count (buffer);
+
+       /* Parse the modelines on the 10 first lines... */
+       while ((gtk_text_iter_get_line (&iter) < 10) &&
+              !gtk_text_iter_is_end (&iter))
+       {
+               gchar *line;
+
+               liter = iter;
+               gtk_text_iter_forward_to_line_end (&iter);
+               line = gtk_text_buffer_get_text (buffer, &liter, &iter, TRUE);
+
+               parse_modeline (line,
+                               1 + gtk_text_iter_get_line (&iter),
+                               line_count,
+                               &options);
+
+               gtk_text_iter_forward_line (&iter);
+
+               g_free (line);
+       }
+
+       /* ...and on the 10 last ones (modelines are not allowed in between) */
+       if (!gtk_text_iter_is_end (&iter))
+       {
+               gint cur_line;
+               guint remaining_lines;
+
+               /* we are on the 11th line (count from 0) */
+               cur_line = gtk_text_iter_get_line (&iter);
+               /* g_assert (10 == cur_line); */
+
+               remaining_lines = line_count - cur_line - 1;
+
+               if (remaining_lines > 10)
+               {
+                       gtk_text_buffer_get_end_iter (buffer, &iter);
+                       gtk_text_iter_backward_lines (&iter, 9);
+               }
+       }
+
+       while (!gtk_text_iter_is_end (&iter))
+       {
+               gchar *line;
+
+               liter = iter;
+               gtk_text_iter_forward_to_line_end (&iter);
+               line = gtk_text_buffer_get_text (buffer, &liter, &iter, TRUE);
+
+               parse_modeline (line,
+                               1 + gtk_text_iter_get_line (&iter),
+                               line_count,
+                               &options);
+
+               gtk_text_iter_forward_line (&iter);
+
+               g_free (line);
+       }
+
+       /* Try to set language */
+       if (has_option (&options, MODELINE_SET_LANGUAGE) && options.language_id)
+       {
+               if (g_ascii_strcasecmp (options.language_id, "text") == 0)
+               {
+                       gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (buffer), NULL);
+               }
+               else
+               {
+                       GtkSourceLanguageManager *manager;
+                       GtkSourceLanguage *language;
+
+                       manager = gtk_source_language_manager_get_default ();
+
+                       language = gtk_source_language_manager_get_language
+                                       (manager, options.language_id);
+                       if (language != NULL)
+                       {
+                               gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (buffer), language);
+                       }
+                       else
+                       {
+                               gedit_debug_message (DEBUG_PLUGINS,
+                                                    "Unknown language `%s'",
+                                                    options.language_id);
+                       }
+               }
+       }
+
+       previous = g_object_get_data (G_OBJECT (buffer),
+                                     MODELINE_OPTIONS_DATA_KEY);
+
+       /* Apply the options we got from modelines and restore defaults if
+          we set them before */
+       if (has_option (&options, MODELINE_SET_INSERT_SPACES))
+       {
+               IdeIndentStyle style;
+               style = options.insert_spaces ? IDE_INDENT_STYLE_SPACES : IDE_INDENT_STYLE_TABS;
+               ide_file_settings_set_indent_style (file_settings, style);
+       }
+        else
+       {
+               ide_file_settings_set_indent_style_set (file_settings, FALSE);
+       }
+
+       if (has_option (&options, MODELINE_SET_TAB_WIDTH))
+       {
+               ide_file_settings_set_tab_width (file_settings, options.tab_width);
+       }
+        else
+       {
+               ide_file_settings_set_tab_width_set (file_settings, FALSE);
+       }
+
+       if (has_option (&options, MODELINE_SET_INDENT_WIDTH))
+       {
+               ide_file_settings_set_indent_width (file_settings, options.indent_width);
+       }
+        else
+       {
+               ide_file_settings_set_indent_width_set (file_settings, FALSE);
+       }
+
+       /* XXX: no wrap mode support in IdeFileSettings yet */
+#if 0
+       if (has_option (&options, MODELINE_SET_WRAP_MODE))
+       {
+               gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (view), options.wrap_mode);
+       }
+       else if (check_previous (view, previous, MODELINE_SET_WRAP_MODE))
+       {
+               GtkWrapMode mode;
+
+               mode = g_settings_get_enum (settings,
+                                           GEDIT_SETTINGS_WRAP_MODE);
+               gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (view), mode);
+       }
+#endif
+
+       if (has_option (&options, MODELINE_SET_RIGHT_MARGIN_POSITION))
+       {
+               ide_file_settings_set_right_margin_position (file_settings, options.right_margin_position);
+       }
+        else
+       {
+               ide_file_settings_set_right_margin_position_set (file_settings, FALSE);
+       }
+
+       if (has_option (&options, MODELINE_SET_SHOW_RIGHT_MARGIN))
+       {
+               ide_file_settings_set_show_right_margin (file_settings, options.display_right_margin);
+       }
+        else
+       {
+               ide_file_settings_set_show_right_margin_set (file_settings, FALSE);
+       }
+
+       if (previous)
+       {
+               g_free (previous->language_id);
+               *previous = options;
+               previous->language_id = g_strdup (options.language_id);
+       }
+       else
+       {
+               previous = g_slice_dup (ModelineOptions, &options);
+               previous->language_id = g_steal_pointer (&options.language_id);
+
+               g_object_set_data_full (G_OBJECT (buffer),
+                                       MODELINE_OPTIONS_DATA_KEY,
+                                       previous,
+                                       (GDestroyNotify)free_modeline_options);
+       }
+
+       g_free (options.language_id);
+}
+
+/* vi:ts=8 */
diff --git a/src/plugins/modelines/modeline-parser.h b/src/plugins/modelines/modeline-parser.h
new file mode 100644
index 000000000..e02a5e9dd
--- /dev/null
+++ b/src/plugins/modelines/modeline-parser.h
@@ -0,0 +1,38 @@
+/*
+ * modelie-parser.h
+ * Emacs, Kate and Vim-style modelines support for gedit.
+ *
+ * Copyright 2005-2007 - Steve Frécinaux <code istique net>
+ *
+ * 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 2, 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/>.
+ */
+
+#ifndef __MODELINE_PARSER_H__
+#define __MODELINE_PARSER_H__
+
+#include <glib.h>
+#include <gtksourceview/gtksource.h>
+#include <libide-code.h>
+
+G_BEGIN_DECLS
+
+void modeline_parser_init           (void);
+void modeline_parser_shutdown       (void);
+void modeline_parser_apply_modeline (GtkTextBuffer   *buffer,
+                                     IdeFileSettings *file_settings);
+
+G_END_DECLS
+
+#endif /* __MODELINE_PARSER_H__ */
+/* ex:set ts=8 noet: */
diff --git a/src/plugins/modelines/modelines-plugin.c b/src/plugins/modelines/modelines-plugin.c
new file mode 100644
index 000000000..5f64312dc
--- /dev/null
+++ b/src/plugins/modelines/modelines-plugin.c
@@ -0,0 +1,37 @@
+/* modelines-plugin.c
+ *
+ * Copyright 2018-2019 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 "modelines-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-code.h>
+
+#include "gbp-modelines-file-settings.h"
+
+_IDE_EXTERN void
+_gbp_modelines_register_types (PeasObjectModule *module)
+{
+  g_io_extension_point_implement (IDE_FILE_SETTINGS_EXTENSION_POINT,
+                                  GBP_TYPE_MODELINES_FILE_SETTINGS,
+                                  IDE_FILE_SETTINGS_EXTENSION_POINT".modelines",
+                                  -100);
+}
diff --git a/src/plugins/modelines/modelines.gresource.xml b/src/plugins/modelines/modelines.gresource.xml
new file mode 100644
index 000000000..44765383e
--- /dev/null
+++ b/src/plugins/modelines/modelines.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/modelines">
+    <file>modelines.plugin</file>
+    <file>language-mappings</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/modelines/modelines.plugin b/src/plugins/modelines/modelines.plugin
new file mode 100644
index 000000000..c8f2e862a
--- /dev/null
+++ b/src/plugins/modelines/modelines.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Depends=editor;
+Description=Support for modelines in source code files
+Embedded=_gbp_modelines_register_types
+Hidden=true
+Module=modelines
+Name=Modelines
diff --git a/src/plugins/mono/meson.build b/src/plugins/mono/meson.build
index 588f77e5b..881aebeff 100644
--- a/src/plugins/mono/meson.build
+++ b/src/plugins/mono/meson.build
@@ -1,11 +1,11 @@
-if get_option('with_mono')
+if get_option('plugin_mono')
 
 install_data('mono_plugin.py', install_dir: plugindir)
 
 configure_file(
           input: 'mono.plugin',
          output: 'mono.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/mono/mono.plugin b/src/plugins/mono/mono.plugin
index b14d6d72b..c726983f0 100644
--- a/src/plugins/mono/mono.plugin
+++ b/src/plugins/mono/mono.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=mono_plugin
-Loader=python3
-Name=Mono
-Description=Provides integration with Mono
 Authors=Christian Hergert <chergert redhat com>
-Copyright=Copyright © 2017 Christian Hergert
 Builtin=true
-Hidden=false
+Copyright=Copyright © 2017-2018 Christian Hergert
+Description=Provides integration with Mono
+Hidden=true
+Loader=python3
+Module=mono_plugin
+Name=Mono
+X-Builder-ABI=@PACKAGE_ABI@
diff --git a/src/plugins/newcomers/gbp-newcomers-project.c b/src/plugins/newcomers/gbp-newcomers-project.c
index e026e517d..4d9f8154c 100644
--- a/src/plugins/newcomers/gbp-newcomers-project.c
+++ b/src/plugins/newcomers/gbp-newcomers-project.c
@@ -1,6 +1,6 @@
 /* gbp-newcomers-project.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-newcomers-project"
@@ -172,8 +174,7 @@ gbp_newcomers_project_class_init (GbpNewcomersProjectClass *klass)
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  gtk_widget_class_set_template_from_resource (widget_class,
-                                               
"/org/gnome/builder/plugins/newcomers-plugin/gbp-newcomers-project.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/newcomers/gbp-newcomers-project.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpNewcomersProject, label);
   gtk_widget_class_bind_template_child (widget_class, GbpNewcomersProject, icon);
   gtk_widget_class_bind_template_child (widget_class, GbpNewcomersProject, tags_box);
diff --git a/src/plugins/newcomers/gbp-newcomers-project.h b/src/plugins/newcomers/gbp-newcomers-project.h
index ee7ce5b18..2aaf4dcd8 100644
--- a/src/plugins/newcomers/gbp-newcomers-project.h
+++ b/src/plugins/newcomers/gbp-newcomers-project.h
@@ -1,6 +1,6 @@
 /* ide-newcomer-project.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/newcomers/gbp-newcomers-section.c b/src/plugins/newcomers/gbp-newcomers-section.c
index c63887bcc..3de236be2 100644
--- a/src/plugins/newcomers/gbp-newcomers-section.c
+++ b/src/plugins/newcomers/gbp-newcomers-section.c
@@ -1,6 +1,6 @@
 /* gbp-newcomers-section.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,14 @@
  *
  * 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-newcomers-section"
 
-#include <ide.h>
+#include <libide-greeter.h>
+#include <libide-vcs.h>
 
 #include "gbp-newcomers-project.h"
 #include "gbp-newcomers-section.h"
@@ -29,6 +32,13 @@ struct _GbpNewcomersSection
   GtkFlowBox *flowbox;
 };
 
+typedef struct
+{
+  GbpNewcomersSection *self;
+  GbpNewcomersProject *project;
+  guint                mode;
+} DelayedActivate;
+
 enum {
   PROP_0,
   PROP_HAS_SELECTION,
@@ -38,6 +48,17 @@ enum {
 static void gbp_newcomers_section_child_activated (GbpNewcomersSection *self,
                                                    GbpNewcomersProject *project,
                                                    GtkFlowBox          *flowbox);
+
+static void
+delayed_activate_free (gpointer data)
+{
+  DelayedActivate *state = data;
+
+  g_clear_object (&state->self);
+  g_clear_object (&state->project);
+  g_slice_free (DelayedActivate, state);
+}
+
 static gint
 gbp_newcomers_section_get_priority (IdeGreeterSection *section)
 {
@@ -154,30 +175,77 @@ G_DEFINE_TYPE_WITH_CODE (GbpNewcomersSection, gbp_newcomers_section, GTK_TYPE_BI
                          G_IMPLEMENT_INTERFACE (IDE_TYPE_GREETER_SECTION,
                                                 greeter_section_iface_init))
 
-static void
-gbp_newcomers_section_child_activated (GbpNewcomersSection *self,
-                                       GbpNewcomersProject *project,
-                                       GtkFlowBox          *flowbox)
+static gboolean
+clear_selection_from_timeout (gpointer data)
 {
+  GbpNewcomersSection *self = data;
+
+  g_assert (GBP_IS_NEWCOMERS_SECTION (self));
+
+  if (self->flowbox != NULL)
+    gtk_flow_box_selected_foreach (self->flowbox,
+                                   (GtkFlowBoxForeachFunc)gtk_flow_box_unselect_child,
+                                   NULL);
+
+  return G_SOURCE_REMOVE;
+}
+
+static gboolean
+do_selection_from_timeout (gpointer data)
+{
+  DelayedActivate *state = data;
   g_autoptr(IdeProjectInfo) project_info = NULL;
-  g_autoptr(IdeVcsUri) vcs_uri = NULL;
   const gchar *name;
   const gchar *uri;
 
-  g_assert (GBP_IS_NEWCOMERS_SECTION (self));
-  g_assert (GBP_IS_NEWCOMERS_PROJECT (project));
-  g_assert (GTK_IS_FLOW_BOX (flowbox));
+  g_assert (state != NULL);
+  g_assert (GBP_IS_NEWCOMERS_SECTION (state->self));
+  g_assert (GBP_IS_NEWCOMERS_PROJECT (state->project));
 
-  name = gbp_newcomers_project_get_name (project);
-  uri = gbp_newcomers_project_get_uri (project);
-  vcs_uri = ide_vcs_uri_new (uri);
+  name = gbp_newcomers_project_get_name (state->project);
+  uri = gbp_newcomers_project_get_uri (state->project);
 
   project_info = g_object_new (IDE_TYPE_PROJECT_INFO,
-                               "vcs-uri", vcs_uri,
+                               "vcs-uri", uri,
                                "name", name,
                                NULL);
 
-  ide_greeter_section_emit_project_activated (IDE_GREETER_SECTION (self), project_info);
+  ide_greeter_section_emit_project_activated (IDE_GREETER_SECTION (state->self),
+                                              project_info);
+
+  g_timeout_add_full (G_PRIORITY_HIGH,
+                      300,
+                      clear_selection_from_timeout,
+                      g_object_ref (state->self),
+                      g_object_unref);
+
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_newcomers_section_child_activated (GbpNewcomersSection *self,
+                                       GbpNewcomersProject *project,
+                                       GtkFlowBox          *flowbox)
+{
+  DelayedActivate *state;
+
+  g_assert (GBP_IS_NEWCOMERS_SECTION (self));
+  g_assert (GBP_IS_NEWCOMERS_PROJECT (project));
+  g_assert (GTK_IS_FLOW_BOX (flowbox));
+
+  state = g_slice_new0 (DelayedActivate);
+  state->self = g_object_ref (self);
+  state->project = g_object_ref (project);
+
+  /* Delay the selection for just a moment so the user can actually
+   * see what selection they made.
+   */
+  g_timeout_add_full (G_PRIORITY_HIGH,
+                      150,
+                      do_selection_from_timeout,
+                      g_steal_pointer (&state),
+                      delayed_activate_free);
 }
 
 static void
@@ -212,10 +280,8 @@ gbp_newcomers_section_class_init (GbpNewcomersSectionClass *klass)
                                                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
   gtk_widget_class_set_css_name (widget_class, "newcomers");
-  gtk_widget_class_set_template_from_resource (widget_class,
-                                               
"/org/gnome/builder/plugins/newcomers-plugin/gbp-newcomers-section.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/newcomers/gbp-newcomers-section.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpNewcomersSection, flowbox);
-  gtk_widget_class_bind_template_callback (widget_class, gbp_newcomers_section_child_activated);
 
   g_type_ensure (GBP_TYPE_NEWCOMERS_PROJECT);
 }
@@ -224,4 +290,10 @@ static void
 gbp_newcomers_section_init (GbpNewcomersSection *self)
 {
   gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->flowbox,
+                           "child-activated",
+                           G_CALLBACK (gbp_newcomers_section_child_activated),
+                           self,
+                           G_CONNECT_SWAPPED | G_CONNECT_AFTER);
 }
diff --git a/src/plugins/newcomers/gbp-newcomers-section.h b/src/plugins/newcomers/gbp-newcomers-section.h
index 544352ed7..72e33f294 100644
--- a/src/plugins/newcomers/gbp-newcomers-section.h
+++ b/src/plugins/newcomers/gbp-newcomers-section.h
@@ -1,6 +1,6 @@
 /* gbp-newcomers-section.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/newcomers/gbp-newcomers-section.ui b/src/plugins/newcomers/gbp-newcomers-section.ui
index d18c510cf..6947026ee 100644
--- a/src/plugins/newcomers/gbp-newcomers-section.ui
+++ b/src/plugins/newcomers/gbp-newcomers-section.ui
@@ -1,6 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <template class="GbpNewcomersSection" parent="GtkBin">
+    <property name="halign">center</property>
+    <property name="hexpand">true</property>
     <child>
       <object class="GtkBox">
         <property name="orientation">vertical</property>
@@ -26,7 +28,6 @@
             <property name="selection-mode">browse</property>
             <property name="min-children-per-line">3</property>
             <property name="max-children-per-line">5</property>
-            <signal name="child-activated" handler="gbp_newcomers_section_child_activated" swapped="true" 
object="GbpNewcomersSection"/>
             <child>
               <object class="GbpNewcomersProject">
                 <property name="name" translatable="yes">Polari</property>
diff --git a/src/plugins/newcomers/meson.build b/src/plugins/newcomers/meson.build
index f461394ee..be45e6b45 100644
--- a/src/plugins/newcomers/meson.build
+++ b/src/plugins/newcomers/meson.build
@@ -1,20 +1,17 @@
-if get_option('with_newcomers')
+if get_option('plugin_newcomers')
 
-newcomers_resources = gnome.compile_resources(
+plugins_sources += files([
+  'newcomers-plugin.c',
+  'gbp-newcomers-project.c',
+  'gbp-newcomers-section.c',
+])
+
+plugin_newcomers_resources = gnome.compile_resources(
   'newcomers-resources',
   'newcomers.gresource.xml',
   c_name: 'gbp_newcomers',
 )
 
-newcomers_sources = [
-  'newcomers-plugin.c',
-  'gbp-newcomers-project.c',
-  'gbp-newcomers-project.h',
-  'gbp-newcomers-section.c',
-  'gbp-newcomers-section.h',
-]
-
-gnome_builder_plugins_sources += files(newcomers_sources)
-gnome_builder_plugins_sources += newcomers_resources[0]
+plugins_sources += plugin_newcomers_resources[0]
 
 endif
diff --git a/src/plugins/newcomers/newcomers-plugin.c b/src/plugins/newcomers/newcomers-plugin.c
index f07ce78a5..6ec9547c8 100644
--- a/src/plugins/newcomers/newcomers-plugin.c
+++ b/src/plugins/newcomers/newcomers-plugin.c
@@ -1,6 +1,6 @@
 /* newcomers-plugin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,15 +14,21 @@
  *
  * 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 "newcomers-plugin"
+
+#include "config.h"
+
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-greeter.h>
 
 #include "gbp-newcomers-section.h"
 
-void
-gbp_newcomers_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_gbp_newcomers_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_GREETER_SECTION,
diff --git a/src/plugins/newcomers/newcomers.gresource.xml b/src/plugins/newcomers/newcomers.gresource.xml
index c0e8b3a45..2ca18ba34 100644
--- a/src/plugins/newcomers/newcomers.gresource.xml
+++ b/src/plugins/newcomers/newcomers.gresource.xml
@@ -1,11 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
 
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/newcomers">
     <file>newcomers.plugin</file>
-  </gresource>
-
-  <gresource prefix="/org/gnome/builder/plugins/newcomers-plugin">
     <file>gbp-newcomers-project.ui</file>
     <file>gbp-newcomers-section.ui</file>
   </gresource>
diff --git a/src/plugins/newcomers/newcomers.plugin b/src/plugins/newcomers/newcomers.plugin
index fe9f44e41..01c297dff 100644
--- a/src/plugins/newcomers/newcomers.plugin
+++ b/src/plugins/newcomers/newcomers.plugin
@@ -1,8 +1,9 @@
 [Plugin]
-Module=newcomers-plugin
-Name=GNOME Newcomers
-Description=Integration with GNOME newcomers initiative
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2017 Christian Hergert
 Builtin=true
-Embedded=gbp_newcomers_register_types
+Copyright=Copyright © 2017-2018 Christian Hergert
+Description=Integration with GNOME newcomers initiative
+Embedded=_gbp_newcomers_register_types
+Module=newcomers
+Depends=greeter;
+Name=GNOME Newcomers
diff --git a/src/plugins/notification/ide-notification-addin.c 
b/src/plugins/notification/ide-notification-addin.c
index 52957a1e0..0d0718607 100644
--- a/src/plugins/notification/ide-notification-addin.c
+++ b/src/plugins/notification/ide-notification-addin.c
@@ -14,12 +14,15 @@
  *
  * 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-notification-addin"
 
 #include <glib/gi18n.h>
 #include <gio/gio.h>
+#include <libide-foundry.h>
 
 #include "ide-notification-addin.h"
 
@@ -27,10 +30,12 @@
 
 struct _IdeNotificationAddin
 {
-  IdeObject  parent_instance;
-  gchar     *last_msg_body;
-  gint64     last_time;
-  guint      supress : 1;
+  IdeObject        parent_instance;
+  IdeNotification *notif;
+  gchar           *last_msg_body;
+  IdeBuildPhase    requested_phase;
+  gint64           last_time;
+  guint            supress : 1;
 };
 
 static void addin_iface_init (IdeBuildPipelineAddinInterface *iface);
@@ -65,14 +70,13 @@ ide_notification_addin_notify (IdeNotificationAddin *self,
                                gboolean              success)
 {
   g_autofree gchar *msg_body = NULL;
+  g_autofree gchar *project_name = NULL;
   g_autoptr(GNotification) notification = NULL;
   g_autoptr(GIcon) icon = NULL;
   GtkApplication *app;
-  const gchar *project_name;
   const gchar *msg_title;
   const gchar *id;
   IdeContext *context;
-  IdeProject *project;
   GtkWindow *window;
 
   g_assert (IDE_IS_NOTIFICATION_ADDIN (self));
@@ -89,9 +93,8 @@ ide_notification_addin_notify (IdeNotificationAddin *self,
     return;
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  project = ide_context_get_project (context);
-  project_name = ide_project_get_name (project);
-  id = ide_project_get_id (project);
+  project_name = ide_context_dup_title (context);
+  id = ide_context_dup_project_id (context);
 
   if (success)
     {
@@ -117,24 +120,38 @@ ide_notification_addin_notify (IdeNotificationAddin *self,
 
 static void
 ide_notification_addin_build_started (IdeNotificationAddin *self,
-                                      IdeBuildPipeline     *build_pipeline,
+                                      IdeBuildPipeline     *pipeline,
                                       IdeBuildManager      *build_manager)
 {
   IdeBuildPhase phase;
 
   g_assert (IDE_IS_NOTIFICATION_ADDIN (self));
-  g_assert (IDE_IS_BUILD_PIPELINE (build_pipeline));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
 
+  if (self->notif != NULL)
+    {
+      ide_notification_withdraw (self->notif);
+      g_clear_object (&self->notif);
+    }
+
   /* We don't care about any build that is advancing to a phase
    * before the BUILD phase. We advanced to CONFIGURE a lot when
    * extracting build flags.
    */
 
-  phase = ide_build_pipeline_get_requested_phase (build_pipeline);
+  phase = ide_build_pipeline_get_requested_phase (pipeline);
   g_assert ((phase & IDE_BUILD_PHASE_MASK) == phase);
 
+  self->requested_phase = phase;
   self->supress = phase < IDE_BUILD_PHASE_BUILD;
+
+  if (self->requested_phase)
+    {
+      self->notif = ide_notification_new ();
+      g_object_bind_property (pipeline, "message", self->notif, "title", G_BINDING_SYNC_CREATE);
+      ide_notification_attach (self->notif, IDE_OBJECT (self));
+    }
 }
 
 static void
@@ -146,18 +163,35 @@ ide_notification_addin_build_failed (IdeNotificationAddin *self,
   g_assert (IDE_IS_BUILD_PIPELINE (build_pipeline));
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
 
+  if (self->notif)
+    ide_notification_set_title (self->notif, _("Build failed"));
+
   ide_notification_addin_notify (self, FALSE);
 }
 
 static void
 ide_notification_addin_build_finished (IdeNotificationAddin *self,
-                                       IdeBuildPipeline     *build_pipeline,
+                                       IdeBuildPipeline     *pipeline,
                                        IdeBuildManager      *build_manager)
 {
   g_assert (IDE_IS_NOTIFICATION_ADDIN (self));
-  g_assert (IDE_IS_BUILD_PIPELINE (build_pipeline));
+  g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
 
+  if (self->notif)
+    {
+      g_autoptr(GIcon) icon = g_themed_icon_new ("emblem-ok-symbolic");
+
+      if (self->requested_phase & IDE_BUILD_PHASE_BUILD)
+        ide_notification_set_title (self->notif, _("Build succeeded"));
+      else if (self->requested_phase & IDE_BUILD_PHASE_CONFIGURE)
+        ide_notification_set_title (self->notif, _("Build configured"));
+      else if (self->requested_phase & IDE_BUILD_PHASE_AUTOGEN)
+        ide_notification_set_title (self->notif, _("Build bootstrapped"));
+
+      ide_notification_set_icon (self->notif, icon);
+    }
+
   ide_notification_addin_notify (self, TRUE);
 }
 
@@ -175,7 +209,7 @@ ide_notification_addin_load (IdeBuildPipelineAddin *addin,
   context = ide_object_get_context (IDE_OBJECT (addin));
   g_assert (IDE_IS_CONTEXT (context));
 
-  build_manager = ide_context_get_build_manager (context);
+  build_manager = ide_build_manager_from_context (context);
   g_assert (IDE_IS_BUILD_MANAGER (build_manager));
 
   g_signal_connect_object (build_manager,
@@ -206,6 +240,12 @@ ide_notification_addin_unload (IdeBuildPipelineAddin *addin,
   g_assert (IDE_IS_NOTIFICATION_ADDIN (self));
   g_assert (IDE_IS_BUILD_PIPELINE (pipeline));
 
+  if (self->notif != NULL)
+    {
+      ide_notification_withdraw (self->notif);
+      g_clear_object (&self->notif);
+    }
+
   g_clear_pointer (&self->last_msg_body, g_free);
 }
 
diff --git a/src/plugins/notification/ide-notification-addin.h 
b/src/plugins/notification/ide-notification-addin.h
index 14b1b1044..18832a59c 100644
--- a/src/plugins/notification/ide-notification-addin.h
+++ b/src/plugins/notification/ide-notification-addin.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/notification/meson.build b/src/plugins/notification/meson.build
index a30684721..e7d23dcd1 100644
--- a/src/plugins/notification/meson.build
+++ b/src/plugins/notification/meson.build
@@ -1,18 +1,16 @@
-if get_option('with_notification')
+if get_option('plugin_notification')
 
-notification_resources = gnome.compile_resources(
+plugins_sources += files([
+  'notification-plugin.c',
+  'ide-notification-addin.c',
+])
+
+plugin_notification_resources = gnome.compile_resources(
   'notification-resources',
   'notification.gresource.xml',
-  c_name: 'ide_notification',
+  c_name: 'gbp_notification',
 )
 
-notification_sources = [
-  'ide-notification-plugin.c',
-  'ide-notification-addin.c',
-  'ide-notification-addin.h',
-]
-
-gnome_builder_plugins_sources += files(notification_sources)
-gnome_builder_plugins_sources += notification_resources[0]
+plugins_sources += plugin_notification_resources[0]
 
 endif
diff --git a/src/plugins/notification/notification-plugin.c b/src/plugins/notification/notification-plugin.c
new file mode 100644
index 000000000..4a53c0afa
--- /dev/null
+++ b/src/plugins/notification/notification-plugin.c
@@ -0,0 +1,34 @@
+/* autotools-plugin.c
+ *
+ * Copyright 2017 Lucie Charvat <luci charvat gmail 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
+ */
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-foundry.h>
+
+#include "ide-notification-addin.h"
+
+_IDE_EXTERN void
+_ide_notification_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUILD_PIPELINE_ADDIN,
+                                              IDE_TYPE_NOTIFICATION_ADDIN);
+}
diff --git a/src/plugins/notification/notification.gresource.xml 
b/src/plugins/notification/notification.gresource.xml
index a4906c684..2ffb079f9 100644
--- a/src/plugins/notification/notification.gresource.xml
+++ b/src/plugins/notification/notification.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/notification">
     <file>notification.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/notification/notification.plugin b/src/plugins/notification/notification.plugin
index 3f1f0c789..079e89883 100644
--- a/src/plugins/notification/notification.plugin
+++ b/src/plugins/notification/notification.plugin
@@ -1,8 +1,9 @@
 [Plugin]
-Module=notification-plugin
-Name=Notification of progress
-Description=Notification of progress when Builder application is not on foreground
 Authors=Lucie Charvat <luci charvat gmail com>
-Copyright=Copyright © 2017 Lucie Charvat
 Builtin=true
-Embedded=ide_notification_register_types
+Copyright=Copyright © 2017 Lucie Charvat
+Description=Notification of progress when Builder application is not on foreground
+Embedded=_ide_notification_register_types
+Hidden=true
+Module=notification
+Name=Notification of progress
diff --git a/src/plugins/npm/meson.build b/src/plugins/npm/meson.build
index 299d50167..5716ffef9 100644
--- a/src/plugins/npm/meson.build
+++ b/src/plugins/npm/meson.build
@@ -1,11 +1,11 @@
-if get_option('with_npm')
+if get_option('plugin_npm')
 
 install_data('npm_plugin.py', install_dir: plugindir)
 
 configure_file(
           input: 'npm.plugin',
          output: 'npm.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/npm/npm.plugin b/src/plugins/npm/npm.plugin
index 2a99618da..80cb1fe0a 100644
--- a/src/plugins/npm/npm.plugin
+++ b/src/plugins/npm/npm.plugin
@@ -1,10 +1,12 @@
 [Plugin]
-Module=npm_plugin
-Loader=python3
-Name=NPM/nodejs
-Description=Provides integration with the NPM package system
 Authors=Giovanni Campagna <gcampagn cs stanford edu>
-Copyright=Copyright © 2016 Christian Hergert, 2017 Giovanni Campagna
 Builtin=true
-X-Project-File-Filter-Pattern=package.json
+Copyright=Copyright © 2016-2018 Christian Hergert, 2017 Giovanni Campagna
+Description=Provides integration with the NPM package system
+Hidden=true
+Loader=python3
+Module=npm_plugin
+Name=NPM/nodejs
+X-Builder-ABI=@PACKAGE_ABI@
 X-Project-File-Filter-Name=NPM package (package.json)
+X-Project-File-Filter-Pattern=package.json
diff --git a/src/plugins/npm/npm_plugin.py b/src/plugins/npm/npm_plugin.py
index a107622fa..d0c115105 100644
--- a/src/plugins/npm/npm_plugin.py
+++ b/src/plugins/npm/npm_plugin.py
@@ -3,8 +3,8 @@
 #
 # npm_plugin.py
 #
-# Copyright 2016 Christian Hergert <chris dronelabs com>
-#               2017 Giovanni Campagna <gcampagn cs stanford edu>
+# Copyright 2016-2018 Christian Hergert <chergert redhat com>
+# Copyright 2017 Giovanni Campagna <gcampagn cs stanford edu>
 #
 # 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
@@ -25,16 +25,21 @@ import threading
 import os
 import json
 
-gi.require_version('Ide', '1.0')
-
 from gi.repository import Gio
 from gi.repository import GLib
 from gi.repository import GObject
 from gi.repository import Ide
 
-Ide.vcs_register_ignored('node_modules')
+Ide.g_file_add_ignored_pattern('node_modules')
+
+class NPMBuildSystemDiscovery(Ide.SimpleBuildSystemDiscovery):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.props.glob = 'package.json'
+        self.props.hint = 'npm_plugin'
+        self.props.priority = 100
 
-class NPMBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
+class NPMBuildSystem(Ide.Object, Ide.BuildSystem):
     project_file = GObject.Property(type=Gio.File)
 
     def do_get_id(self):
@@ -43,35 +48,8 @@ class NPMBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
     def do_get_display_name(self):
         return 'NPM (node.js)'
 
-    def do_init_async(self, io_priority, cancellable, callback, data):
-        task = Gio.Task.new(self, cancellable, callback)
-
-        # This is all done synchronously, doing it in a thread would probably
-        # be somewhat ideal although unnecessary at this time.
-
-        try:
-            # Maybe this is a package.json
-            if self.props.project_file.get_basename() in ('package.json',):
-                task.return_boolean(True)
-                return
-
-            # Maybe this is a directory with a package.json
-            if self.props.project_file.query_file_type(0) == Gio.FileType.DIRECTORY:
-                child = self.props.project_file.get_child('package.json')
-                if child.query_exists(None):
-                    self.props.project_file = child
-                    task.return_boolean(True)
-                    return
-        except Exception as ex:
-            task.return_error(ex)
-
-        task.return_error(Ide.NotSupportedError())
-
-    def do_init_finish(self, task):
-        return task.propagate_boolean()
-
     def do_get_priority(self):
-        return -100
+        return 100
 
 
 class NPMPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
@@ -82,7 +60,7 @@ class NPMPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
 
     def do_load(self, pipeline):
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         # Ignore pipeline unless this is a npm/nodejs project
         if type(build_system) != NPMBuildSystem:
@@ -109,7 +87,7 @@ class NPMPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
         fetch_launcher.push_argv('install')
         stage = Ide.BuildStageLauncher.new(context, fetch_launcher)
         stage.set_name(_("Downloading npm dependencies"))
-        self.track(pipeline.connect(Ide.BuildPhase.DOWNLOADS, 0, stage))
+        self.track(pipeline.attach(Ide.BuildPhase.DOWNLOADS, 0, stage))
 
 
 # The scripts used by the npm build system during build
@@ -137,8 +115,7 @@ class NPMBuildTarget(Ide.Object, Ide.BuildTarget):
 
     def do_get_cwd(self):
         context = self.get_context()
-        project_file = context.get_project_file()
-        return project_file.get_parent().get_path()
+        return context.ref_workdir().get_path()
 
     def do_get_language(self):
         return 'js'
@@ -203,7 +180,7 @@ class NPMBuildTargetProvider(Ide.Object, Ide.BuildTargetProvider):
         task.set_priority(GLib.PRIORITY_LOW)
 
         context = self.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != NPMBuildSystem:
             task.return_error(GLib.Error('Not NPM build system',
@@ -211,7 +188,7 @@ class NPMBuildTargetProvider(Ide.Object, Ide.BuildTargetProvider):
                                          code=Gio.IOErrorEnum.NOT_SUPPORTED))
             return
 
-        project_file = context.get_project_file()
+        project_file = build_system.project_file
         project_file.load_contents_async(cancellable, self._on_package_json_loaded, task)
 
     def do_get_targets_finish(self, result):
diff --git a/src/plugins/omni-gutter/fast-str.c b/src/plugins/omni-gutter/fast-str.c
new file mode 100644
index 000000000..744078997
--- /dev/null
+++ b/src/plugins/omni-gutter/fast-str.c
@@ -0,0 +1,77 @@
+/* fast-str.c
+ *
+ * Copyright 2018-2019 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 "fast-str"
+
+#include "config.h"
+
+#include <stdio.h>
+
+#include "int-array.h"
+#include "fast-str.h"
+
+gint
+_fast_str (guint         value,
+           const gchar **strptr,
+           gchar         alloc_buf[static 12])
+{
+  gint ret;
+
+  /* The first offset is 10,000 so we can use that string but offset
+   * ourselves into that string a bit to get the same number we would
+   * if we had smaller strings available.
+   */
+
+  if (value < 10)
+    {
+      *strptr = int2str[value] + 4;
+      return 1;
+    }
+
+  if (value < 100)
+    {
+      *strptr = int2str[value] + 3;
+      return 2;
+    }
+
+  if (value < 1000)
+    {
+      *strptr = int2str[value] + 2;
+      return 3;
+    }
+
+  if (value < 10000)
+    {
+      *strptr = int2str[value] + 1;
+      return 4;
+    }
+
+  if (value < 20000)
+    {
+      *strptr = int2str[value - 10000];
+      return 5;
+    }
+
+  *strptr = alloc_buf;
+  ret = snprintf (alloc_buf, 12, "%u", value);
+  alloc_buf[11] = 0;
+
+  return ret;
+}
diff --git a/src/plugins/omni-gutter/fast-str.h b/src/plugins/omni-gutter/fast-str.h
new file mode 100644
index 000000000..8054590a0
--- /dev/null
+++ b/src/plugins/omni-gutter/fast-str.h
@@ -0,0 +1,32 @@
+/* fast-str.h
+ *
+ * Copyright 2018-2019 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.h>
+
+G_BEGIN_DECLS
+
+void _fast_str_init (void);
+gint _fast_str      (guint         value,
+                     const gchar **strptr,
+                     gchar         alloc_buf[static 12]);
+
+G_END_DECLS
diff --git a/src/plugins/omni-gutter/gbp-omni-gutter-editor-page-addin.c 
b/src/plugins/omni-gutter/gbp-omni-gutter-editor-page-addin.c
new file mode 100644
index 000000000..9b740f7d4
--- /dev/null
+++ b/src/plugins/omni-gutter/gbp-omni-gutter-editor-page-addin.c
@@ -0,0 +1,79 @@
+/* gbp-omni-gutter-editor-page-addin.c
+ *
+ * Copyright 2018-2019 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-omni-gutter-editor-page-addin"
+
+#include <libide-editor.h>
+
+#include "gbp-omni-gutter-editor-page-addin.h"
+#include "gbp-omni-gutter-renderer.h"
+
+struct _GbpOmniGutterEditorPageAddin
+{
+  GObject parent_instance;
+};
+
+static void
+gbp_omni_gutter_editor_page_addin_load (IdeEditorPageAddin *addin,
+                                        IdeEditorPage      *page)
+{
+  GbpOmniGutterRenderer *gutter;
+  IdeSourceView *view;
+
+  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (page));
+
+  view = ide_editor_page_get_view (page);
+  gutter = gbp_omni_gutter_renderer_new ();
+  ide_source_view_set_gutter (view, IDE_GUTTER (gutter));
+}
+
+static void
+gbp_omni_gutter_editor_page_addin_unload (IdeEditorPageAddin *addin,
+                                          IdeEditorPage      *page)
+{
+  IdeSourceView *view;
+
+  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (page));
+
+  view = ide_editor_page_get_view (page);
+  ide_source_view_set_gutter (view, NULL);
+}
+
+static void
+editor_page_addin_iface_init (IdeEditorPageAddinInterface *iface)
+{
+  iface->load = gbp_omni_gutter_editor_page_addin_load;
+  iface->unload = gbp_omni_gutter_editor_page_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpOmniGutterEditorPageAddin, gbp_omni_gutter_editor_page_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_PAGE_ADDIN, editor_page_addin_iface_init))
+
+static void
+gbp_omni_gutter_editor_page_addin_class_init (GbpOmniGutterEditorPageAddinClass *klass)
+{
+}
+
+static void
+gbp_omni_gutter_editor_page_addin_init (GbpOmniGutterEditorPageAddin *self)
+{
+}
diff --git a/src/plugins/omni-gutter/gbp-omni-gutter-editor-page-addin.h 
b/src/plugins/omni-gutter/gbp-omni-gutter-editor-page-addin.h
new file mode 100644
index 000000000..d289ed0ac
--- /dev/null
+++ b/src/plugins/omni-gutter/gbp-omni-gutter-editor-page-addin.h
@@ -0,0 +1,31 @@
+/* gbp-omni-gutter-editor-page-addin.h
+ *
+ * Copyright 2018-2019 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_OMNI_GUTTER_EDITOR_PAGE_ADDIN (gbp_omni_gutter_editor_page_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpOmniGutterEditorPageAddin, gbp_omni_gutter_editor_page_addin, GBP, 
OMNI_GUTTER_EDITOR_PAGE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/omni-gutter/gbp-omni-gutter-renderer.c 
b/src/plugins/omni-gutter/gbp-omni-gutter-renderer.c
new file mode 100644
index 000000000..efcdfec3d
--- /dev/null
+++ b/src/plugins/omni-gutter/gbp-omni-gutter-renderer.c
@@ -0,0 +1,1750 @@
+/* gbp-omni-gutter-renderer.c
+ *
+ * Copyright 2017-2019 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-omni-gutter-renderer"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <string.h>
+
+#include <libide-core.h>
+#include <libide-code.h>
+#include <libide-debugger.h>
+#include <libide-sourceview.h>
+
+#include "ide-debugger-private.h"
+
+#include "fast-str.h"
+#include "gbp-omni-gutter-renderer.h"
+
+/**
+ * SECTION:gbp-omni-gutter-renderer
+ * @title: GbpOmniGutterRenderer
+ * @short_description: A featureful gutter renderer for the code editor
+ *
+ * This is a #GtkSourceGutterRenderer that knows how to render many of
+ * our components necessary for Builder. Because of the complexity of
+ * Builder, using traditional gutter renderers takes up a great deal
+ * of horizontal space.
+ *
+ * By overlapping some of our components, we can take up less space and
+ * be easier for the user with increased hit-targets.
+ *
+ * Additionally, we can render faster because we can coalesce work.
+ *
+ * Since: 3.32
+ */
+
+#define ARROW_WIDTH      5
+#define CHANGE_WIDTH     2
+#define DELETE_WIDTH     5.0
+#define DELETE_HEIGHT    8.0
+
+#define IS_BREAKPOINT(i)  ((i)->is_breakpoint || (i)->is_countpoint || (i)->is_watchpoint)
+#define IS_DIAGNOSTIC(i)  ((i)->is_error || (i)->is_warning || (i)->is_note)
+#define IS_LINE_CHANGE(i) ((i)->is_add || (i)->is_change || \
+                           (i)->is_delete || (i)->is_next_delete || (i)->is_prev_delete)
+
+struct _GbpOmniGutterRenderer
+{
+  GtkSourceGutterRenderer parent_instance;
+
+  IdeDebuggerBreakpoints *breakpoints;
+
+  GArray *lines;
+
+  DzlSignalGroup *view_signals;
+  DzlSignalGroup *buffer_signals;
+
+  /*
+   * A scaled font description that matches the size of the text
+   * within the source view. Cached to avoid recreating it on ever
+   * frame render.
+   */
+  PangoFontDescription *scaled_font_desc;
+
+  /* TODO: It would be nice to use some basic caching here
+   *       so we don't waste 6Kb-12Kb of data on these surfaces.
+   *       But that can be done later after this patch set merges.
+   */
+  cairo_surface_t *note_surface;
+  cairo_surface_t *warning_surface;
+  cairo_surface_t *error_surface;
+  cairo_surface_t *note_selected_surface;
+  cairo_surface_t *warning_selected_surface;
+  cairo_surface_t *error_selected_surface;
+
+  /*
+   * We cache various colors we need from the style scheme to avoid
+   * looking them up very often, as it is CPU time consuming. We also
+   * use these colors to prime the symbolic colors for the icon surfaces
+   * to they look appropriate for the style scheme.
+   */
+  struct {
+    GdkRGBA fg;
+    GdkRGBA bg;
+    gboolean bold;
+  } text, current, bkpt, ctpt;
+  GdkRGBA stopped_bg;
+  struct {
+    GdkRGBA add;
+    GdkRGBA remove;
+    GdkRGBA change;
+  } changes;
+
+  /*
+   * We need to reuse a single pango layout while drawing all the lines
+   * to keep the overhead low. We don't have pixel caching on the gutter
+   * data so keeping this stuff fast is critical.
+   */
+  PangoLayout *layout;
+
+  /*
+   * We reuse a simple bold attr list for the current line number
+   * information.  This way we don't have to do any pango markup
+   * parsing.
+   */
+  PangoAttrList *bold_attrs;
+
+  /* We stash a copy of how long the line numbers could be. 1000 => 4. */
+  guint n_chars;
+
+  /* While processing the lines, we track what our first line number is
+   * so that differential calculation for each line is cheap by avoiding
+   * accessing GtkTextIter information.
+   */
+  guint begin_line;
+
+  /*
+   * While starting a render, we check to see what the current
+   * breakpoint line is (so we can draw the proper background.
+   *
+   * TODO: Add a callback to the debug manager to avoid querying this
+   *       information on every draw cycle.
+   */
+  gint stopped_line;
+
+  /*
+   * To avoid doing multiple line recalculations inline, we defer our
+   * changed handler until we've re-entered teh main loop. Otherwise
+   * we could handle lots of small changes during automated processing
+   * of the underlying buffer.
+   */
+  guint resize_source;
+
+  /*
+   * The number_width field contains the maximum width of the text as
+   * sized by Pango. It is in pixel units in the scale of the widget
+   * as the underlying components will automatically deal with scaling
+   * for us (as necessary).
+   */
+  gint number_width;
+
+  /*
+   * Calculated size for diagnostics, to be a nearest icon-size based
+   * on the height of the line text.
+   */
+  gint diag_size;
+
+  /*
+   * Some users might want to toggle off individual features of the
+   * omni gutter, and these boolean properties provide that. Other
+   * components map them to GSettings values to be toggled.
+   */
+  guint show_line_changes : 1;
+  guint show_line_numbers : 1;
+  guint show_line_diagnostics : 1;
+};
+
+enum {
+  FOREGROUND,
+  BACKGROUND,
+};
+
+enum {
+  PROP_0,
+  PROP_SHOW_LINE_CHANGES,
+  PROP_SHOW_LINE_NUMBERS,
+  PROP_SHOW_LINE_DIAGNOSTICS,
+  N_PROPS
+};
+
+typedef struct
+{
+  /* The line contains a regular breakpoint */
+  guint is_breakpoint : 1;
+
+  /* The line contains a countpoint styl breakpoint */
+  guint is_countpoint : 1;
+
+  /* The line contains a watchpoint style breakpoint */
+  guint is_watchpoint : 1;
+
+  /* The line is an addition to the buffer */
+  guint is_add : 1;
+
+  /* The line has changed in the buffer */
+  guint is_change : 1;
+
+  /* The line is part of a deleted range in the buffer */
+  guint is_delete : 1;
+
+  /* The previous line was a delete */
+  guint is_prev_delete : 1;
+
+  /* The next line is a delete */
+  guint is_next_delete : 1;
+
+  /* The line contains a diagnostic error */
+  guint is_error : 1;
+
+  /* The line contains a diagnostic warning */
+  guint is_warning : 1;
+
+  /* The line contains a diagnostic note */
+  guint is_note : 1;
+} LineInfo;
+
+static void gbp_omni_gutter_renderer_reload_icons (GbpOmniGutterRenderer *self);
+static void gutter_iface_init                     (IdeGutterInterface    *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GbpOmniGutterRenderer,
+                         gbp_omni_gutter_renderer,
+                         GTK_SOURCE_TYPE_GUTTER_RENDERER,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_GUTTER, gutter_iface_init))
+
+static GParamSpec *properties [N_PROPS];
+
+/*
+ * style_get_is_bold:
+ *
+ * This helper is used to extract the "bold" field from a GtkSourceStyle
+ * within a GtkSourceStyleScheme.
+ *
+ * Returns; %TRUE if @val was set to a trusted value.
+ */
+static gboolean
+style_get_is_bold (GtkSourceStyleScheme *scheme,
+                   const gchar          *style_name,
+                   gboolean             *val)
+{
+  GtkSourceStyle *style;
+
+  g_assert (!scheme || GTK_SOURCE_IS_STYLE_SCHEME (scheme));
+  g_assert (style_name != NULL);
+  g_assert (val != NULL);
+
+  *val = FALSE;
+
+  if (scheme == NULL)
+    return FALSE;
+
+  if (NULL != (style = gtk_source_style_scheme_get_style (scheme, style_name)))
+    {
+      gboolean bold_set = FALSE;
+      g_object_get (style,
+                    "bold-set", &bold_set,
+                    "bold", val,
+                    NULL);
+      return bold_set;
+    }
+
+  return FALSE;
+}
+
+/*
+ * get_style_rgba:
+ *
+ * Gets a #GdkRGBA for a particular field of a style within @scheme.
+ *
+ * @type should be set to BACKGROUND or FOREGROUND.
+ *
+ * If we fail to locate the style, @rgba is set to transparent black.
+ * such as #rgba(0,0,0,0).
+ *
+ * Returns: %TRUE if the value placed into @rgba can be trusted.
+ */
+static gboolean
+get_style_rgba (GtkSourceStyleScheme *scheme,
+                const gchar          *style_name,
+                int                   type,
+                GdkRGBA              *rgba)
+{
+  GtkSourceStyle *style;
+
+  g_assert (!scheme || GTK_SOURCE_IS_STYLE_SCHEME (scheme));
+  g_assert (style_name != NULL);
+  g_assert (type == FOREGROUND || type == BACKGROUND);
+  g_assert (rgba != NULL);
+
+  memset (rgba, 0, sizeof *rgba);
+
+  if (scheme == NULL)
+    return FALSE;
+
+  if (NULL != (style = gtk_source_style_scheme_get_style (scheme, style_name)))
+    {
+      g_autofree gchar *str = NULL;
+      gboolean set = FALSE;
+
+      g_object_get (style,
+                    type ? "background" : "foreground", &str,
+                    type ? "background-set" : "foreground-set", &set,
+                    NULL);
+
+      if (str != NULL)
+        gdk_rgba_parse (rgba, str);
+
+      return set;
+    }
+
+  return FALSE;
+}
+
+static void
+reload_style_colors (GbpOmniGutterRenderer *self,
+                     GtkSourceStyleScheme  *scheme)
+{
+  GtkStyleContext *context;
+  GtkTextView *view;
+  GtkStateFlags state;
+  GdkRGBA fg;
+  GdkRGBA bg;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (!scheme || GTK_SOURCE_IS_STYLE_SCHEME (scheme));
+
+  view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (self));
+  if (view == NULL)
+    return;
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (view));
+  state = gtk_style_context_get_state (context);
+  gtk_style_context_get_color (context, state, &fg);
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
+  gtk_style_context_get_background_color (context, state, &bg);
+  G_GNUC_END_IGNORE_DEPRECATIONS;
+
+  /* Extract common values from style schemes. */
+  if (!get_style_rgba (scheme, "line-numbers", FOREGROUND, &self->text.fg))
+    self->text.fg = fg;
+
+  if (!get_style_rgba (scheme, "line-numbers", BACKGROUND, &self->text.bg))
+    self->text.bg = bg;
+
+  if (!style_get_is_bold (scheme, "line-numbers", &self->text.bold))
+    self->text.bold = FALSE;
+
+  if (!get_style_rgba (scheme, "current-line-number", FOREGROUND, &self->current.fg))
+    self->current.fg = fg;
+
+  if (!get_style_rgba (scheme, "current-line-number", BACKGROUND, &self->current.bg))
+    self->current.bg = bg;
+
+  if (!style_get_is_bold (scheme, "current-line-number", &self->current.bold))
+    self->current.bold = TRUE;
+
+  /* These gutter:: prefix values come from Builder's style-scheme xml
+   * files, but other style schemes may also support them now too.
+   */
+  if (!get_style_rgba (scheme, "gutter::added-line", FOREGROUND, &self->changes.add))
+    gdk_rgba_parse (&self->changes.add, "#8ae234");
+
+  if (!get_style_rgba (scheme, "gutter::changed-line", FOREGROUND, &self->changes.change))
+    gdk_rgba_parse (&self->changes.change, "#fcaf3e");
+
+  if (!get_style_rgba (scheme, "gutter::removed-line", FOREGROUND, &self->changes.remove))
+    gdk_rgba_parse (&self->changes.remove, "#ef2929");
+
+  /*
+   * These debugger:: prefix values come from Builder's style-scheme xml
+   * as well as in the IdeBuffer class. Other style schemes may also
+   * support them, though.
+   */
+  if (!get_style_rgba (scheme, "debugger::current-breakpoint", BACKGROUND, &self->stopped_bg))
+    gdk_rgba_parse (&self->stopped_bg, "#fcaf3e");
+
+  if (!get_style_rgba (scheme, "debugger::breakpoint", FOREGROUND, &self->bkpt.fg))
+    get_style_rgba (scheme, "selection", FOREGROUND, &self->bkpt.fg);
+  if (!get_style_rgba (scheme, "debugger::breakpoint", BACKGROUND, &self->bkpt.bg))
+    get_style_rgba (scheme, "selection", BACKGROUND, &self->bkpt.bg);
+  if (!style_get_is_bold (scheme, "debugger::breakpoint", &self->bkpt.bold))
+    self->bkpt.bold = FALSE;
+
+  /* Slight different color for countpoint, fallback to mix(selection,diff:add) */
+  if (!get_style_rgba (scheme, "debugger::countpoint", FOREGROUND, &self->ctpt.fg))
+    get_style_rgba (scheme, "selection", FOREGROUND, &self->ctpt.fg);
+  if (!get_style_rgba (scheme, "debugger::countpoint", BACKGROUND, &self->ctpt.bg))
+    {
+      get_style_rgba (scheme, "selection", BACKGROUND, &self->ctpt.bg);
+      self->ctpt.bg.red = (self->ctpt.bg.red + self->changes.add.red) / 2.0;
+      self->ctpt.bg.green = (self->ctpt.bg.green + self->changes.add.green) / 2.0;
+      self->ctpt.bg.blue = (self->ctpt.bg.blue + self->changes.add.blue) / 2.0;
+    }
+  if (!style_get_is_bold (scheme, "debugger::countpoint", &self->ctpt.bold))
+    self->ctpt.bold = FALSE;
+}
+
+static void
+collect_breakpoint_info (IdeDebuggerBreakpoint *breakpoint,
+                         gpointer               user_data)
+{
+  struct {
+    GArray *lines;
+    guint begin;
+    guint end;
+  } *bkpt_info = user_data;
+  guint line;
+
+  g_assert (IDE_IS_DEBUGGER_BREAKPOINT (breakpoint));
+  g_assert (bkpt_info != NULL);
+
+  /* Debugger breakpoints are 1-based line numbers */
+  if (!(line = ide_debugger_breakpoint_get_line (breakpoint)))
+    return;
+
+  line--;
+
+  if (line >= bkpt_info->begin && line <= bkpt_info->end)
+    {
+      IdeDebuggerBreakMode mode = ide_debugger_breakpoint_get_mode (breakpoint);
+      LineInfo *info = &g_array_index (bkpt_info->lines, LineInfo, line - bkpt_info->begin);
+
+      info->is_watchpoint = !!(mode & IDE_DEBUGGER_BREAK_WATCHPOINT);
+      info->is_countpoint = !!(mode & IDE_DEBUGGER_BREAK_COUNTPOINT);
+      info->is_breakpoint = !!(mode & IDE_DEBUGGER_BREAK_BREAKPOINT);
+    }
+}
+
+static void
+gbp_omni_gutter_renderer_load_breakpoints (GbpOmniGutterRenderer *self,
+                                           GtkTextIter           *begin,
+                                           GtkTextIter           *end,
+                                           GArray                *lines)
+{
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (begin != NULL);
+  g_assert (lines != NULL);
+  g_assert (lines->len > 0);
+
+  if (self->breakpoints != NULL)
+    {
+      struct {
+        GArray *lines;
+        guint begin;
+        guint end;
+      } info;
+
+      info.lines = lines;
+      info.begin = gtk_text_iter_get_line (begin);
+      info.end = gtk_text_iter_get_line (end);
+
+      ide_debugger_breakpoints_foreach (self->breakpoints,
+                                        (GFunc)collect_breakpoint_info,
+                                        &info);
+    }
+}
+
+static void
+populate_diagnostics_cb (guint                 line,
+                         IdeDiagnosticSeverity severity,
+                         gpointer              user_data)
+{
+  LineInfo *info;
+  struct {
+    GArray *lines;
+    guint   begin_line;
+    guint   end_line;
+  } *state = user_data;
+
+  g_assert (line >= state->begin_line);
+  g_assert (line <= state->end_line);
+
+  info = &g_array_index (state->lines, LineInfo, line - state->begin_line);
+  info->is_warning |= !!(severity & (IDE_DIAGNOSTIC_WARNING | IDE_DIAGNOSTIC_DEPRECATED));
+  info->is_error |= !!(severity & (IDE_DIAGNOSTIC_ERROR | IDE_DIAGNOSTIC_FATAL));
+  info->is_note |= !!(severity & IDE_DIAGNOSTIC_NOTE);
+}
+
+static void
+populate_changes_cb (guint               line,
+                     IdeBufferLineChange change,
+                     gpointer            user_data)
+{
+  LineInfo *info;
+  struct {
+    GArray *lines;
+    guint   begin_line;
+    guint   end_line;
+  } *state = user_data;
+  guint pos;
+
+  g_assert (line >= state->begin_line);
+  g_assert (line <= state->end_line);
+
+  pos = line - state->begin_line;
+
+  info = &g_array_index (state->lines, LineInfo, pos);
+  info->is_add = !!(change & IDE_BUFFER_LINE_CHANGE_ADDED);
+  info->is_change = !!(change & IDE_BUFFER_LINE_CHANGE_CHANGED);
+  info->is_delete = !!(change & IDE_BUFFER_LINE_CHANGE_DELETED);
+
+  if (pos > 0)
+    {
+      LineInfo *last = &g_array_index (state->lines, LineInfo, pos - 1);
+
+      info->is_prev_delete = last->is_delete;
+      last->is_next_delete = info->is_delete;
+    }
+}
+
+static void
+gbp_omni_gutter_renderer_load_basic (GbpOmniGutterRenderer *self,
+                                     GtkTextIter           *begin,
+                                     GArray                *lines)
+{
+  IdeBufferChangeMonitor *monitor;
+  IdeDiagnostics *diagnostics;
+  GtkTextBuffer *buffer;
+  GFile *file;
+  struct {
+    GArray *lines;
+    guint   begin_line;
+    guint   end_line;
+  } state;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (begin != NULL);
+  g_assert (lines != NULL);
+  g_assert (lines->len > 0);
+
+  buffer = gtk_text_iter_get_buffer (begin);
+  if (!IDE_IS_BUFFER (buffer))
+    return;
+
+  file = ide_buffer_get_file (IDE_BUFFER (buffer));
+
+  state.lines = lines;
+  state.begin_line = gtk_text_iter_get_line (begin);
+  state.end_line = state.begin_line + lines->len;
+
+  if ((diagnostics = ide_buffer_get_diagnostics (IDE_BUFFER (buffer))))
+    ide_diagnostics_foreach_line_in_range (diagnostics,
+                                           file,
+                                           state.begin_line,
+                                           state.end_line,
+                                           populate_diagnostics_cb,
+                                           &state);
+
+  if ((monitor = ide_buffer_get_change_monitor (IDE_BUFFER (buffer))))
+    ide_buffer_change_monitor_foreach_change (monitor,
+                                              state.begin_line,
+                                              state.end_line,
+                                              populate_changes_cb,
+                                              &state);
+}
+
+static inline gint
+count_num_digits (gint num_lines)
+{
+  if (num_lines < 100)
+    return 2;
+  else if (num_lines < 1000)
+    return 3;
+  else if (num_lines < 10000)
+    return 4;
+  else if (num_lines < 100000)
+    return 5;
+  else if (num_lines < 1000000)
+    return 6;
+  else
+    return 10;
+}
+
+static gint
+calculate_diagnostics_size (gint height)
+{
+  static guint sizes[] = { 64, 48, 32, 24, 16, 8 };
+
+  for (guint i = 0; i < G_N_ELEMENTS (sizes); i++)
+    {
+      if (height >= sizes[i])
+        return sizes[i];
+    }
+
+  return sizes [G_N_ELEMENTS (sizes) - 1];
+}
+
+static void
+gbp_omni_gutter_renderer_recalculate_size (GbpOmniGutterRenderer *self)
+{
+  g_autofree gchar *numbers = NULL;
+  GtkTextBuffer *buffer;
+  GtkTextView *view;
+  PangoLayout *layout;
+  GtkTextIter end;
+  guint line;
+  int height;
+  gint size = 0;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  /* There is nothing we can do until a view has been attached. */
+  view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (self));
+  if (!IDE_IS_SOURCE_VIEW (view))
+    return;
+
+  /*
+   * First, we need to get the size of the text for the last line of the
+   * buffer (which will be the longest). We size the font with '9' since it
+   * will generally be one of the widest of the numbers. Although, we only
+   * "support" * monospace anyway, so it shouldn't be drastic if we're off.
+   */
+
+  buffer = gtk_text_view_get_buffer (view);
+  gtk_text_buffer_get_end_iter (buffer, &end);
+  line = gtk_text_iter_get_line (&end) + 1;
+
+  self->n_chars = count_num_digits (line);
+  numbers = g_strnfill (self->n_chars, '9');
+
+  /*
+   * Stash the font description for future use.
+   */
+  g_clear_pointer (&self->scaled_font_desc, pango_font_description_free);
+  self->scaled_font_desc = ide_source_view_get_scaled_font_desc (IDE_SOURCE_VIEW (view));
+
+  /*
+   * Get the font description used by the IdeSourceView so we can
+   * match the font styling as much as possible.
+   */
+  layout = gtk_widget_create_pango_layout (GTK_WIDGET (view), numbers);
+  pango_layout_set_font_description (layout, self->scaled_font_desc);
+
+  /*
+   * Now cache the width of the text layout so we can simplify our
+   * positioning later. We simply size everything the same and then
+   * align to the right to reduce the draw overhead.
+   */
+  pango_layout_get_pixel_size (layout, &self->number_width, &height);
+
+  /*
+   * Calculate the nearest size for diagnostics so they scale somewhat
+   * reasonable with the character size.
+   */
+  self->diag_size = calculate_diagnostics_size (MAX (16, height));
+  g_assert (self->diag_size > 0);
+
+  /* Now calculate the size based on enabled features */
+  size = 2;
+  if (self->show_line_diagnostics)
+    size += self->diag_size + 2;
+  if (self->show_line_numbers)
+    size += self->number_width + 2;
+
+  /* The arrow overlaps the changes if we can have breakpoints,
+   * otherwise we just need the space for the line changes.
+   */
+  if (self->breakpoints != NULL)
+    size += ARROW_WIDTH + 2;
+  else if (self->show_line_changes)
+    size += CHANGE_WIDTH + 2;
+
+  /* Update the size and ensure we are re-drawn */
+  gtk_source_gutter_renderer_set_size (GTK_SOURCE_GUTTER_RENDERER (self), size);
+  gtk_source_gutter_renderer_queue_draw (GTK_SOURCE_GUTTER_RENDERER (self));
+
+  g_clear_object (&layout);
+}
+
+static void
+gbp_omni_gutter_renderer_notify_font_desc (GbpOmniGutterRenderer *self,
+                                           GParamSpec            *pspec,
+                                           IdeSourceView         *view)
+{
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  gbp_omni_gutter_renderer_recalculate_size (self);
+  gbp_omni_gutter_renderer_reload_icons (self);
+}
+
+static void
+gbp_omni_gutter_renderer_end (GtkSourceGutterRenderer *renderer)
+{
+  GbpOmniGutterRenderer *self = (GbpOmniGutterRenderer *)renderer;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  g_clear_object (&self->layout);
+}
+
+static void
+gbp_omni_gutter_renderer_begin (GtkSourceGutterRenderer *renderer,
+                                cairo_t                 *cr,
+                                GdkRectangle            *bg_area,
+                                GdkRectangle            *cell_area,
+                                GtkTextIter             *begin,
+                                GtkTextIter             *end)
+{
+  GbpOmniGutterRenderer *self = (GbpOmniGutterRenderer *)renderer;
+  GtkTextTagTable *table;
+  GtkTextBuffer *buffer;
+  IdeSourceView *view;
+  GtkTextTag *tag;
+  GtkTextIter bkpt;
+  guint end_line;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (renderer));
+  g_assert (cr != NULL);
+  g_assert (bg_area != NULL);
+  g_assert (cell_area != NULL);
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+  /*
+   * This is the start of our draw process. The first thing we want to
+   * do is collect as much information as we'll need when doing the
+   * actual draw. That helps us coalesce similar work together, which is
+   * good for the CPU usage. We are *very* sensitive to CPU usage here
+   * as the GtkTextView does not pixel cache the gutter.
+   */
+
+  self->stopped_line = -1;
+
+  /* Locate the current stopped breakpoint if any. */
+  buffer = gtk_text_iter_get_buffer (begin);
+  table = gtk_text_buffer_get_tag_table (buffer);
+  tag = gtk_text_tag_table_lookup (table, "debugger::current-breakpoint");
+  if (tag != NULL)
+    {
+      bkpt = *begin;
+      gtk_text_iter_backward_char (&bkpt);
+      if (gtk_text_iter_forward_to_tag_toggle (&bkpt, tag) &&
+          gtk_text_iter_starts_tag (&bkpt, tag))
+        self->stopped_line = gtk_text_iter_get_line (&bkpt);
+    }
+
+  /*
+   * This function is called before we render any of the lines in
+   * the gutter. To reduce our overhead, we want to collect information
+   * for all of the line numbers upfront.
+   */
+
+  view = IDE_SOURCE_VIEW (gtk_source_gutter_renderer_get_view (renderer));
+
+  self->begin_line = gtk_text_iter_get_line (begin);
+  end_line = gtk_text_iter_get_line (end);
+
+  /* Give ourselves a fresh array to stash our line info */
+  g_array_set_size (self->lines, end_line - self->begin_line + 1);
+  memset (self->lines->data, 0, self->lines->len * sizeof (LineInfo));
+
+  /* Now load breakpoints, diagnostics, and line changes */
+  gbp_omni_gutter_renderer_load_basic (self, begin, self->lines);
+  gbp_omni_gutter_renderer_load_breakpoints (self, begin, end, self->lines);
+
+  /* Create a new layout for rendering lines to */
+  self->layout = gtk_widget_create_pango_layout (GTK_WIDGET (view), "");
+  pango_layout_set_alignment (self->layout, PANGO_ALIGN_RIGHT);
+  pango_layout_set_font_description (self->layout, self->scaled_font_desc);
+  pango_layout_set_width (self->layout, (cell_area->width - ARROW_WIDTH - 4) * PANGO_SCALE);
+}
+
+static gboolean
+gbp_omni_gutter_renderer_query_activatable (GtkSourceGutterRenderer *renderer,
+                                            GtkTextIter             *begin,
+                                            GdkRectangle            *area,
+                                            GdkEvent                *event)
+{
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (renderer));
+  g_assert (begin != NULL);
+  g_assert (area != NULL);
+  g_assert (event != NULL);
+
+  /* Clicking will move the cursor, so always TRUE */
+
+  return TRUE;
+}
+
+static void
+animate_at_iter (GbpOmniGutterRenderer *self,
+                 GdkRectangle          *area,
+                 GtkTextIter           *iter)
+{
+  DzlBoxTheatric *theatric;
+  GtkTextView *view;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (area != NULL);
+  g_assert (iter != NULL);
+
+  /* Show a little bullet animation shooting right */
+
+  view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (self));
+
+  theatric = g_object_new (DZL_TYPE_BOX_THEATRIC,
+                           "alpha", 0.3,
+                           "background", "#729fcf",
+                           "height", area->height,
+                           "target", view,
+                           "width", area->width,
+                           "x", area->x,
+                           "y", area->y,
+                           NULL);
+
+  dzl_object_animate_full (theatric,
+                           DZL_ANIMATION_EASE_IN_CUBIC,
+                           100,
+                           gtk_widget_get_frame_clock (GTK_WIDGET (view)),
+                           g_object_unref,
+                           theatric,
+                           "x", area->x + 250,
+                           "alpha", 0.0,
+                           NULL);
+}
+
+static void
+gbp_omni_gutter_renderer_activate (GtkSourceGutterRenderer *renderer,
+                                   GtkTextIter             *iter,
+                                   GdkRectangle            *area,
+                                   GdkEvent                *event)
+{
+  GbpOmniGutterRenderer *self = (GbpOmniGutterRenderer *)renderer;
+  IdeDebuggerBreakpoint *breakpoint;
+  IdeDebuggerBreakMode break_type = IDE_DEBUGGER_BREAK_NONE;
+  g_autofree gchar *path = NULL;
+  g_autoptr(IdeContext) context = NULL;
+  IdeDebugManager *debug_manager;
+  GtkTextBuffer *buffer;
+  GtkTextIter begin;
+  GtkTextIter end;
+  GFile *file;
+  guint line;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (iter != NULL);
+  g_assert (area != NULL);
+  g_assert (event != NULL);
+
+  /* TODO: We could check for event->button.button to see if we
+   *       can display a popover with information such as
+   *       diagnostics, or breakpoints, or git blame.
+   */
+
+  buffer = gtk_text_iter_get_buffer (iter);
+
+  /* Select this row if it isn't currently selected */
+  if (!gtk_text_buffer_get_selection_bounds (buffer, &begin, &end) &&
+      gtk_text_iter_get_line (&begin) != gtk_text_iter_get_line (iter))
+    gtk_text_buffer_select_range (buffer, iter, iter);
+
+  /* Nothing more we can do if this file doesn't support breakpoints */
+  if (self->breakpoints == NULL)
+    return;
+
+  context = ide_buffer_ref_context (IDE_BUFFER (buffer));
+  debug_manager = ide_debug_manager_from_context (context);
+
+  line = gtk_text_iter_get_line (iter) + 1;
+  file = ide_debugger_breakpoints_get_file (self->breakpoints);
+  path = g_file_get_path (file);
+
+  /* TODO: Should we show a Popover here to select the type? */
+  IDE_TRACE_MSG ("Toggle breakpoint on line %u [breakpoints=%p]",
+                 line, self->breakpoints);
+
+  breakpoint = ide_debugger_breakpoints_get_line (self->breakpoints, line);
+  if (breakpoint != NULL)
+    break_type = ide_debugger_breakpoint_get_mode (breakpoint);
+
+  switch (break_type)
+    {
+    case IDE_DEBUGGER_BREAK_NONE:
+      {
+        g_autoptr(IdeDebuggerBreakpoint) to_insert = NULL;
+
+        to_insert = ide_debugger_breakpoint_new (NULL);
+
+        ide_debugger_breakpoint_set_line (to_insert, line);
+        ide_debugger_breakpoint_set_file (to_insert, path);
+        ide_debugger_breakpoint_set_mode (to_insert, IDE_DEBUGGER_BREAK_BREAKPOINT);
+        ide_debugger_breakpoint_set_enabled (to_insert, TRUE);
+
+        _ide_debug_manager_add_breakpoint (debug_manager, to_insert);
+      }
+      break;
+
+    case IDE_DEBUGGER_BREAK_BREAKPOINT:
+    case IDE_DEBUGGER_BREAK_COUNTPOINT:
+    case IDE_DEBUGGER_BREAK_WATCHPOINT:
+      if (breakpoint != NULL)
+        {
+          _ide_debug_manager_remove_breakpoint (debug_manager, breakpoint);
+          animate_at_iter (self, area, iter);
+        }
+      break;
+
+    default:
+      g_return_if_reached ();
+    }
+
+  /*
+   * We will wait for changes to be applied to the #IdeDebuggerBreakpoints
+   * by the #IdeDebugManager. That will cause the gutter to be invalidated
+   * and redrawn.
+   */
+
+  IDE_EXIT;
+}
+
+static void
+draw_breakpoint_bg (GbpOmniGutterRenderer        *self,
+                    cairo_t                      *cr,
+                    GdkRectangle                 *bg_area,
+                    LineInfo                     *info,
+                    GtkSourceGutterRendererState  state)
+{
+  GdkRectangle area;
+  GdkRGBA rgba;
+
+  g_assert (GTK_SOURCE_IS_GUTTER_RENDERER (self));
+  g_assert (cr != NULL);
+  g_assert (bg_area != NULL);
+
+  /*
+   * This draws a little arrow starting from the left and pointing
+   * over the line changes portion of the gutter.
+   */
+
+  area.x = bg_area->x;
+  area.y = bg_area->y;
+  area.height = bg_area->height;
+  area.width = bg_area->width;
+
+  cairo_move_to (cr, area.x, area.y);
+  cairo_line_to (cr,
+                 dzl_cairo_rectangle_x2 (&area) - ARROW_WIDTH,
+                 area.y);
+  cairo_line_to (cr,
+                 dzl_cairo_rectangle_x2 (&area),
+                 dzl_cairo_rectangle_middle (&area));
+  cairo_line_to (cr,
+                 dzl_cairo_rectangle_x2 (&area) - ARROW_WIDTH,
+                 dzl_cairo_rectangle_y2 (&area));
+  cairo_line_to (cr, area.x, dzl_cairo_rectangle_y2 (&area));
+  cairo_close_path (cr);
+
+  if (info->is_countpoint)
+    rgba = self->ctpt.bg;
+  else
+    rgba = self->bkpt.bg;
+
+  /*
+   * Tweak the brightness based on if we are in pre-light and
+   * if we are also still an active breakpoint.
+   */
+
+  if ((state & GTK_SOURCE_GUTTER_RENDERER_STATE_PRELIT) != 0)
+    {
+      if (IS_BREAKPOINT (info))
+        rgba.alpha *= 0.8;
+      else
+        rgba.alpha *= 0.4;
+    }
+
+  /* And draw... */
+
+  gdk_cairo_set_source_rgba (cr, &rgba);
+  cairo_fill (cr);
+}
+
+static void
+draw_line_change (GbpOmniGutterRenderer        *self,
+                  cairo_t                      *cr,
+                  GdkRectangle                 *area,
+                  LineInfo                     *info,
+                  GtkSourceGutterRendererState  state)
+{
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (cr != NULL);
+  g_assert (area != NULL);
+
+  /*
+   * Draw a simple line with the appropriate color from the style scheme
+   * based on the type of change we have.
+   */
+
+  if (info->is_add || info->is_change)
+    {
+      cairo_rectangle (cr,
+                       area->x + area->width - 2 - CHANGE_WIDTH,
+                       area->y,
+                       CHANGE_WIDTH,
+                       area->y + area->height);
+
+      if (info->is_add)
+        gdk_cairo_set_source_rgba (cr, &self->changes.add);
+      else
+        gdk_cairo_set_source_rgba (cr, &self->changes.change);
+
+      cairo_fill (cr);
+    }
+
+  if (info->is_next_delete && !info->is_delete)
+    {
+      cairo_move_to (cr,
+                     area->x + area->width,
+                     area->y + area->height);
+      cairo_line_to (cr,
+                     area->x + area->width - DELETE_WIDTH,
+                     area->y + area->height);
+      cairo_line_to (cr,
+                     area->x + area->width - DELETE_WIDTH,
+                     area->y + area->height - (DELETE_HEIGHT / 2));
+      cairo_line_to (cr,
+                     area->x + area->width,
+                     area->y + area->height);
+      gdk_cairo_set_source_rgba (cr, &self->changes.remove);
+      cairo_fill (cr);
+    }
+
+  if (info->is_delete && !info->is_prev_delete)
+    {
+      cairo_move_to (cr,
+                     area->x + area->width,
+                     area->y);
+      cairo_line_to (cr,
+                     area->x + area->width - DELETE_WIDTH,
+                     area->y);
+      cairo_line_to (cr,
+                     area->x + area->width - DELETE_WIDTH,
+                     area->y + (DELETE_HEIGHT / 2));
+      cairo_line_to (cr,
+                     area->x + area->width,
+                     area->y);
+      gdk_cairo_set_source_rgba (cr, &self->changes.remove);
+      cairo_fill (cr);
+    }
+}
+
+static void
+draw_diagnostic (GbpOmniGutterRenderer        *self,
+                 cairo_t                      *cr,
+                 GdkRectangle                 *area,
+                 LineInfo                     *info,
+                 gint                          diag_size,
+                 GtkSourceGutterRendererState  state)
+{
+  cairo_surface_t *surface = NULL;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (cr != NULL);
+  g_assert (area != NULL);
+  g_assert (diag_size > 0);
+
+  if (IS_BREAKPOINT (info) || (state & GTK_SOURCE_GUTTER_RENDERER_STATE_PRELIT))
+    {
+      if (info->is_error)
+        surface = self->error_selected_surface;
+      else if (info->is_warning)
+        surface = self->warning_selected_surface;
+      else if (info->is_note)
+        surface = self->note_selected_surface;
+    }
+  else
+    {
+      if (info->is_error)
+        surface = self->error_surface;
+      else if (info->is_warning)
+        surface = self->warning_surface;
+      else if (info->is_note)
+        surface = self->note_surface;
+    }
+
+  if (surface != NULL)
+    {
+      cairo_rectangle (cr,
+                       area->x + 2,
+                       area->y + ((area->height - diag_size) / 2),
+                       diag_size, diag_size);
+      cairo_set_source_surface (cr,
+                                surface,
+                                area->x + 2,
+                                area->y + ((area->height - diag_size) / 2));
+      cairo_paint (cr);
+    }
+}
+
+static void
+gbp_omni_gutter_renderer_draw (GtkSourceGutterRenderer      *renderer,
+                               cairo_t                      *cr,
+                               GdkRectangle                 *bg_area,
+                               GdkRectangle                 *cell_area,
+                               GtkTextIter                  *begin,
+                               GtkTextIter                  *end,
+                               GtkSourceGutterRendererState  state)
+{
+  GbpOmniGutterRenderer *self = (GbpOmniGutterRenderer *)renderer;
+  GtkTextView *view;
+  gboolean has_focus;
+  gboolean highlight_line;
+  guint line;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (cr != NULL);
+  g_assert (bg_area != NULL);
+  g_assert (cell_area != NULL);
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+  /*
+   * This is our primary draw routine. It is called for every line that
+   * is visible. We are incredibly sensitive to performance churn here
+   * so it is important that we be as minimal as possible while
+   * retaining the features we need.
+   */
+
+  view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (self));
+  highlight_line = gtk_source_view_get_highlight_current_line (GTK_SOURCE_VIEW (view));
+  has_focus = gtk_widget_has_focus (GTK_WIDGET (view));
+
+  line = gtk_text_iter_get_line (begin);
+
+  if ((line - self->begin_line) < self->lines->len)
+    {
+      LineInfo *info = &g_array_index (self->lines, LineInfo, line - self->begin_line);
+      gboolean active = state & GTK_SOURCE_GUTTER_RENDERER_STATE_PRELIT;
+      gboolean has_breakpoint = FALSE;
+      gboolean bold = FALSE;
+
+      /*
+       * Draw some background for the line so that it looks like the
+       * breakpoint arrow draws over it. Debugger break line takes
+       * precidence over the current highlight line. Also, ensure that
+       * the view is drawing the highlight line first.
+       */
+      if (line == self->stopped_line)
+        {
+          gdk_cairo_rectangle (cr, bg_area);
+          gdk_cairo_set_source_rgba (cr, &self->stopped_bg);
+          cairo_fill (cr);
+        }
+      else if (highlight_line && has_focus && (state & GTK_SOURCE_GUTTER_RENDERER_STATE_CURSOR))
+        {
+          gdk_cairo_rectangle (cr, bg_area);
+          gdk_cairo_set_source_rgba (cr, &self->current.bg);
+          cairo_fill (cr);
+        }
+
+      /* Draw line changes next so it will show up underneath the
+       * breakpoint arrows.
+       */
+      if (self->show_line_changes && IS_LINE_CHANGE (info))
+        draw_line_change (self, cr, cell_area, info, state);
+
+      /* Draw breakpoint arrows if we have any breakpoints that could
+       * potentially match.
+       */
+      if (self->breakpoints != NULL)
+        {
+          has_breakpoint = IS_BREAKPOINT (info);
+          if (has_breakpoint || active)
+            draw_breakpoint_bg (self, cr, cell_area, info, state);
+        }
+
+      /* Now that we might have an altered background for the line,
+       * we can draw the diagnostic icon (with possibly altered
+       * color for symbolic icon).
+       */
+      if (self->show_line_diagnostics && IS_DIAGNOSTIC (info))
+        draw_diagnostic (self, cr, cell_area, info, self->diag_size, state);
+
+      /*
+       * Now draw the line numbers if we are showing them. Ensure
+       * we tweak the style to match closely to how the default
+       * gtksourceview lines gutter renderer does it.
+       */
+      if (self->show_line_numbers)
+        {
+          const gchar *linestr = NULL;
+          gchar buf[12];
+          gint len;
+
+          len = _fast_str (line + 1, &linestr, buf);
+          pango_layout_set_text (self->layout, linestr, len);
+
+          cairo_move_to (cr, cell_area->x, cell_area->y);
+
+          if (has_breakpoint || (self->breakpoints != NULL && active))
+            {
+              gdk_cairo_set_source_rgba (cr, &self->bkpt.fg);
+              bold = self->bkpt.bold;
+            }
+          else if (state & GTK_SOURCE_GUTTER_RENDERER_STATE_CURSOR)
+            {
+              gdk_cairo_set_source_rgba (cr, &self->current.fg);
+              bold = self->current.bold;
+            }
+          else
+            {
+              gdk_cairo_set_source_rgba (cr, &self->text.fg);
+              bold = self->text.bold;
+            }
+
+          /* Current line is always bold */
+          if (state & GTK_SOURCE_GUTTER_RENDERER_STATE_CURSOR)
+            bold |= self->current.bold;
+
+          pango_layout_set_attributes (self->layout, bold ? self->bold_attrs : NULL);
+          pango_cairo_show_layout (cr, self->layout);
+        }
+    }
+}
+
+static cairo_surface_t *
+get_icon_surface (GbpOmniGutterRenderer *self,
+                  GtkWidget             *widget,
+                  const gchar           *icon_name,
+                  gint                   size,
+                  gboolean               selected)
+{
+  g_autoptr(GtkIconInfo) info = NULL;
+  GtkIconTheme *icon_theme;
+  GdkScreen *screen;
+  GtkIconLookupFlags flags;
+  gint scale;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (GTK_IS_WIDGET (widget));
+  g_assert (icon_name != NULL);
+  g_assert (size > 0);
+
+  /*
+   * This deals with loading a given icon by icon name and trying to
+   * apply our current style as the symbolic colors. We do not support
+   * error/warning/etc for symbolic icons so they are all replaced with
+   * the proper foreground color.
+   *
+   * If selected is set, we alter the color to make sure it will look
+   * good on top of a breakpoint arrow.
+   */
+
+  screen = gtk_widget_get_screen (widget);
+  icon_theme = gtk_icon_theme_get_for_screen (screen);
+
+  flags = GTK_ICON_LOOKUP_USE_BUILTIN;
+  scale = gtk_widget_get_scale_factor (widget);
+
+  info = gtk_icon_theme_lookup_icon_for_scale (icon_theme, icon_name, size, scale, flags);
+
+  if (info != NULL)
+    {
+      g_autoptr(GdkPixbuf) pixbuf = NULL;
+
+      if (gtk_icon_info_is_symbolic (info))
+        {
+          GdkRGBA fg;
+
+          if (selected)
+            fg = self->bkpt.fg;
+          else
+            fg = self->text.fg;
+
+          pixbuf = gtk_icon_info_load_symbolic (info, &fg, &fg, &fg, &fg, NULL, NULL);
+        }
+      else
+        pixbuf = gtk_icon_info_load_icon (info, NULL);
+
+      if (pixbuf != NULL)
+        return gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, NULL);
+    }
+
+  return NULL;
+}
+
+static void
+gbp_omni_gutter_renderer_reload_icons (GbpOmniGutterRenderer *self)
+{
+  GtkTextView *view;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  /*
+   * This isn't ideal (we should find a better way to cache icons that
+   * is safe with scale and foreground color changes we need).
+   *
+   * TODO: Create something similar to pixbuf helpers that allow for
+   *       more control over the cache key so it can be shared between
+   *       multiple instances.
+   */
+
+  g_clear_pointer (&self->note_surface, cairo_surface_destroy);
+  g_clear_pointer (&self->warning_surface, cairo_surface_destroy);
+  g_clear_pointer (&self->error_surface, cairo_surface_destroy);
+  g_clear_pointer (&self->note_selected_surface, cairo_surface_destroy);
+  g_clear_pointer (&self->warning_selected_surface, cairo_surface_destroy);
+  g_clear_pointer (&self->error_selected_surface, cairo_surface_destroy);
+
+  view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (self));
+  if (view == NULL)
+    return;
+
+  self->note_surface = get_icon_surface (self, GTK_WIDGET (view), "dialog-information-symbolic", 
self->diag_size, FALSE);
+  self->warning_surface = get_icon_surface (self, GTK_WIDGET (view), "dialog-warning-symbolic", 
self->diag_size, FALSE);
+  self->error_surface = get_icon_surface (self, GTK_WIDGET (view), "process-stop-symbolic", self->diag_size, 
FALSE);
+
+  self->note_selected_surface = get_icon_surface (self, GTK_WIDGET (view), "dialog-information-symbolic", 
self->diag_size, TRUE);
+  self->warning_selected_surface = get_icon_surface (self, GTK_WIDGET (view), "dialog-warning-symbolic", 
self->diag_size, TRUE);
+  self->error_selected_surface = get_icon_surface (self, GTK_WIDGET (view), "process-stop-symbolic", 
self->diag_size, TRUE);
+}
+
+static void
+gbp_omni_gutter_renderer_reload (GbpOmniGutterRenderer *self)
+{
+  g_autoptr(IdeDebuggerBreakpoints) breakpoints = NULL;
+  GtkTextBuffer *buffer;
+  GtkTextView *view;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (self));
+  buffer = gtk_text_view_get_buffer (view);
+
+  if (IDE_IS_BUFFER (buffer))
+    {
+      g_autoptr(IdeContext) context = NULL;
+      IdeDebugManager *debug_manager;
+      const gchar *lang_id;
+
+      context = ide_buffer_ref_context (IDE_BUFFER (buffer));
+      debug_manager = ide_debug_manager_from_context (context);
+      lang_id = ide_buffer_get_language_id (IDE_BUFFER (buffer));
+
+      if (ide_debug_manager_supports_language (debug_manager, lang_id))
+        {
+          GFile *file = ide_buffer_get_file (IDE_BUFFER (buffer));
+
+          breakpoints = ide_debug_manager_get_breakpoints_for_file (debug_manager, file);
+        }
+    }
+
+  /* Replace our previous breakpoints */
+  g_set_object (&self->breakpoints, breakpoints);
+
+  /* Reload icons and then recalcuate our physical size */
+  gbp_omni_gutter_renderer_recalculate_size (self);
+  gbp_omni_gutter_renderer_reload_icons (self);
+}
+
+static void
+gbp_omni_gutter_renderer_notify_buffer (GbpOmniGutterRenderer *self,
+                                        GParamSpec            *pspec,
+                                        IdeSourceView         *view)
+{
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+
+  if (self->buffer_signals != NULL)
+    {
+      GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+
+      if (!IDE_IS_BUFFER (buffer))
+        buffer = NULL;
+
+      dzl_signal_group_set_target (self->buffer_signals, buffer);
+      gbp_omni_gutter_renderer_reload (self);
+    }
+}
+
+static void
+gbp_omni_gutter_renderer_bind_view (GbpOmniGutterRenderer *self,
+                                    IdeSourceView         *view,
+                                    DzlSignalGroup        *view_signals)
+{
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (IDE_IS_SOURCE_VIEW (view));
+  g_assert (DZL_IS_SIGNAL_GROUP (view_signals));
+
+  gbp_omni_gutter_renderer_notify_buffer (self, NULL, view);
+}
+
+static void
+gbp_omni_gutter_renderer_notify_view (GbpOmniGutterRenderer *self)
+{
+  GtkTextView *view;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (self));
+  if (!IDE_IS_SOURCE_VIEW (view))
+    view = NULL;
+
+  dzl_signal_group_set_target (self->view_signals, view);
+}
+
+static gboolean
+gbp_omni_gutter_renderer_do_recalc (gpointer data)
+{
+  GbpOmniGutterRenderer *self = data;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  self->resize_source = 0;
+
+  gbp_omni_gutter_renderer_recalculate_size (self);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_omni_gutter_renderer_buffer_changed (GbpOmniGutterRenderer *self,
+                                         IdeBuffer             *buffer)
+{
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /* Run immediately at the end of this main loop iteration */
+  if (self->resize_source == 0)
+    self->resize_source = gdk_threads_add_idle_full (G_PRIORITY_HIGH,
+                                                     gbp_omni_gutter_renderer_do_recalc,
+                                                     g_object_ref (self),
+                                                     g_object_unref);
+}
+
+static void
+gbp_omni_gutter_renderer_notify_style_scheme (GbpOmniGutterRenderer *self,
+                                              GParamSpec            *pspec,
+                                              IdeBuffer             *buffer)
+{
+  GtkSourceStyleScheme *scheme;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /* Update our cached rgba colors */
+  scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
+  reload_style_colors (self, scheme);
+
+  /* Regenerate icons matching the scheme colors */
+  gbp_omni_gutter_renderer_reload_icons (self);
+}
+
+static void
+gbp_omni_gutter_renderer_bind_buffer (GbpOmniGutterRenderer *self,
+                                      IdeBuffer             *buffer,
+                                      DzlSignalGroup        *buffer_signals)
+{
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (DZL_IS_SIGNAL_GROUP (buffer_signals));
+
+  gbp_omni_gutter_renderer_notify_style_scheme (self, NULL, buffer);
+}
+
+static void
+gbp_omni_gutter_renderer_constructed (GObject *object)
+{
+  GbpOmniGutterRenderer *self = (GbpOmniGutterRenderer *)object;
+  GtkTextView *view;
+
+  g_assert (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  G_OBJECT_CLASS (gbp_omni_gutter_renderer_parent_class)->constructed (object);
+
+  view = gtk_source_gutter_renderer_get_view (GTK_SOURCE_GUTTER_RENDERER (self));
+  dzl_signal_group_set_target (self->view_signals, view);
+}
+
+static void
+gbp_omni_gutter_renderer_dispose (GObject *object)
+{
+  GbpOmniGutterRenderer *self = (GbpOmniGutterRenderer *)object;
+
+  dzl_clear_source (&self->resize_source);
+
+  g_clear_object (&self->breakpoints);
+  g_clear_pointer (&self->lines, g_array_unref);
+
+  g_clear_pointer (&self->scaled_font_desc, pango_font_description_free);
+
+  g_clear_object (&self->view_signals);
+  g_clear_object (&self->buffer_signals);
+
+  g_clear_pointer (&self->note_surface, cairo_surface_destroy);
+  g_clear_pointer (&self->warning_surface, cairo_surface_destroy);
+  g_clear_pointer (&self->error_surface, cairo_surface_destroy);
+  g_clear_pointer (&self->note_selected_surface, cairo_surface_destroy);
+  g_clear_pointer (&self->warning_selected_surface, cairo_surface_destroy);
+  g_clear_pointer (&self->error_selected_surface, cairo_surface_destroy);
+
+  g_clear_object (&self->layout);
+  g_clear_pointer (&self->bold_attrs, pango_attr_list_unref);
+
+  G_OBJECT_CLASS (gbp_omni_gutter_renderer_parent_class)->dispose (object);
+}
+
+static void
+gbp_omni_gutter_renderer_get_property (GObject    *object,
+                                       guint       prop_id,
+                                       GValue     *value,
+                                       GParamSpec *pspec)
+{
+  GbpOmniGutterRenderer *self = GBP_OMNI_GUTTER_RENDERER (object);
+
+  switch (prop_id)
+    {
+    case PROP_SHOW_LINE_CHANGES:
+      g_value_set_boolean (value, self->show_line_changes);
+      break;
+
+    case PROP_SHOW_LINE_DIAGNOSTICS:
+      g_value_set_boolean (value, self->show_line_diagnostics);
+      break;
+
+    case PROP_SHOW_LINE_NUMBERS:
+      g_value_set_boolean (value, self->show_line_numbers);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_omni_gutter_renderer_set_property (GObject      *object,
+                                       guint         prop_id,
+                                       const GValue *value,
+                                       GParamSpec   *pspec)
+{
+  GbpOmniGutterRenderer *self = GBP_OMNI_GUTTER_RENDERER (object);
+
+  switch (prop_id)
+    {
+    case PROP_SHOW_LINE_CHANGES:
+      gbp_omni_gutter_renderer_set_show_line_changes (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_SHOW_LINE_DIAGNOSTICS:
+      gbp_omni_gutter_renderer_set_show_line_diagnostics (self, g_value_get_boolean (value));
+      break;
+
+    case PROP_SHOW_LINE_NUMBERS:
+      gbp_omni_gutter_renderer_set_show_line_numbers (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_omni_gutter_renderer_class_init (GbpOmniGutterRendererClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkSourceGutterRendererClass *renderer_class = GTK_SOURCE_GUTTER_RENDERER_CLASS (klass);
+
+  object_class->constructed = gbp_omni_gutter_renderer_constructed;
+  object_class->dispose = gbp_omni_gutter_renderer_dispose;
+  object_class->get_property = gbp_omni_gutter_renderer_get_property;
+  object_class->set_property = gbp_omni_gutter_renderer_set_property;
+
+  renderer_class->draw = gbp_omni_gutter_renderer_draw;
+  renderer_class->begin = gbp_omni_gutter_renderer_begin;
+  renderer_class->end = gbp_omni_gutter_renderer_end;
+  renderer_class->query_activatable = gbp_omni_gutter_renderer_query_activatable;
+  renderer_class->activate = gbp_omni_gutter_renderer_activate;
+
+  properties [PROP_SHOW_LINE_CHANGES] =
+    g_param_spec_boolean ("show-line-changes", NULL, NULL, TRUE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_SHOW_LINE_NUMBERS] =
+    g_param_spec_boolean ("show-line-numbers", NULL, NULL, TRUE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  properties [PROP_SHOW_LINE_DIAGNOSTICS] =
+    g_param_spec_boolean ("show-line-diagnostics", NULL, NULL, TRUE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_omni_gutter_renderer_init (GbpOmniGutterRenderer *self)
+{
+  self->diag_size = 16;
+  self->show_line_changes = TRUE;
+  self->show_line_diagnostics = TRUE;
+  self->show_line_diagnostics = TRUE;
+
+  self->lines = g_array_new (FALSE, FALSE, sizeof (LineInfo));
+
+  g_signal_connect (self,
+                    "notify::view",
+                    G_CALLBACK (gbp_omni_gutter_renderer_notify_view),
+                    NULL);
+
+  self->buffer_signals = dzl_signal_group_new (IDE_TYPE_BUFFER);
+
+  g_signal_connect_swapped (self->buffer_signals,
+                            "bind",
+                            G_CALLBACK (gbp_omni_gutter_renderer_bind_buffer),
+                            self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::file",
+                                    G_CALLBACK (gbp_omni_gutter_renderer_reload),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::language",
+                                    G_CALLBACK (gbp_omni_gutter_renderer_reload),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::style-scheme",
+                                    G_CALLBACK (gbp_omni_gutter_renderer_notify_style_scheme),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "changed",
+                                    G_CALLBACK (gbp_omni_gutter_renderer_buffer_changed),
+                                    self);
+
+  self->view_signals = dzl_signal_group_new (IDE_TYPE_SOURCE_VIEW);
+
+  g_signal_connect_swapped (self->view_signals,
+                            "bind",
+                            G_CALLBACK (gbp_omni_gutter_renderer_bind_view),
+                            self);
+
+  dzl_signal_group_connect_swapped (self->view_signals,
+                                    "notify::buffer",
+                                    G_CALLBACK (gbp_omni_gutter_renderer_notify_buffer),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->view_signals,
+                                    "notify::font-desc",
+                                    G_CALLBACK (gbp_omni_gutter_renderer_notify_font_desc),
+                                    self);
+
+  self->bold_attrs = pango_attr_list_new ();
+  pango_attr_list_insert (self->bold_attrs, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
+}
+
+GbpOmniGutterRenderer *
+gbp_omni_gutter_renderer_new (void)
+{
+  return g_object_new (GBP_TYPE_OMNI_GUTTER_RENDERER, NULL);
+}
+
+gboolean
+gbp_omni_gutter_renderer_get_show_line_changes (GbpOmniGutterRenderer *self)
+{
+  g_return_val_if_fail (GBP_IS_OMNI_GUTTER_RENDERER (self), FALSE);
+
+  return self->show_line_changes;
+}
+
+gboolean
+gbp_omni_gutter_renderer_get_show_line_diagnostics (GbpOmniGutterRenderer *self)
+{
+  g_return_val_if_fail (GBP_IS_OMNI_GUTTER_RENDERER (self), FALSE);
+
+  return self->show_line_diagnostics;
+}
+
+gboolean
+gbp_omni_gutter_renderer_get_show_line_numbers (GbpOmniGutterRenderer *self)
+{
+  g_return_val_if_fail (GBP_IS_OMNI_GUTTER_RENDERER (self), FALSE);
+
+  return self->show_line_numbers;
+}
+
+void
+gbp_omni_gutter_renderer_set_show_line_changes (GbpOmniGutterRenderer *self,
+                                                gboolean               show_line_changes)
+{
+  g_return_if_fail (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  show_line_changes = !!show_line_changes;
+
+  if (show_line_changes != self->show_line_changes)
+    {
+      self->show_line_changes = show_line_changes;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_LINE_CHANGES]);
+      gbp_omni_gutter_renderer_recalculate_size (self);
+    }
+}
+
+void
+gbp_omni_gutter_renderer_set_show_line_diagnostics (GbpOmniGutterRenderer *self,
+                                                    gboolean               show_line_diagnostics)
+{
+  g_return_if_fail (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  show_line_diagnostics = !!show_line_diagnostics;
+
+  if (show_line_diagnostics != self->show_line_diagnostics)
+    {
+      self->show_line_diagnostics = show_line_diagnostics;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_LINE_DIAGNOSTICS]);
+      gbp_omni_gutter_renderer_recalculate_size (self);
+    }
+}
+
+void
+gbp_omni_gutter_renderer_set_show_line_numbers (GbpOmniGutterRenderer *self,
+                                                gboolean               show_line_numbers)
+{
+  g_return_if_fail (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  show_line_numbers = !!show_line_numbers;
+
+  if (show_line_numbers != self->show_line_numbers)
+    {
+      self->show_line_numbers = show_line_numbers;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_SHOW_LINE_NUMBERS]);
+      gbp_omni_gutter_renderer_recalculate_size (self);
+    }
+}
+
+static void
+gbp_omni_gutter_renderer_style_changed (IdeGutter *gutter)
+{
+  GbpOmniGutterRenderer *self = (GbpOmniGutterRenderer *)gutter;
+
+  g_return_if_fail (GBP_IS_OMNI_GUTTER_RENDERER (self));
+
+  gbp_omni_gutter_renderer_recalculate_size (self);
+  gbp_omni_gutter_renderer_reload_icons (self);
+}
+
+static void
+gutter_iface_init (IdeGutterInterface *iface)
+{
+  iface->style_changed = gbp_omni_gutter_renderer_style_changed;
+}
diff --git a/src/plugins/omni-gutter/gbp-omni-gutter-renderer.h 
b/src/plugins/omni-gutter/gbp-omni-gutter-renderer.h
new file mode 100644
index 000000000..b9f50231a
--- /dev/null
+++ b/src/plugins/omni-gutter/gbp-omni-gutter-renderer.h
@@ -0,0 +1,42 @@
+/* gbp-omni-gutter-renderer.h
+ *
+ * Copyright 2017-2019 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 <gtksourceview/gtksource.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_OMNI_GUTTER_RENDERER (gbp_omni_gutter_renderer_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpOmniGutterRenderer, gbp_omni_gutter_renderer, GBP, OMNI_GUTTER_RENDERER, 
GtkSourceGutterRenderer)
+
+GbpOmniGutterRenderer *gbp_omni_gutter_renderer_new                       (void);
+gboolean               gbp_omni_gutter_renderer_get_show_line_changes     (GbpOmniGutterRenderer *self);
+gboolean               gbp_omni_gutter_renderer_get_show_line_diagnostics (GbpOmniGutterRenderer *self);
+gboolean               gbp_omni_gutter_renderer_get_show_line_numbers     (GbpOmniGutterRenderer *self);
+void                   gbp_omni_gutter_renderer_set_show_line_changes     (GbpOmniGutterRenderer *self,
+                                                                           gboolean               
show_line_changes);
+void                   gbp_omni_gutter_renderer_set_show_line_diagnostics (GbpOmniGutterRenderer *self,
+                                                                           gboolean               
show_line_diagnostics);
+void                   gbp_omni_gutter_renderer_set_show_line_numbers     (GbpOmniGutterRenderer *self,
+                                                                           gboolean               
show_line_numbers);
+
+G_END_DECLS
diff --git a/src/plugins/omni-gutter/int-array.h b/src/plugins/omni-gutter/int-array.h
new file mode 100644
index 000000000..37ebca571
--- /dev/null
+++ b/src/plugins/omni-gutter/int-array.h
@@ -0,0 +1,1255 @@
+#pragma once
+
+static const char *int2str[] = {
+  "10000", "10001", "10002", "10003", "10004", "10005", "10006", "10007",
+  "10008", "10009", "10010", "10011", "10012", "10013", "10014", "10015",
+  "10016", "10017", "10018", "10019", "10020", "10021", "10022", "10023",
+  "10024", "10025", "10026", "10027", "10028", "10029", "10030", "10031",
+  "10032", "10033", "10034", "10035", "10036", "10037", "10038", "10039",
+  "10040", "10041", "10042", "10043", "10044", "10045", "10046", "10047",
+  "10048", "10049", "10050", "10051", "10052", "10053", "10054", "10055",
+  "10056", "10057", "10058", "10059", "10060", "10061", "10062", "10063",
+  "10064", "10065", "10066", "10067", "10068", "10069", "10070", "10071",
+  "10072", "10073", "10074", "10075", "10076", "10077", "10078", "10079",
+  "10080", "10081", "10082", "10083", "10084", "10085", "10086", "10087",
+  "10088", "10089", "10090", "10091", "10092", "10093", "10094", "10095",
+  "10096", "10097", "10098", "10099", "10100", "10101", "10102", "10103",
+  "10104", "10105", "10106", "10107", "10108", "10109", "10110", "10111",
+  "10112", "10113", "10114", "10115", "10116", "10117", "10118", "10119",
+  "10120", "10121", "10122", "10123", "10124", "10125", "10126", "10127",
+  "10128", "10129", "10130", "10131", "10132", "10133", "10134", "10135",
+  "10136", "10137", "10138", "10139", "10140", "10141", "10142", "10143",
+  "10144", "10145", "10146", "10147", "10148", "10149", "10150", "10151",
+  "10152", "10153", "10154", "10155", "10156", "10157", "10158", "10159",
+  "10160", "10161", "10162", "10163", "10164", "10165", "10166", "10167",
+  "10168", "10169", "10170", "10171", "10172", "10173", "10174", "10175",
+  "10176", "10177", "10178", "10179", "10180", "10181", "10182", "10183",
+  "10184", "10185", "10186", "10187", "10188", "10189", "10190", "10191",
+  "10192", "10193", "10194", "10195", "10196", "10197", "10198", "10199",
+  "10200", "10201", "10202", "10203", "10204", "10205", "10206", "10207",
+  "10208", "10209", "10210", "10211", "10212", "10213", "10214", "10215",
+  "10216", "10217", "10218", "10219", "10220", "10221", "10222", "10223",
+  "10224", "10225", "10226", "10227", "10228", "10229", "10230", "10231",
+  "10232", "10233", "10234", "10235", "10236", "10237", "10238", "10239",
+  "10240", "10241", "10242", "10243", "10244", "10245", "10246", "10247",
+  "10248", "10249", "10250", "10251", "10252", "10253", "10254", "10255",
+  "10256", "10257", "10258", "10259", "10260", "10261", "10262", "10263",
+  "10264", "10265", "10266", "10267", "10268", "10269", "10270", "10271",
+  "10272", "10273", "10274", "10275", "10276", "10277", "10278", "10279",
+  "10280", "10281", "10282", "10283", "10284", "10285", "10286", "10287",
+  "10288", "10289", "10290", "10291", "10292", "10293", "10294", "10295",
+  "10296", "10297", "10298", "10299", "10300", "10301", "10302", "10303",
+  "10304", "10305", "10306", "10307", "10308", "10309", "10310", "10311",
+  "10312", "10313", "10314", "10315", "10316", "10317", "10318", "10319",
+  "10320", "10321", "10322", "10323", "10324", "10325", "10326", "10327",
+  "10328", "10329", "10330", "10331", "10332", "10333", "10334", "10335",
+  "10336", "10337", "10338", "10339", "10340", "10341", "10342", "10343",
+  "10344", "10345", "10346", "10347", "10348", "10349", "10350", "10351",
+  "10352", "10353", "10354", "10355", "10356", "10357", "10358", "10359",
+  "10360", "10361", "10362", "10363", "10364", "10365", "10366", "10367",
+  "10368", "10369", "10370", "10371", "10372", "10373", "10374", "10375",
+  "10376", "10377", "10378", "10379", "10380", "10381", "10382", "10383",
+  "10384", "10385", "10386", "10387", "10388", "10389", "10390", "10391",
+  "10392", "10393", "10394", "10395", "10396", "10397", "10398", "10399",
+  "10400", "10401", "10402", "10403", "10404", "10405", "10406", "10407",
+  "10408", "10409", "10410", "10411", "10412", "10413", "10414", "10415",
+  "10416", "10417", "10418", "10419", "10420", "10421", "10422", "10423",
+  "10424", "10425", "10426", "10427", "10428", "10429", "10430", "10431",
+  "10432", "10433", "10434", "10435", "10436", "10437", "10438", "10439",
+  "10440", "10441", "10442", "10443", "10444", "10445", "10446", "10447",
+  "10448", "10449", "10450", "10451", "10452", "10453", "10454", "10455",
+  "10456", "10457", "10458", "10459", "10460", "10461", "10462", "10463",
+  "10464", "10465", "10466", "10467", "10468", "10469", "10470", "10471",
+  "10472", "10473", "10474", "10475", "10476", "10477", "10478", "10479",
+  "10480", "10481", "10482", "10483", "10484", "10485", "10486", "10487",
+  "10488", "10489", "10490", "10491", "10492", "10493", "10494", "10495",
+  "10496", "10497", "10498", "10499", "10500", "10501", "10502", "10503",
+  "10504", "10505", "10506", "10507", "10508", "10509", "10510", "10511",
+  "10512", "10513", "10514", "10515", "10516", "10517", "10518", "10519",
+  "10520", "10521", "10522", "10523", "10524", "10525", "10526", "10527",
+  "10528", "10529", "10530", "10531", "10532", "10533", "10534", "10535",
+  "10536", "10537", "10538", "10539", "10540", "10541", "10542", "10543",
+  "10544", "10545", "10546", "10547", "10548", "10549", "10550", "10551",
+  "10552", "10553", "10554", "10555", "10556", "10557", "10558", "10559",
+  "10560", "10561", "10562", "10563", "10564", "10565", "10566", "10567",
+  "10568", "10569", "10570", "10571", "10572", "10573", "10574", "10575",
+  "10576", "10577", "10578", "10579", "10580", "10581", "10582", "10583",
+  "10584", "10585", "10586", "10587", "10588", "10589", "10590", "10591",
+  "10592", "10593", "10594", "10595", "10596", "10597", "10598", "10599",
+  "10600", "10601", "10602", "10603", "10604", "10605", "10606", "10607",
+  "10608", "10609", "10610", "10611", "10612", "10613", "10614", "10615",
+  "10616", "10617", "10618", "10619", "10620", "10621", "10622", "10623",
+  "10624", "10625", "10626", "10627", "10628", "10629", "10630", "10631",
+  "10632", "10633", "10634", "10635", "10636", "10637", "10638", "10639",
+  "10640", "10641", "10642", "10643", "10644", "10645", "10646", "10647",
+  "10648", "10649", "10650", "10651", "10652", "10653", "10654", "10655",
+  "10656", "10657", "10658", "10659", "10660", "10661", "10662", "10663",
+  "10664", "10665", "10666", "10667", "10668", "10669", "10670", "10671",
+  "10672", "10673", "10674", "10675", "10676", "10677", "10678", "10679",
+  "10680", "10681", "10682", "10683", "10684", "10685", "10686", "10687",
+  "10688", "10689", "10690", "10691", "10692", "10693", "10694", "10695",
+  "10696", "10697", "10698", "10699", "10700", "10701", "10702", "10703",
+  "10704", "10705", "10706", "10707", "10708", "10709", "10710", "10711",
+  "10712", "10713", "10714", "10715", "10716", "10717", "10718", "10719",
+  "10720", "10721", "10722", "10723", "10724", "10725", "10726", "10727",
+  "10728", "10729", "10730", "10731", "10732", "10733", "10734", "10735",
+  "10736", "10737", "10738", "10739", "10740", "10741", "10742", "10743",
+  "10744", "10745", "10746", "10747", "10748", "10749", "10750", "10751",
+  "10752", "10753", "10754", "10755", "10756", "10757", "10758", "10759",
+  "10760", "10761", "10762", "10763", "10764", "10765", "10766", "10767",
+  "10768", "10769", "10770", "10771", "10772", "10773", "10774", "10775",
+  "10776", "10777", "10778", "10779", "10780", "10781", "10782", "10783",
+  "10784", "10785", "10786", "10787", "10788", "10789", "10790", "10791",
+  "10792", "10793", "10794", "10795", "10796", "10797", "10798", "10799",
+  "10800", "10801", "10802", "10803", "10804", "10805", "10806", "10807",
+  "10808", "10809", "10810", "10811", "10812", "10813", "10814", "10815",
+  "10816", "10817", "10818", "10819", "10820", "10821", "10822", "10823",
+  "10824", "10825", "10826", "10827", "10828", "10829", "10830", "10831",
+  "10832", "10833", "10834", "10835", "10836", "10837", "10838", "10839",
+  "10840", "10841", "10842", "10843", "10844", "10845", "10846", "10847",
+  "10848", "10849", "10850", "10851", "10852", "10853", "10854", "10855",
+  "10856", "10857", "10858", "10859", "10860", "10861", "10862", "10863",
+  "10864", "10865", "10866", "10867", "10868", "10869", "10870", "10871",
+  "10872", "10873", "10874", "10875", "10876", "10877", "10878", "10879",
+  "10880", "10881", "10882", "10883", "10884", "10885", "10886", "10887",
+  "10888", "10889", "10890", "10891", "10892", "10893", "10894", "10895",
+  "10896", "10897", "10898", "10899", "10900", "10901", "10902", "10903",
+  "10904", "10905", "10906", "10907", "10908", "10909", "10910", "10911",
+  "10912", "10913", "10914", "10915", "10916", "10917", "10918", "10919",
+  "10920", "10921", "10922", "10923", "10924", "10925", "10926", "10927",
+  "10928", "10929", "10930", "10931", "10932", "10933", "10934", "10935",
+  "10936", "10937", "10938", "10939", "10940", "10941", "10942", "10943",
+  "10944", "10945", "10946", "10947", "10948", "10949", "10950", "10951",
+  "10952", "10953", "10954", "10955", "10956", "10957", "10958", "10959",
+  "10960", "10961", "10962", "10963", "10964", "10965", "10966", "10967",
+  "10968", "10969", "10970", "10971", "10972", "10973", "10974", "10975",
+  "10976", "10977", "10978", "10979", "10980", "10981", "10982", "10983",
+  "10984", "10985", "10986", "10987", "10988", "10989", "10990", "10991",
+  "10992", "10993", "10994", "10995", "10996", "10997", "10998", "10999",
+  "11000", "11001", "11002", "11003", "11004", "11005", "11006", "11007",
+  "11008", "11009", "11010", "11011", "11012", "11013", "11014", "11015",
+  "11016", "11017", "11018", "11019", "11020", "11021", "11022", "11023",
+  "11024", "11025", "11026", "11027", "11028", "11029", "11030", "11031",
+  "11032", "11033", "11034", "11035", "11036", "11037", "11038", "11039",
+  "11040", "11041", "11042", "11043", "11044", "11045", "11046", "11047",
+  "11048", "11049", "11050", "11051", "11052", "11053", "11054", "11055",
+  "11056", "11057", "11058", "11059", "11060", "11061", "11062", "11063",
+  "11064", "11065", "11066", "11067", "11068", "11069", "11070", "11071",
+  "11072", "11073", "11074", "11075", "11076", "11077", "11078", "11079",
+  "11080", "11081", "11082", "11083", "11084", "11085", "11086", "11087",
+  "11088", "11089", "11090", "11091", "11092", "11093", "11094", "11095",
+  "11096", "11097", "11098", "11099", "11100", "11101", "11102", "11103",
+  "11104", "11105", "11106", "11107", "11108", "11109", "11110", "11111",
+  "11112", "11113", "11114", "11115", "11116", "11117", "11118", "11119",
+  "11120", "11121", "11122", "11123", "11124", "11125", "11126", "11127",
+  "11128", "11129", "11130", "11131", "11132", "11133", "11134", "11135",
+  "11136", "11137", "11138", "11139", "11140", "11141", "11142", "11143",
+  "11144", "11145", "11146", "11147", "11148", "11149", "11150", "11151",
+  "11152", "11153", "11154", "11155", "11156", "11157", "11158", "11159",
+  "11160", "11161", "11162", "11163", "11164", "11165", "11166", "11167",
+  "11168", "11169", "11170", "11171", "11172", "11173", "11174", "11175",
+  "11176", "11177", "11178", "11179", "11180", "11181", "11182", "11183",
+  "11184", "11185", "11186", "11187", "11188", "11189", "11190", "11191",
+  "11192", "11193", "11194", "11195", "11196", "11197", "11198", "11199",
+  "11200", "11201", "11202", "11203", "11204", "11205", "11206", "11207",
+  "11208", "11209", "11210", "11211", "11212", "11213", "11214", "11215",
+  "11216", "11217", "11218", "11219", "11220", "11221", "11222", "11223",
+  "11224", "11225", "11226", "11227", "11228", "11229", "11230", "11231",
+  "11232", "11233", "11234", "11235", "11236", "11237", "11238", "11239",
+  "11240", "11241", "11242", "11243", "11244", "11245", "11246", "11247",
+  "11248", "11249", "11250", "11251", "11252", "11253", "11254", "11255",
+  "11256", "11257", "11258", "11259", "11260", "11261", "11262", "11263",
+  "11264", "11265", "11266", "11267", "11268", "11269", "11270", "11271",
+  "11272", "11273", "11274", "11275", "11276", "11277", "11278", "11279",
+  "11280", "11281", "11282", "11283", "11284", "11285", "11286", "11287",
+  "11288", "11289", "11290", "11291", "11292", "11293", "11294", "11295",
+  "11296", "11297", "11298", "11299", "11300", "11301", "11302", "11303",
+  "11304", "11305", "11306", "11307", "11308", "11309", "11310", "11311",
+  "11312", "11313", "11314", "11315", "11316", "11317", "11318", "11319",
+  "11320", "11321", "11322", "11323", "11324", "11325", "11326", "11327",
+  "11328", "11329", "11330", "11331", "11332", "11333", "11334", "11335",
+  "11336", "11337", "11338", "11339", "11340", "11341", "11342", "11343",
+  "11344", "11345", "11346", "11347", "11348", "11349", "11350", "11351",
+  "11352", "11353", "11354", "11355", "11356", "11357", "11358", "11359",
+  "11360", "11361", "11362", "11363", "11364", "11365", "11366", "11367",
+  "11368", "11369", "11370", "11371", "11372", "11373", "11374", "11375",
+  "11376", "11377", "11378", "11379", "11380", "11381", "11382", "11383",
+  "11384", "11385", "11386", "11387", "11388", "11389", "11390", "11391",
+  "11392", "11393", "11394", "11395", "11396", "11397", "11398", "11399",
+  "11400", "11401", "11402", "11403", "11404", "11405", "11406", "11407",
+  "11408", "11409", "11410", "11411", "11412", "11413", "11414", "11415",
+  "11416", "11417", "11418", "11419", "11420", "11421", "11422", "11423",
+  "11424", "11425", "11426", "11427", "11428", "11429", "11430", "11431",
+  "11432", "11433", "11434", "11435", "11436", "11437", "11438", "11439",
+  "11440", "11441", "11442", "11443", "11444", "11445", "11446", "11447",
+  "11448", "11449", "11450", "11451", "11452", "11453", "11454", "11455",
+  "11456", "11457", "11458", "11459", "11460", "11461", "11462", "11463",
+  "11464", "11465", "11466", "11467", "11468", "11469", "11470", "11471",
+  "11472", "11473", "11474", "11475", "11476", "11477", "11478", "11479",
+  "11480", "11481", "11482", "11483", "11484", "11485", "11486", "11487",
+  "11488", "11489", "11490", "11491", "11492", "11493", "11494", "11495",
+  "11496", "11497", "11498", "11499", "11500", "11501", "11502", "11503",
+  "11504", "11505", "11506", "11507", "11508", "11509", "11510", "11511",
+  "11512", "11513", "11514", "11515", "11516", "11517", "11518", "11519",
+  "11520", "11521", "11522", "11523", "11524", "11525", "11526", "11527",
+  "11528", "11529", "11530", "11531", "11532", "11533", "11534", "11535",
+  "11536", "11537", "11538", "11539", "11540", "11541", "11542", "11543",
+  "11544", "11545", "11546", "11547", "11548", "11549", "11550", "11551",
+  "11552", "11553", "11554", "11555", "11556", "11557", "11558", "11559",
+  "11560", "11561", "11562", "11563", "11564", "11565", "11566", "11567",
+  "11568", "11569", "11570", "11571", "11572", "11573", "11574", "11575",
+  "11576", "11577", "11578", "11579", "11580", "11581", "11582", "11583",
+  "11584", "11585", "11586", "11587", "11588", "11589", "11590", "11591",
+  "11592", "11593", "11594", "11595", "11596", "11597", "11598", "11599",
+  "11600", "11601", "11602", "11603", "11604", "11605", "11606", "11607",
+  "11608", "11609", "11610", "11611", "11612", "11613", "11614", "11615",
+  "11616", "11617", "11618", "11619", "11620", "11621", "11622", "11623",
+  "11624", "11625", "11626", "11627", "11628", "11629", "11630", "11631",
+  "11632", "11633", "11634", "11635", "11636", "11637", "11638", "11639",
+  "11640", "11641", "11642", "11643", "11644", "11645", "11646", "11647",
+  "11648", "11649", "11650", "11651", "11652", "11653", "11654", "11655",
+  "11656", "11657", "11658", "11659", "11660", "11661", "11662", "11663",
+  "11664", "11665", "11666", "11667", "11668", "11669", "11670", "11671",
+  "11672", "11673", "11674", "11675", "11676", "11677", "11678", "11679",
+  "11680", "11681", "11682", "11683", "11684", "11685", "11686", "11687",
+  "11688", "11689", "11690", "11691", "11692", "11693", "11694", "11695",
+  "11696", "11697", "11698", "11699", "11700", "11701", "11702", "11703",
+  "11704", "11705", "11706", "11707", "11708", "11709", "11710", "11711",
+  "11712", "11713", "11714", "11715", "11716", "11717", "11718", "11719",
+  "11720", "11721", "11722", "11723", "11724", "11725", "11726", "11727",
+  "11728", "11729", "11730", "11731", "11732", "11733", "11734", "11735",
+  "11736", "11737", "11738", "11739", "11740", "11741", "11742", "11743",
+  "11744", "11745", "11746", "11747", "11748", "11749", "11750", "11751",
+  "11752", "11753", "11754", "11755", "11756", "11757", "11758", "11759",
+  "11760", "11761", "11762", "11763", "11764", "11765", "11766", "11767",
+  "11768", "11769", "11770", "11771", "11772", "11773", "11774", "11775",
+  "11776", "11777", "11778", "11779", "11780", "11781", "11782", "11783",
+  "11784", "11785", "11786", "11787", "11788", "11789", "11790", "11791",
+  "11792", "11793", "11794", "11795", "11796", "11797", "11798", "11799",
+  "11800", "11801", "11802", "11803", "11804", "11805", "11806", "11807",
+  "11808", "11809", "11810", "11811", "11812", "11813", "11814", "11815",
+  "11816", "11817", "11818", "11819", "11820", "11821", "11822", "11823",
+  "11824", "11825", "11826", "11827", "11828", "11829", "11830", "11831",
+  "11832", "11833", "11834", "11835", "11836", "11837", "11838", "11839",
+  "11840", "11841", "11842", "11843", "11844", "11845", "11846", "11847",
+  "11848", "11849", "11850", "11851", "11852", "11853", "11854", "11855",
+  "11856", "11857", "11858", "11859", "11860", "11861", "11862", "11863",
+  "11864", "11865", "11866", "11867", "11868", "11869", "11870", "11871",
+  "11872", "11873", "11874", "11875", "11876", "11877", "11878", "11879",
+  "11880", "11881", "11882", "11883", "11884", "11885", "11886", "11887",
+  "11888", "11889", "11890", "11891", "11892", "11893", "11894", "11895",
+  "11896", "11897", "11898", "11899", "11900", "11901", "11902", "11903",
+  "11904", "11905", "11906", "11907", "11908", "11909", "11910", "11911",
+  "11912", "11913", "11914", "11915", "11916", "11917", "11918", "11919",
+  "11920", "11921", "11922", "11923", "11924", "11925", "11926", "11927",
+  "11928", "11929", "11930", "11931", "11932", "11933", "11934", "11935",
+  "11936", "11937", "11938", "11939", "11940", "11941", "11942", "11943",
+  "11944", "11945", "11946", "11947", "11948", "11949", "11950", "11951",
+  "11952", "11953", "11954", "11955", "11956", "11957", "11958", "11959",
+  "11960", "11961", "11962", "11963", "11964", "11965", "11966", "11967",
+  "11968", "11969", "11970", "11971", "11972", "11973", "11974", "11975",
+  "11976", "11977", "11978", "11979", "11980", "11981", "11982", "11983",
+  "11984", "11985", "11986", "11987", "11988", "11989", "11990", "11991",
+  "11992", "11993", "11994", "11995", "11996", "11997", "11998", "11999",
+  "12000", "12001", "12002", "12003", "12004", "12005", "12006", "12007",
+  "12008", "12009", "12010", "12011", "12012", "12013", "12014", "12015",
+  "12016", "12017", "12018", "12019", "12020", "12021", "12022", "12023",
+  "12024", "12025", "12026", "12027", "12028", "12029", "12030", "12031",
+  "12032", "12033", "12034", "12035", "12036", "12037", "12038", "12039",
+  "12040", "12041", "12042", "12043", "12044", "12045", "12046", "12047",
+  "12048", "12049", "12050", "12051", "12052", "12053", "12054", "12055",
+  "12056", "12057", "12058", "12059", "12060", "12061", "12062", "12063",
+  "12064", "12065", "12066", "12067", "12068", "12069", "12070", "12071",
+  "12072", "12073", "12074", "12075", "12076", "12077", "12078", "12079",
+  "12080", "12081", "12082", "12083", "12084", "12085", "12086", "12087",
+  "12088", "12089", "12090", "12091", "12092", "12093", "12094", "12095",
+  "12096", "12097", "12098", "12099", "12100", "12101", "12102", "12103",
+  "12104", "12105", "12106", "12107", "12108", "12109", "12110", "12111",
+  "12112", "12113", "12114", "12115", "12116", "12117", "12118", "12119",
+  "12120", "12121", "12122", "12123", "12124", "12125", "12126", "12127",
+  "12128", "12129", "12130", "12131", "12132", "12133", "12134", "12135",
+  "12136", "12137", "12138", "12139", "12140", "12141", "12142", "12143",
+  "12144", "12145", "12146", "12147", "12148", "12149", "12150", "12151",
+  "12152", "12153", "12154", "12155", "12156", "12157", "12158", "12159",
+  "12160", "12161", "12162", "12163", "12164", "12165", "12166", "12167",
+  "12168", "12169", "12170", "12171", "12172", "12173", "12174", "12175",
+  "12176", "12177", "12178", "12179", "12180", "12181", "12182", "12183",
+  "12184", "12185", "12186", "12187", "12188", "12189", "12190", "12191",
+  "12192", "12193", "12194", "12195", "12196", "12197", "12198", "12199",
+  "12200", "12201", "12202", "12203", "12204", "12205", "12206", "12207",
+  "12208", "12209", "12210", "12211", "12212", "12213", "12214", "12215",
+  "12216", "12217", "12218", "12219", "12220", "12221", "12222", "12223",
+  "12224", "12225", "12226", "12227", "12228", "12229", "12230", "12231",
+  "12232", "12233", "12234", "12235", "12236", "12237", "12238", "12239",
+  "12240", "12241", "12242", "12243", "12244", "12245", "12246", "12247",
+  "12248", "12249", "12250", "12251", "12252", "12253", "12254", "12255",
+  "12256", "12257", "12258", "12259", "12260", "12261", "12262", "12263",
+  "12264", "12265", "12266", "12267", "12268", "12269", "12270", "12271",
+  "12272", "12273", "12274", "12275", "12276", "12277", "12278", "12279",
+  "12280", "12281", "12282", "12283", "12284", "12285", "12286", "12287",
+  "12288", "12289", "12290", "12291", "12292", "12293", "12294", "12295",
+  "12296", "12297", "12298", "12299", "12300", "12301", "12302", "12303",
+  "12304", "12305", "12306", "12307", "12308", "12309", "12310", "12311",
+  "12312", "12313", "12314", "12315", "12316", "12317", "12318", "12319",
+  "12320", "12321", "12322", "12323", "12324", "12325", "12326", "12327",
+  "12328", "12329", "12330", "12331", "12332", "12333", "12334", "12335",
+  "12336", "12337", "12338", "12339", "12340", "12341", "12342", "12343",
+  "12344", "12345", "12346", "12347", "12348", "12349", "12350", "12351",
+  "12352", "12353", "12354", "12355", "12356", "12357", "12358", "12359",
+  "12360", "12361", "12362", "12363", "12364", "12365", "12366", "12367",
+  "12368", "12369", "12370", "12371", "12372", "12373", "12374", "12375",
+  "12376", "12377", "12378", "12379", "12380", "12381", "12382", "12383",
+  "12384", "12385", "12386", "12387", "12388", "12389", "12390", "12391",
+  "12392", "12393", "12394", "12395", "12396", "12397", "12398", "12399",
+  "12400", "12401", "12402", "12403", "12404", "12405", "12406", "12407",
+  "12408", "12409", "12410", "12411", "12412", "12413", "12414", "12415",
+  "12416", "12417", "12418", "12419", "12420", "12421", "12422", "12423",
+  "12424", "12425", "12426", "12427", "12428", "12429", "12430", "12431",
+  "12432", "12433", "12434", "12435", "12436", "12437", "12438", "12439",
+  "12440", "12441", "12442", "12443", "12444", "12445", "12446", "12447",
+  "12448", "12449", "12450", "12451", "12452", "12453", "12454", "12455",
+  "12456", "12457", "12458", "12459", "12460", "12461", "12462", "12463",
+  "12464", "12465", "12466", "12467", "12468", "12469", "12470", "12471",
+  "12472", "12473", "12474", "12475", "12476", "12477", "12478", "12479",
+  "12480", "12481", "12482", "12483", "12484", "12485", "12486", "12487",
+  "12488", "12489", "12490", "12491", "12492", "12493", "12494", "12495",
+  "12496", "12497", "12498", "12499", "12500", "12501", "12502", "12503",
+  "12504", "12505", "12506", "12507", "12508", "12509", "12510", "12511",
+  "12512", "12513", "12514", "12515", "12516", "12517", "12518", "12519",
+  "12520", "12521", "12522", "12523", "12524", "12525", "12526", "12527",
+  "12528", "12529", "12530", "12531", "12532", "12533", "12534", "12535",
+  "12536", "12537", "12538", "12539", "12540", "12541", "12542", "12543",
+  "12544", "12545", "12546", "12547", "12548", "12549", "12550", "12551",
+  "12552", "12553", "12554", "12555", "12556", "12557", "12558", "12559",
+  "12560", "12561", "12562", "12563", "12564", "12565", "12566", "12567",
+  "12568", "12569", "12570", "12571", "12572", "12573", "12574", "12575",
+  "12576", "12577", "12578", "12579", "12580", "12581", "12582", "12583",
+  "12584", "12585", "12586", "12587", "12588", "12589", "12590", "12591",
+  "12592", "12593", "12594", "12595", "12596", "12597", "12598", "12599",
+  "12600", "12601", "12602", "12603", "12604", "12605", "12606", "12607",
+  "12608", "12609", "12610", "12611", "12612", "12613", "12614", "12615",
+  "12616", "12617", "12618", "12619", "12620", "12621", "12622", "12623",
+  "12624", "12625", "12626", "12627", "12628", "12629", "12630", "12631",
+  "12632", "12633", "12634", "12635", "12636", "12637", "12638", "12639",
+  "12640", "12641", "12642", "12643", "12644", "12645", "12646", "12647",
+  "12648", "12649", "12650", "12651", "12652", "12653", "12654", "12655",
+  "12656", "12657", "12658", "12659", "12660", "12661", "12662", "12663",
+  "12664", "12665", "12666", "12667", "12668", "12669", "12670", "12671",
+  "12672", "12673", "12674", "12675", "12676", "12677", "12678", "12679",
+  "12680", "12681", "12682", "12683", "12684", "12685", "12686", "12687",
+  "12688", "12689", "12690", "12691", "12692", "12693", "12694", "12695",
+  "12696", "12697", "12698", "12699", "12700", "12701", "12702", "12703",
+  "12704", "12705", "12706", "12707", "12708", "12709", "12710", "12711",
+  "12712", "12713", "12714", "12715", "12716", "12717", "12718", "12719",
+  "12720", "12721", "12722", "12723", "12724", "12725", "12726", "12727",
+  "12728", "12729", "12730", "12731", "12732", "12733", "12734", "12735",
+  "12736", "12737", "12738", "12739", "12740", "12741", "12742", "12743",
+  "12744", "12745", "12746", "12747", "12748", "12749", "12750", "12751",
+  "12752", "12753", "12754", "12755", "12756", "12757", "12758", "12759",
+  "12760", "12761", "12762", "12763", "12764", "12765", "12766", "12767",
+  "12768", "12769", "12770", "12771", "12772", "12773", "12774", "12775",
+  "12776", "12777", "12778", "12779", "12780", "12781", "12782", "12783",
+  "12784", "12785", "12786", "12787", "12788", "12789", "12790", "12791",
+  "12792", "12793", "12794", "12795", "12796", "12797", "12798", "12799",
+  "12800", "12801", "12802", "12803", "12804", "12805", "12806", "12807",
+  "12808", "12809", "12810", "12811", "12812", "12813", "12814", "12815",
+  "12816", "12817", "12818", "12819", "12820", "12821", "12822", "12823",
+  "12824", "12825", "12826", "12827", "12828", "12829", "12830", "12831",
+  "12832", "12833", "12834", "12835", "12836", "12837", "12838", "12839",
+  "12840", "12841", "12842", "12843", "12844", "12845", "12846", "12847",
+  "12848", "12849", "12850", "12851", "12852", "12853", "12854", "12855",
+  "12856", "12857", "12858", "12859", "12860", "12861", "12862", "12863",
+  "12864", "12865", "12866", "12867", "12868", "12869", "12870", "12871",
+  "12872", "12873", "12874", "12875", "12876", "12877", "12878", "12879",
+  "12880", "12881", "12882", "12883", "12884", "12885", "12886", "12887",
+  "12888", "12889", "12890", "12891", "12892", "12893", "12894", "12895",
+  "12896", "12897", "12898", "12899", "12900", "12901", "12902", "12903",
+  "12904", "12905", "12906", "12907", "12908", "12909", "12910", "12911",
+  "12912", "12913", "12914", "12915", "12916", "12917", "12918", "12919",
+  "12920", "12921", "12922", "12923", "12924", "12925", "12926", "12927",
+  "12928", "12929", "12930", "12931", "12932", "12933", "12934", "12935",
+  "12936", "12937", "12938", "12939", "12940", "12941", "12942", "12943",
+  "12944", "12945", "12946", "12947", "12948", "12949", "12950", "12951",
+  "12952", "12953", "12954", "12955", "12956", "12957", "12958", "12959",
+  "12960", "12961", "12962", "12963", "12964", "12965", "12966", "12967",
+  "12968", "12969", "12970", "12971", "12972", "12973", "12974", "12975",
+  "12976", "12977", "12978", "12979", "12980", "12981", "12982", "12983",
+  "12984", "12985", "12986", "12987", "12988", "12989", "12990", "12991",
+  "12992", "12993", "12994", "12995", "12996", "12997", "12998", "12999",
+  "13000", "13001", "13002", "13003", "13004", "13005", "13006", "13007",
+  "13008", "13009", "13010", "13011", "13012", "13013", "13014", "13015",
+  "13016", "13017", "13018", "13019", "13020", "13021", "13022", "13023",
+  "13024", "13025", "13026", "13027", "13028", "13029", "13030", "13031",
+  "13032", "13033", "13034", "13035", "13036", "13037", "13038", "13039",
+  "13040", "13041", "13042", "13043", "13044", "13045", "13046", "13047",
+  "13048", "13049", "13050", "13051", "13052", "13053", "13054", "13055",
+  "13056", "13057", "13058", "13059", "13060", "13061", "13062", "13063",
+  "13064", "13065", "13066", "13067", "13068", "13069", "13070", "13071",
+  "13072", "13073", "13074", "13075", "13076", "13077", "13078", "13079",
+  "13080", "13081", "13082", "13083", "13084", "13085", "13086", "13087",
+  "13088", "13089", "13090", "13091", "13092", "13093", "13094", "13095",
+  "13096", "13097", "13098", "13099", "13100", "13101", "13102", "13103",
+  "13104", "13105", "13106", "13107", "13108", "13109", "13110", "13111",
+  "13112", "13113", "13114", "13115", "13116", "13117", "13118", "13119",
+  "13120", "13121", "13122", "13123", "13124", "13125", "13126", "13127",
+  "13128", "13129", "13130", "13131", "13132", "13133", "13134", "13135",
+  "13136", "13137", "13138", "13139", "13140", "13141", "13142", "13143",
+  "13144", "13145", "13146", "13147", "13148", "13149", "13150", "13151",
+  "13152", "13153", "13154", "13155", "13156", "13157", "13158", "13159",
+  "13160", "13161", "13162", "13163", "13164", "13165", "13166", "13167",
+  "13168", "13169", "13170", "13171", "13172", "13173", "13174", "13175",
+  "13176", "13177", "13178", "13179", "13180", "13181", "13182", "13183",
+  "13184", "13185", "13186", "13187", "13188", "13189", "13190", "13191",
+  "13192", "13193", "13194", "13195", "13196", "13197", "13198", "13199",
+  "13200", "13201", "13202", "13203", "13204", "13205", "13206", "13207",
+  "13208", "13209", "13210", "13211", "13212", "13213", "13214", "13215",
+  "13216", "13217", "13218", "13219", "13220", "13221", "13222", "13223",
+  "13224", "13225", "13226", "13227", "13228", "13229", "13230", "13231",
+  "13232", "13233", "13234", "13235", "13236", "13237", "13238", "13239",
+  "13240", "13241", "13242", "13243", "13244", "13245", "13246", "13247",
+  "13248", "13249", "13250", "13251", "13252", "13253", "13254", "13255",
+  "13256", "13257", "13258", "13259", "13260", "13261", "13262", "13263",
+  "13264", "13265", "13266", "13267", "13268", "13269", "13270", "13271",
+  "13272", "13273", "13274", "13275", "13276", "13277", "13278", "13279",
+  "13280", "13281", "13282", "13283", "13284", "13285", "13286", "13287",
+  "13288", "13289", "13290", "13291", "13292", "13293", "13294", "13295",
+  "13296", "13297", "13298", "13299", "13300", "13301", "13302", "13303",
+  "13304", "13305", "13306", "13307", "13308", "13309", "13310", "13311",
+  "13312", "13313", "13314", "13315", "13316", "13317", "13318", "13319",
+  "13320", "13321", "13322", "13323", "13324", "13325", "13326", "13327",
+  "13328", "13329", "13330", "13331", "13332", "13333", "13334", "13335",
+  "13336", "13337", "13338", "13339", "13340", "13341", "13342", "13343",
+  "13344", "13345", "13346", "13347", "13348", "13349", "13350", "13351",
+  "13352", "13353", "13354", "13355", "13356", "13357", "13358", "13359",
+  "13360", "13361", "13362", "13363", "13364", "13365", "13366", "13367",
+  "13368", "13369", "13370", "13371", "13372", "13373", "13374", "13375",
+  "13376", "13377", "13378", "13379", "13380", "13381", "13382", "13383",
+  "13384", "13385", "13386", "13387", "13388", "13389", "13390", "13391",
+  "13392", "13393", "13394", "13395", "13396", "13397", "13398", "13399",
+  "13400", "13401", "13402", "13403", "13404", "13405", "13406", "13407",
+  "13408", "13409", "13410", "13411", "13412", "13413", "13414", "13415",
+  "13416", "13417", "13418", "13419", "13420", "13421", "13422", "13423",
+  "13424", "13425", "13426", "13427", "13428", "13429", "13430", "13431",
+  "13432", "13433", "13434", "13435", "13436", "13437", "13438", "13439",
+  "13440", "13441", "13442", "13443", "13444", "13445", "13446", "13447",
+  "13448", "13449", "13450", "13451", "13452", "13453", "13454", "13455",
+  "13456", "13457", "13458", "13459", "13460", "13461", "13462", "13463",
+  "13464", "13465", "13466", "13467", "13468", "13469", "13470", "13471",
+  "13472", "13473", "13474", "13475", "13476", "13477", "13478", "13479",
+  "13480", "13481", "13482", "13483", "13484", "13485", "13486", "13487",
+  "13488", "13489", "13490", "13491", "13492", "13493", "13494", "13495",
+  "13496", "13497", "13498", "13499", "13500", "13501", "13502", "13503",
+  "13504", "13505", "13506", "13507", "13508", "13509", "13510", "13511",
+  "13512", "13513", "13514", "13515", "13516", "13517", "13518", "13519",
+  "13520", "13521", "13522", "13523", "13524", "13525", "13526", "13527",
+  "13528", "13529", "13530", "13531", "13532", "13533", "13534", "13535",
+  "13536", "13537", "13538", "13539", "13540", "13541", "13542", "13543",
+  "13544", "13545", "13546", "13547", "13548", "13549", "13550", "13551",
+  "13552", "13553", "13554", "13555", "13556", "13557", "13558", "13559",
+  "13560", "13561", "13562", "13563", "13564", "13565", "13566", "13567",
+  "13568", "13569", "13570", "13571", "13572", "13573", "13574", "13575",
+  "13576", "13577", "13578", "13579", "13580", "13581", "13582", "13583",
+  "13584", "13585", "13586", "13587", "13588", "13589", "13590", "13591",
+  "13592", "13593", "13594", "13595", "13596", "13597", "13598", "13599",
+  "13600", "13601", "13602", "13603", "13604", "13605", "13606", "13607",
+  "13608", "13609", "13610", "13611", "13612", "13613", "13614", "13615",
+  "13616", "13617", "13618", "13619", "13620", "13621", "13622", "13623",
+  "13624", "13625", "13626", "13627", "13628", "13629", "13630", "13631",
+  "13632", "13633", "13634", "13635", "13636", "13637", "13638", "13639",
+  "13640", "13641", "13642", "13643", "13644", "13645", "13646", "13647",
+  "13648", "13649", "13650", "13651", "13652", "13653", "13654", "13655",
+  "13656", "13657", "13658", "13659", "13660", "13661", "13662", "13663",
+  "13664", "13665", "13666", "13667", "13668", "13669", "13670", "13671",
+  "13672", "13673", "13674", "13675", "13676", "13677", "13678", "13679",
+  "13680", "13681", "13682", "13683", "13684", "13685", "13686", "13687",
+  "13688", "13689", "13690", "13691", "13692", "13693", "13694", "13695",
+  "13696", "13697", "13698", "13699", "13700", "13701", "13702", "13703",
+  "13704", "13705", "13706", "13707", "13708", "13709", "13710", "13711",
+  "13712", "13713", "13714", "13715", "13716", "13717", "13718", "13719",
+  "13720", "13721", "13722", "13723", "13724", "13725", "13726", "13727",
+  "13728", "13729", "13730", "13731", "13732", "13733", "13734", "13735",
+  "13736", "13737", "13738", "13739", "13740", "13741", "13742", "13743",
+  "13744", "13745", "13746", "13747", "13748", "13749", "13750", "13751",
+  "13752", "13753", "13754", "13755", "13756", "13757", "13758", "13759",
+  "13760", "13761", "13762", "13763", "13764", "13765", "13766", "13767",
+  "13768", "13769", "13770", "13771", "13772", "13773", "13774", "13775",
+  "13776", "13777", "13778", "13779", "13780", "13781", "13782", "13783",
+  "13784", "13785", "13786", "13787", "13788", "13789", "13790", "13791",
+  "13792", "13793", "13794", "13795", "13796", "13797", "13798", "13799",
+  "13800", "13801", "13802", "13803", "13804", "13805", "13806", "13807",
+  "13808", "13809", "13810", "13811", "13812", "13813", "13814", "13815",
+  "13816", "13817", "13818", "13819", "13820", "13821", "13822", "13823",
+  "13824", "13825", "13826", "13827", "13828", "13829", "13830", "13831",
+  "13832", "13833", "13834", "13835", "13836", "13837", "13838", "13839",
+  "13840", "13841", "13842", "13843", "13844", "13845", "13846", "13847",
+  "13848", "13849", "13850", "13851", "13852", "13853", "13854", "13855",
+  "13856", "13857", "13858", "13859", "13860", "13861", "13862", "13863",
+  "13864", "13865", "13866", "13867", "13868", "13869", "13870", "13871",
+  "13872", "13873", "13874", "13875", "13876", "13877", "13878", "13879",
+  "13880", "13881", "13882", "13883", "13884", "13885", "13886", "13887",
+  "13888", "13889", "13890", "13891", "13892", "13893", "13894", "13895",
+  "13896", "13897", "13898", "13899", "13900", "13901", "13902", "13903",
+  "13904", "13905", "13906", "13907", "13908", "13909", "13910", "13911",
+  "13912", "13913", "13914", "13915", "13916", "13917", "13918", "13919",
+  "13920", "13921", "13922", "13923", "13924", "13925", "13926", "13927",
+  "13928", "13929", "13930", "13931", "13932", "13933", "13934", "13935",
+  "13936", "13937", "13938", "13939", "13940", "13941", "13942", "13943",
+  "13944", "13945", "13946", "13947", "13948", "13949", "13950", "13951",
+  "13952", "13953", "13954", "13955", "13956", "13957", "13958", "13959",
+  "13960", "13961", "13962", "13963", "13964", "13965", "13966", "13967",
+  "13968", "13969", "13970", "13971", "13972", "13973", "13974", "13975",
+  "13976", "13977", "13978", "13979", "13980", "13981", "13982", "13983",
+  "13984", "13985", "13986", "13987", "13988", "13989", "13990", "13991",
+  "13992", "13993", "13994", "13995", "13996", "13997", "13998", "13999",
+  "14000", "14001", "14002", "14003", "14004", "14005", "14006", "14007",
+  "14008", "14009", "14010", "14011", "14012", "14013", "14014", "14015",
+  "14016", "14017", "14018", "14019", "14020", "14021", "14022", "14023",
+  "14024", "14025", "14026", "14027", "14028", "14029", "14030", "14031",
+  "14032", "14033", "14034", "14035", "14036", "14037", "14038", "14039",
+  "14040", "14041", "14042", "14043", "14044", "14045", "14046", "14047",
+  "14048", "14049", "14050", "14051", "14052", "14053", "14054", "14055",
+  "14056", "14057", "14058", "14059", "14060", "14061", "14062", "14063",
+  "14064", "14065", "14066", "14067", "14068", "14069", "14070", "14071",
+  "14072", "14073", "14074", "14075", "14076", "14077", "14078", "14079",
+  "14080", "14081", "14082", "14083", "14084", "14085", "14086", "14087",
+  "14088", "14089", "14090", "14091", "14092", "14093", "14094", "14095",
+  "14096", "14097", "14098", "14099", "14100", "14101", "14102", "14103",
+  "14104", "14105", "14106", "14107", "14108", "14109", "14110", "14111",
+  "14112", "14113", "14114", "14115", "14116", "14117", "14118", "14119",
+  "14120", "14121", "14122", "14123", "14124", "14125", "14126", "14127",
+  "14128", "14129", "14130", "14131", "14132", "14133", "14134", "14135",
+  "14136", "14137", "14138", "14139", "14140", "14141", "14142", "14143",
+  "14144", "14145", "14146", "14147", "14148", "14149", "14150", "14151",
+  "14152", "14153", "14154", "14155", "14156", "14157", "14158", "14159",
+  "14160", "14161", "14162", "14163", "14164", "14165", "14166", "14167",
+  "14168", "14169", "14170", "14171", "14172", "14173", "14174", "14175",
+  "14176", "14177", "14178", "14179", "14180", "14181", "14182", "14183",
+  "14184", "14185", "14186", "14187", "14188", "14189", "14190", "14191",
+  "14192", "14193", "14194", "14195", "14196", "14197", "14198", "14199",
+  "14200", "14201", "14202", "14203", "14204", "14205", "14206", "14207",
+  "14208", "14209", "14210", "14211", "14212", "14213", "14214", "14215",
+  "14216", "14217", "14218", "14219", "14220", "14221", "14222", "14223",
+  "14224", "14225", "14226", "14227", "14228", "14229", "14230", "14231",
+  "14232", "14233", "14234", "14235", "14236", "14237", "14238", "14239",
+  "14240", "14241", "14242", "14243", "14244", "14245", "14246", "14247",
+  "14248", "14249", "14250", "14251", "14252", "14253", "14254", "14255",
+  "14256", "14257", "14258", "14259", "14260", "14261", "14262", "14263",
+  "14264", "14265", "14266", "14267", "14268", "14269", "14270", "14271",
+  "14272", "14273", "14274", "14275", "14276", "14277", "14278", "14279",
+  "14280", "14281", "14282", "14283", "14284", "14285", "14286", "14287",
+  "14288", "14289", "14290", "14291", "14292", "14293", "14294", "14295",
+  "14296", "14297", "14298", "14299", "14300", "14301", "14302", "14303",
+  "14304", "14305", "14306", "14307", "14308", "14309", "14310", "14311",
+  "14312", "14313", "14314", "14315", "14316", "14317", "14318", "14319",
+  "14320", "14321", "14322", "14323", "14324", "14325", "14326", "14327",
+  "14328", "14329", "14330", "14331", "14332", "14333", "14334", "14335",
+  "14336", "14337", "14338", "14339", "14340", "14341", "14342", "14343",
+  "14344", "14345", "14346", "14347", "14348", "14349", "14350", "14351",
+  "14352", "14353", "14354", "14355", "14356", "14357", "14358", "14359",
+  "14360", "14361", "14362", "14363", "14364", "14365", "14366", "14367",
+  "14368", "14369", "14370", "14371", "14372", "14373", "14374", "14375",
+  "14376", "14377", "14378", "14379", "14380", "14381", "14382", "14383",
+  "14384", "14385", "14386", "14387", "14388", "14389", "14390", "14391",
+  "14392", "14393", "14394", "14395", "14396", "14397", "14398", "14399",
+  "14400", "14401", "14402", "14403", "14404", "14405", "14406", "14407",
+  "14408", "14409", "14410", "14411", "14412", "14413", "14414", "14415",
+  "14416", "14417", "14418", "14419", "14420", "14421", "14422", "14423",
+  "14424", "14425", "14426", "14427", "14428", "14429", "14430", "14431",
+  "14432", "14433", "14434", "14435", "14436", "14437", "14438", "14439",
+  "14440", "14441", "14442", "14443", "14444", "14445", "14446", "14447",
+  "14448", "14449", "14450", "14451", "14452", "14453", "14454", "14455",
+  "14456", "14457", "14458", "14459", "14460", "14461", "14462", "14463",
+  "14464", "14465", "14466", "14467", "14468", "14469", "14470", "14471",
+  "14472", "14473", "14474", "14475", "14476", "14477", "14478", "14479",
+  "14480", "14481", "14482", "14483", "14484", "14485", "14486", "14487",
+  "14488", "14489", "14490", "14491", "14492", "14493", "14494", "14495",
+  "14496", "14497", "14498", "14499", "14500", "14501", "14502", "14503",
+  "14504", "14505", "14506", "14507", "14508", "14509", "14510", "14511",
+  "14512", "14513", "14514", "14515", "14516", "14517", "14518", "14519",
+  "14520", "14521", "14522", "14523", "14524", "14525", "14526", "14527",
+  "14528", "14529", "14530", "14531", "14532", "14533", "14534", "14535",
+  "14536", "14537", "14538", "14539", "14540", "14541", "14542", "14543",
+  "14544", "14545", "14546", "14547", "14548", "14549", "14550", "14551",
+  "14552", "14553", "14554", "14555", "14556", "14557", "14558", "14559",
+  "14560", "14561", "14562", "14563", "14564", "14565", "14566", "14567",
+  "14568", "14569", "14570", "14571", "14572", "14573", "14574", "14575",
+  "14576", "14577", "14578", "14579", "14580", "14581", "14582", "14583",
+  "14584", "14585", "14586", "14587", "14588", "14589", "14590", "14591",
+  "14592", "14593", "14594", "14595", "14596", "14597", "14598", "14599",
+  "14600", "14601", "14602", "14603", "14604", "14605", "14606", "14607",
+  "14608", "14609", "14610", "14611", "14612", "14613", "14614", "14615",
+  "14616", "14617", "14618", "14619", "14620", "14621", "14622", "14623",
+  "14624", "14625", "14626", "14627", "14628", "14629", "14630", "14631",
+  "14632", "14633", "14634", "14635", "14636", "14637", "14638", "14639",
+  "14640", "14641", "14642", "14643", "14644", "14645", "14646", "14647",
+  "14648", "14649", "14650", "14651", "14652", "14653", "14654", "14655",
+  "14656", "14657", "14658", "14659", "14660", "14661", "14662", "14663",
+  "14664", "14665", "14666", "14667", "14668", "14669", "14670", "14671",
+  "14672", "14673", "14674", "14675", "14676", "14677", "14678", "14679",
+  "14680", "14681", "14682", "14683", "14684", "14685", "14686", "14687",
+  "14688", "14689", "14690", "14691", "14692", "14693", "14694", "14695",
+  "14696", "14697", "14698", "14699", "14700", "14701", "14702", "14703",
+  "14704", "14705", "14706", "14707", "14708", "14709", "14710", "14711",
+  "14712", "14713", "14714", "14715", "14716", "14717", "14718", "14719",
+  "14720", "14721", "14722", "14723", "14724", "14725", "14726", "14727",
+  "14728", "14729", "14730", "14731", "14732", "14733", "14734", "14735",
+  "14736", "14737", "14738", "14739", "14740", "14741", "14742", "14743",
+  "14744", "14745", "14746", "14747", "14748", "14749", "14750", "14751",
+  "14752", "14753", "14754", "14755", "14756", "14757", "14758", "14759",
+  "14760", "14761", "14762", "14763", "14764", "14765", "14766", "14767",
+  "14768", "14769", "14770", "14771", "14772", "14773", "14774", "14775",
+  "14776", "14777", "14778", "14779", "14780", "14781", "14782", "14783",
+  "14784", "14785", "14786", "14787", "14788", "14789", "14790", "14791",
+  "14792", "14793", "14794", "14795", "14796", "14797", "14798", "14799",
+  "14800", "14801", "14802", "14803", "14804", "14805", "14806", "14807",
+  "14808", "14809", "14810", "14811", "14812", "14813", "14814", "14815",
+  "14816", "14817", "14818", "14819", "14820", "14821", "14822", "14823",
+  "14824", "14825", "14826", "14827", "14828", "14829", "14830", "14831",
+  "14832", "14833", "14834", "14835", "14836", "14837", "14838", "14839",
+  "14840", "14841", "14842", "14843", "14844", "14845", "14846", "14847",
+  "14848", "14849", "14850", "14851", "14852", "14853", "14854", "14855",
+  "14856", "14857", "14858", "14859", "14860", "14861", "14862", "14863",
+  "14864", "14865", "14866", "14867", "14868", "14869", "14870", "14871",
+  "14872", "14873", "14874", "14875", "14876", "14877", "14878", "14879",
+  "14880", "14881", "14882", "14883", "14884", "14885", "14886", "14887",
+  "14888", "14889", "14890", "14891", "14892", "14893", "14894", "14895",
+  "14896", "14897", "14898", "14899", "14900", "14901", "14902", "14903",
+  "14904", "14905", "14906", "14907", "14908", "14909", "14910", "14911",
+  "14912", "14913", "14914", "14915", "14916", "14917", "14918", "14919",
+  "14920", "14921", "14922", "14923", "14924", "14925", "14926", "14927",
+  "14928", "14929", "14930", "14931", "14932", "14933", "14934", "14935",
+  "14936", "14937", "14938", "14939", "14940", "14941", "14942", "14943",
+  "14944", "14945", "14946", "14947", "14948", "14949", "14950", "14951",
+  "14952", "14953", "14954", "14955", "14956", "14957", "14958", "14959",
+  "14960", "14961", "14962", "14963", "14964", "14965", "14966", "14967",
+  "14968", "14969", "14970", "14971", "14972", "14973", "14974", "14975",
+  "14976", "14977", "14978", "14979", "14980", "14981", "14982", "14983",
+  "14984", "14985", "14986", "14987", "14988", "14989", "14990", "14991",
+  "14992", "14993", "14994", "14995", "14996", "14997", "14998", "14999",
+  "15000", "15001", "15002", "15003", "15004", "15005", "15006", "15007",
+  "15008", "15009", "15010", "15011", "15012", "15013", "15014", "15015",
+  "15016", "15017", "15018", "15019", "15020", "15021", "15022", "15023",
+  "15024", "15025", "15026", "15027", "15028", "15029", "15030", "15031",
+  "15032", "15033", "15034", "15035", "15036", "15037", "15038", "15039",
+  "15040", "15041", "15042", "15043", "15044", "15045", "15046", "15047",
+  "15048", "15049", "15050", "15051", "15052", "15053", "15054", "15055",
+  "15056", "15057", "15058", "15059", "15060", "15061", "15062", "15063",
+  "15064", "15065", "15066", "15067", "15068", "15069", "15070", "15071",
+  "15072", "15073", "15074", "15075", "15076", "15077", "15078", "15079",
+  "15080", "15081", "15082", "15083", "15084", "15085", "15086", "15087",
+  "15088", "15089", "15090", "15091", "15092", "15093", "15094", "15095",
+  "15096", "15097", "15098", "15099", "15100", "15101", "15102", "15103",
+  "15104", "15105", "15106", "15107", "15108", "15109", "15110", "15111",
+  "15112", "15113", "15114", "15115", "15116", "15117", "15118", "15119",
+  "15120", "15121", "15122", "15123", "15124", "15125", "15126", "15127",
+  "15128", "15129", "15130", "15131", "15132", "15133", "15134", "15135",
+  "15136", "15137", "15138", "15139", "15140", "15141", "15142", "15143",
+  "15144", "15145", "15146", "15147", "15148", "15149", "15150", "15151",
+  "15152", "15153", "15154", "15155", "15156", "15157", "15158", "15159",
+  "15160", "15161", "15162", "15163", "15164", "15165", "15166", "15167",
+  "15168", "15169", "15170", "15171", "15172", "15173", "15174", "15175",
+  "15176", "15177", "15178", "15179", "15180", "15181", "15182", "15183",
+  "15184", "15185", "15186", "15187", "15188", "15189", "15190", "15191",
+  "15192", "15193", "15194", "15195", "15196", "15197", "15198", "15199",
+  "15200", "15201", "15202", "15203", "15204", "15205", "15206", "15207",
+  "15208", "15209", "15210", "15211", "15212", "15213", "15214", "15215",
+  "15216", "15217", "15218", "15219", "15220", "15221", "15222", "15223",
+  "15224", "15225", "15226", "15227", "15228", "15229", "15230", "15231",
+  "15232", "15233", "15234", "15235", "15236", "15237", "15238", "15239",
+  "15240", "15241", "15242", "15243", "15244", "15245", "15246", "15247",
+  "15248", "15249", "15250", "15251", "15252", "15253", "15254", "15255",
+  "15256", "15257", "15258", "15259", "15260", "15261", "15262", "15263",
+  "15264", "15265", "15266", "15267", "15268", "15269", "15270", "15271",
+  "15272", "15273", "15274", "15275", "15276", "15277", "15278", "15279",
+  "15280", "15281", "15282", "15283", "15284", "15285", "15286", "15287",
+  "15288", "15289", "15290", "15291", "15292", "15293", "15294", "15295",
+  "15296", "15297", "15298", "15299", "15300", "15301", "15302", "15303",
+  "15304", "15305", "15306", "15307", "15308", "15309", "15310", "15311",
+  "15312", "15313", "15314", "15315", "15316", "15317", "15318", "15319",
+  "15320", "15321", "15322", "15323", "15324", "15325", "15326", "15327",
+  "15328", "15329", "15330", "15331", "15332", "15333", "15334", "15335",
+  "15336", "15337", "15338", "15339", "15340", "15341", "15342", "15343",
+  "15344", "15345", "15346", "15347", "15348", "15349", "15350", "15351",
+  "15352", "15353", "15354", "15355", "15356", "15357", "15358", "15359",
+  "15360", "15361", "15362", "15363", "15364", "15365", "15366", "15367",
+  "15368", "15369", "15370", "15371", "15372", "15373", "15374", "15375",
+  "15376", "15377", "15378", "15379", "15380", "15381", "15382", "15383",
+  "15384", "15385", "15386", "15387", "15388", "15389", "15390", "15391",
+  "15392", "15393", "15394", "15395", "15396", "15397", "15398", "15399",
+  "15400", "15401", "15402", "15403", "15404", "15405", "15406", "15407",
+  "15408", "15409", "15410", "15411", "15412", "15413", "15414", "15415",
+  "15416", "15417", "15418", "15419", "15420", "15421", "15422", "15423",
+  "15424", "15425", "15426", "15427", "15428", "15429", "15430", "15431",
+  "15432", "15433", "15434", "15435", "15436", "15437", "15438", "15439",
+  "15440", "15441", "15442", "15443", "15444", "15445", "15446", "15447",
+  "15448", "15449", "15450", "15451", "15452", "15453", "15454", "15455",
+  "15456", "15457", "15458", "15459", "15460", "15461", "15462", "15463",
+  "15464", "15465", "15466", "15467", "15468", "15469", "15470", "15471",
+  "15472", "15473", "15474", "15475", "15476", "15477", "15478", "15479",
+  "15480", "15481", "15482", "15483", "15484", "15485", "15486", "15487",
+  "15488", "15489", "15490", "15491", "15492", "15493", "15494", "15495",
+  "15496", "15497", "15498", "15499", "15500", "15501", "15502", "15503",
+  "15504", "15505", "15506", "15507", "15508", "15509", "15510", "15511",
+  "15512", "15513", "15514", "15515", "15516", "15517", "15518", "15519",
+  "15520", "15521", "15522", "15523", "15524", "15525", "15526", "15527",
+  "15528", "15529", "15530", "15531", "15532", "15533", "15534", "15535",
+  "15536", "15537", "15538", "15539", "15540", "15541", "15542", "15543",
+  "15544", "15545", "15546", "15547", "15548", "15549", "15550", "15551",
+  "15552", "15553", "15554", "15555", "15556", "15557", "15558", "15559",
+  "15560", "15561", "15562", "15563", "15564", "15565", "15566", "15567",
+  "15568", "15569", "15570", "15571", "15572", "15573", "15574", "15575",
+  "15576", "15577", "15578", "15579", "15580", "15581", "15582", "15583",
+  "15584", "15585", "15586", "15587", "15588", "15589", "15590", "15591",
+  "15592", "15593", "15594", "15595", "15596", "15597", "15598", "15599",
+  "15600", "15601", "15602", "15603", "15604", "15605", "15606", "15607",
+  "15608", "15609", "15610", "15611", "15612", "15613", "15614", "15615",
+  "15616", "15617", "15618", "15619", "15620", "15621", "15622", "15623",
+  "15624", "15625", "15626", "15627", "15628", "15629", "15630", "15631",
+  "15632", "15633", "15634", "15635", "15636", "15637", "15638", "15639",
+  "15640", "15641", "15642", "15643", "15644", "15645", "15646", "15647",
+  "15648", "15649", "15650", "15651", "15652", "15653", "15654", "15655",
+  "15656", "15657", "15658", "15659", "15660", "15661", "15662", "15663",
+  "15664", "15665", "15666", "15667", "15668", "15669", "15670", "15671",
+  "15672", "15673", "15674", "15675", "15676", "15677", "15678", "15679",
+  "15680", "15681", "15682", "15683", "15684", "15685", "15686", "15687",
+  "15688", "15689", "15690", "15691", "15692", "15693", "15694", "15695",
+  "15696", "15697", "15698", "15699", "15700", "15701", "15702", "15703",
+  "15704", "15705", "15706", "15707", "15708", "15709", "15710", "15711",
+  "15712", "15713", "15714", "15715", "15716", "15717", "15718", "15719",
+  "15720", "15721", "15722", "15723", "15724", "15725", "15726", "15727",
+  "15728", "15729", "15730", "15731", "15732", "15733", "15734", "15735",
+  "15736", "15737", "15738", "15739", "15740", "15741", "15742", "15743",
+  "15744", "15745", "15746", "15747", "15748", "15749", "15750", "15751",
+  "15752", "15753", "15754", "15755", "15756", "15757", "15758", "15759",
+  "15760", "15761", "15762", "15763", "15764", "15765", "15766", "15767",
+  "15768", "15769", "15770", "15771", "15772", "15773", "15774", "15775",
+  "15776", "15777", "15778", "15779", "15780", "15781", "15782", "15783",
+  "15784", "15785", "15786", "15787", "15788", "15789", "15790", "15791",
+  "15792", "15793", "15794", "15795", "15796", "15797", "15798", "15799",
+  "15800", "15801", "15802", "15803", "15804", "15805", "15806", "15807",
+  "15808", "15809", "15810", "15811", "15812", "15813", "15814", "15815",
+  "15816", "15817", "15818", "15819", "15820", "15821", "15822", "15823",
+  "15824", "15825", "15826", "15827", "15828", "15829", "15830", "15831",
+  "15832", "15833", "15834", "15835", "15836", "15837", "15838", "15839",
+  "15840", "15841", "15842", "15843", "15844", "15845", "15846", "15847",
+  "15848", "15849", "15850", "15851", "15852", "15853", "15854", "15855",
+  "15856", "15857", "15858", "15859", "15860", "15861", "15862", "15863",
+  "15864", "15865", "15866", "15867", "15868", "15869", "15870", "15871",
+  "15872", "15873", "15874", "15875", "15876", "15877", "15878", "15879",
+  "15880", "15881", "15882", "15883", "15884", "15885", "15886", "15887",
+  "15888", "15889", "15890", "15891", "15892", "15893", "15894", "15895",
+  "15896", "15897", "15898", "15899", "15900", "15901", "15902", "15903",
+  "15904", "15905", "15906", "15907", "15908", "15909", "15910", "15911",
+  "15912", "15913", "15914", "15915", "15916", "15917", "15918", "15919",
+  "15920", "15921", "15922", "15923", "15924", "15925", "15926", "15927",
+  "15928", "15929", "15930", "15931", "15932", "15933", "15934", "15935",
+  "15936", "15937", "15938", "15939", "15940", "15941", "15942", "15943",
+  "15944", "15945", "15946", "15947", "15948", "15949", "15950", "15951",
+  "15952", "15953", "15954", "15955", "15956", "15957", "15958", "15959",
+  "15960", "15961", "15962", "15963", "15964", "15965", "15966", "15967",
+  "15968", "15969", "15970", "15971", "15972", "15973", "15974", "15975",
+  "15976", "15977", "15978", "15979", "15980", "15981", "15982", "15983",
+  "15984", "15985", "15986", "15987", "15988", "15989", "15990", "15991",
+  "15992", "15993", "15994", "15995", "15996", "15997", "15998", "15999",
+  "16000", "16001", "16002", "16003", "16004", "16005", "16006", "16007",
+  "16008", "16009", "16010", "16011", "16012", "16013", "16014", "16015",
+  "16016", "16017", "16018", "16019", "16020", "16021", "16022", "16023",
+  "16024", "16025", "16026", "16027", "16028", "16029", "16030", "16031",
+  "16032", "16033", "16034", "16035", "16036", "16037", "16038", "16039",
+  "16040", "16041", "16042", "16043", "16044", "16045", "16046", "16047",
+  "16048", "16049", "16050", "16051", "16052", "16053", "16054", "16055",
+  "16056", "16057", "16058", "16059", "16060", "16061", "16062", "16063",
+  "16064", "16065", "16066", "16067", "16068", "16069", "16070", "16071",
+  "16072", "16073", "16074", "16075", "16076", "16077", "16078", "16079",
+  "16080", "16081", "16082", "16083", "16084", "16085", "16086", "16087",
+  "16088", "16089", "16090", "16091", "16092", "16093", "16094", "16095",
+  "16096", "16097", "16098", "16099", "16100", "16101", "16102", "16103",
+  "16104", "16105", "16106", "16107", "16108", "16109", "16110", "16111",
+  "16112", "16113", "16114", "16115", "16116", "16117", "16118", "16119",
+  "16120", "16121", "16122", "16123", "16124", "16125", "16126", "16127",
+  "16128", "16129", "16130", "16131", "16132", "16133", "16134", "16135",
+  "16136", "16137", "16138", "16139", "16140", "16141", "16142", "16143",
+  "16144", "16145", "16146", "16147", "16148", "16149", "16150", "16151",
+  "16152", "16153", "16154", "16155", "16156", "16157", "16158", "16159",
+  "16160", "16161", "16162", "16163", "16164", "16165", "16166", "16167",
+  "16168", "16169", "16170", "16171", "16172", "16173", "16174", "16175",
+  "16176", "16177", "16178", "16179", "16180", "16181", "16182", "16183",
+  "16184", "16185", "16186", "16187", "16188", "16189", "16190", "16191",
+  "16192", "16193", "16194", "16195", "16196", "16197", "16198", "16199",
+  "16200", "16201", "16202", "16203", "16204", "16205", "16206", "16207",
+  "16208", "16209", "16210", "16211", "16212", "16213", "16214", "16215",
+  "16216", "16217", "16218", "16219", "16220", "16221", "16222", "16223",
+  "16224", "16225", "16226", "16227", "16228", "16229", "16230", "16231",
+  "16232", "16233", "16234", "16235", "16236", "16237", "16238", "16239",
+  "16240", "16241", "16242", "16243", "16244", "16245", "16246", "16247",
+  "16248", "16249", "16250", "16251", "16252", "16253", "16254", "16255",
+  "16256", "16257", "16258", "16259", "16260", "16261", "16262", "16263",
+  "16264", "16265", "16266", "16267", "16268", "16269", "16270", "16271",
+  "16272", "16273", "16274", "16275", "16276", "16277", "16278", "16279",
+  "16280", "16281", "16282", "16283", "16284", "16285", "16286", "16287",
+  "16288", "16289", "16290", "16291", "16292", "16293", "16294", "16295",
+  "16296", "16297", "16298", "16299", "16300", "16301", "16302", "16303",
+  "16304", "16305", "16306", "16307", "16308", "16309", "16310", "16311",
+  "16312", "16313", "16314", "16315", "16316", "16317", "16318", "16319",
+  "16320", "16321", "16322", "16323", "16324", "16325", "16326", "16327",
+  "16328", "16329", "16330", "16331", "16332", "16333", "16334", "16335",
+  "16336", "16337", "16338", "16339", "16340", "16341", "16342", "16343",
+  "16344", "16345", "16346", "16347", "16348", "16349", "16350", "16351",
+  "16352", "16353", "16354", "16355", "16356", "16357", "16358", "16359",
+  "16360", "16361", "16362", "16363", "16364", "16365", "16366", "16367",
+  "16368", "16369", "16370", "16371", "16372", "16373", "16374", "16375",
+  "16376", "16377", "16378", "16379", "16380", "16381", "16382", "16383",
+  "16384", "16385", "16386", "16387", "16388", "16389", "16390", "16391",
+  "16392", "16393", "16394", "16395", "16396", "16397", "16398", "16399",
+  "16400", "16401", "16402", "16403", "16404", "16405", "16406", "16407",
+  "16408", "16409", "16410", "16411", "16412", "16413", "16414", "16415",
+  "16416", "16417", "16418", "16419", "16420", "16421", "16422", "16423",
+  "16424", "16425", "16426", "16427", "16428", "16429", "16430", "16431",
+  "16432", "16433", "16434", "16435", "16436", "16437", "16438", "16439",
+  "16440", "16441", "16442", "16443", "16444", "16445", "16446", "16447",
+  "16448", "16449", "16450", "16451", "16452", "16453", "16454", "16455",
+  "16456", "16457", "16458", "16459", "16460", "16461", "16462", "16463",
+  "16464", "16465", "16466", "16467", "16468", "16469", "16470", "16471",
+  "16472", "16473", "16474", "16475", "16476", "16477", "16478", "16479",
+  "16480", "16481", "16482", "16483", "16484", "16485", "16486", "16487",
+  "16488", "16489", "16490", "16491", "16492", "16493", "16494", "16495",
+  "16496", "16497", "16498", "16499", "16500", "16501", "16502", "16503",
+  "16504", "16505", "16506", "16507", "16508", "16509", "16510", "16511",
+  "16512", "16513", "16514", "16515", "16516", "16517", "16518", "16519",
+  "16520", "16521", "16522", "16523", "16524", "16525", "16526", "16527",
+  "16528", "16529", "16530", "16531", "16532", "16533", "16534", "16535",
+  "16536", "16537", "16538", "16539", "16540", "16541", "16542", "16543",
+  "16544", "16545", "16546", "16547", "16548", "16549", "16550", "16551",
+  "16552", "16553", "16554", "16555", "16556", "16557", "16558", "16559",
+  "16560", "16561", "16562", "16563", "16564", "16565", "16566", "16567",
+  "16568", "16569", "16570", "16571", "16572", "16573", "16574", "16575",
+  "16576", "16577", "16578", "16579", "16580", "16581", "16582", "16583",
+  "16584", "16585", "16586", "16587", "16588", "16589", "16590", "16591",
+  "16592", "16593", "16594", "16595", "16596", "16597", "16598", "16599",
+  "16600", "16601", "16602", "16603", "16604", "16605", "16606", "16607",
+  "16608", "16609", "16610", "16611", "16612", "16613", "16614", "16615",
+  "16616", "16617", "16618", "16619", "16620", "16621", "16622", "16623",
+  "16624", "16625", "16626", "16627", "16628", "16629", "16630", "16631",
+  "16632", "16633", "16634", "16635", "16636", "16637", "16638", "16639",
+  "16640", "16641", "16642", "16643", "16644", "16645", "16646", "16647",
+  "16648", "16649", "16650", "16651", "16652", "16653", "16654", "16655",
+  "16656", "16657", "16658", "16659", "16660", "16661", "16662", "16663",
+  "16664", "16665", "16666", "16667", "16668", "16669", "16670", "16671",
+  "16672", "16673", "16674", "16675", "16676", "16677", "16678", "16679",
+  "16680", "16681", "16682", "16683", "16684", "16685", "16686", "16687",
+  "16688", "16689", "16690", "16691", "16692", "16693", "16694", "16695",
+  "16696", "16697", "16698", "16699", "16700", "16701", "16702", "16703",
+  "16704", "16705", "16706", "16707", "16708", "16709", "16710", "16711",
+  "16712", "16713", "16714", "16715", "16716", "16717", "16718", "16719",
+  "16720", "16721", "16722", "16723", "16724", "16725", "16726", "16727",
+  "16728", "16729", "16730", "16731", "16732", "16733", "16734", "16735",
+  "16736", "16737", "16738", "16739", "16740", "16741", "16742", "16743",
+  "16744", "16745", "16746", "16747", "16748", "16749", "16750", "16751",
+  "16752", "16753", "16754", "16755", "16756", "16757", "16758", "16759",
+  "16760", "16761", "16762", "16763", "16764", "16765", "16766", "16767",
+  "16768", "16769", "16770", "16771", "16772", "16773", "16774", "16775",
+  "16776", "16777", "16778", "16779", "16780", "16781", "16782", "16783",
+  "16784", "16785", "16786", "16787", "16788", "16789", "16790", "16791",
+  "16792", "16793", "16794", "16795", "16796", "16797", "16798", "16799",
+  "16800", "16801", "16802", "16803", "16804", "16805", "16806", "16807",
+  "16808", "16809", "16810", "16811", "16812", "16813", "16814", "16815",
+  "16816", "16817", "16818", "16819", "16820", "16821", "16822", "16823",
+  "16824", "16825", "16826", "16827", "16828", "16829", "16830", "16831",
+  "16832", "16833", "16834", "16835", "16836", "16837", "16838", "16839",
+  "16840", "16841", "16842", "16843", "16844", "16845", "16846", "16847",
+  "16848", "16849", "16850", "16851", "16852", "16853", "16854", "16855",
+  "16856", "16857", "16858", "16859", "16860", "16861", "16862", "16863",
+  "16864", "16865", "16866", "16867", "16868", "16869", "16870", "16871",
+  "16872", "16873", "16874", "16875", "16876", "16877", "16878", "16879",
+  "16880", "16881", "16882", "16883", "16884", "16885", "16886", "16887",
+  "16888", "16889", "16890", "16891", "16892", "16893", "16894", "16895",
+  "16896", "16897", "16898", "16899", "16900", "16901", "16902", "16903",
+  "16904", "16905", "16906", "16907", "16908", "16909", "16910", "16911",
+  "16912", "16913", "16914", "16915", "16916", "16917", "16918", "16919",
+  "16920", "16921", "16922", "16923", "16924", "16925", "16926", "16927",
+  "16928", "16929", "16930", "16931", "16932", "16933", "16934", "16935",
+  "16936", "16937", "16938", "16939", "16940", "16941", "16942", "16943",
+  "16944", "16945", "16946", "16947", "16948", "16949", "16950", "16951",
+  "16952", "16953", "16954", "16955", "16956", "16957", "16958", "16959",
+  "16960", "16961", "16962", "16963", "16964", "16965", "16966", "16967",
+  "16968", "16969", "16970", "16971", "16972", "16973", "16974", "16975",
+  "16976", "16977", "16978", "16979", "16980", "16981", "16982", "16983",
+  "16984", "16985", "16986", "16987", "16988", "16989", "16990", "16991",
+  "16992", "16993", "16994", "16995", "16996", "16997", "16998", "16999",
+  "17000", "17001", "17002", "17003", "17004", "17005", "17006", "17007",
+  "17008", "17009", "17010", "17011", "17012", "17013", "17014", "17015",
+  "17016", "17017", "17018", "17019", "17020", "17021", "17022", "17023",
+  "17024", "17025", "17026", "17027", "17028", "17029", "17030", "17031",
+  "17032", "17033", "17034", "17035", "17036", "17037", "17038", "17039",
+  "17040", "17041", "17042", "17043", "17044", "17045", "17046", "17047",
+  "17048", "17049", "17050", "17051", "17052", "17053", "17054", "17055",
+  "17056", "17057", "17058", "17059", "17060", "17061", "17062", "17063",
+  "17064", "17065", "17066", "17067", "17068", "17069", "17070", "17071",
+  "17072", "17073", "17074", "17075", "17076", "17077", "17078", "17079",
+  "17080", "17081", "17082", "17083", "17084", "17085", "17086", "17087",
+  "17088", "17089", "17090", "17091", "17092", "17093", "17094", "17095",
+  "17096", "17097", "17098", "17099", "17100", "17101", "17102", "17103",
+  "17104", "17105", "17106", "17107", "17108", "17109", "17110", "17111",
+  "17112", "17113", "17114", "17115", "17116", "17117", "17118", "17119",
+  "17120", "17121", "17122", "17123", "17124", "17125", "17126", "17127",
+  "17128", "17129", "17130", "17131", "17132", "17133", "17134", "17135",
+  "17136", "17137", "17138", "17139", "17140", "17141", "17142", "17143",
+  "17144", "17145", "17146", "17147", "17148", "17149", "17150", "17151",
+  "17152", "17153", "17154", "17155", "17156", "17157", "17158", "17159",
+  "17160", "17161", "17162", "17163", "17164", "17165", "17166", "17167",
+  "17168", "17169", "17170", "17171", "17172", "17173", "17174", "17175",
+  "17176", "17177", "17178", "17179", "17180", "17181", "17182", "17183",
+  "17184", "17185", "17186", "17187", "17188", "17189", "17190", "17191",
+  "17192", "17193", "17194", "17195", "17196", "17197", "17198", "17199",
+  "17200", "17201", "17202", "17203", "17204", "17205", "17206", "17207",
+  "17208", "17209", "17210", "17211", "17212", "17213", "17214", "17215",
+  "17216", "17217", "17218", "17219", "17220", "17221", "17222", "17223",
+  "17224", "17225", "17226", "17227", "17228", "17229", "17230", "17231",
+  "17232", "17233", "17234", "17235", "17236", "17237", "17238", "17239",
+  "17240", "17241", "17242", "17243", "17244", "17245", "17246", "17247",
+  "17248", "17249", "17250", "17251", "17252", "17253", "17254", "17255",
+  "17256", "17257", "17258", "17259", "17260", "17261", "17262", "17263",
+  "17264", "17265", "17266", "17267", "17268", "17269", "17270", "17271",
+  "17272", "17273", "17274", "17275", "17276", "17277", "17278", "17279",
+  "17280", "17281", "17282", "17283", "17284", "17285", "17286", "17287",
+  "17288", "17289", "17290", "17291", "17292", "17293", "17294", "17295",
+  "17296", "17297", "17298", "17299", "17300", "17301", "17302", "17303",
+  "17304", "17305", "17306", "17307", "17308", "17309", "17310", "17311",
+  "17312", "17313", "17314", "17315", "17316", "17317", "17318", "17319",
+  "17320", "17321", "17322", "17323", "17324", "17325", "17326", "17327",
+  "17328", "17329", "17330", "17331", "17332", "17333", "17334", "17335",
+  "17336", "17337", "17338", "17339", "17340", "17341", "17342", "17343",
+  "17344", "17345", "17346", "17347", "17348", "17349", "17350", "17351",
+  "17352", "17353", "17354", "17355", "17356", "17357", "17358", "17359",
+  "17360", "17361", "17362", "17363", "17364", "17365", "17366", "17367",
+  "17368", "17369", "17370", "17371", "17372", "17373", "17374", "17375",
+  "17376", "17377", "17378", "17379", "17380", "17381", "17382", "17383",
+  "17384", "17385", "17386", "17387", "17388", "17389", "17390", "17391",
+  "17392", "17393", "17394", "17395", "17396", "17397", "17398", "17399",
+  "17400", "17401", "17402", "17403", "17404", "17405", "17406", "17407",
+  "17408", "17409", "17410", "17411", "17412", "17413", "17414", "17415",
+  "17416", "17417", "17418", "17419", "17420", "17421", "17422", "17423",
+  "17424", "17425", "17426", "17427", "17428", "17429", "17430", "17431",
+  "17432", "17433", "17434", "17435", "17436", "17437", "17438", "17439",
+  "17440", "17441", "17442", "17443", "17444", "17445", "17446", "17447",
+  "17448", "17449", "17450", "17451", "17452", "17453", "17454", "17455",
+  "17456", "17457", "17458", "17459", "17460", "17461", "17462", "17463",
+  "17464", "17465", "17466", "17467", "17468", "17469", "17470", "17471",
+  "17472", "17473", "17474", "17475", "17476", "17477", "17478", "17479",
+  "17480", "17481", "17482", "17483", "17484", "17485", "17486", "17487",
+  "17488", "17489", "17490", "17491", "17492", "17493", "17494", "17495",
+  "17496", "17497", "17498", "17499", "17500", "17501", "17502", "17503",
+  "17504", "17505", "17506", "17507", "17508", "17509", "17510", "17511",
+  "17512", "17513", "17514", "17515", "17516", "17517", "17518", "17519",
+  "17520", "17521", "17522", "17523", "17524", "17525", "17526", "17527",
+  "17528", "17529", "17530", "17531", "17532", "17533", "17534", "17535",
+  "17536", "17537", "17538", "17539", "17540", "17541", "17542", "17543",
+  "17544", "17545", "17546", "17547", "17548", "17549", "17550", "17551",
+  "17552", "17553", "17554", "17555", "17556", "17557", "17558", "17559",
+  "17560", "17561", "17562", "17563", "17564", "17565", "17566", "17567",
+  "17568", "17569", "17570", "17571", "17572", "17573", "17574", "17575",
+  "17576", "17577", "17578", "17579", "17580", "17581", "17582", "17583",
+  "17584", "17585", "17586", "17587", "17588", "17589", "17590", "17591",
+  "17592", "17593", "17594", "17595", "17596", "17597", "17598", "17599",
+  "17600", "17601", "17602", "17603", "17604", "17605", "17606", "17607",
+  "17608", "17609", "17610", "17611", "17612", "17613", "17614", "17615",
+  "17616", "17617", "17618", "17619", "17620", "17621", "17622", "17623",
+  "17624", "17625", "17626", "17627", "17628", "17629", "17630", "17631",
+  "17632", "17633", "17634", "17635", "17636", "17637", "17638", "17639",
+  "17640", "17641", "17642", "17643", "17644", "17645", "17646", "17647",
+  "17648", "17649", "17650", "17651", "17652", "17653", "17654", "17655",
+  "17656", "17657", "17658", "17659", "17660", "17661", "17662", "17663",
+  "17664", "17665", "17666", "17667", "17668", "17669", "17670", "17671",
+  "17672", "17673", "17674", "17675", "17676", "17677", "17678", "17679",
+  "17680", "17681", "17682", "17683", "17684", "17685", "17686", "17687",
+  "17688", "17689", "17690", "17691", "17692", "17693", "17694", "17695",
+  "17696", "17697", "17698", "17699", "17700", "17701", "17702", "17703",
+  "17704", "17705", "17706", "17707", "17708", "17709", "17710", "17711",
+  "17712", "17713", "17714", "17715", "17716", "17717", "17718", "17719",
+  "17720", "17721", "17722", "17723", "17724", "17725", "17726", "17727",
+  "17728", "17729", "17730", "17731", "17732", "17733", "17734", "17735",
+  "17736", "17737", "17738", "17739", "17740", "17741", "17742", "17743",
+  "17744", "17745", "17746", "17747", "17748", "17749", "17750", "17751",
+  "17752", "17753", "17754", "17755", "17756", "17757", "17758", "17759",
+  "17760", "17761", "17762", "17763", "17764", "17765", "17766", "17767",
+  "17768", "17769", "17770", "17771", "17772", "17773", "17774", "17775",
+  "17776", "17777", "17778", "17779", "17780", "17781", "17782", "17783",
+  "17784", "17785", "17786", "17787", "17788", "17789", "17790", "17791",
+  "17792", "17793", "17794", "17795", "17796", "17797", "17798", "17799",
+  "17800", "17801", "17802", "17803", "17804", "17805", "17806", "17807",
+  "17808", "17809", "17810", "17811", "17812", "17813", "17814", "17815",
+  "17816", "17817", "17818", "17819", "17820", "17821", "17822", "17823",
+  "17824", "17825", "17826", "17827", "17828", "17829", "17830", "17831",
+  "17832", "17833", "17834", "17835", "17836", "17837", "17838", "17839",
+  "17840", "17841", "17842", "17843", "17844", "17845", "17846", "17847",
+  "17848", "17849", "17850", "17851", "17852", "17853", "17854", "17855",
+  "17856", "17857", "17858", "17859", "17860", "17861", "17862", "17863",
+  "17864", "17865", "17866", "17867", "17868", "17869", "17870", "17871",
+  "17872", "17873", "17874", "17875", "17876", "17877", "17878", "17879",
+  "17880", "17881", "17882", "17883", "17884", "17885", "17886", "17887",
+  "17888", "17889", "17890", "17891", "17892", "17893", "17894", "17895",
+  "17896", "17897", "17898", "17899", "17900", "17901", "17902", "17903",
+  "17904", "17905", "17906", "17907", "17908", "17909", "17910", "17911",
+  "17912", "17913", "17914", "17915", "17916", "17917", "17918", "17919",
+  "17920", "17921", "17922", "17923", "17924", "17925", "17926", "17927",
+  "17928", "17929", "17930", "17931", "17932", "17933", "17934", "17935",
+  "17936", "17937", "17938", "17939", "17940", "17941", "17942", "17943",
+  "17944", "17945", "17946", "17947", "17948", "17949", "17950", "17951",
+  "17952", "17953", "17954", "17955", "17956", "17957", "17958", "17959",
+  "17960", "17961", "17962", "17963", "17964", "17965", "17966", "17967",
+  "17968", "17969", "17970", "17971", "17972", "17973", "17974", "17975",
+  "17976", "17977", "17978", "17979", "17980", "17981", "17982", "17983",
+  "17984", "17985", "17986", "17987", "17988", "17989", "17990", "17991",
+  "17992", "17993", "17994", "17995", "17996", "17997", "17998", "17999",
+  "18000", "18001", "18002", "18003", "18004", "18005", "18006", "18007",
+  "18008", "18009", "18010", "18011", "18012", "18013", "18014", "18015",
+  "18016", "18017", "18018", "18019", "18020", "18021", "18022", "18023",
+  "18024", "18025", "18026", "18027", "18028", "18029", "18030", "18031",
+  "18032", "18033", "18034", "18035", "18036", "18037", "18038", "18039",
+  "18040", "18041", "18042", "18043", "18044", "18045", "18046", "18047",
+  "18048", "18049", "18050", "18051", "18052", "18053", "18054", "18055",
+  "18056", "18057", "18058", "18059", "18060", "18061", "18062", "18063",
+  "18064", "18065", "18066", "18067", "18068", "18069", "18070", "18071",
+  "18072", "18073", "18074", "18075", "18076", "18077", "18078", "18079",
+  "18080", "18081", "18082", "18083", "18084", "18085", "18086", "18087",
+  "18088", "18089", "18090", "18091", "18092", "18093", "18094", "18095",
+  "18096", "18097", "18098", "18099", "18100", "18101", "18102", "18103",
+  "18104", "18105", "18106", "18107", "18108", "18109", "18110", "18111",
+  "18112", "18113", "18114", "18115", "18116", "18117", "18118", "18119",
+  "18120", "18121", "18122", "18123", "18124", "18125", "18126", "18127",
+  "18128", "18129", "18130", "18131", "18132", "18133", "18134", "18135",
+  "18136", "18137", "18138", "18139", "18140", "18141", "18142", "18143",
+  "18144", "18145", "18146", "18147", "18148", "18149", "18150", "18151",
+  "18152", "18153", "18154", "18155", "18156", "18157", "18158", "18159",
+  "18160", "18161", "18162", "18163", "18164", "18165", "18166", "18167",
+  "18168", "18169", "18170", "18171", "18172", "18173", "18174", "18175",
+  "18176", "18177", "18178", "18179", "18180", "18181", "18182", "18183",
+  "18184", "18185", "18186", "18187", "18188", "18189", "18190", "18191",
+  "18192", "18193", "18194", "18195", "18196", "18197", "18198", "18199",
+  "18200", "18201", "18202", "18203", "18204", "18205", "18206", "18207",
+  "18208", "18209", "18210", "18211", "18212", "18213", "18214", "18215",
+  "18216", "18217", "18218", "18219", "18220", "18221", "18222", "18223",
+  "18224", "18225", "18226", "18227", "18228", "18229", "18230", "18231",
+  "18232", "18233", "18234", "18235", "18236", "18237", "18238", "18239",
+  "18240", "18241", "18242", "18243", "18244", "18245", "18246", "18247",
+  "18248", "18249", "18250", "18251", "18252", "18253", "18254", "18255",
+  "18256", "18257", "18258", "18259", "18260", "18261", "18262", "18263",
+  "18264", "18265", "18266", "18267", "18268", "18269", "18270", "18271",
+  "18272", "18273", "18274", "18275", "18276", "18277", "18278", "18279",
+  "18280", "18281", "18282", "18283", "18284", "18285", "18286", "18287",
+  "18288", "18289", "18290", "18291", "18292", "18293", "18294", "18295",
+  "18296", "18297", "18298", "18299", "18300", "18301", "18302", "18303",
+  "18304", "18305", "18306", "18307", "18308", "18309", "18310", "18311",
+  "18312", "18313", "18314", "18315", "18316", "18317", "18318", "18319",
+  "18320", "18321", "18322", "18323", "18324", "18325", "18326", "18327",
+  "18328", "18329", "18330", "18331", "18332", "18333", "18334", "18335",
+  "18336", "18337", "18338", "18339", "18340", "18341", "18342", "18343",
+  "18344", "18345", "18346", "18347", "18348", "18349", "18350", "18351",
+  "18352", "18353", "18354", "18355", "18356", "18357", "18358", "18359",
+  "18360", "18361", "18362", "18363", "18364", "18365", "18366", "18367",
+  "18368", "18369", "18370", "18371", "18372", "18373", "18374", "18375",
+  "18376", "18377", "18378", "18379", "18380", "18381", "18382", "18383",
+  "18384", "18385", "18386", "18387", "18388", "18389", "18390", "18391",
+  "18392", "18393", "18394", "18395", "18396", "18397", "18398", "18399",
+  "18400", "18401", "18402", "18403", "18404", "18405", "18406", "18407",
+  "18408", "18409", "18410", "18411", "18412", "18413", "18414", "18415",
+  "18416", "18417", "18418", "18419", "18420", "18421", "18422", "18423",
+  "18424", "18425", "18426", "18427", "18428", "18429", "18430", "18431",
+  "18432", "18433", "18434", "18435", "18436", "18437", "18438", "18439",
+  "18440", "18441", "18442", "18443", "18444", "18445", "18446", "18447",
+  "18448", "18449", "18450", "18451", "18452", "18453", "18454", "18455",
+  "18456", "18457", "18458", "18459", "18460", "18461", "18462", "18463",
+  "18464", "18465", "18466", "18467", "18468", "18469", "18470", "18471",
+  "18472", "18473", "18474", "18475", "18476", "18477", "18478", "18479",
+  "18480", "18481", "18482", "18483", "18484", "18485", "18486", "18487",
+  "18488", "18489", "18490", "18491", "18492", "18493", "18494", "18495",
+  "18496", "18497", "18498", "18499", "18500", "18501", "18502", "18503",
+  "18504", "18505", "18506", "18507", "18508", "18509", "18510", "18511",
+  "18512", "18513", "18514", "18515", "18516", "18517", "18518", "18519",
+  "18520", "18521", "18522", "18523", "18524", "18525", "18526", "18527",
+  "18528", "18529", "18530", "18531", "18532", "18533", "18534", "18535",
+  "18536", "18537", "18538", "18539", "18540", "18541", "18542", "18543",
+  "18544", "18545", "18546", "18547", "18548", "18549", "18550", "18551",
+  "18552", "18553", "18554", "18555", "18556", "18557", "18558", "18559",
+  "18560", "18561", "18562", "18563", "18564", "18565", "18566", "18567",
+  "18568", "18569", "18570", "18571", "18572", "18573", "18574", "18575",
+  "18576", "18577", "18578", "18579", "18580", "18581", "18582", "18583",
+  "18584", "18585", "18586", "18587", "18588", "18589", "18590", "18591",
+  "18592", "18593", "18594", "18595", "18596", "18597", "18598", "18599",
+  "18600", "18601", "18602", "18603", "18604", "18605", "18606", "18607",
+  "18608", "18609", "18610", "18611", "18612", "18613", "18614", "18615",
+  "18616", "18617", "18618", "18619", "18620", "18621", "18622", "18623",
+  "18624", "18625", "18626", "18627", "18628", "18629", "18630", "18631",
+  "18632", "18633", "18634", "18635", "18636", "18637", "18638", "18639",
+  "18640", "18641", "18642", "18643", "18644", "18645", "18646", "18647",
+  "18648", "18649", "18650", "18651", "18652", "18653", "18654", "18655",
+  "18656", "18657", "18658", "18659", "18660", "18661", "18662", "18663",
+  "18664", "18665", "18666", "18667", "18668", "18669", "18670", "18671",
+  "18672", "18673", "18674", "18675", "18676", "18677", "18678", "18679",
+  "18680", "18681", "18682", "18683", "18684", "18685", "18686", "18687",
+  "18688", "18689", "18690", "18691", "18692", "18693", "18694", "18695",
+  "18696", "18697", "18698", "18699", "18700", "18701", "18702", "18703",
+  "18704", "18705", "18706", "18707", "18708", "18709", "18710", "18711",
+  "18712", "18713", "18714", "18715", "18716", "18717", "18718", "18719",
+  "18720", "18721", "18722", "18723", "18724", "18725", "18726", "18727",
+  "18728", "18729", "18730", "18731", "18732", "18733", "18734", "18735",
+  "18736", "18737", "18738", "18739", "18740", "18741", "18742", "18743",
+  "18744", "18745", "18746", "18747", "18748", "18749", "18750", "18751",
+  "18752", "18753", "18754", "18755", "18756", "18757", "18758", "18759",
+  "18760", "18761", "18762", "18763", "18764", "18765", "18766", "18767",
+  "18768", "18769", "18770", "18771", "18772", "18773", "18774", "18775",
+  "18776", "18777", "18778", "18779", "18780", "18781", "18782", "18783",
+  "18784", "18785", "18786", "18787", "18788", "18789", "18790", "18791",
+  "18792", "18793", "18794", "18795", "18796", "18797", "18798", "18799",
+  "18800", "18801", "18802", "18803", "18804", "18805", "18806", "18807",
+  "18808", "18809", "18810", "18811", "18812", "18813", "18814", "18815",
+  "18816", "18817", "18818", "18819", "18820", "18821", "18822", "18823",
+  "18824", "18825", "18826", "18827", "18828", "18829", "18830", "18831",
+  "18832", "18833", "18834", "18835", "18836", "18837", "18838", "18839",
+  "18840", "18841", "18842", "18843", "18844", "18845", "18846", "18847",
+  "18848", "18849", "18850", "18851", "18852", "18853", "18854", "18855",
+  "18856", "18857", "18858", "18859", "18860", "18861", "18862", "18863",
+  "18864", "18865", "18866", "18867", "18868", "18869", "18870", "18871",
+  "18872", "18873", "18874", "18875", "18876", "18877", "18878", "18879",
+  "18880", "18881", "18882", "18883", "18884", "18885", "18886", "18887",
+  "18888", "18889", "18890", "18891", "18892", "18893", "18894", "18895",
+  "18896", "18897", "18898", "18899", "18900", "18901", "18902", "18903",
+  "18904", "18905", "18906", "18907", "18908", "18909", "18910", "18911",
+  "18912", "18913", "18914", "18915", "18916", "18917", "18918", "18919",
+  "18920", "18921", "18922", "18923", "18924", "18925", "18926", "18927",
+  "18928", "18929", "18930", "18931", "18932", "18933", "18934", "18935",
+  "18936", "18937", "18938", "18939", "18940", "18941", "18942", "18943",
+  "18944", "18945", "18946", "18947", "18948", "18949", "18950", "18951",
+  "18952", "18953", "18954", "18955", "18956", "18957", "18958", "18959",
+  "18960", "18961", "18962", "18963", "18964", "18965", "18966", "18967",
+  "18968", "18969", "18970", "18971", "18972", "18973", "18974", "18975",
+  "18976", "18977", "18978", "18979", "18980", "18981", "18982", "18983",
+  "18984", "18985", "18986", "18987", "18988", "18989", "18990", "18991",
+  "18992", "18993", "18994", "18995", "18996", "18997", "18998", "18999",
+  "19000", "19001", "19002", "19003", "19004", "19005", "19006", "19007",
+  "19008", "19009", "19010", "19011", "19012", "19013", "19014", "19015",
+  "19016", "19017", "19018", "19019", "19020", "19021", "19022", "19023",
+  "19024", "19025", "19026", "19027", "19028", "19029", "19030", "19031",
+  "19032", "19033", "19034", "19035", "19036", "19037", "19038", "19039",
+  "19040", "19041", "19042", "19043", "19044", "19045", "19046", "19047",
+  "19048", "19049", "19050", "19051", "19052", "19053", "19054", "19055",
+  "19056", "19057", "19058", "19059", "19060", "19061", "19062", "19063",
+  "19064", "19065", "19066", "19067", "19068", "19069", "19070", "19071",
+  "19072", "19073", "19074", "19075", "19076", "19077", "19078", "19079",
+  "19080", "19081", "19082", "19083", "19084", "19085", "19086", "19087",
+  "19088", "19089", "19090", "19091", "19092", "19093", "19094", "19095",
+  "19096", "19097", "19098", "19099", "19100", "19101", "19102", "19103",
+  "19104", "19105", "19106", "19107", "19108", "19109", "19110", "19111",
+  "19112", "19113", "19114", "19115", "19116", "19117", "19118", "19119",
+  "19120", "19121", "19122", "19123", "19124", "19125", "19126", "19127",
+  "19128", "19129", "19130", "19131", "19132", "19133", "19134", "19135",
+  "19136", "19137", "19138", "19139", "19140", "19141", "19142", "19143",
+  "19144", "19145", "19146", "19147", "19148", "19149", "19150", "19151",
+  "19152", "19153", "19154", "19155", "19156", "19157", "19158", "19159",
+  "19160", "19161", "19162", "19163", "19164", "19165", "19166", "19167",
+  "19168", "19169", "19170", "19171", "19172", "19173", "19174", "19175",
+  "19176", "19177", "19178", "19179", "19180", "19181", "19182", "19183",
+  "19184", "19185", "19186", "19187", "19188", "19189", "19190", "19191",
+  "19192", "19193", "19194", "19195", "19196", "19197", "19198", "19199",
+  "19200", "19201", "19202", "19203", "19204", "19205", "19206", "19207",
+  "19208", "19209", "19210", "19211", "19212", "19213", "19214", "19215",
+  "19216", "19217", "19218", "19219", "19220", "19221", "19222", "19223",
+  "19224", "19225", "19226", "19227", "19228", "19229", "19230", "19231",
+  "19232", "19233", "19234", "19235", "19236", "19237", "19238", "19239",
+  "19240", "19241", "19242", "19243", "19244", "19245", "19246", "19247",
+  "19248", "19249", "19250", "19251", "19252", "19253", "19254", "19255",
+  "19256", "19257", "19258", "19259", "19260", "19261", "19262", "19263",
+  "19264", "19265", "19266", "19267", "19268", "19269", "19270", "19271",
+  "19272", "19273", "19274", "19275", "19276", "19277", "19278", "19279",
+  "19280", "19281", "19282", "19283", "19284", "19285", "19286", "19287",
+  "19288", "19289", "19290", "19291", "19292", "19293", "19294", "19295",
+  "19296", "19297", "19298", "19299", "19300", "19301", "19302", "19303",
+  "19304", "19305", "19306", "19307", "19308", "19309", "19310", "19311",
+  "19312", "19313", "19314", "19315", "19316", "19317", "19318", "19319",
+  "19320", "19321", "19322", "19323", "19324", "19325", "19326", "19327",
+  "19328", "19329", "19330", "19331", "19332", "19333", "19334", "19335",
+  "19336", "19337", "19338", "19339", "19340", "19341", "19342", "19343",
+  "19344", "19345", "19346", "19347", "19348", "19349", "19350", "19351",
+  "19352", "19353", "19354", "19355", "19356", "19357", "19358", "19359",
+  "19360", "19361", "19362", "19363", "19364", "19365", "19366", "19367",
+  "19368", "19369", "19370", "19371", "19372", "19373", "19374", "19375",
+  "19376", "19377", "19378", "19379", "19380", "19381", "19382", "19383",
+  "19384", "19385", "19386",
+  "19387", "19388", "19389", "19390", "19391", "19392", "19393", "19394",
+  "19395", "19396", "19397", "19398", "19399", "19400", "19401", "19402",
+  "19403", "19404", "19405", "19406", "19407", "19408", "19409", "19410",
+  "19411", "19412", "19413", "19414", "19415", "19416", "19417", "19418",
+  "19419", "19420", "19421", "19422", "19423", "19424", "19425", "19426",
+  "19427", "19428", "19429", "19430", "19431", "19432", "19433", "19434",
+  "19435", "19436", "19437", "19438", "19439", "19440", "19441", "19442",
+  "19443", "19444", "19445", "19446", "19447", "19448", "19449", "19450",
+  "19451", "19452", "19453", "19454", "19455", "19456", "19457", "19458",
+  "19459", "19460", "19461", "19462", "19463", "19464", "19465", "19466",
+  "19467", "19468", "19469", "19470", "19471", "19472", "19473", "19474",
+  "19475", "19476", "19477", "19478", "19479", "19480", "19481", "19482",
+  "19483", "19484", "19485", "19486", "19487", "19488", "19489", "19490",
+  "19491", "19492", "19493", "19494", "19495", "19496", "19497", "19498",
+  "19499", "19500", "19501", "19502", "19503", "19504", "19505", "19506",
+  "19507", "19508", "19509", "19510", "19511", "19512", "19513", "19514",
+  "19515", "19516", "19517", "19518", "19519", "19520", "19521", "19522",
+  "19523", "19524", "19525", "19526", "19527", "19528", "19529", "19530",
+  "19531", "19532", "19533", "19534", "19535", "19536", "19537", "19538",
+  "19539", "19540", "19541", "19542", "19543", "19544", "19545", "19546",
+  "19547", "19548", "19549", "19550", "19551", "19552", "19553", "19554",
+  "19555", "19556", "19557", "19558", "19559", "19560", "19561", "19562",
+  "19563", "19564", "19565", "19566", "19567", "19568", "19569", "19570",
+  "19571", "19572", "19573", "19574", "19575", "19576", "19577", "19578",
+  "19579", "19580", "19581", "19582", "19583", "19584", "19585", "19586",
+  "19587", "19588", "19589", "19590", "19591", "19592", "19593", "19594",
+  "19595", "19596", "19597", "19598", "19599", "19600", "19601", "19602",
+  "19603", "19604", "19605", "19606", "19607", "19608", "19609", "19610",
+  "19611", "19612", "19613", "19614", "19615", "19616", "19617", "19618",
+  "19619", "19620", "19621", "19622", "19623", "19624", "19625", "19626",
+  "19627", "19628", "19629", "19630", "19631", "19632", "19633", "19634",
+  "19635", "19636", "19637", "19638", "19639", "19640", "19641", "19642",
+  "19643", "19644", "19645", "19646", "19647", "19648", "19649", "19650",
+  "19651", "19652", "19653", "19654", "19655", "19656", "19657", "19658",
+  "19659", "19660", "19661", "19662", "19663", "19664", "19665", "19666",
+  "19667", "19668", "19669", "19670", "19671", "19672", "19673", "19674",
+  "19675", "19676", "19677", "19678", "19679", "19680", "19681", "19682",
+  "19683", "19684", "19685", "19686", "19687", "19688", "19689", "19690",
+  "19691", "19692", "19693", "19694", "19695", "19696", "19697", "19698",
+  "19699", "19700", "19701", "19702", "19703", "19704", "19705", "19706",
+  "19707", "19708", "19709", "19710", "19711", "19712", "19713", "19714",
+  "19715", "19716", "19717", "19718", "19719", "19720", "19721", "19722",
+  "19723", "19724", "19725", "19726", "19727", "19728", "19729", "19730",
+  "19731", "19732", "19733", "19734", "19735", "19736", "19737", "19738",
+  "19739", "19740", "19741", "19742", "19743", "19744", "19745", "19746",
+  "19747", "19748", "19749", "19750", "19751", "19752", "19753", "19754",
+  "19755", "19756", "19757", "19758", "19759", "19760", "19761", "19762",
+  "19763", "19764", "19765", "19766", "19767", "19768", "19769", "19770",
+  "19771", "19772", "19773", "19774", "19775", "19776", "19777", "19778",
+  "19779", "19780", "19781", "19782", "19783", "19784", "19785", "19786",
+  "19787", "19788", "19789", "19790", "19791", "19792", "19793", "19794",
+  "19795", "19796", "19797", "19798", "19799", "19800", "19801", "19802",
+  "19803", "19804", "19805", "19806", "19807", "19808", "19809", "19810",
+  "19811", "19812", "19813", "19814", "19815", "19816", "19817", "19818",
+  "19819", "19820", "19821", "19822", "19823", "19824", "19825", "19826",
+  "19827", "19828", "19829", "19830", "19831", "19832", "19833", "19834",
+  "19835", "19836", "19837", "19838", "19839", "19840", "19841", "19842",
+  "19843", "19844", "19845", "19846", "19847", "19848", "19849", "19850",
+  "19851", "19852", "19853", "19854", "19855", "19856", "19857", "19858",
+  "19859", "19860", "19861", "19862", "19863", "19864", "19865", "19866",
+  "19867", "19868", "19869", "19870", "19871", "19872", "19873", "19874",
+  "19875", "19876", "19877", "19878", "19879", "19880", "19881", "19882",
+  "19883", "19884", "19885", "19886", "19887", "19888", "19889", "19890",
+  "19891", "19892", "19893", "19894", "19895", "19896", "19897", "19898",
+  "19899", "19900", "19901", "19902", "19903", "19904", "19905", "19906",
+  "19907", "19908", "19909", "19910", "19911", "19912", "19913", "19914",
+  "19915", "19916", "19917", "19918", "19919", "19920", "19921", "19922",
+  "19923", "19924", "19925", "19926", "19927", "19928", "19929", "19930",
+  "19931", "19932", "19933", "19934", "19935", "19936", "19937", "19938",
+  "19939", "19940", "19941", "19942", "19943", "19944", "19945", "19946",
+  "19947", "19948", "19949", "19950", "19951", "19952", "19953", "19954",
+  "19955", "19956", "19957", "19958", "19959", "19960", "19961", "19962",
+  "19963", "19964", "19965", "19966", "19967", "19968", "19969", "19970",
+  "19971", "19972", "19973", "19974", "19975", "19976", "19977", "19978",
+  "19979", "19980", "19981", "19982", "19983", "19984", "19985", "19986",
+  "19987", "19988", "19989", "19990", "19991", "19992", "19993", "19994",
+  "19995", "19996", "19997", "19998", "19999",
+};
diff --git a/src/plugins/omni-gutter/meson.build b/src/plugins/omni-gutter/meson.build
new file mode 100644
index 000000000..80f4168b8
--- /dev/null
+++ b/src/plugins/omni-gutter/meson.build
@@ -0,0 +1,14 @@
+plugins_sources += files([
+  'omni-gutter-plugin.c',
+  'gbp-omni-gutter-renderer.c',
+  'gbp-omni-gutter-editor-page-addin.c',
+  'fast-str.c',
+])
+
+omni_gutter_resources = gnome.compile_resources(
+  'omni-gutter-resources',
+  'omni-gutter.gresource.xml',
+  c_name: 'gbp_omni_gutter',
+)
+
+plugins_sources += omni_gutter_resources[0]
diff --git a/src/plugins/omni-gutter/omni-gutter-plugin.c b/src/plugins/omni-gutter/omni-gutter-plugin.c
new file mode 100644
index 000000000..2683b784d
--- /dev/null
+++ b/src/plugins/omni-gutter/omni-gutter-plugin.c
@@ -0,0 +1,36 @@
+/* omni-gutter-plugin.c
+ *
+ * Copyright 2018-2019 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 "omni-gutter-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-editor.h>
+
+#include "gbp-omni-gutter-editor-page-addin.h"
+
+_IDE_EXTERN void
+_gbp_omni_gutter_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                              GBP_TYPE_OMNI_GUTTER_EDITOR_PAGE_ADDIN);
+}
diff --git a/src/plugins/omni-gutter/omni-gutter.gresource.xml 
b/src/plugins/omni-gutter/omni-gutter.gresource.xml
new file mode 100644
index 000000000..8942387fd
--- /dev/null
+++ b/src/plugins/omni-gutter/omni-gutter.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/omni-gutter">
+    <file>omni-gutter.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/omni-gutter/omni-gutter.plugin b/src/plugins/omni-gutter/omni-gutter.plugin
new file mode 100644
index 000000000..e12bb79d9
--- /dev/null
+++ b/src/plugins/omni-gutter/omni-gutter.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2018 Christian Hergert
+Depends=editor;
+Description=Integrated gutter for the source code editor
+Embedded=_gbp_omni_gutter_register_types
+Hidden=true
+Module=omni-gutter
+Name=OmniGutter
diff --git a/src/plugins/phpize/meson.build b/src/plugins/phpize/meson.build
index 520f8f5a6..0c2fab0c9 100644
--- a/src/plugins/phpize/meson.build
+++ b/src/plugins/phpize/meson.build
@@ -1,11 +1,11 @@
-if get_option('with_phpize')
+if get_option('plugin_phpize')
 
 install_data('phpize_plugin.py', install_dir: plugindir)
 
 configure_file(
           input: 'phpize.plugin',
          output: 'phpize.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/phpize/phpize.plugin b/src/plugins/phpize/phpize.plugin
index f8982b201..1342f6628 100644
--- a/src/plugins/phpize/phpize.plugin
+++ b/src/plugins/phpize/phpize.plugin
@@ -1,11 +1,12 @@
 [Plugin]
-Module=phpize_plugin
-Loader=python3
-Name=PHPize
-Description=Provides integration with phpize-based PHP extensions
 Authors=Christian Hergert <chergert redhat com>
-Copyright=Copyright © 2017 Christian Hergert
 Builtin=true
-Hidden=false
-X-Project-File-Filter-Pattern=config.m4
+Copyright=Copyright © 2017 Christian Hergert
+Description=Provides integration with phpize-based PHP extensions
+Hidden=true
+Loader=python3
+Module=phpize_plugin
+Name=PHPize
 X-Project-File-Filter-Name=PHPize Project (config.m4)
+X-Project-File-Filter-Pattern=config.m4
+X-Builder-ABI=@PACKAGE_ABI@
diff --git a/src/plugins/phpize/phpize_plugin.py b/src/plugins/phpize/phpize_plugin.py
index 43a26e1ea..f162a1e31 100644
--- a/src/plugins/phpize/phpize_plugin.py
+++ b/src/plugins/phpize/phpize_plugin.py
@@ -20,7 +20,6 @@
 #
 
 import os
-import gi
 
 from gi.repository import Ide
 from gi.repository import GLib
@@ -48,7 +47,7 @@ def get_file_type(path):
         return _TYPE_CPLUSPLUS
     return _TYPE_NONE
 
-class PHPizeBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
+class PHPizeBuildSystem(Ide.Object, Ide.BuildSystem):
     """
     This is the the basis of the build system. It provides access to
     some information about the project (like CFLAGS/CXXFLAGS, build targets,
@@ -63,29 +62,12 @@ class PHPizeBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
         return 'PHPize'
 
     def do_get_priority(self):
-        return 500
+        return 3000
 
-    def do_init_async(self, priority, cancel, callback, data=None):
-        task = Gio.Task.new(self, cancel, callback)
-        task.set_priority(priority)
-
-        project_file = self.get_context().get_project_file()
-        if project_file.get_basename() == 'config.m4':
-            task.return_boolean(True)
-        else:
-            child = project_file.get_child('config.m4')
-            exists = child.query_exists(cancel)
-            if exists:
-                self.props.project_file = child
-            task.return_boolean(exists)
-
-    def do_init_finish(self, result):
-        return result.propagate_boolean()
-
-    def do_get_build_flags_async(self, ifile, cancellable, callback, data=None):
+    def do_get_build_flags_async(self, file, cancellable, callback, data=None):
         task = Gio.Task.new(self, cancellable, callback)
         task.build_flags = []
-        task.type = get_file_type(ifile.get_path())
+        task.type = get_file_type(file.get_path())
 
         if not task.type:
             task.return_boolean(True)
@@ -94,12 +76,11 @@ class PHPizeBuildSystem(Ide.Object, Ide.BuildSystem, Gio.AsyncInitable):
         # To get the build flags, we run make with some custom code to
         # print variables, and then extract the values based on the file type.
         # But before, we must advance the pipeline through CONFIGURE.
-        build_manager = self.get_context().get_build_manager()
+        build_manager = Ide.BuildManager.from_context(context)
         build_manager.execute_async(Ide.BuildPhase.CONFIGURE, None, self._get_build_flags_build_cb, task)
 
     def do_get_build_flags_finish(self, result):
-        if result.propagate_boolean():
-            return result.build_flags
+        return result.build_flags
 
     def _get_build_flags_build_cb(self, build_manager, result, task):
         """
@@ -191,7 +172,7 @@ class PHPizeBuildPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
     """
     def do_load(self, pipeline):
         context = pipeline.get_context()
-        build_system = context.get_build_system()
+        build_system = Ide.BuildSystem.from_context(context)
 
         if type(build_system) != PHPizeBuildSystem:
             return
@@ -209,7 +190,7 @@ class PHPizeBuildPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
         bootstrap_stage = Ide.BuildStageLauncher.new(context, bootstrap_launcher)
         bootstrap_stage.set_name(_("Bootstrapping project"))
         bootstrap_stage.set_completed(os.path.exists(os.path.join(srcdir, 'configure')))
-        self.track(pipeline.connect(Ide.BuildPhase.AUTOGEN, 0, bootstrap_stage))
+        self.track(pipeline.attach(Ide.BuildPhase.AUTOGEN, 0, bootstrap_stage))
 
         # Configure the project using autoconf. We run from builddir.
         config_launcher = pipeline.create_launcher()
@@ -224,7 +205,7 @@ class PHPizeBuildPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
             config_launcher.push_args(config_opts)
         config_stage = Ide.BuildStageLauncher.new(context, config_launcher)
         config_stage.set_name(_("Configuring project"))
-        self.track(pipeline.connect(Ide.BuildPhase.CONFIGURE, 0, config_stage))
+        self.track(pipeline.attach(Ide.BuildPhase.CONFIGURE, 0, config_stage))
 
         # Build the project using make.
         build_launcher = pipeline.create_launcher()
@@ -238,7 +219,7 @@ class PHPizeBuildPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
         build_stage.set_name(_("Building project"))
         build_stage.set_clean_launcher(clean_launcher)
         build_stage.connect('query', self._query)
-        self.track(pipeline.connect(Ide.BuildPhase.BUILD, 0, build_stage))
+        self.track(pipeline.attach(Ide.BuildPhase.BUILD, 0, build_stage))
 
         # Use "make install" to install the project.
         install_launcher = pipeline.create_launcher()
@@ -246,8 +227,8 @@ class PHPizeBuildPipelineAddin(Ide.Object, Ide.BuildPipelineAddin):
         install_launcher.push_argv('install')
         install_stage = Ide.BuildStageLauncher.new(context, install_launcher)
         install_stage.set_name(_("Installing project"))
-        self.track(pipeline.connect(Ide.BuildPhase.INSTALL, 0, install_stage))
+        self.track(pipeline.attach(Ide.BuildPhase.INSTALL, 0, install_stage))
 
-    def _query(self, stage, pipeline, cancellable):
+    def _query(self, stage, pipeline, targets, cancellable):
         # Always defer to make for completion status
         stage.set_completed(False)
diff --git a/src/plugins/project-tree/gbp-new-file-popover.c b/src/plugins/project-tree/gbp-new-file-popover.c
new file mode 100644
index 000000000..fa24efc17
--- /dev/null
+++ b/src/plugins/project-tree/gbp-new-file-popover.c
@@ -0,0 +1,421 @@
+/* gbp-new-file-popover.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-new-file-popover"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+#include <libide-threading.h>
+
+#include "gbp-new-file-popover.h"
+
+struct _GbpNewFilePopover
+{
+  GtkPopover    parent_instance;
+
+  GFileType     file_type;
+  GFile        *directory;
+  IdeTask      *task;
+
+  GtkButton    *button;
+  GtkEntry     *entry;
+  GtkLabel     *message;
+  GtkLabel     *title;
+};
+
+G_DEFINE_TYPE (GbpNewFilePopover, gbp_new_file_popover, GTK_TYPE_POPOVER)
+
+enum {
+  PROP_0,
+  PROP_DIRECTORY,
+  PROP_FILE_TYPE,
+  N_PROPS
+};
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_new_file_popover_button_clicked (GbpNewFilePopover *self,
+                                     GtkButton        *button)
+{
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  const gchar *path;
+
+  g_assert (GBP_IS_NEW_FILE_POPOVER (self));
+  g_assert (GTK_IS_BUTTON (button));
+
+  if (self->directory == NULL)
+    return;
+
+  path = gtk_entry_get_text (self->entry);
+  if (dzl_str_empty0 (path))
+    return;
+
+  file = g_file_get_child (self->directory, path);
+
+  if ((task = g_steal_pointer (&self->task)))
+    ide_task_return_pointer (task, g_steal_pointer (&file), g_object_unref);
+}
+
+static void
+gbp_new_file_popover_entry_activate (GbpNewFilePopover *self,
+                                     GtkEntry         *entry)
+{
+  g_assert (GBP_IS_NEW_FILE_POPOVER (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  if (gtk_widget_get_sensitive (GTK_WIDGET (self->button)))
+    gtk_widget_activate (GTK_WIDGET (self->button));
+}
+
+static void
+gbp_new_file_popover_query_info_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(GFileInfo) file_info = NULL;
+  g_autoptr(GbpNewFilePopover) self = user_data;
+  g_autoptr(GError) error = NULL;
+  GFileType file_type;
+
+  file_info = g_file_query_info_finish (file, result, &error);
+
+  if (file_info == NULL &&
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    return;
+
+  if ((file_info == NULL) &&
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+    {
+      gtk_label_set_label (self->message, NULL);
+      gtk_widget_set_sensitive (GTK_WIDGET (self->button), TRUE);
+      return;
+    }
+
+  if (file_info == NULL)
+    {
+      gtk_label_set_label (self->message, error->message);
+      return;
+    }
+
+  file_type = g_file_info_get_file_type (file_info);
+
+  if (file_type == G_FILE_TYPE_DIRECTORY)
+    gtk_label_set_label (self->message,
+                         _("A folder with that name already exists."));
+  else
+    gtk_label_set_label (self->message,
+                         _("A file with that name already exists."));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), FALSE);
+}
+
+static void
+gbp_new_file_popover_check_exists (GbpNewFilePopover *self,
+                                   GFile             *directory,
+                                   const gchar       *path)
+{
+  g_autoptr(GFile) child = NULL;
+  GCancellable *cancellable = NULL;
+
+  g_assert (GBP_IS_NEW_FILE_POPOVER (self));
+  g_assert (!directory || G_IS_FILE (directory));
+
+  gtk_label_set_label (self->message, NULL);
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), FALSE);
+
+  if (directory == NULL)
+    return;
+
+  if (ide_str_empty0 (path))
+    return;
+
+  child = g_file_get_child (directory, path);
+
+  if (self->task)
+    cancellable = ide_task_get_cancellable (self->task);
+
+  g_file_query_info_async (child,
+                           G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                           G_FILE_QUERY_INFO_NONE,
+                           G_PRIORITY_DEFAULT,
+                           cancellable,
+                           gbp_new_file_popover_query_info_cb,
+                           g_object_ref (self));
+
+}
+
+static void
+gbp_new_file_popover_entry_changed (GbpNewFilePopover *self,
+                                    GtkEntry         *entry)
+{
+  const gchar *text;
+
+  g_assert (GBP_IS_NEW_FILE_POPOVER (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  text = gtk_entry_get_text (entry);
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), !dzl_str_empty0 (text));
+
+  gbp_new_file_popover_check_exists (self, self->directory, text);
+}
+
+static void
+gbp_new_file_popover_closed (GtkPopover *popover)
+{
+  GbpNewFilePopover *self = (GbpNewFilePopover *)popover;
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (GBP_IS_NEW_FILE_POPOVER (self));
+
+  if ((task = g_steal_pointer (&self->task)))
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_CANCELLED,
+                               "The popover was closed");
+}
+
+static void
+gbp_new_file_popover_finalize (GObject *object)
+{
+  GbpNewFilePopover *self = (GbpNewFilePopover *)object;
+
+  g_assert (self->task == NULL);
+
+  g_clear_object (&self->directory);
+
+  G_OBJECT_CLASS (gbp_new_file_popover_parent_class)->finalize (object);
+}
+
+static void
+gbp_new_file_popover_get_property (GObject    *object,
+                                   guint       prop_id,
+                                   GValue     *value,
+                                   GParamSpec *pspec)
+{
+  GbpNewFilePopover *self = GBP_NEW_FILE_POPOVER(object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      g_value_set_object (value, gbp_new_file_popover_get_directory (self));
+      break;
+
+    case PROP_FILE_TYPE:
+      g_value_set_enum (value, gbp_new_file_popover_get_file_type (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+/**
+ * gbp_new_file_popover_set_property:
+ * @object: (in): a #GObject.
+ * @prop_id: (in): The property identifier.
+ * @value: (in): The given property.
+ * @pspec: (in): a #ParamSpec.
+ *
+ * Set a given #GObject property.
+ *
+ * Since: 3.32
+ */
+static void
+gbp_new_file_popover_set_property (GObject      *object,
+                                   guint         prop_id,
+                                   const GValue *value,
+                                   GParamSpec   *pspec)
+{
+  GbpNewFilePopover *self = GBP_NEW_FILE_POPOVER(object);
+
+  switch (prop_id)
+    {
+    case PROP_DIRECTORY:
+      gbp_new_file_popover_set_directory (self, g_value_get_object (value));
+      break;
+
+    case PROP_FILE_TYPE:
+      gbp_new_file_popover_set_file_type (self, g_value_get_enum (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_new_file_popover_class_init (GbpNewFilePopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkPopoverClass *popover_class = GTK_POPOVER_CLASS (klass);
+
+  object_class->finalize = gbp_new_file_popover_finalize;
+  object_class->get_property = gbp_new_file_popover_get_property;
+  object_class->set_property = gbp_new_file_popover_set_property;
+
+  popover_class->closed = gbp_new_file_popover_closed;
+
+  properties [PROP_DIRECTORY] =
+    g_param_spec_object ("directory",
+                         "Directory",
+                         "Directory",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_FILE_TYPE] =
+    g_param_spec_enum ("file-type",
+                       "File Type",
+                       "The file type to create.",
+                       G_TYPE_FILE_TYPE,
+                       G_FILE_TYPE_REGULAR,
+                       (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/project-tree/gbp-new-file-popover.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpNewFilePopover, button);
+  gtk_widget_class_bind_template_child (widget_class, GbpNewFilePopover, entry);
+  gtk_widget_class_bind_template_child (widget_class, GbpNewFilePopover, message);
+  gtk_widget_class_bind_template_child (widget_class, GbpNewFilePopover, title);
+}
+
+static void
+gbp_new_file_popover_init (GbpNewFilePopover *self)
+{
+  self->file_type = G_FILE_TYPE_REGULAR;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->entry,
+                           "activate",
+                           G_CALLBACK (gbp_new_file_popover_entry_activate),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->entry,
+                           "changed",
+                           G_CALLBACK (gbp_new_file_popover_entry_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->button,
+                           "clicked",
+                           G_CALLBACK (gbp_new_file_popover_button_clicked),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+GFileType
+gbp_new_file_popover_get_file_type (GbpNewFilePopover *self)
+{
+  g_return_val_if_fail (GBP_IS_NEW_FILE_POPOVER (self), 0);
+
+  return self->file_type;
+}
+
+void
+gbp_new_file_popover_set_file_type (GbpNewFilePopover *self,
+                                    GFileType          file_type)
+{
+  g_return_if_fail (GBP_IS_NEW_FILE_POPOVER (self));
+  g_return_if_fail ((file_type == G_FILE_TYPE_REGULAR) ||
+                    (file_type == G_FILE_TYPE_DIRECTORY));
+
+  if (file_type != self->file_type)
+    {
+      self->file_type = file_type;
+
+      if (file_type == G_FILE_TYPE_REGULAR)
+        gtk_label_set_label (self->title, _("File Name"));
+      else
+        gtk_label_set_label (self->title, _("Folder Name"));
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE_TYPE]);
+    }
+}
+
+void
+gbp_new_file_popover_set_directory (GbpNewFilePopover *self,
+                                    GFile             *directory)
+{
+  g_return_if_fail (GBP_IS_NEW_FILE_POPOVER (self));
+  g_return_if_fail (G_IS_FILE (directory));
+
+  if (g_set_object (&self->directory, directory))
+    {
+      const gchar *path;
+
+      path = gtk_entry_get_text (self->entry);
+      gbp_new_file_popover_check_exists (self, directory, path);
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_DIRECTORY]);
+    }
+}
+
+/**
+ * gbp_new_file_popover_get_directory:
+ *
+ * Returns: (transfer none) (nullable): a #GFile or %NULL.
+ *
+ * Since: 3.32
+ */
+GFile *
+gbp_new_file_popover_get_directory (GbpNewFilePopover *self)
+{
+  g_return_val_if_fail (GBP_IS_NEW_FILE_POPOVER (self), NULL);
+
+  return self->directory;
+}
+
+void
+gbp_new_file_popover_display_async (GbpNewFilePopover   *self,
+                                    IdeTree             *tree,
+                                    IdeTreeNode         *node,
+                                    GCancellable        *cancellable,
+                                    GAsyncReadyCallback  callback,
+                                    gpointer             user_data)
+{
+  g_return_if_fail (GBP_IS_NEW_FILE_POPOVER (self));
+  g_return_if_fail (IDE_IS_TREE (tree));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+  g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_return_if_fail (self->task == NULL);
+
+  self->task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (self->task, gbp_new_file_popover_display_async);
+
+  ide_tree_expand_node (tree, node);
+  ide_tree_show_popover_at_node (tree, node, GTK_POPOVER (self));
+}
+
+GFile *
+gbp_new_file_popover_display_finish (GbpNewFilePopover  *self,
+                                     GAsyncResult       *result,
+                                     GError            **error)
+{
+  g_return_val_if_fail (GBP_IS_NEW_FILE_POPOVER (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/project-tree/gbp-new-file-popover.h b/src/plugins/project-tree/gbp-new-file-popover.h
new file mode 100644
index 000000000..1a7b51aff
--- /dev/null
+++ b/src/plugins/project-tree/gbp-new-file-popover.h
@@ -0,0 +1,48 @@
+/* gbp-new-file-popover.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 <gtk/gtk.h>
+#include <libide-tree.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_NEW_FILE_POPOVER (gbp_new_file_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpNewFilePopover, gbp_new_file_popover, GBP, NEW_FILE_POPOVER, GtkPopover)
+
+GFileType  gbp_new_file_popover_get_file_type  (GbpNewFilePopover    *self);
+void       gbp_new_file_popover_set_file_type  (GbpNewFilePopover    *self,
+                                                GFileType             file_type);
+void       gbp_new_file_popover_set_directory  (GbpNewFilePopover    *self,
+                                                GFile                *directory);
+GFile     *gbp_new_file_popover_get_directory  (GbpNewFilePopover    *self);
+void       gbp_new_file_popover_display_async  (GbpNewFilePopover    *self,
+                                                IdeTree              *tree,
+                                                IdeTreeNode          *node,
+                                                GCancellable         *cancellable,
+                                                GAsyncReadyCallback   callback,
+                                                gpointer              user_data);
+GFile     *gbp_new_file_popover_display_finish (GbpNewFilePopover    *self,
+                                                GAsyncResult         *result,
+                                                GError              **error);
+
+G_END_DECLS
diff --git a/src/plugins/project-tree/gbp-new-file-popover.ui 
b/src/plugins/project-tree/gbp-new-file-popover.ui
new file mode 100644
index 000000000..5cbce4915
--- /dev/null
+++ b/src/plugins/project-tree/gbp-new-file-popover.ui
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.16 -->
+  <template class="GbpNewFilePopover" parent="GtkPopover">
+    <child>
+      <object class="GtkBox">
+        <property name="border-width">12</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel" id="title">
+            <property name="label" translatable="yes">File Name</property>
+            <property name="xalign">0.0</property>
+            <property name="visible">true</property>
+            <attributes>
+              <attribute name="weight" value="bold"/>
+            </attributes>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="spacing">9</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkEntry" id="entry">
+                <property name="width-chars">20</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="button">
+                <property name="sensitive">false</property>
+                <property name="label" translatable="yes">_Create</property>
+                <property name="use-underline">true</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="suggested-action"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="message">
+            <property name="xalign">0.0</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/project-tree/gbp-project-tree-addin.c 
b/src/plugins/project-tree/gbp-project-tree-addin.c
new file mode 100644
index 000000000..d3d0a7f48
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree-addin.c
@@ -0,0 +1,896 @@
+/* gbp-project-tree-addin.c
+ *
+ * Copyright 2018-2019 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-project-tree-addin"
+
+#include "config.h"
+
+#include <dazzle.h>
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+#include <libide-projects.h>
+#include <libide-tree.h>
+#include <libide-vcs.h>
+
+#include "gbp-project-tree-addin.h"
+
+struct _GbpProjectTreeAddin
+{
+  GObject       parent_instance;
+
+  IdeTree      *tree;
+  IdeTreeModel *model;
+  GSettings    *settings;
+
+  guint         sort_directories_first : 1;
+  guint         show_ignored_files : 1;
+};
+
+typedef struct
+{
+  GFile       *file;
+  IdeTreeNode *node;
+} FindFileNode;
+
+static gboolean
+project_file_is_ignored (IdeProjectFile *project_file,
+                         IdeVcs         *vcs)
+{
+  g_autoptr(GFile) file = NULL;
+
+  g_assert (IDE_IS_PROJECT_FILE (project_file));
+
+  file = ide_project_file_ref_file (project_file);
+
+  return ide_vcs_is_ignored (vcs, file, NULL);
+}
+
+static gint
+compare_files (gconstpointer a,
+               gconstpointer b,
+               gpointer      user_data)
+{
+  GbpProjectTreeAddin *self = user_data;
+  IdeProjectFile *file_a = *(IdeProjectFile **)a;
+  IdeProjectFile *file_b = *(IdeProjectFile **)b;
+
+  g_assert (IDE_IS_PROJECT_FILE (file_a));
+  g_assert (IDE_IS_PROJECT_FILE (file_b));
+
+  if (self->sort_directories_first)
+    return ide_project_file_compare_directories_first (file_a, file_b);
+  else
+    return ide_project_file_compare (file_a, file_b);
+}
+
+static IdeTreeNode *
+create_file_node (IdeProjectFile *file)
+{
+  IdeTreeNode *child;
+
+  g_assert (IDE_IS_PROJECT_FILE (file));
+
+  child = ide_tree_node_new ();
+  ide_tree_node_set_item (child, G_OBJECT (file));
+  ide_tree_node_set_display_name (child, ide_project_file_get_display_name (file));
+  ide_tree_node_set_icon (child, ide_project_file_get_symbolic_icon (file));
+  g_object_set (child, "destroy-item", TRUE, NULL);
+
+  if (ide_project_file_is_directory (file))
+    {
+      ide_tree_node_set_children_possible (child, TRUE);
+      ide_tree_node_set_expanded_icon_name (child, "folder-open-symbolic");
+    }
+
+  return g_steal_pointer (&child);
+}
+
+static void
+gbp_project_tree_addin_file_list_children_cb (GObject      *object,
+                                              GAsyncResult *result,
+                                              gpointer      user_data)
+{
+  IdeProjectFile *project_file = (IdeProjectFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GPtrArray) children = NULL;
+  g_autoptr(GError) error = NULL;
+  GbpProjectTreeAddin *self;
+  IdeTreeNode *last = NULL;
+  IdeTreeNode *node;
+  IdeTreeNode *root;
+  IdeContext *context;
+  IdeVcs *vcs;
+
+  g_assert (IDE_IS_PROJECT_FILE (project_file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!(children = ide_project_file_list_children_finish (project_file, result, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  IDE_PTR_ARRAY_SET_FREE_FUNC (children, g_object_unref);
+
+  self = ide_task_get_source_object (task);
+  node = ide_task_get_task_data (task);
+  root = ide_tree_node_get_root (node);
+  context = ide_tree_node_get_item (root);
+  vcs = ide_vcs_from_context (context);
+
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  g_ptr_array_sort_with_data (children, compare_files, self);
+
+  for (guint i = 0; i < children->len; i++)
+    {
+      IdeProjectFile *file = g_ptr_array_index (children, i);
+      g_autoptr(IdeTreeNode) child = NULL;
+
+      if (!self->show_ignored_files)
+        {
+          if (project_file_is_ignored (file, vcs))
+            continue;
+        }
+
+      child = create_file_node (file);
+
+      if (last == NULL)
+        ide_tree_node_append (node, child);
+      else
+        ide_tree_node_insert_after (last, child);
+
+      last = child;
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_project_tree_addin_build_children_async (IdeTreeAddin        *addin,
+                                             IdeTreeNode         *node,
+                                             GCancellable        *cancellable,
+                                             GAsyncReadyCallback  callback,
+                                             gpointer             user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  task = ide_task_new (addin, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_project_tree_addin_build_children_async);
+  ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+
+  if (ide_tree_node_holds (node, IDE_TYPE_CONTEXT))
+    {
+      IdeContext *context = ide_tree_node_get_item (node);
+      g_autoptr(IdeTreeNode) files = NULL;
+      g_autoptr(IdeTreeNode) targets = NULL;
+      //g_autoptr(IdeTreeNode) tests = NULL;
+      g_autoptr(IdeProjectFile) root_file = NULL;
+      g_autoptr(GFile) workdir = ide_context_ref_workdir (context);
+      g_autoptr(GFile) parent = g_file_get_parent (workdir);
+      g_autoptr(GFileInfo) info = NULL;
+      g_autofree gchar *name = NULL;
+
+#if 0
+      tests = g_object_new (IDE_TYPE_TREE_NODE,
+                            "icon-name", "builder-unit-tests-symbolic",
+                            "item", NULL,
+                            "display-name", _("Unit Tests"),
+                            "children-possible", TRUE,
+                            NULL);
+      ide_tree_node_append (node, tests);
+#endif
+
+      info = g_file_info_new ();
+      name = g_file_get_basename (workdir);
+      g_file_info_set_name (info, name);
+      g_file_info_set_display_name (info, name);
+      g_file_info_set_content_type (info, "inode/directory");
+      g_file_info_set_file_type (info, G_FILE_TYPE_DIRECTORY);
+      g_file_info_set_is_symlink (info, FALSE);
+      root_file = ide_project_file_new (parent, info);
+      files = create_file_node (root_file);
+      ide_tree_node_set_display_name (files, _("Files"));
+      ide_tree_node_set_icon_name (files, "view-list-symbolic");
+      ide_tree_node_set_expanded_icon_name (files, "view-list-symbolic");
+      ide_tree_node_append (node, files);
+    }
+  else if (ide_tree_node_holds (node, IDE_TYPE_PROJECT_FILE))
+    {
+      IdeProjectFile *project_file = ide_tree_node_get_item (node);
+
+      ide_project_file_list_children_async (project_file,
+                                            cancellable,
+                                            gbp_project_tree_addin_file_list_children_cb,
+                                            g_steal_pointer (&task));
+
+      return;
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_project_tree_addin_build_children_finish (IdeTreeAddin  *addin,
+                                              GAsyncResult  *result,
+                                              GError       **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static gboolean
+gbp_project_tree_addin_node_activated (IdeTreeAddin *addin,
+                                       IdeTree      *tree,
+                                       IdeTreeNode  *node)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  if (ide_tree_node_holds (node, IDE_TYPE_PROJECT_FILE))
+    {
+      IdeProjectFile *project_file = ide_tree_node_get_item (node);
+      g_autoptr(GFile) file = NULL;
+      IdeWorkbench *workbench;
+
+      /* Ignore directories, we want to expand them */
+      if (ide_project_file_is_directory (project_file))
+        return FALSE;
+
+      file = ide_project_file_ref_file (project_file);
+      workbench = ide_widget_get_workbench (GTK_WIDGET (tree));
+
+      ide_workbench_open_async (workbench, file, NULL, 0, NULL, NULL, NULL);
+
+      return TRUE;
+    }
+
+  return FALSE;
+}
+
+static IdeTreeNodeVisit
+traverse_cb (IdeTreeNode *node,
+             gpointer     user_data)
+{
+  FindFileNode *find = user_data;
+
+  if (ide_tree_node_holds (node, IDE_TYPE_PROJECT_FILE))
+    {
+      IdeProjectFile *project_file = ide_tree_node_get_item (node);
+      g_autoptr(GFile) file = ide_project_file_ref_file (project_file);
+
+      if (g_file_equal (find->file, file))
+        {
+          find->node = node;
+          return IDE_TREE_NODE_VISIT_BREAK;
+        }
+
+      if (g_file_has_prefix (find->file, file))
+        return IDE_TREE_NODE_VISIT_CHILDREN;
+    }
+
+  return IDE_TREE_NODE_VISIT_CONTINUE;
+}
+
+static IdeTreeNode *
+find_file_node (IdeTree *tree,
+                GFile   *file)
+{
+  GtkTreeModel *model;
+  IdeTreeNode *root;
+  FindFileNode find;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (G_IS_FILE (file));
+
+  model = gtk_tree_view_get_model (GTK_TREE_VIEW (tree));
+  root = ide_tree_model_get_root (IDE_TREE_MODEL (model));
+
+  find.file = file;
+  find.node = NULL;
+
+  ide_tree_node_traverse (root,
+                          G_PRE_ORDER,
+                          G_TRAVERSE_ALL,
+                          -1,
+                          traverse_cb,
+                          &find);
+
+  return find.node;
+}
+
+static GList *
+collect_files (GFile *file,
+               GFile *stop_at)
+{
+  g_autoptr(GFile) copy = g_object_ref (file);
+  GList *list = NULL;
+
+  g_assert (g_file_has_prefix (file, stop_at));
+
+  while (!g_file_equal (copy, stop_at))
+    {
+      GFile *stolen = g_steal_pointer (&copy);
+
+      list = g_list_prepend (list, stolen);
+      copy = g_file_get_parent (stolen);
+    }
+
+  return g_steal_pointer (&list);
+}
+
+static void
+gbp_project_tree_addin_add_file (GbpProjectTreeAddin *self,
+                                 GFile               *file)
+{
+  g_autolist(GFile) list = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  IdeTreeNode *parent = NULL;
+  IdeContext *context;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    g_autofree gchar *uri = g_file_get_uri (file);
+    IDE_TRACE_MSG ("Adding file to tree \"%s\"", uri);
+  }
+#endif
+
+  context = ide_widget_get_context (GTK_WIDGET (self->tree));
+  workdir = ide_context_ref_workdir (context);
+
+  if (!g_file_has_prefix (file, workdir))
+    return;
+
+  list = collect_files (file, workdir);
+
+  for (const GList *iter = list; iter; iter = iter->next)
+    {
+      GFile *item = iter->data;
+      g_autoptr(IdeProjectFile) project_file = NULL;
+      g_autoptr(GFileInfo) info = NULL;
+      g_autoptr(IdeTreeNode) node = NULL;
+      g_autoptr(GFile) directory = NULL;
+
+      g_assert (G_IS_FILE (item));
+
+      if ((parent = find_file_node (self->tree, item)))
+        {
+          if (!ide_tree_node_expanded (self->tree, parent))
+            IDE_EXIT;
+
+          continue;
+        }
+
+      directory = g_file_get_parent (item);
+      parent = find_file_node (self->tree, directory);
+
+      info = g_file_query_info (item,
+                                IDE_PROJECT_FILE_ATTRIBUTES,
+                                G_FILE_QUERY_INFO_NONE,
+                                NULL, NULL);
+
+      if (info == NULL)
+        IDE_EXIT;
+
+      project_file = ide_project_file_new (directory, info);
+      node = create_file_node (project_file);
+
+      /* TODO: Sort item */
+      ide_tree_node_append (parent, node);
+    }
+
+  IDE_EXIT;
+}
+
+static void
+gbp_project_tree_addin_remove_file (GbpProjectTreeAddin *self,
+                                    GFile               *file)
+{
+  IdeTreeNode *selected;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+
+#ifdef IDE_ENABLE_TRACE
+  {
+    g_autofree gchar *uri = g_file_get_uri (file);
+    IDE_TRACE_MSG ("Removing file from tree \"%s\"", uri);
+  }
+#endif
+
+  if ((selected = find_file_node (self->tree, file)))
+    ide_tree_node_remove (ide_tree_node_get_parent (selected), selected);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_project_tree_addin_changed_cb (GbpProjectTreeAddin *self,
+                                   GFile               *file,
+                                   GFile               *other_file,
+                                   GFileMonitorEvent    event,
+                                   IdeVcsMonitor       *monitor)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+  g_assert (!other_file || G_IS_FILE (other_file));
+  g_assert (IDE_IS_VCS_MONITOR (monitor));
+
+  if (event == G_FILE_MONITOR_EVENT_CREATED)
+    gbp_project_tree_addin_add_file (self, file);
+  else if (event == G_FILE_MONITOR_EVENT_DELETED)
+    gbp_project_tree_addin_remove_file (self, file);
+}
+
+static void
+gbp_project_tree_addin_reloaded_cb (GbpProjectTreeAddin *self,
+                                    IdeVcsMonitor       *monitor)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
+  g_assert (IDE_IS_VCS_MONITOR (monitor));
+
+  gtk_widget_queue_resize (GTK_WIDGET (self->tree));
+}
+
+static void
+gbp_project_tree_addin_load (IdeTreeAddin *addin,
+                             IdeTree      *tree,
+                             IdeTreeModel *model)
+{
+  static const GtkTargetEntry drag_targets[] = {
+    { (gchar *)"GTK_TREE_MODEL_ROW", GTK_TARGET_SAME_WIDGET, 0 },
+    { (gchar *)"text/uri-list", 0, 0 },
+  };
+
+  GbpProjectTreeAddin *self = (GbpProjectTreeAddin *)addin;
+  IdeVcsMonitor *monitor;
+  IdeWorkbench *workbench;
+
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  self->tree = tree;
+  self->model = model;
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (tree));
+  monitor = ide_workbench_get_vcs_monitor (workbench);
+
+  g_signal_connect_object (monitor,
+                           "changed",
+                           G_CALLBACK (gbp_project_tree_addin_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (monitor,
+                           "reloaded",
+                           G_CALLBACK (gbp_project_tree_addin_reloaded_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_tree_view_enable_model_drag_source (GTK_TREE_VIEW (tree),
+                                          GDK_BUTTON1_MASK,
+                                          drag_targets, G_N_ELEMENTS (drag_targets),
+                                          GDK_ACTION_COPY | GDK_ACTION_MOVE);
+  gtk_tree_view_enable_model_drag_dest (GTK_TREE_VIEW (tree),
+                                        drag_targets, G_N_ELEMENTS (drag_targets),
+                                        GDK_ACTION_COPY | GDK_ACTION_MOVE);
+}
+
+static void
+gbp_project_tree_addin_unload (IdeTreeAddin *addin,
+                               IdeTree      *tree,
+                               IdeTreeModel *model)
+{
+  GbpProjectTreeAddin *self = (GbpProjectTreeAddin *)addin;
+
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  self->tree = NULL;
+  self->model = NULL;
+}
+
+static gboolean
+gbp_project_tree_addin_node_draggable (IdeTreeAddin *addin,
+                                       IdeTreeNode  *node)
+{
+  return ide_tree_node_holds (node, IDE_TYPE_PROJECT_FILE);
+}
+
+static gboolean
+gbp_project_tree_addin_node_droppable (IdeTreeAddin     *addin,
+                                       IdeTreeNode      *drag_node,
+                                       IdeTreeNode      *drop_node,
+                                       GtkSelectionData *selection)
+{
+  IdeProjectFile *drop_file = NULL;
+  g_auto(GStrv) uris = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (addin));
+  g_assert (!drag_node || IDE_IS_TREE_NODE (drag_node));
+  g_assert (!drop_node || IDE_IS_TREE_NODE (drop_node));
+
+  /* Must drop on a file */
+  if (drop_node == NULL ||
+      !ide_tree_node_holds (drop_node, IDE_TYPE_PROJECT_FILE))
+    return FALSE;
+
+  /* The drop file must be a directory */
+  drop_file = ide_tree_node_get_item (drop_node);
+  if (!ide_project_file_is_directory (drop_file))
+    return FALSE;
+
+  /* We need a uri list or file node */
+  uris = gtk_selection_data_get_uris (selection);
+  if ((uris == NULL || uris[0] == NULL) && drag_node == NULL)
+    return FALSE;
+
+  /* If we have a drag node, make sure it's a file */
+  if (drag_node != NULL &&
+      !ide_tree_node_holds (drop_node, IDE_TYPE_PROJECT_FILE))
+    return FALSE;
+
+  return TRUE;
+}
+
+static void
+gbp_project_tree_addin_notify_progress_cb (DzlFileTransfer *transfer,
+                                           GParamSpec      *pspec,
+                                           IdeNotification *notif)
+{
+  g_autofree gchar *body = NULL;
+  DzlFileTransferStat stbuf;
+  gchar count[16];
+  gchar total[16];
+  gdouble progress;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (DZL_IS_FILE_TRANSFER (transfer));
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  dzl_file_transfer_stat (transfer, &stbuf);
+
+  progress = dzl_file_transfer_get_progress (transfer);
+  ide_notification_set_progress (notif, progress);
+
+  g_snprintf (count, sizeof count, "%"G_GINT64_FORMAT, stbuf.n_files);
+  g_snprintf (total, sizeof total, "%"G_GINT64_FORMAT, stbuf.n_files_total);
+
+  if (stbuf.n_files_total == 1)
+    body = g_strdup_printf (_("Copying 1 file"));
+  else
+    /* translators: first %s is replaced with completed number of files, second %s with total number of 
files */
+    body = g_strdup_printf (_("Copying %s of %s files"), count, total);
+
+  ide_notification_set_body (notif, body);
+}
+
+static void
+gbp_project_tree_addin_transfer_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  DzlFileTransfer *transfer = (DzlFileTransfer *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  GbpProjectTreeAddin *self;
+  IdeNotification *notif;
+  DzlFileTransferStat stbuf;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (DZL_IS_FILE_TRANSFER (transfer));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  self = ide_task_get_source_object (task);
+  notif = ide_task_get_task_data (task);
+
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
+  g_assert (notif != NULL);
+  g_assert (IDE_IS_NOTIFICATION (notif));
+
+  gbp_project_tree_addin_notify_progress_cb (transfer, NULL, notif);
+  ide_notification_set_progress (notif, 1.0);
+
+  if (!dzl_file_transfer_execute_finish (transfer, result, &error))
+    {
+      ide_notification_set_title (notif, _("Failed to copy files"));
+      ide_notification_set_body (notif, error->message);
+      ide_task_return_error (task, g_steal_pointer (&error));
+    }
+  else
+    {
+      GPtrArray *sources;
+
+      dzl_file_transfer_stat (transfer, &stbuf);
+
+      ide_notification_set_title (notif, _("Files copied"));
+
+      if (stbuf.n_files_total == 1)
+        {
+          ide_notification_set_body (notif, _("Copied 1 file"));
+        }
+      else
+        {
+          g_autofree gchar *format = NULL;
+          gchar count[16];
+
+          g_snprintf (count, sizeof count, "%"G_GINT64_FORMAT, stbuf.n_files_total);
+          format = g_strdup_printf (_("Copied %s files"), count);
+          ide_notification_set_body (notif, format);
+        }
+
+      sources = g_object_get_data (G_OBJECT (task), "SOURCE_FILES");
+
+      if (sources != NULL)
+        {
+          IdeContext *context;
+          IdeProject *project;
+
+          /*
+           * We avoid deleting files here and instead just trash the
+           * existing files to help reduce any chance that we delete
+           * user data.
+           *
+           * Also, this will only trash files that are within our
+           * project directory. Currently, I'm considering that a
+           * feature, but when I trust file-deletion more, we can
+           * open it up in IdeProject.
+           */
+
+          context = ide_object_get_context (IDE_OBJECT (self->model));
+          project = ide_project_from_context (context);
+
+          for (guint i = 0; i < sources->len; i++)
+            {
+              GFile *source = g_ptr_array_index (sources, i);
+
+              g_assert (G_IS_FILE (source));
+
+              ide_project_trash_file_async (project, source, NULL, NULL, NULL);
+            }
+        }
+
+      ide_task_return_boolean (task, TRUE);
+    }
+
+  ide_notification_withdraw_in_seconds (notif, -1);
+
+  IDE_EXIT;
+}
+
+static void
+gbp_project_tree_addin_node_dropped_async (IdeTreeAddin        *addin,
+                                           IdeTreeNode         *drag_node,
+                                           IdeTreeNode         *drop_node,
+                                           GtkSelectionData    *selection,
+                                           GdkDragAction        actions,
+                                           GCancellable        *cancellable,
+                                           GAsyncReadyCallback  callback,
+                                           gpointer             user_data)
+{
+  GbpProjectTreeAddin *self = (GbpProjectTreeAddin *)addin;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(DzlFileTransfer) transfer = NULL;
+  g_autoptr(GFile) src_file = NULL;
+  g_autoptr(GFile) dst_dir = NULL;
+  g_autoptr(IdeNotification) notif = NULL;
+  g_autoptr(GPtrArray) srcs = NULL;
+  g_auto(GStrv) uris = NULL;
+  IdeProjectFile *drag_file;
+  IdeProjectFile *drop_file;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
+  g_assert (!drag_node || IDE_IS_TREE_NODE (drag_node));
+  g_assert (!drop_node || IDE_IS_TREE_NODE (drop_node));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_project_tree_addin_node_dropped_async);
+
+  if (!gbp_project_tree_addin_node_droppable (addin, drag_node, drop_node, selection))
+    {
+      ide_task_return_boolean (task, TRUE);
+      IDE_EXIT;
+    }
+
+  srcs = g_ptr_array_new_with_free_func (g_object_unref);
+  uris = gtk_selection_data_get_uris (selection);
+
+  if (uris != NULL)
+    {
+      for (guint i = 0; uris[i]; i++)
+        g_ptr_array_add (srcs, g_file_new_for_uri (uris[i]));
+    }
+
+  drop_file = ide_tree_node_get_item (drop_node);
+  g_assert (drop_file != NULL);
+  g_assert (ide_project_file_is_directory (drop_file));
+
+  if (drag_node != NULL)
+    {
+      drag_file = ide_tree_node_get_item (drag_node);
+      src_file = ide_project_file_ref_file (drag_file);
+      g_assert (G_IS_FILE (src_file));
+      g_ptr_array_add (srcs, g_object_ref (src_file));
+    }
+
+  dst_dir = ide_project_file_ref_file (drop_file);
+  g_assert (G_IS_FILE (dst_dir));
+
+  transfer = dzl_file_transfer_new ();
+  dzl_file_transfer_set_flags (transfer, DZL_FILE_TRANSFER_FLAGS_NONE);
+  g_signal_connect_object (transfer,
+                           "notify::progress",
+                           G_CALLBACK (gbp_project_tree_addin_notify_progress_cb),
+                           notif,
+                           0);
+
+  for (guint i = 0; i < srcs->len; i++)
+    {
+      GFile *source = g_ptr_array_index (srcs, i);
+      g_autofree gchar *name = NULL;
+      g_autoptr(GFile) dst_file = NULL;
+
+      name = g_file_get_basename (source);
+      g_assert (name != NULL);
+
+      dst_file = g_file_get_child (dst_dir, name);
+      g_assert (G_IS_FILE (dst_file));
+
+      if (srcs->len == 1 && g_file_equal (source, dst_file))
+        {
+          ide_task_return_boolean (task, TRUE);
+          IDE_EXIT;
+        }
+
+      dzl_file_transfer_add (transfer, source, dst_file);
+    }
+
+  if (actions == GDK_ACTION_MOVE)
+    g_object_set_data_full (G_OBJECT (task),
+                            "SOURCE_FILES",
+                            g_steal_pointer (&srcs),
+                            (GDestroyNotify)g_ptr_array_unref);
+
+  notif = ide_notification_new ();
+  ide_notification_set_title (notif, _("Copying files…"));
+  ide_notification_set_body (notif, _("Files will be copied in a moment"));
+  ide_notification_set_has_progress (notif, TRUE);
+  ide_notification_attach (notif, IDE_OBJECT (self->model));
+  ide_task_set_task_data (task, g_object_ref (notif), g_object_unref);
+
+  dzl_file_transfer_execute_async (transfer,
+                                   G_PRIORITY_DEFAULT,
+                                   cancellable,
+                                   gbp_project_tree_addin_transfer_cb,
+                                   g_steal_pointer (&task));
+
+  IDE_EXIT;
+}
+
+static gboolean
+gbp_project_tree_addin_node_dropped_finish (IdeTreeAddin  *addin,
+                                            GAsyncResult  *result,
+                                            GError       **error)
+{
+  gboolean ret;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TASK (result));
+
+  ret = ide_task_propagate_boolean (IDE_TASK (result), error);
+
+  IDE_RETURN (ret);
+}
+
+static void
+tree_addin_iface_init (IdeTreeAddinInterface *iface)
+{
+  iface->load = gbp_project_tree_addin_load;
+  iface->unload = gbp_project_tree_addin_unload;
+  iface->build_children_async = gbp_project_tree_addin_build_children_async;
+  iface->build_children_finish = gbp_project_tree_addin_build_children_finish;
+  iface->node_activated = gbp_project_tree_addin_node_activated;
+  iface->node_draggable = gbp_project_tree_addin_node_draggable;
+  iface->node_droppable = gbp_project_tree_addin_node_droppable;
+  iface->node_dropped_async = gbp_project_tree_addin_node_dropped_async;
+  iface->node_dropped_finish = gbp_project_tree_addin_node_dropped_finish;
+}
+
+static void
+gbp_project_tree_addin_settings_changed (GbpProjectTreeAddin *self,
+                                         const gchar         *key,
+                                         GSettings           *settings)
+{
+  g_assert (GBP_IS_PROJECT_TREE_ADDIN (self));
+  g_assert (G_IS_SETTINGS (settings));
+
+  self->sort_directories_first = g_settings_get_boolean (self->settings, "sort-directories-first");
+  self->show_ignored_files = g_settings_get_boolean (self->settings, "show-ignored-files");
+
+  if (self->model != NULL)
+    ide_tree_model_invalidate (self->model, NULL);
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpProjectTreeAddin, gbp_project_tree_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_TREE_ADDIN, tree_addin_iface_init))
+
+static void
+gbp_project_tree_addin_dispose (GObject *object)
+{
+  GbpProjectTreeAddin *self = (GbpProjectTreeAddin *)object;
+
+  g_clear_object (&self->settings);
+
+  G_OBJECT_CLASS (gbp_project_tree_addin_parent_class)->dispose (object);
+}
+
+static void
+gbp_project_tree_addin_class_init (GbpProjectTreeAddinClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gbp_project_tree_addin_dispose;
+}
+
+static void
+gbp_project_tree_addin_init (GbpProjectTreeAddin *self)
+{
+  self->settings = g_settings_new ("org.gnome.builder.project-tree");
+
+  g_signal_connect_object (self->settings,
+                           "changed",
+                           G_CALLBACK (gbp_project_tree_addin_settings_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gbp_project_tree_addin_settings_changed (self, NULL, self->settings);
+}
diff --git a/src/plugins/project-tree/gbp-project-tree-addin.h 
b/src/plugins/project-tree/gbp-project-tree-addin.h
new file mode 100644
index 000000000..525b8fba3
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree-addin.h
@@ -0,0 +1,31 @@
+/* gbp-project-tree-addin.h
+ *
+ * Copyright 2018-2019 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_PROJECT_TREE_ADDIN (gbp_project_tree_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpProjectTreeAddin, gbp_project_tree_addin, GBP, PROJECT_TREE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/project-tree/gbp-project-tree-pane-actions.c 
b/src/plugins/project-tree/gbp-project-tree-pane-actions.c
new file mode 100644
index 000000000..acb795a0a
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree-pane-actions.c
@@ -0,0 +1,634 @@
+/* gbp-project-tree-pane-actions.c
+ *
+ * Copyright 2018-2019 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-project-tree-pane-actions"
+
+#include "config.h"
+
+#include <libide-editor.h>
+#include <libide-projects.h>
+#include <vte/vte.h>
+
+#include "gbp-project-tree-private.h"
+#include "gbp-rename-file-popover.h"
+#include "gbp-new-file-popover.h"
+
+typedef struct
+{
+  IdeTreeNode *node;
+  GFile       *file;
+  GFileType    file_type;
+  guint        needs_collapse : 1;
+} NewState;
+
+static void
+new_state_free (NewState *state)
+{
+  g_clear_object (&state->node);
+  g_clear_object (&state->file);
+  g_slice_free (NewState, state);
+}
+
+static void
+new_action_completed_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  GbpProjectTreePane *self = (GbpProjectTreePane *)object;
+  NewState *state;
+
+  g_assert (GBP_IS_PROJECT_TREE_PANE (self));
+  g_assert (IDE_IS_TASK (result));
+
+  state = ide_task_get_task_data (IDE_TASK (result));
+  g_assert (state != NULL);
+  g_assert (IDE_IS_TREE_NODE (state->node));
+
+  if (state->needs_collapse)
+    ide_tree_collapse_node (self->tree, state->node);
+
+  /* Open the file if we created a regular file */
+  if (state->file_type == G_FILE_TYPE_REGULAR)
+    {
+      IdeWorkbench *workbench;
+
+      if (!(workbench = ide_widget_get_workbench (GTK_WIDGET (self->tree))))
+        return;
+
+      if (state->file != NULL)
+        ide_workbench_open_async (workbench, state->file, "editor", 0, NULL, NULL, NULL);
+    }
+}
+
+static void
+gbp_project_tree_pane_actions_mkdir_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!g_file_make_directory_finish (file, result, &error))
+    g_warning ("Failed to make directory: %s", error->message);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_project_tree_pane_actions_mkfile_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!g_file_create_finish (file, result, &error))
+    g_warning ("Failed to make file: %s", error->message);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_project_tree_pane_actions_new_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  GbpNewFilePopover *popover = (GbpNewFilePopover *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GFile) file = NULL;
+  GCancellable *cancellable;
+  NewState *state;
+
+  g_assert (GBP_IS_NEW_FILE_POPOVER (popover));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!(file = gbp_new_file_popover_display_finish (popover, result, &error)))
+    {
+      ide_task_return_error (task, g_steal_pointer (&error));
+      return;
+    }
+
+  cancellable = ide_task_get_cancellable (task);
+  state = ide_task_get_task_data (task);
+
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+  g_assert (state != NULL);
+  g_assert (IDE_IS_TREE_NODE (state->node));
+  g_assert (state->file_type);
+  g_assert (state->file == NULL);
+
+  state->file = g_object_ref (file);
+
+  if (state->file_type == G_FILE_TYPE_DIRECTORY)
+    g_file_make_directory_async (file,
+                                 G_PRIORITY_DEFAULT,
+                                 cancellable,
+                                 gbp_project_tree_pane_actions_mkdir_cb,
+                                 g_steal_pointer (&task));
+  else if (state->file_type == G_FILE_TYPE_REGULAR)
+    g_file_create_async (file,
+                         G_FILE_CREATE_NONE,
+                         G_PRIORITY_DEFAULT,
+                         cancellable,
+                         gbp_project_tree_pane_actions_mkfile_cb,
+                         g_steal_pointer (&task));
+  else
+    g_assert_not_reached ();
+
+  gtk_widget_destroy (GTK_WIDGET (popover));
+}
+
+static void
+gbp_project_tree_pane_actions_new (GbpProjectTreePane *self,
+                                   GFileType           file_type)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GFile) directory = NULL;
+  GbpNewFilePopover *popover;
+  IdeProjectFile *project_file;
+  IdeTreeNode *selected;
+  NewState *state;
+
+  g_assert (GBP_IS_PROJECT_TREE_PANE (self));
+  g_assert (file_type == G_FILE_TYPE_REGULAR ||
+            file_type == G_FILE_TYPE_DIRECTORY);
+
+  /* Nothing to do if there was no selection */
+  if (!(selected = ide_tree_get_selected_node (self->tree)))
+    return;
+
+  /* Select parent if we got an empty node or it's not a directory */
+  if (!ide_tree_node_holds (selected, IDE_TYPE_PROJECT_FILE) ||
+      !(project_file = ide_tree_node_get_item (selected)) ||
+      !ide_project_file_is_directory (project_file))
+    {
+      IdeTreeNode *parent = ide_tree_node_get_parent (selected);
+
+      if (!ide_tree_node_holds (parent, IDE_TYPE_PROJECT_FILE))
+        return;
+
+      project_file = ide_tree_node_get_item (parent);
+      selected = parent;
+
+      ide_tree_select_node (self->tree, parent);
+    }
+
+  /* Now create our async task to keep track of everything during
+   * the asynchronous nature of this workflow (the user entering
+   * infromation, maybe cancelling, and async file creation).
+   */
+  directory = ide_project_file_ref_file (project_file);
+
+  popover = g_object_new (GBP_TYPE_NEW_FILE_POPOVER,
+                          "directory", directory,
+                          "file-type", file_type,
+                          "position", GTK_POS_RIGHT,
+                          NULL);
+
+
+  state = g_slice_new0 (NewState);
+  state->needs_collapse = !ide_tree_node_expanded (self->tree, selected);
+  state->file_type = file_type;
+  state->node = g_object_ref (selected);
+
+  task = ide_task_new (self, NULL, new_action_completed_cb, NULL);
+  ide_task_set_source_tag (task, gbp_project_tree_pane_actions_new);
+  ide_task_set_task_data (task, state, new_state_free);
+
+  gbp_new_file_popover_display_async (popover,
+                                      self->tree,
+                                      selected,
+                                      NULL,
+                                      gbp_project_tree_pane_actions_new_cb,
+                                      g_steal_pointer (&task));
+}
+
+static void
+close_matching_pages (GtkWidget *widget,
+                      gpointer   user_data)
+{
+  IdePage *page = (IdePage *)widget;
+  GFile *file = user_data;
+  GFile *this_file;
+
+  g_assert (IDE_IS_PAGE (page));
+  g_assert (G_IS_FILE (file));
+
+  if (!IDE_IS_EDITOR_PAGE (page))
+    return;
+
+  if (!(this_file = ide_editor_page_get_file (IDE_EDITOR_PAGE (page))))
+    return;
+
+  if (g_file_equal (this_file, file))
+    gtk_widget_destroy (widget);
+}
+
+#define DEFINE_ACTION_HANDLER(short_name, BODY)                       \
+static void                                                           \
+gbp_project_tree_pane_actions_##short_name (GSimpleAction *action,    \
+                                            GVariant      *param,     \
+                                            gpointer       user_data) \
+{                                                                     \
+  GbpProjectTreePane *self = user_data;                               \
+                                                                      \
+  g_assert (G_IS_SIMPLE_ACTION (action));                             \
+  g_assert (GBP_IS_PROJECT_TREE_PANE (self));                         \
+                                                                      \
+  BODY                                                                \
+}
+
+DEFINE_ACTION_HANDLER (new_file, {
+  gbp_project_tree_pane_actions_new (self, G_FILE_TYPE_REGULAR);
+});
+
+DEFINE_ACTION_HANDLER (new_folder, {
+  gbp_project_tree_pane_actions_new (self, G_FILE_TYPE_DIRECTORY);
+});
+
+DEFINE_ACTION_HANDLER (open, {
+  IdeProjectFile *project_file;
+  g_autoptr(GFile) file = NULL;
+  IdeWorkbench *workbench;
+  IdeTreeNode *selected;
+
+  if (!(selected = ide_tree_get_selected_node (self->tree)) ||
+      !ide_tree_node_holds (selected, IDE_TYPE_PROJECT_FILE) ||
+      !(project_file = ide_tree_node_get_item (selected)))
+    return;
+
+  file = ide_project_file_ref_file (project_file);
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+
+  ide_workbench_open_async (workbench,
+                            file,
+                            NULL,
+                            IDE_BUFFER_OPEN_FLAGS_NONE,
+                            NULL, NULL, NULL);
+});
+
+static void
+gbp_project_tree_pane_actions_rename_cb (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  IdeProject *project = (IdeProject *)object;
+  g_autoptr(GbpProjectTreePane) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE_PANE (self));
+
+  if (!ide_project_rename_file_finish (project, result, &error))
+    g_warning ("Failed to rename file: %s", error->message);
+}
+
+static void
+gbp_project_tree_pane_actions_rename_display_cb (GObject      *object,
+                                                 GAsyncResult *result,
+                                                 gpointer      user_data)
+{
+  GbpRenameFilePopover *popover = (GbpRenameFilePopover *)object;
+  g_autoptr(GbpProjectTreePane) self = user_data;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(GFile) dst = NULL;
+  IdeProject *project;
+  IdeContext *context;
+  GFile *src;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE_PANE (self));
+
+  if (!(dst = gbp_rename_file_popover_display_finish (popover, result, &error)))
+    goto destroy;
+
+  src = gbp_rename_file_popover_get_file (popover);
+  context = ide_widget_get_context (GTK_WIDGET (self));
+  project = ide_project_from_context (context);
+
+  ide_project_rename_file_async (project,
+                                 src,
+                                 dst,
+                                 NULL,
+                                 gbp_project_tree_pane_actions_rename_cb,
+                                 g_object_ref (self));
+
+destroy:
+  gtk_widget_destroy (GTK_WIDGET (popover));
+}
+
+DEFINE_ACTION_HANDLER (rename, {
+  IdeProjectFile *project_file;
+  g_autoptr(GFile) file = NULL;
+  GbpRenameFilePopover *popover;
+  IdeWorkbench *workbench;
+  IdeTreeNode *selected;
+  gboolean is_dir;
+
+  if (!(selected = ide_tree_get_selected_node (self->tree)) ||
+      !ide_tree_node_holds (selected, IDE_TYPE_PROJECT_FILE) ||
+      !(project_file = ide_tree_node_get_item (selected)))
+    return;
+
+  is_dir = ide_project_file_is_directory (project_file);
+  file = ide_project_file_ref_file (project_file);
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self->tree));
+  ide_workbench_foreach_page (workbench, close_matching_pages, file);
+
+  popover = g_object_new (GBP_TYPE_RENAME_FILE_POPOVER,
+                          "position", GTK_POS_LEFT,
+                          "is-directory", is_dir,
+                          "file", file,
+                          NULL);
+
+  gbp_rename_file_popover_display_async (popover,
+                                         self->tree,
+                                         selected,
+                                         NULL,
+                                         gbp_project_tree_pane_actions_rename_display_cb,
+                                         g_object_ref (self));
+});
+
+static void
+gbp_project_tree_pane_actions_trash_cb (GObject      *object,
+                                        GAsyncResult *result,
+                                        gpointer      user_data)
+{
+  IdeProjectFile *project_file = (IdeProjectFile *)object;
+  g_autoptr(IdeTreeNode) node = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeTreeNode *parent;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_PROJECT_FILE (project_file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  if (!ide_project_file_trash_finish (project_file, result, &error))
+    return;
+
+  if ((parent = ide_tree_node_get_parent (node)))
+    ide_tree_node_remove (parent, node);
+}
+
+DEFINE_ACTION_HANDLER (trash, {
+  IdeProjectFile *project_file;
+  g_autoptr(GFile) file = NULL;
+  IdeWorkbench *workbench;
+  IdeTreeNode *selected;
+
+  if (!(selected = ide_tree_get_selected_node (self->tree)) ||
+      !ide_tree_node_holds (selected, IDE_TYPE_PROJECT_FILE) ||
+      !(project_file = ide_tree_node_get_item (selected)))
+    return;
+
+  file = ide_project_file_ref_file (project_file);
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self->tree));
+  ide_workbench_foreach_page (workbench, close_matching_pages, file);
+
+  ide_project_file_trash_async (project_file,
+                                NULL,
+                                gbp_project_tree_pane_actions_trash_cb,
+                                g_object_ref (selected));
+});
+
+DEFINE_ACTION_HANDLER (open_containing_folder, {
+  IdeProjectFile *project_file;
+  g_autoptr(GFile) file = NULL;
+  IdeTreeNode *selected;
+
+  if (!(selected = ide_tree_get_selected_node (self->tree)) ||
+      !ide_tree_node_holds (selected, IDE_TYPE_PROJECT_FILE) ||
+      !(project_file = ide_tree_node_get_item (selected)))
+    return;
+
+  file = ide_project_file_ref_file (project_file);
+  dzl_file_manager_show (file, NULL);
+});
+
+DEFINE_ACTION_HANDLER (open_with_hint, {
+  IdeProjectFile *project_file;
+  g_autoptr(GFile) file = NULL;
+  IdeWorkbench *workbench;
+  IdeTreeNode *selected;
+  const gchar *hint;
+
+  if (!(selected = ide_tree_get_selected_node (self->tree)) ||
+      !ide_tree_node_holds (selected, IDE_TYPE_PROJECT_FILE) ||
+      !(project_file = ide_tree_node_get_item (selected)) ||
+      !(hint = g_variant_get_string (param, NULL)))
+    return;
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (self));
+  file = ide_project_file_ref_file (project_file);
+
+  ide_workbench_open_async (workbench,
+                            file,
+                            hint,
+                            IDE_BUFFER_OPEN_FLAGS_NONE,
+                            NULL, NULL, NULL);
+});
+
+/* Based on gdesktopappinfo.c in GIO */
+static gchar *
+find_terminal_executable (void)
+{
+  gsize i;
+  gchar *path = NULL;
+  g_autoptr(GSettings) terminal_settings = NULL;
+  g_autofree gchar *gsettings_terminal = NULL;
+  const gchar *terminals[] = {
+    NULL,                     /* GSettings */
+    "x-terminal-emulator",    /* Debian's alternative system */
+    "gnome-terminal",
+    NULL,                     /* getenv ("TERM") */
+    "nxterm", "color-xterm",
+    "rxvt", "xterm", "dtterm"
+  };
+
+  /* This is deprecated, but at least the user can specify it! */
+  terminal_settings = g_settings_new ("org.gnome.desktop.default-applications.terminal");
+  gsettings_terminal = g_settings_get_string (terminal_settings, "exec");
+  terminals[0] = gsettings_terminal;
+
+  /* This is generally one of the fallback terminals */
+  terminals[3] = g_getenv ("TERM");
+
+  for (i = 0; i < G_N_ELEMENTS (terminals) && path == NULL; ++i)
+    {
+      if (terminals[i] != NULL)
+        {
+          G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+          path = g_find_program_in_path (terminals[i]);
+          G_GNUC_END_IGNORE_DEPRECATIONS
+        }
+    }
+
+  return path;
+}
+
+static void
+gbp_project_tree_pane_actions_open_in_terminal (GSimpleAction *action,
+                                                GVariant      *param,
+                                                gpointer       user_data)
+{
+  GbpProjectTreePane *self = user_data;
+  IdeProjectFile *project_file;
+  g_autoptr(GFile) file = NULL;
+  IdeTreeNode *selected;
+  g_autofree gchar *terminal_executable = NULL;
+  const gchar *argv[] = { NULL, NULL };
+  g_auto(GStrv) env = NULL;
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GError) error = NULL;
+
+  if (!(selected = ide_tree_get_selected_node (self->tree)) ||
+      !ide_tree_node_holds (selected, IDE_TYPE_PROJECT_FILE) ||
+      !(project_file = ide_tree_node_get_item (selected)))
+    return;
+
+  if (ide_project_file_is_directory (project_file))
+    workdir = ide_project_file_ref_file (project_file);
+  else
+    workdir = g_object_ref (ide_project_file_get_directory (project_file));
+
+  if (!g_file_is_native (workdir))
+    {
+      g_warning ("Not a native directory, cannot open terminal");
+      return;
+    }
+
+  terminal_executable = find_terminal_executable ();
+  argv[0] = terminal_executable;
+  g_return_if_fail (terminal_executable != NULL);
+
+  env = g_get_environ ();
+
+  {
+    /*
+     * Overwrite SHELL to the users default shell.
+     * Failure to do so typically results in /bin/sh being used.
+     */
+    g_autofree gchar *shell = vte_get_user_shell ();
+    env = g_environ_setenv (env, "SHELL", shell, TRUE);
+  }
+
+  /* Can't use GdkAppLaunchContext as
+   * we cannot set the working directory.
+   */
+  if (!g_spawn_async (g_file_peek_path (workdir),
+                      (gchar **)argv, env,
+                      G_SPAWN_STDERR_TO_DEV_NULL,
+                      NULL, NULL, NULL, &error))
+    /* translators: %s is replaced with the error message */
+    g_warning ("Failed to spawn terminal: %s", error->message);
+}
+
+static const GActionEntry entries[] = {
+  { "new-file", gbp_project_tree_pane_actions_new_file },
+  { "new-folder", gbp_project_tree_pane_actions_new_folder },
+  { "open", gbp_project_tree_pane_actions_open },
+  { "open-with-hint", gbp_project_tree_pane_actions_open_with_hint, "s" },
+  { "open-containing-folder", gbp_project_tree_pane_actions_open_containing_folder },
+  { "open-in-terminal", gbp_project_tree_pane_actions_open_in_terminal },
+  { "rename", gbp_project_tree_pane_actions_rename },
+  { "trash", gbp_project_tree_pane_actions_trash },
+};
+
+void
+_gbp_project_tree_pane_init_actions (GbpProjectTreePane *self)
+{
+  g_autoptr(GSimpleActionGroup) actions = NULL;
+
+  g_assert (GBP_IS_PROJECT_TREE_PANE (self));
+
+  actions = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (actions),
+                                   entries,
+                                   G_N_ELEMENTS (entries),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (self->tree),
+                                  "project-tree",
+                                  G_ACTION_GROUP (actions));
+
+  _gbp_project_tree_pane_update_actions (self);
+}
+
+void
+_gbp_project_tree_pane_update_actions (GbpProjectTreePane *self)
+{
+  GtkTreeSelection *selection;
+  gboolean is_file = FALSE;
+  gboolean is_dir = FALSE;
+
+  g_assert (GBP_IS_PROJECT_TREE_PANE (self));
+
+  if ((selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->tree))))
+    {
+      GtkTreeIter iter;
+
+      if (gtk_tree_selection_get_selected (selection, NULL, &iter))
+        {
+          IdeTreeModel *model = IDE_TREE_MODEL (gtk_tree_view_get_model (GTK_TREE_VIEW (self->tree)));
+          IdeTreeNode *node = ide_tree_model_get_node (model, &iter);
+          GObject *item = ide_tree_node_get_item (node);
+
+          if ((is_file = IDE_IS_PROJECT_FILE (item)))
+            is_dir = ide_project_file_is_directory (IDE_PROJECT_FILE (item));
+        }
+    }
+
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "project-tree", "new-file",
+                             "enabled", is_file,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "project-tree", "new-folder",
+                             "enabled", is_file,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "project-tree", "trash",
+                             "enabled", is_file,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "project-tree", "rename",
+                             "enabled", is_file,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "project-tree", "open",
+                             "enabled", is_file && !is_dir,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "project-tree", "open-with-hint",
+                             "enabled", is_file && !is_dir,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "project-tree", "open-containing-folder",
+                             "enabled", is_file,
+                             NULL);
+  dzl_gtk_widget_action_set (GTK_WIDGET (self->tree), "project-tree", "open-in-terminal",
+                             "enabled", is_file,
+                             NULL);
+}
diff --git a/src/plugins/project-tree/gbp-project-tree-pane.c 
b/src/plugins/project-tree/gbp-project-tree-pane.c
new file mode 100644
index 000000000..9070e4b92
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree-pane.c
@@ -0,0 +1,62 @@
+/* gbp-project-tree-pane.c
+ *
+ * Copyright 2018-2019 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-project-tree-pane"
+
+#include "config.h"
+
+#include "gbp-project-tree-private.h"
+#include "gbp-project-tree.h"
+
+G_DEFINE_TYPE (GbpProjectTreePane, gbp_project_tree_pane, IDE_TYPE_PANE)
+
+static void
+gbp_project_tree_pane_class_init (GbpProjectTreePaneClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/project-tree/gbp-project-tree-pane.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpProjectTreePane, tree);
+
+  g_type_ensure (GBP_TYPE_PROJECT_TREE);
+}
+
+static void
+gbp_project_tree_pane_init (GbpProjectTreePane *self)
+{
+  GtkTreeSelection *selection;
+  IdeApplication *app;
+  GMenu *menu;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  app = IDE_APPLICATION_DEFAULT;
+  menu = dzl_application_get_menu_by_id (DZL_APPLICATION (app), "project-tree-menu");
+  ide_tree_set_context_menu (self->tree, menu);
+
+  selection = gtk_tree_view_get_selection (GTK_TREE_VIEW (self->tree));
+  g_signal_connect_object (selection,
+                           "changed",
+                           G_CALLBACK (_gbp_project_tree_pane_update_actions),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  _gbp_project_tree_pane_init_actions (self);
+}
diff --git a/src/plugins/project-tree/gbp-project-tree-pane.h 
b/src/plugins/project-tree/gbp-project-tree-pane.h
new file mode 100644
index 000000000..9a47d78eb
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree-pane.h
@@ -0,0 +1,31 @@
+/* gbp-project-tree-pane.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_PROJECT_TREE_PANE (gbp_project_tree_pane_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpProjectTreePane, gbp_project_tree_pane, GBP, PROJECT_TREE_PANE, IdePane)
+
+G_END_DECLS
diff --git a/src/plugins/project-tree/gbp-project-tree-pane.ui 
b/src/plugins/project-tree/gbp-project-tree-pane.ui
new file mode 100644
index 000000000..c99ca7bbd
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree-pane.ui
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpProjectTreePane" parent="IdePane">
+    <child>
+      <object class="GtkScrolledWindow">
+        <property name="visible">true</property>
+        <child>
+          <object class="GbpProjectTree" id="tree">
+            <property name="level-indentation">16</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="i-wanna-be-list-box"/>
+              <class name="project-tree"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/project-tree/gbp-project-tree-private.h 
b/src/plugins/project-tree/gbp-project-tree-private.h
new file mode 100644
index 000000000..ba0aa052d
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree-private.h
@@ -0,0 +1,40 @@
+/* gbp-project-tree-private.h
+ *
+ * Copyright 2018-2019 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-gui.h>
+#include <libide-tree.h>
+
+#include "gbp-project-tree-pane.h"
+
+G_BEGIN_DECLS
+
+struct _GbpProjectTreePane
+{
+  IdePane       parent_instance;
+  IdeTree      *tree;
+  guint         has_loaded : 1;
+};
+
+void _gbp_project_tree_pane_init_actions   (GbpProjectTreePane *self);
+void _gbp_project_tree_pane_update_actions (GbpProjectTreePane *self);
+
+G_END_DECLS
diff --git a/src/plugins/project-tree/gbp-project-tree-workspace-addin.c 
b/src/plugins/project-tree/gbp-project-tree-workspace-addin.c
new file mode 100644
index 000000000..394bf326a
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree-workspace-addin.c
@@ -0,0 +1,102 @@
+/* gbp-project-tree-workspace-addin.c
+ *
+ * Copyright 2018-2019 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-project-tree-workspace-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+
+#include "gbp-project-tree-workspace-addin.h"
+#include "gbp-project-tree-pane.h"
+
+struct _GbpProjectTreeWorkspaceAddin
+{
+  GObject             parent_instance;
+  GbpProjectTreePane *pane;
+};
+
+static void
+gbp_project_tree_workspace_addin_load (IdeWorkspaceAddin *addin,
+                                       IdeWorkspace      *workspace)
+{
+  GbpProjectTreeWorkspaceAddin *self = (GbpProjectTreeWorkspaceAddin *)addin;
+  IdeEditorSidebar *sidebar;
+  IdeSurface *surface;
+
+  g_assert (GBP_IS_PROJECT_TREE_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace));
+
+  surface = ide_workspace_get_surface_by_name (workspace, "editor");
+  g_assert (IDE_IS_EDITOR_SURFACE (surface));
+
+  sidebar = ide_editor_surface_get_sidebar (IDE_EDITOR_SURFACE (surface));
+  g_assert (IDE_IS_EDITOR_SIDEBAR (sidebar));
+
+  self->pane = g_object_new (GBP_TYPE_PROJECT_TREE_PANE,
+                             "visible", TRUE,
+                             NULL);
+  g_signal_connect (self->pane,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->pane);
+  ide_editor_sidebar_add_section (sidebar,
+                                  "project-tree",
+                                  _("Project Tree"),
+                                  "view-list-symbolic",
+                                  NULL, NULL,
+                                  GTK_WIDGET (self->pane),
+                                  0);
+}
+
+static void
+gbp_project_tree_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                         IdeWorkspace      *workspace)
+{
+  GbpProjectTreeWorkspaceAddin *self = (GbpProjectTreeWorkspaceAddin *)addin;
+
+  g_assert (GBP_IS_PROJECT_TREE_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace));
+
+  if (self->pane != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->pane));
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_project_tree_workspace_addin_load;
+  iface->unload = gbp_project_tree_workspace_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpProjectTreeWorkspaceAddin, gbp_project_tree_workspace_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
+
+static void
+gbp_project_tree_workspace_addin_class_init (GbpProjectTreeWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_project_tree_workspace_addin_init (GbpProjectTreeWorkspaceAddin *self)
+{
+}
diff --git a/src/plugins/project-tree/gbp-project-tree-workspace-addin.h 
b/src/plugins/project-tree/gbp-project-tree-workspace-addin.h
new file mode 100644
index 000000000..796a0163c
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-project-tree-workspace-addin.h
+ *
+ * Copyright 2018-2019 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_PROJECT_TREE_WORKSPACE_ADDIN (gbp_project_tree_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpProjectTreeWorkspaceAddin, gbp_project_tree_workspace_addin, GBP, 
PROJECT_TREE_WORKSPACE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/project-tree/gbp-project-tree.c b/src/plugins/project-tree/gbp-project-tree.c
new file mode 100644
index 000000000..5fd644118
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree.c
@@ -0,0 +1,178 @@
+/* gbp-project-tree.c
+ *
+ * Copyright 2018-2019 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-project-tree"
+
+#include "config.h"
+
+#include <libide-gui.h>
+#include <libide-projects.h>
+
+#include "gbp-project-tree.h"
+
+struct _GbpProjectTree
+{
+  IdeTree parent_instance;
+};
+
+G_DEFINE_TYPE (GbpProjectTree, gbp_project_tree, IDE_TYPE_TREE)
+
+static IdeTreeNodeVisit
+locate_project_files (IdeTreeNode *node,
+                      gpointer     user_data)
+{
+  IdeTreeNode **out_node = user_data;
+
+  if (ide_tree_node_holds (node, IDE_TYPE_PROJECT_FILE))
+    {
+      *out_node = node;
+      return IDE_TREE_NODE_VISIT_BREAK;
+    }
+
+  return IDE_TREE_NODE_VISIT_CONTINUE;
+}
+
+static void
+project_files_expanded_cb (GObject      *object,
+                           GAsyncResult *result,
+                           gpointer      user_data)
+{
+  IdeTreeModel *model = (IdeTreeModel *)object;
+  g_autoptr(IdeTask) task = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (ide_tree_model_expand_finish (model, result, NULL))
+    {
+      g_autoptr(GtkTreePath) path = NULL;
+      GbpProjectTree *self;
+      IdeTreeNode *node;
+
+      self = ide_task_get_source_object (task);
+      node = ide_task_get_task_data (task);
+
+      g_assert (GBP_IS_PROJECT_TREE (self));
+      g_assert (IDE_IS_TREE_NODE (node));
+
+      if ((path = ide_tree_node_get_path (node)))
+        gtk_tree_view_expand_row (GTK_TREE_VIEW (self), path, FALSE);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_project_tree_expand_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeTreeModel *model = (IdeTreeModel *)object;
+  g_autoptr(IdeTask) task = user_data;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_MODEL (model));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (ide_tree_model_expand_finish (model, result, NULL))
+    {
+      IdeTreeNode *root = ide_tree_model_get_root (model);
+      IdeTreeNode *node = NULL;
+
+      ide_tree_node_traverse (root,
+                              G_PRE_ORDER,
+                              G_TRAVERSE_ALL,
+                              1,
+                              locate_project_files,
+                              &node);
+
+      if (node == NULL)
+        goto cleanup;
+
+      ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+
+      ide_tree_model_expand_async (model,
+                                   node,
+                                   NULL,
+                                   project_files_expanded_cb,
+                                   g_steal_pointer (&task));
+
+      return;
+    }
+
+cleanup:
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_project_tree_hierarchy_changed (GtkWidget *widget,
+                                    GtkWidget *old_toplevel)
+{
+  GbpProjectTree *self = (GbpProjectTree *)widget;
+  GtkWidget *toplevel;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_PROJECT_TREE (self));
+
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+
+  if (IDE_IS_WORKSPACE (toplevel))
+    {
+      IdeContext *context = ide_widget_get_context (GTK_WIDGET (toplevel));
+      g_autoptr(IdeTreeNode) root = ide_tree_node_new ();
+      g_autoptr(IdeTreeModel) model = NULL;
+      g_autoptr(IdeTask) task = NULL;
+
+      model = g_object_new (IDE_TYPE_TREE_MODEL,
+                            "kind", "project-tree",
+                            "tree", self,
+                            NULL);
+      gtk_tree_view_set_model (GTK_TREE_VIEW (self), GTK_TREE_MODEL (model));
+
+      ide_tree_node_set_item (root, context);
+      ide_object_append (IDE_OBJECT (context), IDE_OBJECT (model));
+      ide_tree_model_set_root (model, root);
+
+      task = ide_task_new (self, NULL, NULL, NULL);
+      ide_task_set_source_tag (task, gbp_project_tree_hierarchy_changed);
+
+      ide_tree_model_expand_async (model,
+                                   root,
+                                   NULL,
+                                   gbp_project_tree_expand_cb,
+                                   g_steal_pointer (&task));
+    }
+}
+
+static void
+gbp_project_tree_class_init (GbpProjectTreeClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  widget_class->hierarchy_changed = gbp_project_tree_hierarchy_changed;
+}
+
+static void
+gbp_project_tree_init (GbpProjectTree *self)
+{
+}
diff --git a/src/plugins/project-tree/gbp-project-tree.h b/src/plugins/project-tree/gbp-project-tree.h
new file mode 100644
index 000000000..1a1404dbe
--- /dev/null
+++ b/src/plugins/project-tree/gbp-project-tree.h
@@ -0,0 +1,31 @@
+/* gbp-project-tree.h
+ *
+ * Copyright 2018-2019 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-tree.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_PROJECT_TREE (gbp_project_tree_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpProjectTree, gbp_project_tree, GBP, PROJECT_TREE, IdeTree)
+
+G_END_DECLS
diff --git a/src/plugins/project-tree/gbp-rename-file-popover.c 
b/src/plugins/project-tree/gbp-rename-file-popover.c
new file mode 100644
index 000000000..2453d000f
--- /dev/null
+++ b/src/plugins/project-tree/gbp-rename-file-popover.c
@@ -0,0 +1,452 @@
+/* gbp-rename-file-popover.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-rename-file-popover"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+
+#include "gbp-rename-file-popover.h"
+
+struct _GbpRenameFilePopover
+{
+  GtkPopover    parent_instance;
+
+  GCancellable *cancellable;
+  GFile        *file;
+  IdeTask      *task;
+
+  GtkEntry     *entry;
+  GtkButton    *button;
+  GtkLabel     *label;
+  GtkLabel     *message;
+
+  guint         is_directory : 1;
+};
+
+enum {
+  PROP_0,
+  PROP_FILE,
+  PROP_IS_DIRECTORY,
+  N_PROPS
+};
+
+enum {
+  RENAME_FILE,
+  N_SIGNALS
+};
+
+G_DEFINE_TYPE (GbpRenameFilePopover, gbp_rename_file_popover, GTK_TYPE_POPOVER)
+
+static GParamSpec *properties [N_PROPS];
+static guint signals [N_SIGNALS];
+
+GFile *
+gbp_rename_file_popover_get_file (GbpRenameFilePopover *self)
+{
+  g_return_val_if_fail (GBP_IS_RENAME_FILE_POPOVER (self), NULL);
+
+  return self->file;
+}
+
+static void
+gbp_rename_file_popover_set_file (GbpRenameFilePopover *self,
+                                 GFile               *file)
+{
+  g_return_if_fail (GBP_IS_RENAME_FILE_POPOVER (self));
+  g_return_if_fail (G_IS_FILE (file));
+
+  if (g_set_object (&self->file, file))
+    {
+      if (file != NULL)
+        {
+          g_autofree gchar *name = NULL;
+          g_autofree gchar *label = NULL;
+
+          name = g_file_get_basename (file);
+          label = g_strdup_printf (_("Rename %s"), name);
+
+          gtk_label_set_label (self->label, label);
+          gtk_entry_set_text (self->entry, name);
+        }
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_FILE]);
+    }
+}
+
+static void
+gbp_rename_file_popover_set_is_directory (GbpRenameFilePopover *self,
+                                         gboolean             is_directory)
+{
+  g_return_if_fail (GBP_IS_RENAME_FILE_POPOVER (self));
+
+  is_directory = !!is_directory;
+
+  if (is_directory != self->is_directory)
+    {
+      self->is_directory = is_directory;
+      g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_IS_DIRECTORY]);
+    }
+}
+
+static void
+gbp_rename_file_popover__file_query_info (GObject      *object,
+                                         GAsyncResult *result,
+                                         gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(GFileInfo) file_info = NULL;
+  g_autoptr(GbpRenameFilePopover) self = user_data;
+  g_autoptr(GError) error = NULL;
+  GFileType file_type;
+
+  file_info = g_file_query_info_finish (file, result, &error);
+
+  if (file_info == NULL &&
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+    return;
+
+  if ((file_info == NULL) &&
+      g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
+    {
+      gtk_label_set_label (self->message, NULL);
+      gtk_widget_set_sensitive (GTK_WIDGET (self->button), TRUE);
+      return;
+    }
+
+  if (file_info == NULL)
+    {
+      gtk_label_set_label (self->message, error->message);
+      return;
+    }
+
+  file_type = g_file_info_get_file_type (file_info);
+
+  if (file_type == G_FILE_TYPE_DIRECTORY)
+    gtk_label_set_label (self->message,
+                         _("A folder with that name already exists."));
+  else
+    gtk_label_set_label (self->message,
+                         _("A file with that name already exists."));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), FALSE);
+}
+
+static void
+gbp_rename_file_popover__entry_changed (GbpRenameFilePopover *self,
+                                       GtkEntry            *entry)
+{
+  g_autoptr(GFile) parent = NULL;
+  g_autoptr(GFile) file = NULL;
+  const gchar *text;
+
+  g_assert (GBP_IS_RENAME_FILE_POPOVER (self));
+  g_assert (GTK_IS_ENTRY (entry));
+  g_assert (self->file != NULL);
+  g_assert (G_IS_FILE (self->file));
+
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), FALSE);
+  gtk_label_set_label (self->message, NULL);
+
+  text = gtk_entry_get_text (entry);
+  if (ide_str_empty0 (text))
+    return;
+
+  if (self->cancellable)
+    {
+      g_cancellable_cancel (self->cancellable);
+      g_clear_object (&self->cancellable);
+    }
+
+  self->cancellable = g_cancellable_new ();
+
+  parent = g_file_get_parent (self->file);
+  file = g_file_get_child (parent, text);
+
+  g_file_query_info_async (file,
+                           G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                           G_FILE_QUERY_INFO_NONE,
+                           G_PRIORITY_DEFAULT,
+                           self->cancellable,
+                           gbp_rename_file_popover__file_query_info,
+                           g_object_ref (self));
+}
+
+static void
+gbp_rename_file_popover__entry_activate (GbpRenameFilePopover *self,
+                                        GtkEntry            *entry)
+{
+  g_assert (GBP_IS_RENAME_FILE_POPOVER (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  if (gtk_widget_get_sensitive (GTK_WIDGET (self->button)))
+    gtk_widget_activate (GTK_WIDGET (self->button));
+}
+
+static void
+gbp_rename_file_popover__entry_focus_in_event (GbpRenameFilePopover *self,
+                                              GdkEvent            *event,
+                                              GtkEntry            *entry)
+{
+  const gchar *name;
+  const gchar *tmp;
+
+  g_assert (GBP_IS_RENAME_FILE_POPOVER (self));
+  g_assert (GTK_IS_ENTRY (entry));
+
+  name = gtk_entry_get_text (entry);
+
+  if (NULL != (tmp = strrchr (name, '.')))
+    gtk_editable_select_region (GTK_EDITABLE (entry), 0, tmp - name);
+}
+
+static void
+gbp_rename_file_popover__button_clicked (GbpRenameFilePopover *self,
+                                        GtkButton           *button)
+{
+  g_autoptr(GFile) file = NULL;
+  g_autoptr(GFile) parent = NULL;
+  g_autoptr(IdeTask) task = NULL;
+  const gchar *path;
+
+  g_assert (GBP_IS_RENAME_FILE_POPOVER (self));
+  g_assert (GTK_IS_BUTTON (button));
+  g_assert (self->file != NULL);
+  g_assert (G_IS_FILE (self->file));
+
+  path = gtk_entry_get_text (self->entry);
+  if (ide_str_empty0 (path))
+    return;
+
+  parent = g_file_get_parent (self->file);
+  file = g_file_get_child (parent, path);
+
+  /* only activate once */
+  gtk_widget_set_sensitive (GTK_WIDGET (self->button), FALSE);
+
+  g_signal_emit (self, signals [RENAME_FILE], 0, self->file, file);
+
+  /* Complete our async op */
+  if ((task = g_steal_pointer (&self->task)))
+    ide_task_return_pointer (task, g_steal_pointer (&file), g_object_unref);
+}
+
+static void
+gbp_rename_file_popover_closed (GtkPopover *popover)
+{
+  g_autoptr(IdeTask) task = NULL;
+  GbpRenameFilePopover *self = (GbpRenameFilePopover *)popover;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_RENAME_FILE_POPOVER (self));
+
+  if ((task = g_steal_pointer (&self->task)))
+    ide_task_return_new_error (task,
+                               G_IO_ERROR,
+                               G_IO_ERROR_CANCELLED,
+                               "The popover was cancelled");
+}
+
+static void
+gbp_rename_file_popover_finalize (GObject *object)
+{
+  GbpRenameFilePopover *self = (GbpRenameFilePopover *)object;
+
+  if (self->cancellable != NULL)
+    {
+      if (!g_cancellable_is_cancelled (self->cancellable))
+        g_cancellable_cancel (self->cancellable);
+      g_clear_object (&self->cancellable);
+    }
+
+  g_clear_object (&self->file);
+
+  g_assert (self->task == NULL);
+
+  G_OBJECT_CLASS (gbp_rename_file_popover_parent_class)->finalize (object);
+}
+
+static void
+gbp_rename_file_popover_get_property (GObject    *object,
+                                     guint       prop_id,
+                                     GValue     *value,
+                                     GParamSpec *pspec)
+{
+  GbpRenameFilePopover *self = GBP_RENAME_FILE_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      g_value_set_object (value, self->file);
+      break;
+
+    case PROP_IS_DIRECTORY:
+      g_value_set_boolean (value, self->is_directory);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_rename_file_popover_set_property (GObject      *object,
+                                     guint         prop_id,
+                                     const GValue *value,
+                                     GParamSpec   *pspec)
+{
+  GbpRenameFilePopover *self = GBP_RENAME_FILE_POPOVER (object);
+
+  switch (prop_id)
+    {
+    case PROP_FILE:
+      gbp_rename_file_popover_set_file (self, g_value_get_object (value));
+      break;
+
+    case PROP_IS_DIRECTORY:
+      gbp_rename_file_popover_set_is_directory (self, g_value_get_boolean (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_rename_file_popover_class_init (GbpRenameFilePopoverClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkPopoverClass *popover_class = GTK_POPOVER_CLASS (klass);
+
+  object_class->finalize = gbp_rename_file_popover_finalize;
+  object_class->get_property = gbp_rename_file_popover_get_property;
+  object_class->set_property = gbp_rename_file_popover_set_property;
+
+  popover_class->closed = gbp_rename_file_popover_closed;
+
+  properties [PROP_FILE] =
+    g_param_spec_object ("file",
+                         "File",
+                         "File",
+                         G_TYPE_FILE,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_IS_DIRECTORY] =
+    g_param_spec_boolean ("is-directory",
+                          "Is Directory",
+                          "Is Directory",
+                          FALSE,
+                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+
+  signals [RENAME_FILE] =
+    g_signal_new ("rename-file",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_FILE,
+                  G_TYPE_FILE);
+
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/project-tree/gbp-rename-file-popover.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpRenameFilePopover, button);
+  gtk_widget_class_bind_template_child (widget_class, GbpRenameFilePopover, entry);
+  gtk_widget_class_bind_template_child (widget_class, GbpRenameFilePopover, label);
+  gtk_widget_class_bind_template_child (widget_class, GbpRenameFilePopover, message);
+}
+
+static void
+gbp_rename_file_popover_init (GbpRenameFilePopover *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  g_signal_connect_object (self->entry,
+                           "changed",
+                           G_CALLBACK (gbp_rename_file_popover__entry_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->entry,
+                           "activate",
+                           G_CALLBACK (gbp_rename_file_popover__entry_activate),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->button,
+                           "clicked",
+                           G_CALLBACK (gbp_rename_file_popover__button_clicked),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->entry,
+                           "focus-in-event",
+                           G_CALLBACK (gbp_rename_file_popover__entry_focus_in_event),
+                           self,
+                           G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+}
+
+void
+gbp_rename_file_popover_display_async (GbpRenameFilePopover *self,
+                                       IdeTree              *tree,
+                                       IdeTreeNode          *node,
+                                       GCancellable         *cancellable,
+                                       GAsyncReadyCallback   callback,
+                                       gpointer              user_data)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_return_if_fail (IDE_IS_MAIN_THREAD ());
+  g_return_if_fail (GBP_IS_RENAME_FILE_POPOVER (self));
+  g_return_if_fail (IDE_IS_TREE (tree));
+  g_return_if_fail (IDE_IS_TREE_NODE (node));
+  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_rename_file_popover_display_async);
+
+  if (self->task != NULL)
+    {
+      ide_task_return_new_error (task,
+                                 G_IO_ERROR,
+                                 G_IO_ERROR_FAILED,
+                                 "Already displayed popover");
+      return;
+    }
+
+  self->task = g_steal_pointer (&task);
+
+  ide_tree_show_popover_at_node (tree, node, GTK_POPOVER (self));
+}
+
+GFile *
+gbp_rename_file_popover_display_finish (GbpRenameFilePopover  *self,
+                                        GAsyncResult          *result,
+                                        GError               **error)
+{
+  g_return_val_if_fail (IDE_IS_MAIN_THREAD (), NULL);
+  g_return_val_if_fail (GBP_IS_RENAME_FILE_POPOVER (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/project-tree/gbp-rename-file-popover.h 
b/src/plugins/project-tree/gbp-rename-file-popover.h
new file mode 100644
index 000000000..a7897e56d
--- /dev/null
+++ b/src/plugins/project-tree/gbp-rename-file-popover.h
@@ -0,0 +1,42 @@
+/* gbp-rename-file-popover.h
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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-tree.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_RENAME_FILE_POPOVER (gbp_rename_file_popover_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpRenameFilePopover, gbp_rename_file_popover, GBP, RENAME_FILE_POPOVER, GtkPopover)
+
+GFile *gbp_rename_file_popover_get_file       (GbpRenameFilePopover  *self);
+void   gbp_rename_file_popover_display_async  (GbpRenameFilePopover  *self,
+                                               IdeTree               *tree,
+                                               IdeTreeNode           *node,
+                                               GCancellable          *cancellable,
+                                               GAsyncReadyCallback    callback,
+                                               gpointer               user_data);
+GFile *gbp_rename_file_popover_display_finish (GbpRenameFilePopover  *self,
+                                               GAsyncResult          *result,
+                                               GError               **error);
+
+G_END_DECLS
diff --git a/src/plugins/project-tree/gbp-rename-file-popover.ui 
b/src/plugins/project-tree/gbp-rename-file-popover.ui
new file mode 100644
index 000000000..9da58ddc6
--- /dev/null
+++ b/src/plugins/project-tree/gbp-rename-file-popover.ui
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.16 -->
+  <template class="GbpRenameFilePopover" parent="GtkPopover">
+    <child>
+      <object class="GtkBox">
+        <property name="border-width">12</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="label" translatable="yes">File Name</property>
+            <property name="xalign">0.0</property>
+            <property name="visible">true</property>
+            <attributes>
+              <attribute name="weight" value="bold"/>
+            </attributes>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="spacing">9</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="GtkEntry" id="entry">
+                <property name="width-chars">20</property>
+                <property name="visible">true</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="button">
+                <property name="sensitive">false</property>
+                <property name="label" translatable="yes">_Rename</property>
+                <property name="use-underline">true</property>
+                <property name="visible">true</property>
+                <style>
+                  <class name="destructive-action"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkLabel" id="message">
+            <property name="xalign">0.0</property>
+            <property name="visible">true</property>
+            <style>
+              <class name="dim-label"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/plugins/project-tree/gtk/menus.ui b/src/plugins/project-tree/gtk/menus.ui
index 903289782..519e139ef 100644
--- a/src/plugins/project-tree/gtk/menus.ui
+++ b/src/plugins/project-tree/gtk/menus.ui
@@ -1,107 +1,85 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <menu id="ide-source-view-popup-menu">
-    <section id="ide-source-view-popup-menu-reveal-section">
+  <menu id="project-tree-menu">
+    <section id="project-tree-menu-placeholder0"/>
+    <section id="project-tree-menu-new-section">
       <item>
-        <attribute name="label" translatable="yes">Re_veal in Project Tree</attribute>
-        <attribute name="action">project-tree.reveal</attribute>
-      </item>
-    </section>
-  </menu>
-  <menu id="gb-project-tree-popup-menu">
-    <section id="gb-project-tree-new-section">
-      <item>
-        <attribute name="label" translatable="yes">New _File</attribute>
+        <attribute name="id">project-tree-menu-new-file</attribute>
+        <attribute name="label" translatable="yes">New File…</attribute>
         <attribute name="action">project-tree.new-file</attribute>
       </item>
       <item>
-        <attribute name="label" translatable="yes">_New Folder</attribute>
-        <attribute name="action">project-tree.new-directory</attribute>
+        <attribute name="id">project-tree-menu-new-folder</attribute>
+        <attribute name="label" translatable="yes">New Folder…</attribute>
+        <attribute name="action">project-tree.new-folder</attribute>
       </item>
     </section>
-    <section id="gb-project-tree-open-section">
+    <section id="project-tree-menu-placeholder1"/>
+    <section id="project-tree-menu-open-section">
       <item>
-        <attribute name="label" translatable="yes">_Open</attribute>
+        <attribute name="id">project-tree-menu-open</attribute>
+        <attribute name="label" translatable="yes">Open</attribute>
         <attribute name="action">project-tree.open</attribute>
-        <attribute name="accel">Return</attribute>
       </item>
-      <submenu id="gb-project-tree-open-with-submenu">
-        <attribute name="label" translatable="yes">Open _With</attribute>
-        <section id="gb-project-tree-open-with-internal-section">
+      <submenu id="project-tree-menu-open-with-menu">
+        <attribute name="label" translatable="yes">Open With…</attribute>
+        <section id="project-tree-menu-open-with-section">
           <item>
-            <attribute name="id">gb-project-tree-open-with-editor</attribute>
+            <attribute name="id">project-tree-menu-open-editor</attribute>
             <attribute name="label" translatable="yes">Source Code Editor</attribute>
             <attribute name="action">project-tree.open-with-hint</attribute>
-            <attribute name="target" type="s">"editor"</attribute>
+            <attribute name="target" type="s">'editor'</attribute>
+          </item>
+          <item>
+            <attribute name="id">project-tree-menu-open-editor</attribute>
+            <attribute name="label" translatable="yes">UI Designer</attribute>
+            <attribute name="action">project-tree.open-with-hint</attribute>
+            <attribute name="target" type="s">'glade'</attribute>
           </item>
-        </section>
-        <section id="gb-project-tree-open-by-mime-section">
         </section>
       </submenu>
-    </section>
-    <section id="gb-project-tree-open-containing-section">
       <item>
-        <attribute name="label" translatable="yes">_Open Containing Folder</attribute>
+        <attribute name="id">project-tree-menu-open-folder</attribute>
+        <attribute name="label" translatable="yes">Open Containing Folder</attribute>
         <attribute name="action">project-tree.open-containing-folder</attribute>
       </item>
       <item>
-        <attribute name="label" translatable="yes">_Open in Terminal</attribute>
+        <attribute name="id">project-tree-menu-open-terminal</attribute>
+        <attribute name="label" translatable="yes">Open in Terminal</attribute>
         <attribute name="action">project-tree.open-in-terminal</attribute>
       </item>
     </section>
-    <section id="gb-project-tree-find-section"/>
-    <section id="gb-project-tree-rename-section">
-      <item>
-        <attribute name="label" translatable="yes">_Rename</attribute>
-        <attribute name="action">project-tree.rename-file</attribute>
-        <attribute name="accel">F2</attribute>
-      </item>
-    </section>
-    <section id="gb-project-tree-move-to-trash-section">
-      <item>
-        <attribute name="label" translatable="yes">Mo_ve to Trash</attribute>
-        <attribute name="action">project-tree.move-to-trash</attribute>
-        <attribute name="accel">Delete</attribute>
-      </item>
-    </section>
-    <section id="gb-project-tree-build-section">
+    <section id="project-tree-menu-placeholder2"/>
+    <section id="project-tree-menu-destructive-section">
       <item>
-        <attribute name="label" translatable="yes">_Build</attribute>
-        <attribute name="action">workbench.build</attribute>
+        <attribute name="id">project-tree-menu-rename</attribute>
+        <attribute name="label" translatable="yes">Rename</attribute>
+        <attribute name="action">project-tree.rename</attribute>
       </item>
       <item>
-        <attribute name="label" translatable="yes">_Rebuild</attribute>
-        <attribute name="action">workbench.rebuild</attribute>
+        <attribute name="id">project-tree-menu-trash</attribute>
+        <attribute name="label" translatable="yes">Move to Trash</attribute>
+        <attribute name="action">project-tree.trash</attribute>
       </item>
     </section>
-    <section id="gb-project-tree-display-options-section">
-      <submenu id="gb-project-tree-display-options-submenu">
+    <section id="project-tree-menu-placeholder3"/>
+    <section id="project-tree-menu-display-options-parent-section">
+      <submenu id="project-tree-menu-display-options">
         <attribute name="label" translatable="yes">Display Options</attribute>
-        <section id="gb-project-tree-display-options-show-section">
-          <item>
-            <attribute name="label" translatable="yes">Show Icons</attribute>
-            <attribute name="action">project-tree.show-icons</attribute>
-          </item>
+        <section id="project-tree-menu-display-options-section">
           <item>
+            <attribute name="id">project-tree-menu-show-ignored</attribute>
             <attribute name="label" translatable="yes">Show Ignored Files</attribute>
             <attribute name="action">project-tree.show-ignored-files</attribute>
           </item>
           <item>
+            <attribute name="id">project-tree-menu-sort-directories-first</attribute>
             <attribute name="label" translatable="yes">Sort Directories First</attribute>
             <attribute name="action">project-tree.sort-directories-first</attribute>
           </item>
         </section>
-        <section id="gb-project-tree-display-options-nodes-section">
-          <item>
-            <attribute name="label" translatable="yes">_Collapse All Nodes</attribute>
-            <attribute name="action">project-tree.collapse-all-nodes</attribute>
-          </item>
-          <item>
-            <attribute name="label" translatable="yes">_Refresh</attribute>
-            <attribute name="action">project-tree.refresh</attribute>
-          </item>
-        </section>
       </submenu>
     </section>
+    <section id="project-tree-menu-placeholder4"/>
   </menu>
 </interface>
diff --git a/src/plugins/project-tree/meson.build b/src/plugins/project-tree/meson.build
index 4ca024259..c95aa92be 100644
--- a/src/plugins/project-tree/meson.build
+++ b/src/plugins/project-tree/meson.build
@@ -1,39 +1,18 @@
-if get_option('with_project_tree')
+plugins_sources += files([
+  'project-tree-plugin.c',
+  'gbp-project-tree.c',
+  'gbp-project-tree-addin.c',
+  'gbp-project-tree-pane.c',
+  'gbp-project-tree-pane-actions.c',
+  'gbp-project-tree-workspace-addin.c',
+  'gbp-new-file-popover.c',
+  'gbp-rename-file-popover.c',
+])
 
-project_tree_resources = gnome.compile_resources(
-  'project-tree-resources',
+plugin_project_tree_resources = gnome.compile_resources(
+  'gbp-project-tree-resources',
   'project-tree.gresource.xml',
-  c_name: 'gb_project_tree',
+  c_name: 'gbp_project_tree',
 )
 
-project_tree_sources = [
-  'gb-new-file-popover.c',
-  'gb-new-file-popover.h',
-  'gb-project-file.c',
-  'gb-project-file.h',
-  'gb-project-tree-actions.c',
-  'gb-project-tree-actions.h',
-  'gb-project-tree-builder.c',
-  'gb-project-tree-builder.h',
-  'gb-project-tree.c',
-  'gb-project-tree.h',
-  'gb-project-tree-editor-addin.c',
-  'gb-project-tree-editor-addin.h',
-  'gb-project-tree-private.h',
-  'gb-project-tree-shortcuts.c',
-  'gb-rename-file-popover.c',
-  'gb-rename-file-popover.h',
-  'gb-project-tree-addin.c',
-  'gb-project-tree-addin.h',
-  'gb-vcs-tree-builder.c',
-  'gb-vcs-tree-builder.h',
-  'project-tree-plugin.c',
-]
-
-gnome_builder_plugins_deps += dependency('vte-2.91', version: '>=0.40.2')
-gnome_builder_plugins_args += '-DHAVE_VTE'
-
-gnome_builder_plugins_sources += files(project_tree_sources)
-gnome_builder_plugins_sources += project_tree_resources[0]
-
-endif
+plugins_sources += plugin_project_tree_resources[0]
diff --git a/src/plugins/project-tree/project-tree-plugin.c b/src/plugins/project-tree/project-tree-plugin.c
index 67293e9c9..1221ebadd 100644
--- a/src/plugins/project-tree/project-tree-plugin.c
+++ b/src/plugins/project-tree/project-tree-plugin.c
@@ -1,6 +1,6 @@
 /* project-tree-plugin.c
  *
- * Copyright 2015 Christian Hergert <chergert redhat com>
+ * Copyright 2015-2019 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
@@ -14,21 +14,28 @@
  *
  * 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 "project-tree-plugin"
+
+#include "config.h"
+
+#include <libide-gui.h>
+#include <libide-tree.h>
 #include <libpeas/peas.h>
-#include <ide.h>
 
-#include "gb-project-tree-addin.h"
-#include "gb-project-tree-editor-addin.h"
+#include "gbp-project-tree-addin.h"
+#include "gbp-project-tree-workspace-addin.h"
 
 void
-gb_project_tree_register_types (PeasObjectModule *module)
+_gbp_project_tree_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_WORKBENCH_ADDIN,
-                                              GB_TYPE_PROJECT_TREE_ADDIN);
+                                              IDE_TYPE_TREE_ADDIN,
+                                              GBP_TYPE_PROJECT_TREE_ADDIN);
   peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_EDITOR_VIEW_ADDIN,
-                                              GB_TYPE_PROJECT_TREE_EDITOR_ADDIN);
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_PROJECT_TREE_WORKSPACE_ADDIN);
 }
diff --git a/src/plugins/project-tree/project-tree.gresource.xml 
b/src/plugins/project-tree/project-tree.gresource.xml
index 0c6b165b8..075b14326 100644
--- a/src/plugins/project-tree/project-tree.gresource.xml
+++ b/src/plugins/project-tree/project-tree.gresource.xml
@@ -1,12 +1,11 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/project-tree">
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+    <file preprocess="xml-stripblanks">gbp-project-tree-pane.ui</file>
+    <file preprocess="xml-stripblanks">gbp-new-file-popover.ui</file>
+    <file preprocess="xml-stripblanks">gbp-rename-file-popover.ui</file>
     <file>project-tree.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/project-tree-plugin">
-    <file>gb-new-file-popover.ui</file>
-    <file>gb-rename-file-popover.ui</file>
-    <file>gtk/menus.ui</file>
     <file>themes/shared.css</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/project-tree/project-tree.plugin b/src/plugins/project-tree/project-tree.plugin
index c8dd44a06..8e7566092 100644
--- a/src/plugins/project-tree/project-tree.plugin
+++ b/src/plugins/project-tree/project-tree.plugin
@@ -1,10 +1,12 @@
 [Plugin]
-Module=project-tree-plugin
-Name=Project Tree
-Description=Provides a project tree
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
-Depends=editor
 Builtin=true
+Copyright=Copyright © 2014-2018 Christian Hergert
+Depends=editor;
+Description=Builder's project creation wizard
+Embedded=_gbp_project_tree_register_types
 Hidden=true
-Embedded=gb_project_tree_register_types
+Module=project-tree
+Name=Project Tree
+X-Workspace-Kind=primary;
+X-Tree-Kind=project-tree;
diff --git a/src/plugins/project-tree/themes/shared.css b/src/plugins/project-tree/themes/shared.css
index 79880fb6d..a6c2bd476 100644
--- a/src/plugins/project-tree/themes/shared.css
+++ b/src/plugins/project-tree/themes/shared.css
@@ -5,3 +5,11 @@ ideeditorsidebar treeview.project-tree {
   -gtk-icon-source: none;
   padding-left: 6px;
 }
+
+.vcs-added {
+  color: @success_color;
+}
+
+.vcs-changed {
+  color: @warning_color;
+}
diff --git a/src/plugins/python-gi-imports-completion/meson.build 
b/src/plugins/python-gi-imports-completion/meson.build
index 23d0ab98d..312e6cc55 100644
--- a/src/plugins/python-gi-imports-completion/meson.build
+++ b/src/plugins/python-gi-imports-completion/meson.build
@@ -1,13 +1,9 @@
-if get_option('with_python_gi_imports_completion')
-
 install_data('python_gi_imports_completion.py', install_dir: plugindir)
 
 configure_file(
           input: 'python-gi-imports-completion.plugin',
          output: 'python-gi-imports-completion.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
-
-endif
diff --git a/src/plugins/python-gi-imports-completion/python-gi-imports-completion.plugin 
b/src/plugins/python-gi-imports-completion/python-gi-imports-completion.plugin
index 340fb2216..0b83c5219 100644
--- a/src/plugins/python-gi-imports-completion/python-gi-imports-completion.plugin
+++ b/src/plugins/python-gi-imports-completion/python-gi-imports-completion.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=python_gi_imports_completion
-Loader=python3
-Name=Python GObject Introspection Imports Auto-Completion
-Description=Provides autocompletion for importing GObject Introspection enabled Libraries in Python.
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
+Copyright=Copyright © 2015 Christian Hergert
+Description=Provides autocompletion for importing GObject Introspection enabled Libraries in Python.
+Loader=python3
+Module=python_gi_imports_completion
+Name=Python GObject Introspection Imports Auto-Completion
+X-Builder-ABI=@PACKAGE_ABI@
 X-Completion-Provider-Languages=python,python3
diff --git a/src/plugins/python-gi-imports-completion/python_gi_imports_completion.py 
b/src/plugins/python-gi-imports-completion/python_gi_imports_completion.py
index e796dd9d9..bd0139174 100644
--- a/src/plugins/python-gi-imports-completion/python_gi_imports_completion.py
+++ b/src/plugins/python-gi-imports-completion/python_gi_imports_completion.py
@@ -23,11 +23,6 @@
 import gi
 import os
 
-gi.require_version('Gtk', '3.0')
-gi.require_version('GtkSource', '4')
-gi.require_version('GIRepository', '2.0')
-gi.require_version('Ide', '1.0')
-
 from gi.repository import GIRepository
 from gi.repository import Gio
 from gi.repository import GLib
diff --git a/src/plugins/python-pack/ide-python-indenter.c b/src/plugins/python-pack/ide-python-indenter.c
index 13c8aab87..2599a3284 100644
--- a/src/plugins/python-pack/ide-python-indenter.c
+++ b/src/plugins/python-pack/ide-python-indenter.c
@@ -1,6 +1,6 @@
 /* ide-python-indenter.c
  *
- * Copyright 2014-2015 Christian Hergert <christian hergert me>
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,12 +14,14 @@
  *
  * 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 "ide-python-indenter"
 
-#include <libpeas/peas.h>
 #include <gtksourceview/gtksource.h>
+#include <libide-sourceview.h>
 #include <string.h>
 
 #include "ide-python-indenter.h"
diff --git a/src/plugins/python-pack/ide-python-indenter.h b/src/plugins/python-pack/ide-python-indenter.h
index 0add3a8ee..e158fb27b 100644
--- a/src/plugins/python-pack/ide-python-indenter.h
+++ b/src/plugins/python-pack/ide-python-indenter.h
@@ -1,6 +1,6 @@
 /* ide-python-indenter.h
  *
- * Copyright 2014 Christian Hergert <christian hergert me>
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/python-pack/meson.build b/src/plugins/python-pack/meson.build
index bdddd8818..a369bfa9f 100644
--- a/src/plugins/python-pack/meson.build
+++ b/src/plugins/python-pack/meson.build
@@ -1,18 +1,16 @@
-if get_option('with_python_pack')
+if get_option('plugin_python_pack')
 
-python_pack_resources = gnome.compile_resources(
-  'python-pack-resources',
-  'python-pack.gresource.xml',
-  c_name: 'ide_python_pack',
-)
-
-python_pack_sources = [
+plugins_sources += files([
   'ide-python-indenter.c',
-  'ide-python-indenter.h',
   'python-pack-plugin.c',
-]
+])
+
+plugin_python_pack_resources = gnome.compile_resources(
+  'gbp-python-pack-resources',
+  'python-pack.gresource.xml',
+  c_name: 'gbp_python_pack',
+)
 
-gnome_builder_plugins_sources += files(python_pack_sources)
-gnome_builder_plugins_sources += python_pack_resources[0]
+plugins_sources += plugin_python_pack_resources[0]
 
 endif
diff --git a/src/plugins/python-pack/python-pack-plugin.c b/src/plugins/python-pack/python-pack-plugin.c
index efc10f1d6..76d4cca9d 100644
--- a/src/plugins/python-pack/python-pack-plugin.c
+++ b/src/plugins/python-pack/python-pack-plugin.c
@@ -1,6 +1,6 @@
 /* python-pack-plugin.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,14 +14,21 @@
  *
  * 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
  */
 
+#include "config.h"
+
 #include <libpeas/peas.h>
+#include <libide-sourceview.h>
 
 #include "ide-python-indenter.h"
 
-void
-ide_python_pack_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_ide_python_pack_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module, IDE_TYPE_INDENTER, IDE_TYPE_PYTHON_INDENTER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_INDENTER,
+                                              IDE_TYPE_PYTHON_INDENTER);
 }
diff --git a/src/plugins/python-pack/python-pack.gresource.xml 
b/src/plugins/python-pack/python-pack.gresource.xml
index a272e6fd7..d82a4475b 100644
--- a/src/plugins/python-pack/python-pack.gresource.xml
+++ b/src/plugins/python-pack/python-pack.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/python-pack">
     <file>python-pack.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/python-pack/python-pack.plugin b/src/plugins/python-pack/python-pack.plugin
index 97401a27e..1ae54ac79 100644
--- a/src/plugins/python-pack/python-pack.plugin
+++ b/src/plugins/python-pack/python-pack.plugin
@@ -1,12 +1,13 @@
 [Plugin]
-Module=python-pack-plugin
-Name=Python Auto-Indenter
-Description=Provides auto-indentation for the Python programming language
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
-Embedded=ide_python_pack_register_types
-X-Indenter-Languages=python,python3
-X-Indenter-Languages-Priority=0
-X-Completion-Provider-Languages=python,python3
+Copyright=Copyright © 2015 Christian Hergert
+Depends=editor;
+Description=Provides auto-indentation for the Python programming language
+Embedded=_ide_python_pack_register_types
+Module=python-pack
+Name=Python Auto-Indenter
 X-Completion-Provider-Languages-Priority=0
+X-Completion-Provider-Languages=python,python3
+X-Indenter-Languages-Priority=0
+X-Indenter-Languages=python,python3
diff --git a/src/plugins/qemu/gbp-qemu-device-provider.c b/src/plugins/qemu/gbp-qemu-device-provider.c
index 6c1f4732c..9b9d05afa 100644
--- a/src/plugins/qemu/gbp-qemu-device-provider.c
+++ b/src/plugins/qemu/gbp-qemu-device-provider.c
@@ -1,6 +1,6 @@
 /* gbp-qemu-device-provider.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -79,7 +79,7 @@ gbp_qemu_device_provider_load_worker (IdeTask      *task,
   g_autofree gchar *status = NULL;
   g_autoptr(GError) error = NULL;
   g_autoptr(GPtrArray) devices = NULL;
-  IdeContext *context;
+  GbpQemuDeviceProvider *self;
 
   IDE_ENTRY;
 
@@ -87,6 +87,9 @@ gbp_qemu_device_provider_load_worker (IdeTask      *task,
   g_assert (GBP_IS_QEMU_DEVICE_PROVIDER (source_object));
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
+  self = ide_task_get_source_object (task);
+  g_assert (GBP_IS_QEMU_DEVICE_PROVIDER (self));
+
   devices = g_ptr_array_new_with_free_func (g_object_unref);
 
   /* The first thing we need to do is ensure that binfmt is available
@@ -125,8 +128,6 @@ gbp_qemu_device_provider_load_worker (IdeTask      *task,
       IDE_EXIT;
     }
 
-  context = ide_object_get_context (source_object);
-
   /* Now locate which of the machines are registered. Qemu has a huge
    * list of these, so we only check for ones we think are likely to
    * be used. If you want support for more, let us know.
@@ -162,9 +163,9 @@ gbp_qemu_device_provider_load_worker (IdeTask      *task,
           device = g_object_new (IDE_TYPE_LOCAL_DEVICE,
                                  "id", machines[i].filename,
                                  "triplet", triplet,
-                                 "context", context,
                                  "display-name", display_name,
                                  NULL);
+          ide_object_append (IDE_OBJECT (self), IDE_OBJECT (device));
           g_ptr_array_add (devices, g_steal_pointer (&device));
         }
     }
diff --git a/src/plugins/qemu/gbp-qemu-device-provider.h b/src/plugins/qemu/gbp-qemu-device-provider.h
index 279e96d47..a51c5697b 100644
--- a/src/plugins/qemu/gbp-qemu-device-provider.h
+++ b/src/plugins/qemu/gbp-qemu-device-provider.h
@@ -1,6 +1,6 @@
 /* gbp-qemu-device-provider.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -20,7 +20,7 @@
 
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/qemu/meson.build b/src/plugins/qemu/meson.build
index 0b50c10cb..ca36d1bb7 100644
--- a/src/plugins/qemu/meson.build
+++ b/src/plugins/qemu/meson.build
@@ -1,17 +1,16 @@
-if get_option('with_qemu')
+if get_option('plugin_qemu')
 
-qemu_resources = gnome.compile_resources(
+plugins_sources += files([
+  'qemu-plugin.c',
+  'gbp-qemu-device-provider.c',
+])
+
+plugin_qemu_resources = gnome.compile_resources(
   'qemu-resources',
   'qemu.gresource.xml',
   c_name: 'gbp_qemu',
 )
 
-qemu_sources = [
-  'gbp-qemu-plugin.c',
-  'gbp-qemu-device-provider.c',
-]
-
-gnome_builder_plugins_sources += files(qemu_sources)
-gnome_builder_plugins_sources += qemu_resources[0]
+plugins_sources += plugin_qemu_resources[0]
 
 endif
diff --git a/src/plugins/qemu/qemu-plugin.c b/src/plugins/qemu/qemu-plugin.c
new file mode 100644
index 000000000..7d3b17194
--- /dev/null
+++ b/src/plugins/qemu/qemu-plugin.c
@@ -0,0 +1,34 @@
+/* qemu-plugin.c
+ *
+ * Copyright 2018-2019 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
+ */
+
+#include "config.h"
+
+#include <libide-foundry.h>
+#include <libpeas/peas.h>
+
+#include "gbp-qemu-device-provider.h"
+
+_IDE_EXTERN void
+_gbp_qemu_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_DEVICE_PROVIDER,
+                                              GBP_TYPE_QEMU_DEVICE_PROVIDER);
+}
diff --git a/src/plugins/qemu/qemu.gresource.xml b/src/plugins/qemu/qemu.gresource.xml
index b854ffac2..3a85e49c2 100644
--- a/src/plugins/qemu/qemu.gresource.xml
+++ b/src/plugins/qemu/qemu.gresource.xml
@@ -1,8 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/qemu">
     <file>qemu.plugin</file>
   </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/qemu-plugin">
-  </gresource>
 </gresources>
diff --git a/src/plugins/qemu/qemu.plugin b/src/plugins/qemu/qemu.plugin
index 7462f9658..c7700b763 100644
--- a/src/plugins/qemu/qemu.plugin
+++ b/src/plugins/qemu/qemu.plugin
@@ -1,8 +1,9 @@
 [Plugin]
-Module=qemu-plugin
-Name=Qemu
-Description=Integration with Qemu cross-architecture emulation
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2018 Christian Hergert
 Builtin=true
-Embedded=gbp_qemu_register_types
+Copyright=Copyright © 2018 Christian Hergert
+Depends=deviceui;
+Description=Integration with Qemu cross-architecture emulation
+Embedded=_gbp_qemu_register_types
+Module=qemu
+Name=Qemu
diff --git a/src/plugins/quick-highlight/gbp-quick-highlight-editor-page-addin.c 
b/src/plugins/quick-highlight/gbp-quick-highlight-editor-page-addin.c
new file mode 100644
index 000000000..fd0d14428
--- /dev/null
+++ b/src/plugins/quick-highlight/gbp-quick-highlight-editor-page-addin.c
@@ -0,0 +1,276 @@
+/* gbp-quick-highlight-editor-page-addin.c
+ *
+ * Copyright 2016 Martin Blanchard <tchaik gmx com>
+ * Copyright 2017-2019 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-quick-highlight-editor-page-addin"
+
+#include <libide-editor.h>
+
+#include "gbp-quick-highlight-editor-page-addin.h"
+
+#define HIGHLIGHT_STYLE_NAME "quick-highlight-match"
+
+struct _GbpQuickHighlightEditorPageAddin
+{
+  GObject                 parent_instance;
+
+  IdeEditorPage          *view;
+
+  DzlSignalGroup         *buffer_signals;
+  DzlSignalGroup         *search_signals;
+  GtkSourceSearchContext *search_context;
+
+  guint                   queued_match_source;
+
+  guint                   has_selection : 1;
+  guint                   search_active : 1;
+};
+
+static gboolean
+do_delayed_quick_highlight (GbpQuickHighlightEditorPageAddin *self)
+{
+  GtkSourceSearchSettings *search_settings;
+  g_autofree gchar *slice = NULL;
+  IdeBuffer *buffer;
+  GtkTextIter begin;
+  GtkTextIter end;
+
+  g_assert (GBP_IS_QUICK_HIGHLIGHT_EDITOR_PAGE_ADDIN (self));
+  g_assert (self->view != NULL);
+
+  self->queued_match_source = 0;
+
+  /*
+   * Get the curretn selection, if any. Short circuit if we find a situation
+   * that should have caused us to cancel the current quick-highlight.
+   */
+  buffer = ide_editor_page_get_buffer (self->view);
+  if (self->search_active ||
+      !self->has_selection ||
+      !gtk_text_buffer_get_selection_bounds (GTK_TEXT_BUFFER (buffer), &begin, &end))
+    {
+      g_clear_object (&self->search_context);
+      return G_SOURCE_REMOVE;
+    }
+
+  /*
+   * If the current selection goes across a line, then ignore trying to match
+   * anything similar as it's unlikely to be what the user wants.
+   */
+  gtk_text_iter_order (&begin, &end);
+  if (gtk_text_iter_get_line (&begin) != gtk_text_iter_get_line (&end))
+    {
+      g_clear_object (&self->search_context);
+      return G_SOURCE_REMOVE;
+    }
+
+  /*
+   * Create our search context to scan the buffer if necessary.
+   */
+  if (self->search_context == NULL)
+    {
+      g_autoptr(GtkSourceSearchSettings) settings = NULL;
+      GtkSourceStyleScheme *style_scheme;
+      GtkSourceStyle *style = NULL;
+
+      style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
+      if (style_scheme != NULL)
+        style = gtk_source_style_scheme_get_style (style_scheme, HIGHLIGHT_STYLE_NAME);
+
+      settings = g_object_new (GTK_SOURCE_TYPE_SEARCH_SETTINGS,
+                               "at-word-boundaries", FALSE,
+                               "case-sensitive", TRUE,
+                               "regex-enabled", FALSE,
+                               NULL);
+
+      /* Set highlight to false initially, or we get the wrong style from
+       * the the GtkSourceSearchContext.
+       */
+      self->search_context = g_object_new (GTK_SOURCE_TYPE_SEARCH_CONTEXT,
+                                           "buffer", buffer,
+                                           "highlight", FALSE,
+                                           "match-style", style,
+                                           "settings", settings,
+                                           NULL);
+    }
+
+  search_settings = gtk_source_search_context_get_settings (self->search_context);
+
+  /* Now assign our search text */
+  slice = gtk_text_iter_get_slice (&begin, &end);
+  gtk_source_search_settings_set_search_text (search_settings, slice);
+
+  /* (Re)enable highlight so that we have the correct style */
+  gtk_source_search_context_set_highlight (self->search_context, TRUE);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+buffer_cursor_moved (GbpQuickHighlightEditorPageAddin *self,
+                     const GtkTextIter                *location,
+                     IdeBuffer                        *buffer)
+{
+  g_assert (GBP_IS_QUICK_HIGHLIGHT_EDITOR_PAGE_ADDIN (self));
+  g_assert (location != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (self->has_selection && !self->search_active)
+    {
+      if (self->queued_match_source == 0)
+        self->queued_match_source =
+          gdk_threads_add_idle_full (G_PRIORITY_LOW + 100,
+                                     (GSourceFunc) do_delayed_quick_highlight,
+                                     g_object_ref (self),
+                                     g_object_unref);
+    }
+  else
+    {
+      dzl_clear_source (&self->queued_match_source);
+      g_clear_object (&self->search_context);
+    }
+}
+
+static void
+buffer_notify_style_scheme (GbpQuickHighlightEditorPageAddin *self,
+                            GParamSpec                       *pspec,
+                            IdeBuffer                        *buffer)
+{
+  g_assert (GBP_IS_QUICK_HIGHLIGHT_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (self->search_context != NULL)
+    {
+      GtkSourceStyleScheme *style_scheme;
+      GtkSourceStyle *style = NULL;
+
+      style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
+      if (style_scheme != NULL)
+        style = gtk_source_style_scheme_get_style (style_scheme, HIGHLIGHT_STYLE_NAME);
+
+      gtk_source_search_context_set_match_style (self->search_context, style);
+    }
+}
+
+static void
+buffer_notify_has_selection (GbpQuickHighlightEditorPageAddin *self,
+                             GParamSpec                       *pspec,
+                             IdeBuffer                        *buffer)
+{
+  g_assert (GBP_IS_QUICK_HIGHLIGHT_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  self->has_selection = gtk_text_buffer_get_has_selection (GTK_TEXT_BUFFER (buffer));
+}
+
+static void
+search_notify_active (GbpQuickHighlightEditorPageAddin *self,
+                      GParamSpec                       *pspec,
+                      IdeEditorSearch                  *search)
+{
+  g_assert (GBP_IS_QUICK_HIGHLIGHT_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_SEARCH (search));
+
+  self->search_active = ide_editor_search_get_active (search);
+  do_delayed_quick_highlight (self);
+}
+
+static void
+gbp_quick_highlight_editor_page_addin_load (IdeEditorPageAddin *addin,
+                                            IdeEditorPage      *view)
+{
+  GbpQuickHighlightEditorPageAddin *self = (GbpQuickHighlightEditorPageAddin *)addin;
+
+  g_assert (GBP_IS_QUICK_HIGHLIGHT_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  self->view = view;
+
+  self->buffer_signals = dzl_signal_group_new (IDE_TYPE_BUFFER);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::has-selection",
+                                    G_CALLBACK (buffer_notify_has_selection),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::style-scheme",
+                                    G_CALLBACK (buffer_notify_style_scheme),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "cursor-moved",
+                                    G_CALLBACK (buffer_cursor_moved),
+                                    self);
+
+  self->search_signals = dzl_signal_group_new (IDE_TYPE_EDITOR_SEARCH);
+
+  dzl_signal_group_connect_swapped (self->search_signals,
+                                    "notify::active",
+                                    G_CALLBACK (search_notify_active),
+                                    self);
+
+  dzl_signal_group_set_target (self->buffer_signals, ide_editor_page_get_buffer (view));
+  dzl_signal_group_set_target (self->search_signals, ide_editor_page_get_search (view));
+}
+
+static void
+gbp_quick_highlight_editor_page_addin_unload (IdeEditorPageAddin *addin,
+                                              IdeEditorPage      *view)
+{
+  GbpQuickHighlightEditorPageAddin *self = (GbpQuickHighlightEditorPageAddin *)addin;
+
+  g_assert (GBP_IS_QUICK_HIGHLIGHT_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  g_clear_object (&self->search_context);
+  dzl_clear_source (&self->queued_match_source);
+
+  dzl_signal_group_set_target (self->buffer_signals, NULL);
+  g_clear_object (&self->buffer_signals);
+
+  dzl_signal_group_set_target (self->search_signals, NULL);
+  g_clear_object (&self->search_signals);
+
+  self->view = NULL;
+}
+
+static void
+editor_view_addin_iface_init (IdeEditorPageAddinInterface *iface)
+{
+  iface->load = gbp_quick_highlight_editor_page_addin_load;
+  iface->unload = gbp_quick_highlight_editor_page_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpQuickHighlightEditorPageAddin,
+                         gbp_quick_highlight_editor_page_addin,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                                editor_view_addin_iface_init))
+
+static void
+gbp_quick_highlight_editor_page_addin_class_init (GbpQuickHighlightEditorPageAddinClass *klass)
+{
+}
+
+static void
+gbp_quick_highlight_editor_page_addin_init (GbpQuickHighlightEditorPageAddin *self)
+{
+}
diff --git a/src/plugins/quick-highlight/gbp-quick-highlight-editor-page-addin.h 
b/src/plugins/quick-highlight/gbp-quick-highlight-editor-page-addin.h
new file mode 100644
index 000000000..b376482b8
--- /dev/null
+++ b/src/plugins/quick-highlight/gbp-quick-highlight-editor-page-addin.h
@@ -0,0 +1,31 @@
+/* gbp-quick-highlight-editor-page-addin.h
+ *
+ * Copyright 2016 Martin Blanchard <tchaik gmx 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_QUICK_HIGHLIGHT_EDITOR_PAGE_ADDIN (gbp_quick_highlight_editor_page_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpQuickHighlightEditorPageAddin, gbp_quick_highlight_editor_page_addin, GBP, 
QUICK_HIGHLIGHT_EDITOR_PAGE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/quick-highlight/gbp-quick-highlight-preferences.c 
b/src/plugins/quick-highlight/gbp-quick-highlight-preferences.c
index 8b95e23f5..656a076e8 100644
--- a/src/plugins/quick-highlight/gbp-quick-highlight-preferences.c
+++ b/src/plugins/quick-highlight/gbp-quick-highlight-preferences.c
@@ -1,6 +1,6 @@
 /* gbp-quick-highlight-preferences.c
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-quick-highlight-preferences"
diff --git a/src/plugins/quick-highlight/gbp-quick-highlight-preferences.h 
b/src/plugins/quick-highlight/gbp-quick-highlight-preferences.h
index 8d924bfa6..800553a9d 100644
--- a/src/plugins/quick-highlight/gbp-quick-highlight-preferences.h
+++ b/src/plugins/quick-highlight/gbp-quick-highlight-preferences.h
@@ -1,6 +1,6 @@
 /* gbp-quick-highlight-preferences.h
  *
- * Copyright 2016 Christian Hergert <chergert redhat com>
+ * Copyright 2016-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/quick-highlight/meson.build b/src/plugins/quick-highlight/meson.build
index a22cdb442..5bb496fb7 100644
--- a/src/plugins/quick-highlight/meson.build
+++ b/src/plugins/quick-highlight/meson.build
@@ -1,20 +1,17 @@
-if get_option('with_quick_highlight')
+if get_option('plugin_quick_highlight')
 
-quick_highlight_resources = gnome.compile_resources(
+plugins_sources += files([
+  'quick-highlight-plugin.c',
+  'gbp-quick-highlight-editor-page-addin.c',
+  'gbp-quick-highlight-preferences.c',
+])
+
+plugin_quick_highlight_resources = gnome.compile_resources(
   'quick-highlight-resources',
   'quick-highlight.gresource.xml',
   c_name: 'gbp_quick_highlight',
 )
 
-quick_highlight_sources = [
-  'gbp-quick-highlight-plugin.c',
-  'gbp-quick-highlight-editor-view-addin.c',
-  'gbp-quick-highlight-editor-view-addin.h',
-  'gbp-quick-highlight-preferences.c',
-  'gbp-quick-highlight-preferences.h',
-]
-
-gnome_builder_plugins_sources += files(quick_highlight_sources)
-gnome_builder_plugins_sources += quick_highlight_resources[0]
+plugins_sources += plugin_quick_highlight_resources[0]
 
 endif
diff --git a/src/plugins/quick-highlight/quick-highlight-plugin.c 
b/src/plugins/quick-highlight/quick-highlight-plugin.c
new file mode 100644
index 000000000..9dc527835
--- /dev/null
+++ b/src/plugins/quick-highlight/quick-highlight-plugin.c
@@ -0,0 +1,38 @@
+/* quick-highlight-plugin.c
+ *
+ * Copyright 2016 Martin Blanchard <tchaik gmx 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
+ */
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-editor.h>
+
+#include "gbp-quick-highlight-editor-page-addin.h"
+#include "gbp-quick-highlight-preferences.h"
+
+_IDE_EXTERN void
+_gbp_quick_highlight_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                              GBP_TYPE_QUICK_HIGHLIGHT_EDITOR_PAGE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_QUICK_HIGHLIGHT_PREFERENCES);
+}
diff --git a/src/plugins/quick-highlight/quick-highlight.gresource.xml 
b/src/plugins/quick-highlight/quick-highlight.gresource.xml
index 721f6084d..53101c8d6 100644
--- a/src/plugins/quick-highlight/quick-highlight.gresource.xml
+++ b/src/plugins/quick-highlight/quick-highlight.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/quick-highlight">
     <file>quick-highlight.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/quick-highlight/quick-highlight.plugin 
b/src/plugins/quick-highlight/quick-highlight.plugin
index 5272941da..da23a6879 100644
--- a/src/plugins/quick-highlight/quick-highlight.plugin
+++ b/src/plugins/quick-highlight/quick-highlight.plugin
@@ -1,8 +1,9 @@
 [Plugin]
-Module=quick-highlight-plugin
-Name=Quick Highlight
-Description=Highlights every occurrences of selected text
 Authors=Martin Blanchard <tchaik gmx com>
-Copyright=Copyright © 2016 Martin Blanchard
 Builtin=true
-Embedded=gbp_quick_highlight_register_types
+Copyright=Copyright © 2016 Martin Blanchard
+Description=Highlights every occurrences of selected text
+Embedded=_gbp_quick_highlight_register_types
+Hidden=true
+Module=quick-highlight
+Name=Quick Highlight
diff --git a/src/plugins/recent/gbp-recent-project-row.c b/src/plugins/recent/gbp-recent-project-row.c
index be82b0649..cf035b990 100644
--- a/src/plugins/recent/gbp-recent-project-row.c
+++ b/src/plugins/recent/gbp-recent-project-row.c
@@ -1,6 +1,6 @@
 /* gbp-recent-project-row.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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-recent-project-row"
@@ -334,8 +336,7 @@ gbp_recent_project_row_class_init (GbpRecentProjectRowClass *klass)
   object_class->get_property = gbp_recent_project_row_get_property;
   object_class->set_property = gbp_recent_project_row_set_property;
 
-  gtk_widget_class_set_template_from_resource (widget_class,
-                                               
"/org/gnome/builder/plugins/recent-plugin/gbp-recent-project-row.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/recent/gbp-recent-project-row.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpRecentProjectRow, checkbox);
   gtk_widget_class_bind_template_child (widget_class, GbpRecentProjectRow, date_label);
   gtk_widget_class_bind_template_child (widget_class, GbpRecentProjectRow, description_label);
diff --git a/src/plugins/recent/gbp-recent-project-row.h b/src/plugins/recent/gbp-recent-project-row.h
index 38b35f3c7..87cf92d8d 100644
--- a/src/plugins/recent/gbp-recent-project-row.h
+++ b/src/plugins/recent/gbp-recent-project-row.h
@@ -1,6 +1,6 @@
 /* ide-greeter-project-row.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-projects.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/recent/gbp-recent-section.c b/src/plugins/recent/gbp-recent-section.c
index a1a8be284..0a375e919 100644
--- a/src/plugins/recent/gbp-recent-section.c
+++ b/src/plugins/recent/gbp-recent-section.c
@@ -1,6 +1,6 @@
 /* gbp-recent-section.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,14 @@
  *
  * 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-recent-section"
 
-#include <ide.h>
+#include <glib/gi18n.h>
+#include <libide-greeter.h>
 
 #include "gbp-recent-project-row.h"
 #include "gbp-recent-section.h"
@@ -241,6 +244,8 @@ static gboolean
 can_purge_project_directory (GFile *directory)
 {
   g_autoptr(GFile) projects_dir = NULL;
+  g_autoptr(GFile) home_dir = NULL;
+  g_autoptr(GFile) downloads_dir = NULL;
   g_autofree gchar *uri = NULL;
   GFileType file_type;
 
@@ -255,8 +260,9 @@ can_purge_project_directory (GFile *directory)
       return FALSE;
     }
 
-  projects_dir = ide_application_get_projects_directory (IDE_APPLICATION_DEFAULT);
-  g_assert (G_IS_FILE (projects_dir));
+  projects_dir = g_file_new_for_path (ide_get_projects_dir ());
+  home_dir = g_file_new_for_path (g_get_home_dir ());
+  downloads_dir = g_file_new_for_path (g_get_user_special_dir (G_USER_DIRECTORY_DOWNLOAD));
 
   /* Refuse to delete anything outside of projects dir to be paranoid */
   if (!g_file_has_prefix (directory, projects_dir))
@@ -265,9 +271,11 @@ can_purge_project_directory (GFile *directory)
       return FALSE;
     }
 
-  if (g_file_equal (directory, projects_dir))
+  if (g_file_equal (directory, projects_dir) ||
+      g_file_equal (directory, home_dir) ||
+      g_file_equal (directory, downloads_dir))
     {
-      g_critical ("Refusing to purge the projects directory");
+      g_critical ("Refusing to purge the project's directory");
       return FALSE;
     }
 
@@ -305,6 +313,35 @@ gbp_recent_section_reap_cb (GObject      *object,
   IDE_EXIT;
 }
 
+static void
+gbp_recent_section_remove_file (DzlDirectoryReaper *reaper,
+                                GFile              *file,
+                                GtkTextBuffer      *buffer)
+{
+  GtkTextIter iter;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (DZL_IS_DIRECTORY_REAPER (reaper));
+  g_assert (G_IS_FILE (file));
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+
+  gtk_text_buffer_get_end_iter (buffer, &iter);
+
+  if (g_file_is_native (file))
+    {
+      /* translators: %s is replaced with the path of the file to be deleted and \n for a new line */
+      g_autofree gchar *formatted = g_strdup_printf (_("Removing %s\n"), g_file_peek_path (file));
+      gtk_text_buffer_insert (buffer, &iter, formatted, -1);
+    }
+  else
+    {
+      /* translators: %s is replaced with the path of the file to be deleted and \n for a new line */
+      g_autofree gchar *uri = g_file_get_uri (file);
+      g_autofree gchar *formatted = g_strdup_printf (_("Removing %s\n"), uri);
+      gtk_text_buffer_insert (buffer, &iter, formatted, -1);
+    }
+}
+
 static void
 gbp_recent_section_purge_selected_full (IdeGreeterSection *section,
                                         gboolean           purge_sources)
@@ -313,16 +350,19 @@ gbp_recent_section_purge_selected_full (IdeGreeterSection *section,
   g_autoptr(DzlDirectoryReaper) reaper = NULL;
   g_autoptr(GPtrArray) directories = NULL;
   IdeRecentProjects *projects;
+  GtkWidget *workspace;
   GList *infos = NULL;
 
   g_assert (GBP_IS_RECENT_SECTION (self));
 
+  workspace = gtk_widget_get_toplevel (GTK_WIDGET (section));
+
   gtk_container_foreach (GTK_CONTAINER (self->listbox),
                          gbp_recent_section_collect_selected_cb,
                          &infos);
 
   /* Remove the projects from the list of recent projects */
-  projects = ide_application_get_recent_projects (IDE_APPLICATION_DEFAULT);
+  projects = ide_recent_projects_get_default ();
   ide_recent_projects_remove (projects, infos);
 
   /* Now asynchronously remove all the project files */
@@ -341,6 +381,18 @@ gbp_recent_section_purge_selected_full (IdeGreeterSection *section,
 
       g_assert (G_IS_FILE (directory) || G_IS_FILE (file));
 
+      /* If the IdeProjectInfo:file is a directory, refuse to delete the
+       * pre-stated directory as it might be a parent which is really Home, or
+       * something like that. This just helps ensure we're a bit safer when
+       * dealing with user data.
+       */
+      if (file != NULL &&
+          g_file_query_file_type (file, 0, NULL) == G_FILE_TYPE_DIRECTORY)
+        {
+          if (directory == NULL || g_file_has_prefix (file, directory))
+            directory = file;
+        }
+
       if (directory == NULL)
         {
           if (g_file_query_file_type (file, 0, NULL) == G_FILE_TYPE_DIRECTORY)
@@ -366,7 +418,7 @@ gbp_recent_section_purge_selected_full (IdeGreeterSection *section,
        * might expect to be reomved.
        */
 
-      id = ide_project_create_id (name);
+      id = ide_create_project_id (name);
 
       if (name != NULL)
         {
@@ -389,6 +441,48 @@ gbp_recent_section_purge_selected_full (IdeGreeterSection *section,
       clear_settings_with_path ("org.gnome.builder.project", path);
     }
 
+  if (purge_sources)
+    {
+      GtkDialog *dialog;
+      GtkWidget *scroller;
+      GtkWidget *view;
+      GtkWidget *content_area;
+      GtkTextBuffer *buffer;
+
+      dialog = GTK_DIALOG (gtk_dialog_new ());
+      gtk_window_set_title (GTK_WINDOW (dialog), _("Removing Files…"));
+      gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (workspace));
+      gtk_dialog_add_button (dialog, _("Close"), GTK_RESPONSE_CLOSE);
+      gtk_window_set_default_size (GTK_WINDOW (dialog), 700, 500);
+      content_area = gtk_dialog_get_content_area (dialog);
+      gtk_container_set_border_width (GTK_CONTAINER (content_area), 12);
+      gtk_box_set_spacing (GTK_BOX (content_area), 12);
+
+      scroller = gtk_scrolled_window_new (NULL, NULL);
+      gtk_widget_set_vexpand (scroller, TRUE);
+      gtk_container_add (GTK_CONTAINER (content_area), scroller);
+      gtk_widget_show (scroller);
+
+      view = gtk_text_view_new ();
+      gtk_text_view_set_editable (GTK_TEXT_VIEW (view), FALSE);
+      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+      gtk_container_add (GTK_CONTAINER (scroller), view);
+      gtk_widget_show (view);
+
+      g_signal_connect_object (reaper,
+                               "remove-file",
+                               G_CALLBACK (gbp_recent_section_remove_file),
+                               buffer,
+                               0);
+
+      g_signal_connect (dialog,
+                        "response",
+                        G_CALLBACK (gtk_widget_destroy),
+                        NULL);
+
+      gtk_window_present (GTK_WINDOW (dialog));
+    }
+
   dzl_directory_reaper_execute_async (reaper,
                                       NULL,
                                       gbp_recent_section_reap_cb,
@@ -493,7 +587,7 @@ gbp_recent_section_constructed (GObject *object)
 
   G_OBJECT_CLASS (gbp_recent_section_parent_class)->constructed (object);
 
-  projects = ide_application_get_recent_projects (IDE_APPLICATION_DEFAULT);
+  projects = ide_recent_projects_get_default ();
 
   gtk_list_box_bind_model (self->listbox,
                            G_LIST_MODEL (projects),
@@ -536,8 +630,7 @@ gbp_recent_section_class_init (GbpRecentSectionClass *klass)
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
   gtk_widget_class_set_css_name (widget_class, "recent");
-  gtk_widget_class_set_template_from_resource (widget_class,
-                                               
"/org/gnome/builder/plugins/recent-plugin/gbp-recent-section.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/recent/gbp-recent-section.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpRecentSection, listbox);
   gtk_widget_class_bind_template_callback (widget_class, gbp_recent_section_row_activated);
 
@@ -556,10 +649,10 @@ on_button_press_event_cb (GtkListBox       *listbox,
 
   if (ev->button == GDK_BUTTON_SECONDARY)
     {
-      dzl_gtk_widget_action (GTK_WIDGET (self),
-                             "greeter",
-                             "state",
-                             g_variant_new_string ("selection"));
+      GtkWidget *workspace;
+
+      workspace = gtk_widget_get_ancestor (GTK_WIDGET (self), IDE_TYPE_GREETER_WORKSPACE);
+      ide_greeter_workspace_set_selection_mode (IDE_GREETER_WORKSPACE (workspace), TRUE);
 
       if ((row = gtk_list_box_get_row_at_y (listbox, ev->y)))
         {
diff --git a/src/plugins/recent/gbp-recent-section.h b/src/plugins/recent/gbp-recent-section.h
index 2dfeeb738..3892a1108 100644
--- a/src/plugins/recent/gbp-recent-section.h
+++ b/src/plugins/recent/gbp-recent-section.h
@@ -1,6 +1,6 @@
 /* gbp-recent-section.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/recent/gbp-recent-section.ui b/src/plugins/recent/gbp-recent-section.ui
index bbe07d2a6..0a6e84bd5 100644
--- a/src/plugins/recent/gbp-recent-section.ui
+++ b/src/plugins/recent/gbp-recent-section.ui
@@ -1,8 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
   <template class="GbpRecentSection" parent="GtkBin">
+    <property name="halign">center</property>
     <child>
       <object class="GtkBox">
+        <property name="hexpand">true</property>
         <property name="orientation">vertical</property>
         <property name="spacing">6</property>
         <property name="visible">true</property>
diff --git a/src/plugins/recent/gbp-recent-workbench-addin.c b/src/plugins/recent/gbp-recent-workbench-addin.c
new file mode 100644
index 000000000..927a8c6ea
--- /dev/null
+++ b/src/plugins/recent/gbp-recent-workbench-addin.c
@@ -0,0 +1,248 @@
+/* gbp-recent-workbench-addin.c
+ *
+ * Copyright 2018-2019 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-recent-workbench-addin"
+
+#include "config.h"
+
+#include <libide-projects.h>
+#include <libide-gui.h>
+
+#include "gbp-recent-workbench-addin.h"
+
+struct _GbpRecentWorkbenchAddin
+{
+  GObject       parent_instance;
+  IdeWorkbench *workbench;
+};
+
+static gboolean
+directory_is_ignored (GFile *file)
+{
+  g_autofree gchar *relative_path = NULL;
+  g_autoptr(GFile) downloads_dir = NULL;
+  g_autoptr(GFile) home_dir = NULL;
+  GFileType file_type;
+
+  g_assert (G_IS_FILE (file));
+
+  home_dir = g_file_new_for_path (g_get_home_dir ());
+  relative_path = g_file_get_relative_path (home_dir, file);
+  file_type = g_file_query_file_type (file, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL);
+
+  if (!g_file_has_prefix (file, home_dir))
+    return TRUE;
+
+  downloads_dir = g_file_new_for_path (g_get_user_special_dir (G_USER_DIRECTORY_DOWNLOAD));
+
+  if (downloads_dir != NULL &&
+      (g_file_equal (file, downloads_dir) ||
+       g_file_has_prefix (file, downloads_dir)))
+    return TRUE;
+
+  /* realtive_path should be valid here because we are within the home_prefix. */
+  g_assert (relative_path != NULL);
+
+  /*
+   * Ignore dot directories, except .local.
+   * We've had too many bug reports with people creating things
+   * like gnome-shell extensions in their .local directory.
+   */
+  if (relative_path[0] == '.' &&
+      !g_str_has_prefix (relative_path, ".local"G_DIR_SEPARATOR_S))
+    return TRUE;
+
+  if (file_type != G_FILE_TYPE_DIRECTORY)
+    {
+      g_autoptr(GFile) parent = g_file_get_parent (file);
+
+      if (g_file_equal (home_dir, parent))
+        return TRUE;
+    }
+
+  return FALSE;
+}
+
+static void
+gbp_recent_workbench_addin_add_recent (GbpRecentWorkbenchAddin *self,
+                                       IdeProjectInfo          *project_info)
+{
+  g_autofree gchar *recent_projects_path = NULL;
+  g_autoptr(GBookmarkFile) projects_file = NULL;
+  g_autoptr(GPtrArray) groups = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *uri = NULL;
+  g_autofree gchar *app_exec = NULL;
+  g_autofree gchar *dir = NULL;
+  IdeBuildSystem *build_system;
+  IdeDoap *doap;
+  GFile *file;
+  GFile *directory;
+
+  g_assert (GBP_IS_RECENT_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_WORKBENCH (self->workbench));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+
+  IDE_ENTRY;
+
+  if (!(file = ide_project_info_get_file (project_info)) ||
+      directory_is_ignored (file))
+    IDE_EXIT;
+
+  recent_projects_path = g_build_filename (g_get_user_data_dir (),
+                                           ide_get_program_name (),
+                                           IDE_RECENT_PROJECTS_BOOKMARK_FILENAME,
+                                           NULL);
+
+  projects_file = g_bookmark_file_new ();
+
+  if (!g_bookmark_file_load_from_file (projects_file, recent_projects_path, &error))
+    {
+      /*
+       * If there was an error loading the file and the error is not "File does not
+       * exist" then stop saving operation
+       */
+      if (error != NULL &&
+          !g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+        {
+          g_warning ("Unable to open recent projects \"%s\" file: %s",
+                     recent_projects_path, error->message);
+          IDE_EXIT;
+        }
+    }
+
+  uri = g_file_get_uri (file);
+  app_exec = g_strdup_printf ("%s -p %%p", ide_get_program_name ());
+
+  g_bookmark_file_set_title (projects_file, uri, ide_project_info_get_name (project_info));
+  g_bookmark_file_set_mime_type (projects_file, uri, "application/x-builder-project");
+  g_bookmark_file_add_application (projects_file, uri, ide_get_program_name (), app_exec);
+  g_bookmark_file_set_is_private (projects_file, uri, FALSE);
+
+  doap = ide_project_info_get_doap (project_info);
+
+  /* attach project description to recent info */
+  if (doap != NULL)
+    g_bookmark_file_set_description (projects_file, uri, ide_doap_get_shortdesc (doap));
+
+  /* attach discovered languages to recent info */
+  groups = g_ptr_array_new_with_free_func (g_free);
+  g_ptr_array_add (groups, g_strdup (IDE_RECENT_PROJECTS_GROUP));
+  if (doap != NULL)
+    {
+      gchar **languages;
+
+      if ((languages = ide_doap_get_languages (doap)))
+        {
+          for (guint i = 0; languages[i]; i++)
+            g_ptr_array_add (groups,
+                             g_strdup_printf ("%s%s",
+                                              IDE_RECENT_PROJECTS_LANGUAGE_GROUP_PREFIX,
+                                              languages[i]));
+        }
+    }
+
+  g_bookmark_file_set_groups (projects_file, uri, (const gchar **)groups->pdata, groups->len);
+
+  build_system = ide_workbench_get_build_system (self->workbench);
+
+  if (build_system != NULL)
+    {
+      g_autofree gchar *build_system_name = NULL;
+      g_autofree gchar *build_system_group = NULL;
+
+      build_system_name = ide_build_system_get_display_name (build_system);
+      build_system_group = g_strdup_printf ("%s%s", IDE_RECENT_PROJECTS_BUILD_SYSTEM_GROUP_PREFIX, 
build_system_name);
+      g_bookmark_file_add_group (projects_file, uri, build_system_group);
+    }
+
+  if ((directory = ide_project_info_get_directory (project_info)))
+    {
+      g_autofree gchar *dir_group = NULL;
+      g_autofree gchar *diruri = g_file_get_uri (directory);
+
+      dir_group = g_strdup_printf ("%s%s", IDE_RECENT_PROJECTS_DIRECTORY, diruri);
+      g_bookmark_file_add_group (projects_file, uri, dir_group);
+    }
+
+  IDE_TRACE_MSG ("Registering %s as recent project.", uri);
+
+  /* ensure the containing directory exists */
+  dir = g_path_get_dirname (recent_projects_path);
+  g_mkdir_with_parents (dir, 0750);
+
+  if (!g_bookmark_file_to_file (projects_file, recent_projects_path, &error))
+    {
+      g_warning ("Unable to save recent projects %s file: %s",
+                 recent_projects_path, error->message);
+      g_clear_error (&error);
+    }
+
+  IDE_EXIT;
+}
+
+
+static void
+gbp_recent_workbench_addin_project_loaded (IdeWorkbenchAddin *addin,
+                                           IdeProjectInfo    *project_info)
+{
+  GbpRecentWorkbenchAddin *self = (GbpRecentWorkbenchAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_RECENT_WORKBENCH_ADDIN (self));
+  g_assert (IDE_IS_PROJECT_INFO (project_info));
+
+  gbp_recent_workbench_addin_add_recent (self, project_info);
+}
+
+static void
+gbp_recent_workbench_addin_load (IdeWorkbenchAddin *addin,
+                                 IdeWorkbench      *workbench)
+{
+  GBP_RECENT_WORKBENCH_ADDIN (addin)->workbench = workbench;
+}
+
+static void
+gbp_recent_workbench_addin_unload (IdeWorkbenchAddin *addin,
+                                   IdeWorkbench      *workbench)
+{
+  GBP_RECENT_WORKBENCH_ADDIN (addin)->workbench = NULL;
+}
+
+static void
+workbench_addin_iface_init (IdeWorkbenchAddinInterface *iface)
+{
+  iface->load = gbp_recent_workbench_addin_load;
+  iface->unload = gbp_recent_workbench_addin_unload;
+  iface->project_loaded = gbp_recent_workbench_addin_project_loaded;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpRecentWorkbenchAddin, gbp_recent_workbench_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKBENCH_ADDIN, workbench_addin_iface_init))
+
+static void
+gbp_recent_workbench_addin_class_init (GbpRecentWorkbenchAddinClass *klass)
+{
+}
+
+static void
+gbp_recent_workbench_addin_init (GbpRecentWorkbenchAddin *self)
+{
+}
diff --git a/src/plugins/recent/gbp-recent-workbench-addin.h b/src/plugins/recent/gbp-recent-workbench-addin.h
new file mode 100644
index 000000000..8023ea4f8
--- /dev/null
+++ b/src/plugins/recent/gbp-recent-workbench-addin.h
@@ -0,0 +1,31 @@
+/* gbp-recent-workbench-addin.h
+ *
+ * Copyright 2018-2019 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_RECENT_WORKBENCH_ADDIN (gbp_recent_workbench_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpRecentWorkbenchAddin, gbp_recent_workbench_addin, GBP, RECENT_WORKBENCH_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/recent/meson.build b/src/plugins/recent/meson.build
index e8501df05..382b996ab 100644
--- a/src/plugins/recent/meson.build
+++ b/src/plugins/recent/meson.build
@@ -1,16 +1,14 @@
-recent_resources = gnome.compile_resources(
+plugins_sources += files([
+  'recent-plugin.c',
+  'gbp-recent-project-row.c',
+  'gbp-recent-section.c',
+  'gbp-recent-workbench-addin.c',
+])
+
+plugin_recent_resources = gnome.compile_resources(
   'recent-resources',
   'recent.gresource.xml',
   c_name: 'gbp_recent',
 )
 
-recent_sources = [
-  'recent-plugin.c',
-  'gbp-recent-project-row.c',
-  'gbp-recent-project-row.h',
-  'gbp-recent-section.c',
-  'gbp-recent-section.h',
-]
-
-gnome_builder_plugins_sources += files(recent_sources)
-gnome_builder_plugins_sources += recent_resources[0]
+plugins_sources += plugin_recent_resources[0]
diff --git a/src/plugins/recent/recent-plugin.c b/src/plugins/recent/recent-plugin.c
index 313c1d886..47ae53258 100644
--- a/src/plugins/recent/recent-plugin.c
+++ b/src/plugins/recent/recent-plugin.c
@@ -1,6 +1,6 @@
 /* recent-plugin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,17 +14,28 @@
  *
  * 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 "recent-plugin"
+
+#include "config.h"
+
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-greeter.h>
+#include <libide-gui.h>
 
 #include "gbp-recent-section.h"
+#include "gbp-recent-workbench-addin.h"
 
-void
-gbp_recent_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_gbp_recent_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_GREETER_SECTION,
                                               GBP_TYPE_RECENT_SECTION);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKBENCH_ADDIN,
+                                              GBP_TYPE_RECENT_WORKBENCH_ADDIN);
 }
diff --git a/src/plugins/recent/recent.gresource.xml b/src/plugins/recent/recent.gresource.xml
index 587e695a1..d875645e8 100644
--- a/src/plugins/recent/recent.gresource.xml
+++ b/src/plugins/recent/recent.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/recent">
     <file>recent.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/recent-plugin">
     <file>gbp-recent-project-row.ui</file>
     <file>gbp-recent-section.ui</file>
   </gresource>
diff --git a/src/plugins/recent/recent.plugin b/src/plugins/recent/recent.plugin
index d5c3385d3..a721610e3 100644
--- a/src/plugins/recent/recent.plugin
+++ b/src/plugins/recent/recent.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=recent-plugin
-Name=Recent Projects
-Description=Shows recent projects in the greeter
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2017 Christian Hergert
 Builtin=true
+Copyright=Copyright © 2017-2018 Christian Hergert
+Description=Shows recent projects in the greeter
+Embedded=_gbp_recent_register_types
 Hidden=true
-Embedded=gbp_recent_register_types
+Module=recent
+Depends=greeter;
+Name=Recent Projects
diff --git a/src/plugins/restore-cursor/gbp-restore-cursor-buffer-addin.c 
b/src/plugins/restore-cursor/gbp-restore-cursor-buffer-addin.c
new file mode 100644
index 000000000..2ad872ab1
--- /dev/null
+++ b/src/plugins/restore-cursor/gbp-restore-cursor-buffer-addin.c
@@ -0,0 +1,151 @@
+/* gbp-restore-cursor-buffer-addin.c
+ *
+ * Copyright 2018-2019 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-restore-cursor-buffer-addin"
+
+#include "config.h"
+
+#include <libide-code.h>
+
+#include "ide-buffer-private.h"
+
+#include "gbp-restore-cursor-buffer-addin.h"
+
+#define IDE_FILE_ATTRIBUTE_POSITION "metadata::libide-position"
+
+struct _GbpRestoreCursorBufferAddin
+{
+  GObject parent_instance;
+};
+
+static void
+gbp_restore_cursor_buffer_addin_file_saved (IdeBufferAddin *addin,
+                                            IdeBuffer      *buffer,
+                                            GFile          *file)
+{
+  g_autofree gchar *position = NULL;
+  g_autoptr(GError) error = NULL;
+  GtkTextMark *insert;
+  GtkTextIter iter;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_RESTORE_CURSOR_BUFFER_ADDIN (addin));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+
+  insert = gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (buffer));
+  gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer), &iter, insert);
+  position = g_strdup_printf ("%u:%u",
+                              gtk_text_iter_get_line (&iter),
+                              gtk_text_iter_get_line_offset (&iter));
+
+  if (!g_file_set_attribute_string (file, IDE_FILE_ATTRIBUTE_POSITION, position, 0, NULL, &error))
+    g_warning ("Failed to persist cursor position: %s", error->message);
+}
+
+static void
+gbp_restore_cursor_buffer_addin_file_loaded_cb (GObject      *object,
+                                                GAsyncResult *result,
+                                                gpointer      user_data)
+{
+  GFile *file = (GFile *)object;
+  g_autoptr(IdeBuffer) buffer = user_data;
+  g_autoptr(GFileInfo) file_info = NULL;
+  g_autoptr(GError) error = NULL;
+  const gchar *attr;
+  guint line_offset = 0;
+  guint line = 0;
+
+  g_assert (G_IS_FILE (file));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /* Don't do anything if the user already moved */
+  if (_ide_buffer_can_restore_cursor (buffer))
+    return;
+
+  if (!(file_info = g_file_query_info_finish (file, result, &error)))
+    return;
+
+  if (!g_file_info_has_attribute (file_info, IDE_FILE_ATTRIBUTE_POSITION) ||
+      !(attr = g_file_info_get_attribute_string (file_info, IDE_FILE_ATTRIBUTE_POSITION)))
+    return;
+
+  if (sscanf (attr, "%u:%u", &line, &line_offset) >= 1)
+    {
+      GtkTextIter iter;
+
+      IDE_TRACE_MSG ("Restoring insert mark to %u:%u", line + 1, line_offset + 1);
+      gtk_text_buffer_get_iter_at_line_offset (GTK_TEXT_BUFFER (buffer),
+                                               &iter,
+                                               line,
+                                               line_offset);
+      gtk_text_buffer_select_range (GTK_TEXT_BUFFER (buffer), &iter, &iter);
+
+      /* TODO: Notify view that we need to scroll? */
+    }
+}
+
+static void
+gbp_restore_cursor_buffer_addin_file_loaded (IdeBufferAddin *addin,
+                                             IdeBuffer      *buffer,
+                                             GFile          *file)
+{
+  g_autoptr(GSettings) settings = NULL;
+  g_autoptr(GFileInfo) file_info = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_RESTORE_CURSOR_BUFFER_ADDIN (addin));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+
+  /* Make sure our setting isn't disabled */
+  settings = g_settings_new ("org.gnome.builder.editor");
+  if (!g_settings_get_boolean (settings, "restore-insert-mark"))
+    return;
+
+  g_file_query_info_async (file,
+                           IDE_FILE_ATTRIBUTE_POSITION,
+                           G_FILE_QUERY_INFO_NONE,
+                           G_PRIORITY_HIGH,
+                           NULL,
+                           gbp_restore_cursor_buffer_addin_file_loaded_cb,
+                           g_object_ref (buffer));
+}
+
+static void
+buffer_addin_iface_init (IdeBufferAddinInterface *iface)
+{
+  iface->file_loaded = gbp_restore_cursor_buffer_addin_file_loaded;
+  iface->file_saved = gbp_restore_cursor_buffer_addin_file_saved;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpRestoreCursorBufferAddin, gbp_restore_cursor_buffer_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUFFER_ADDIN, buffer_addin_iface_init))
+
+static void
+gbp_restore_cursor_buffer_addin_class_init (GbpRestoreCursorBufferAddinClass *klass)
+{
+}
+
+static void
+gbp_restore_cursor_buffer_addin_init (GbpRestoreCursorBufferAddin *self)
+{
+}
diff --git a/src/plugins/restore-cursor/gbp-restore-cursor-buffer-addin.h 
b/src/plugins/restore-cursor/gbp-restore-cursor-buffer-addin.h
new file mode 100644
index 000000000..e6396e8cf
--- /dev/null
+++ b/src/plugins/restore-cursor/gbp-restore-cursor-buffer-addin.h
@@ -0,0 +1,31 @@
+/* gbp-restore-cursor-buffer-addin.h
+ *
+ * Copyright 2018-2019 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_RESTORE_CURSOR_BUFFER_ADDIN (gbp_restore_cursor_buffer_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpRestoreCursorBufferAddin, gbp_restore_cursor_buffer_addin, GBP, 
RESTORE_CURSOR_BUFFER_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/restore-cursor/meson.build b/src/plugins/restore-cursor/meson.build
new file mode 100644
index 000000000..5ebbe8810
--- /dev/null
+++ b/src/plugins/restore-cursor/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'restore-cursor-plugin.c',
+  'gbp-restore-cursor-buffer-addin.c',
+])
+
+plugin_restore_cursor_resources = gnome.compile_resources(
+  'gbp-restore-cursor-resources',
+  'restore-cursor.gresource.xml',
+  c_name: 'gbp_restore_cursor',
+)
+
+plugins_sources += plugin_restore_cursor_resources[0]
diff --git a/src/plugins/restore-cursor/restore-cursor-plugin.c 
b/src/plugins/restore-cursor/restore-cursor-plugin.c
new file mode 100644
index 000000000..d11164791
--- /dev/null
+++ b/src/plugins/restore-cursor/restore-cursor-plugin.c
@@ -0,0 +1,36 @@
+/* restore-cursor-plugin.c
+ *
+ * Copyright 2018-2019 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 "restore-cursor-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-code.h>
+
+#include "gbp-restore-cursor-buffer-addin.h"
+
+_IDE_EXTERN void
+_gbp_restore_cursor_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUFFER_ADDIN,
+                                              GBP_TYPE_RESTORE_CURSOR_BUFFER_ADDIN);
+}
diff --git a/src/plugins/restore-cursor/restore-cursor.gresource.xml 
b/src/plugins/restore-cursor/restore-cursor.gresource.xml
new file mode 100644
index 000000000..5832a901c
--- /dev/null
+++ b/src/plugins/restore-cursor/restore-cursor.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/restore-cursor">
+    <file>restore-cursor.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/restore-cursor/restore-cursor.plugin 
b/src/plugins/restore-cursor/restore-cursor.plugin
new file mode 100644
index 000000000..3b2d24362
--- /dev/null
+++ b/src/plugins/restore-cursor/restore-cursor.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2018 Christian Hergert
+Depends=editor;
+Description=Restore cursors when a buffer is re-opened.
+Embedded=_gbp_restore_cursor_register_types
+Hidden=true
+Module=restore-cursor
+Name=Restore Cursor
diff --git a/src/plugins/retab/gbp-retab-editor-page-addin.c b/src/plugins/retab/gbp-retab-editor-page-addin.c
new file mode 100644
index 000000000..49c496d7b
--- /dev/null
+++ b/src/plugins/retab/gbp-retab-editor-page-addin.c
@@ -0,0 +1,226 @@
+
+/* gbp-retab-editor-page-addin.c
+ *
+ * Copyright 2017 Lucie Charvat <luci charvat gmail 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-retab-editor-page-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+#include <libide-editor.h>
+
+#include "gbp-retab-editor-page-addin.h"
+
+struct _GbpRetabEditorPageAddin
+{
+  GObject        parent_instance;
+  IdeEditorPage *editor_view;
+};
+
+static gint
+get_buffer_range_indent (GtkTextBuffer *buffer,
+                         gint           line,
+                         gboolean       to_spaces)
+{
+  GtkTextIter iter;
+  gint indent = 0;
+
+  gtk_text_buffer_get_iter_at_line (buffer, &iter, line);
+
+  while (!gtk_text_iter_ends_line (&iter) && g_unichar_isspace(gtk_text_iter_get_char (&iter)))
+    {
+      gtk_text_iter_forward_char (&iter);
+      ++indent;
+    }
+
+  return indent;
+}
+
+/* Removes indent that is mean to be changes and inserts
+ * tabs and/or spaces insted */
+static void
+gbp_retab_editor_page_addin_retab (GtkTextBuffer *buffer,
+                            gint           line,
+                            gint           tab_width,
+                            gint           indent,
+                            gboolean       to_spaces)
+{
+  g_autoptr(GString) new_indent = g_string_new (NULL);
+  GtkTextIter iter;
+  GtkTextIter begin;
+  GtkTextIter end;
+  gint tab_num = 0;
+  gint space_num = 0;
+
+  g_assert (GTK_IS_TEXT_BUFFER (buffer));
+  g_assert (line >= 0 && line < gtk_text_buffer_get_line_count(buffer));
+  g_assert (tab_width != 0);
+  g_assert (new_indent != NULL);
+
+  gtk_text_buffer_get_iter_at_line (buffer, &iter, line);
+
+  while (!gtk_text_iter_ends_line (&iter) &&
+         g_unichar_isspace(gtk_text_iter_get_char (&iter)))
+    {
+      if (gtk_text_iter_get_char (&iter) == ' ')
+        ++space_num;
+      else if (gtk_text_iter_get_char (&iter) == '\t')
+        ++tab_num;
+
+      gtk_text_iter_forward_char (&iter);
+    }
+
+  if (to_spaces)
+    {
+      for (gint tab = 0; tab < tab_num * tab_width; ++tab)
+        g_string_append_c(new_indent, ' ');
+
+      for (gint space = 0; space < space_num; ++space)
+        g_string_append_c(new_indent, ' ');
+    }
+  else
+    {
+      for (gint tab = 0; tab < tab_num + (space_num / tab_width); ++tab)
+        g_string_append_c(new_indent, '\t');
+
+      for (gint space = 0; space < space_num % tab_width; ++space)
+        g_string_append_c(new_indent, ' ');
+    }
+
+  gtk_text_buffer_get_iter_at_line(buffer, &begin, line);
+  gtk_text_buffer_get_iter_at_line_offset (buffer, &end, line, indent);
+  gtk_text_buffer_delete (buffer, &begin, &end);
+
+  if (new_indent->len)
+    gtk_text_buffer_insert (buffer, &begin, new_indent->str, new_indent->len);
+}
+
+static void
+gbp_retab_editor_page_addin_action (GSimpleAction *action,
+                             GVariant      *variant,
+                             gpointer       user_data)
+{
+  GbpRetabEditorPageAddin *self = user_data;
+  IdeSourceView *source_view;
+  GtkTextBuffer *buffer;
+  IdeCompletion *completion;
+  guint tab_width;
+  gint start_line;
+  gint end_line;
+  gint indent;
+  GtkTextIter begin;
+  GtkTextIter end;
+  gboolean editable;
+  gboolean to_spaces;
+
+  g_assert (GBP_IS_RETAB_EDITOR_PAGE_ADDIN (self));
+  g_assert (G_IS_SIMPLE_ACTION (action));
+
+  buffer = GTK_TEXT_BUFFER (ide_editor_page_get_buffer (self->editor_view));
+  source_view = ide_editor_page_get_view (self->editor_view);
+
+  g_assert (IDE_IS_SOURCE_VIEW (source_view));
+
+  editable = gtk_text_view_get_editable (GTK_TEXT_VIEW (source_view));
+  completion = ide_source_view_get_completion (IDE_SOURCE_VIEW (source_view));
+  tab_width = gtk_source_view_get_tab_width(GTK_SOURCE_VIEW (source_view));
+  to_spaces = gtk_source_view_get_insert_spaces_instead_of_tabs(GTK_SOURCE_VIEW (source_view));
+
+  if (!editable)
+    return;
+
+  gtk_text_buffer_get_selection_bounds (buffer, &begin, &end);
+  gtk_text_iter_order (&begin, &end);
+
+  if (!gtk_text_iter_equal (&begin, &end) && gtk_text_iter_starts_line (&end))
+    gtk_text_iter_backward_char (&end);
+
+  start_line = gtk_text_iter_get_line (&begin);
+  end_line = gtk_text_iter_get_line (&end);
+
+  ide_completion_block_interactive (completion);
+  gtk_text_buffer_begin_user_action (buffer);
+
+  for (gint line = start_line; line <= end_line; ++line)
+    {
+      indent = get_buffer_range_indent (buffer, line, to_spaces);
+      if (indent > 0)
+        gbp_retab_editor_page_addin_retab (buffer, line, tab_width, indent, to_spaces);
+    }
+
+  gtk_text_buffer_end_user_action (buffer);
+  ide_completion_unblock_interactive (completion);
+}
+
+static const GActionEntry actions[] = {
+  { "retab", gbp_retab_editor_page_addin_action },
+};
+
+static void
+gbp_retab_editor_page_addin_load (IdeEditorPageAddin *addin,
+                           IdeEditorPage      *view)
+{
+  GbpRetabEditorPageAddin *self = (GbpRetabEditorPageAddin *)addin;
+  GActionGroup *group;
+
+  g_assert (GBP_IS_RETAB_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  self->editor_view = view;
+
+  group = gtk_widget_get_action_group (GTK_WIDGET (view), "editor-page");
+  g_action_map_add_action_entries (G_ACTION_MAP (group), actions, G_N_ELEMENTS (actions), self);
+}
+
+static void
+gbp_retab_editor_page_addin_unload (IdeEditorPageAddin *addin,
+                             IdeEditorPage      *view)
+{
+  GActionGroup *group;
+
+  g_assert (GBP_IS_RETAB_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  group = gtk_widget_get_action_group (GTK_WIDGET (view), "editor-page");
+  g_action_map_remove_action (G_ACTION_MAP (group), "retab");
+}
+
+static void
+editor_view_addin_iface_init (IdeEditorPageAddinInterface *iface)
+{
+  iface->load = gbp_retab_editor_page_addin_load;
+  iface->unload = gbp_retab_editor_page_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpRetabEditorPageAddin, gbp_retab_editor_page_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                                editor_view_addin_iface_init))
+
+
+static void
+gbp_retab_editor_page_addin_class_init (GbpRetabEditorPageAddinClass *klass)
+{
+}
+
+static void
+gbp_retab_editor_page_addin_init (GbpRetabEditorPageAddin *self)
+{
+}
diff --git a/src/plugins/retab/gbp-retab-editor-page-addin.h b/src/plugins/retab/gbp-retab-editor-page-addin.h
new file mode 100644
index 000000000..9cb6073bc
--- /dev/null
+++ b/src/plugins/retab/gbp-retab-editor-page-addin.h
@@ -0,0 +1,29 @@
+/* gbp-retab-editor-page-addin.h
+ *
+ * Copyright 2017 Lucie Charvat <luci charvat gmail 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
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_RETAB_EDITOR_PAGE_ADDIN (gbp_retab_editor_page_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpRetabEditorPageAddin, gbp_retab_editor_page_addin, GBP, RETAB_EDITOR_PAGE_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/retab/meson.build b/src/plugins/retab/meson.build
index c0b61aa49..ecb8e9476 100644
--- a/src/plugins/retab/meson.build
+++ b/src/plugins/retab/meson.build
@@ -1,18 +1,16 @@
-if get_option('with_retab')
+if get_option('plugin_retab')
 
-retab_resources = gnome.compile_resources(
-  'gbp-retab-resources',
+plugins_sources += files([
+  'retab-plugin.c',
+  'gbp-retab-editor-page-addin.c',
+])
+
+plugin_retab_resources = gnome.compile_resources(
+  'retab-resources',
   'retab.gresource.xml',
   c_name: 'gbp_retab',
 )
 
-retab_sources = [
-  'gbp-retab-plugin.c',
-  'gbp-retab-view-addin.c',
-  'gbp-retab-view-addin.h',
-]
-
-gnome_builder_plugins_sources += files(retab_sources)
-gnome_builder_plugins_sources += retab_resources[0]
+plugins_sources += plugin_retab_resources[0]
 
 endif
diff --git a/src/plugins/retab/retab-plugin.c b/src/plugins/retab/retab-plugin.c
new file mode 100644
index 000000000..eb79d16af
--- /dev/null
+++ b/src/plugins/retab/retab-plugin.c
@@ -0,0 +1,36 @@
+/* retab-plugin.c
+ *
+ * Copyright 2017 Lucie Charvat <luci charvat gmail 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 "retab-plugin"
+
+#include "config.h"
+
+#include <libide-editor.h>
+#include <libpeas/peas.h>
+
+#include "gbp-retab-editor-page-addin.h"
+
+_IDE_EXTERN void
+_gbp_retab_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                              GBP_TYPE_RETAB_EDITOR_PAGE_ADDIN);
+}
diff --git a/src/plugins/retab/retab.gresource.xml b/src/plugins/retab/retab.gresource.xml
index c0b78703d..ddfd8d045 100644
--- a/src/plugins/retab/retab.gresource.xml
+++ b/src/plugins/retab/retab.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/retab">
     <file>retab.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/retab-plugin">
-    <file>gtk/menus.ui</file>
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/retab/retab.plugin b/src/plugins/retab/retab.plugin
index 5e9f1cfd5..c7bc1aa42 100644
--- a/src/plugins/retab/retab.plugin
+++ b/src/plugins/retab/retab.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=retab-plugin
-Name=Retab
-Description=Retab lines with Builder editor.
 Authors=Lucie Charvat <luci charvat gmail com>
-Copyright=Copyright © 2017 Lucie Charvat
 Builtin=true
+Copyright=Copyright © 2017 Lucie Charvat
 Depends=editor
-Embedded=gbp_retab_register_types
+Description=Retab lines with Builder editor.
+Embedded=_gbp_retab_register_types
+Hidden=true
+Module=retab
+Name=Retab
diff --git a/src/plugins/rls/meson.build b/src/plugins/rls/meson.build
new file mode 100644
index 000000000..44d524a3c
--- /dev/null
+++ b/src/plugins/rls/meson.build
@@ -0,0 +1,13 @@
+if get_option('plugin_rls')
+
+install_data('rls_plugin.py', install_dir: plugindir)
+
+configure_file(
+          input: 'rls.plugin',
+         output: 'rls.plugin',
+  configuration: config_h,
+        install: true,
+    install_dir: plugindir,
+)
+
+endif
diff --git a/src/plugins/rls/rls.plugin b/src/plugins/rls/rls.plugin
new file mode 100644
index 000000000..00237beb7
--- /dev/null
+++ b/src/plugins/rls/rls.plugin
@@ -0,0 +1,17 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2016-2018 Christian Hergert
+Description=Provides auto-completion, diagnostics, and other IDE features
+Loader=python3
+Hidden=true
+Module=rls_plugin
+Name=Rust Language Server Integration
+X-Completion-Provider-Languages=rust
+X-Diagnostic-Provider-Languages=rust
+X-Formatter-Languages=rust
+X-Highlighter-Languages=rust
+X-Hover-Provider-Languages=rust
+X-Rename-Provider-Languages=rust
+X-Symbol-Resolver-Languages=rust
+X-Builder-ABI=@PACKAGE_ABI@
diff --git a/src/plugins/rls/rls_plugin.py b/src/plugins/rls/rls_plugin.py
new file mode 100644
index 000000000..94d445483
--- /dev/null
+++ b/src/plugins/rls/rls_plugin.py
@@ -0,0 +1,254 @@
+#!/usr/bin/env python
+
+# rust_langserv_plugin.py
+#
+# Copyright 2016 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/>.
+
+"""
+This plugin provides integration with the Rust Language Server.
+It builds off the generic language service components in libide
+by bridging them to our supervised Rust Language Server.
+"""
+
+import gi
+import os
+
+from gi.repository import GLib
+from gi.repository import Gio
+from gi.repository import GObject
+from gi.repository import Ide
+
+DEV_MODE = False
+
+class RlsService(Ide.Object):
+    _client = None
+    _has_started = False
+    _supervisor = None
+    _monitor = None
+
+    @classmethod
+    def from_context(klass, context):
+        return context.ensure_child_typed(RlsService)
+
+    @GObject.Property(type=Ide.LspClient)
+    def client(self):
+        return self._client
+
+    @client.setter
+    def client(self, value):
+        self._client = value
+        self.notify('client')
+
+    def do_parent_set(self, parent):
+        """
+        After the context has been loaded, we want to watch the project
+        Cargo.toml for changes if we find one. That will allow us to
+        restart the process as necessary to pick up changes.
+        """
+        if parent is None:
+            return
+
+        context = self.get_context()
+        workdir = context.ref_workdir()
+        cargo_toml = workdir.get_child('Cargo.toml')
+
+        if cargo_toml.query_exists():
+            try:
+                self._monitor = cargo_toml.monitor(0, None)
+                self._monitor.set_rate_limit(5 * 1000) # 5 Seconds
+                self._monitor.connect('changed', self._monitor_changed_cb)
+            except Exception as ex:
+                Ide.debug('Failed to monitor Cargo.toml for changes:', repr(ex))
+
+    def _monitor_changed_cb(self, monitor, file, other_file, event_type):
+        """
+        This method is called when Cargo.toml has changed. We need to
+        cancel any supervised process and force the language server to
+        restart. Otherwise, we risk it not picking up necessary changes.
+        """
+        if self._supervisor is not None:
+            subprocess = self._supervisor.get_subprocess()
+            if subprocess is not None:
+                subprocess.force_exit()
+
+    def do_stop(self):
+        """
+        Stops the Rust Language Server upon request to shutdown the
+        RlsService.
+        """
+        if self._monitor is not None:
+            monitor, self._monitor = self._monitor, None
+            if monitor is not None:
+                monitor.cancel()
+
+        if self._supervisor is not None:
+            supervisor, self._supervisor = self._supervisor, None
+            supervisor.stop()
+
+    def _ensure_started(self):
+        """
+        Start the rust service which provides communication with the
+        Rust Language Server. We supervise our own instance of the
+        language server and restart it as necessary using the
+        Ide.SubprocessSupervisor.
+
+        Various extension points (diagnostics, symbol providers, etc) use
+        the RlsService to access the rust components they need.
+        """
+        # To avoid starting the `rls` process unconditionally at startup,
+        # we lazily start it when the first provider tries to bind a client
+        # to its :client property.
+        if not self._has_started:
+            self._has_started = True
+
+            # Setup a launcher to spawn the rust language server
+            launcher = self._create_launcher()
+            launcher.set_clear_env(False)
+            sysroot = self._discover_sysroot()
+            if sysroot:
+                launcher.setenv("SYS_ROOT", sysroot, True)
+                launcher.setenv("LD_LIBRARY_PATH", os.path.join(sysroot, "lib"), True)
+            if DEV_MODE:
+                launcher.setenv('RUST_LOG', 'debug', True)
+
+            # Locate the directory of the project and run rls from there.
+            workdir = self.get_context().ref_workdir()
+            launcher.set_cwd(workdir.get_path())
+
+            # If rls was installed with Cargo, try to discover that
+            # to save the user having to update PATH.
+            path_to_rls = os.path.expanduser("~/.cargo/bin/rls")
+            if os.path.exists(path_to_rls):
+                old_path = os.getenv('PATH')
+                new_path = os.path.expanduser('~/.cargo/bin')
+                if old_path is not None:
+                    new_path += os.path.pathsep + old_path
+                launcher.setenv('PATH', new_path, True)
+            else:
+                path_to_rls = "rls"
+
+            # Setup our Argv. We want to communicate over STDIN/STDOUT,
+            # so it does not require any command line options.
+            launcher.push_argv(path_to_rls)
+
+            # Spawn our peer process and monitor it for
+            # crashes. We may need to restart it occasionally.
+            self._supervisor = Ide.SubprocessSupervisor()
+            self._supervisor.connect('spawned', self._rls_spawned)
+            self._supervisor.set_launcher(launcher)
+            self._supervisor.start()
+
+    def _rls_spawned(self, supervisor, subprocess):
+        """
+        This callback is executed when the `rls` process is spawned.
+        We can use the stdin/stdout to create a channel for our
+        LspClient.
+        """
+        stdin = subprocess.get_stdin_pipe()
+        stdout = subprocess.get_stdout_pipe()
+        io_stream = Gio.SimpleIOStream.new(stdout, stdin)
+
+        if self._client:
+            self._client.stop()
+            self._client.destroy()
+
+        self._client = Ide.LspClient.new(io_stream)
+        self.append(self._client)
+        self._client.add_language('rust')
+        self._client.start()
+        self.notify('client')
+
+    def _create_launcher(self):
+        """
+        Creates a launcher to be used by the rust service. This needs
+        to be run on the host because we do not currently bundle rust
+        inside our flatpak.
+
+        In the future, we might be able to rely on the runtime for
+        the tooling. Maybe even the program if flatpak-builder has
+        prebuilt our dependencies.
+        """
+        flags = Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE
+        if not DEV_MODE:
+            flags |= Gio.SubprocessFlags.STDERR_SILENCE
+        launcher = Ide.SubprocessLauncher()
+        launcher.set_flags(flags)
+        launcher.set_cwd(GLib.get_home_dir())
+        launcher.set_run_on_host(True)
+        return launcher
+
+    def _discover_sysroot(self):
+        """
+        The Rust Language Server needs to know where the sysroot is of
+        the Rust installation we are using. This is simple enough to
+        get, by using `rust --print sysroot` as the rust-language-server
+        documentation suggests.
+        """
+        for rustc in ['rustc', os.path.expanduser('~/.cargo/bin/rustc')]:
+            try:
+                launcher = self._create_launcher()
+                launcher.push_args([rustc, '--print', 'sysroot'])
+                subprocess = launcher.spawn()
+                _, stdout, _ = subprocess.communicate_utf8()
+                return stdout.strip()
+            except:
+                pass
+
+    @classmethod
+    def bind_client(klass, provider):
+        """
+        This helper tracks changes to our client as it might happen when
+        our `rls` process has crashed.
+        """
+        context = provider.get_context()
+        self = RlsService.from_context(context)
+        self._ensure_started()
+        self.bind_property('client', provider, 'client', GObject.BindingFlags.SYNC_CREATE)
+
+class RlsDiagnosticProvider(Ide.LspDiagnosticProvider):
+    def do_load(self):
+        RlsService.bind_client(self)
+
+class RlsCompletionProvider(Ide.LspCompletionProvider):
+    def do_load(self, context):
+        RlsService.bind_client(self)
+
+    def do_get_priority(self, context):
+        # This provider only activates when it is very likely that we
+        # want the results. So use high priority (negative is better).
+        return -1000
+
+class RlsRenameProvider(Ide.LspRenameProvider):
+    def do_load(self):
+        RlsService.bind_client(self)
+
+class RlsSymbolResolver(Ide.LspSymbolResolver):
+    def do_load(self):
+        RlsService.bind_client(self)
+
+class RlsHighlighter(Ide.LspHighlighter):
+    def do_load(self):
+        RlsService.bind_client(self)
+
+class RlsFormatter(Ide.LspFormatter):
+    def do_load(self):
+        RlsService.bind_client(self)
+
+class RlsHoverProvider(Ide.LspHoverProvider):
+    def do_prepare(self):
+        self.props.category = 'Rust'
+        self.props.priority = 200
+        RlsService.bind_client(self)
diff --git a/src/plugins/rustup/meson.build b/src/plugins/rustup/meson.build
index bb4e754de..904296102 100644
--- a/src/plugins/rustup/meson.build
+++ b/src/plugins/rustup/meson.build
@@ -1,4 +1,4 @@
-if get_option('with_rustup')
+if get_option('plugin_rustup')
 
 rustup_resources = gnome.compile_resources(
   'rustup_plugin',
@@ -13,7 +13,7 @@ install_data('rustup_plugin.py', install_dir: plugindir)
 configure_file(
           input: 'rustup.plugin',
          output: 'rustup.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/rustup/rustup.gresource.xml b/src/plugins/rustup/rustup.gresource.xml
index cf7bc4003..e3940b5f3 100644
--- a/src/plugins/rustup/rustup.gresource.xml
+++ b/src/plugins/rustup/rustup.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins/rustup_plugin">
+  <gresource prefix="/plugins/rustup_plugin">
     <file>rustup.sh</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/rustup/rustup.plugin b/src/plugins/rustup/rustup.plugin
index 82758e43e..c9d94ef59 100644
--- a/src/plugins/rustup/rustup.plugin
+++ b/src/plugins/rustup/rustup.plugin
@@ -1,8 +1,10 @@
 [Plugin]
-Module=rustup_plugin
-Name=RustUp
-Loader=python3
-Description=Helps keep your rust installation up to date!
 Authors=Christian Hergert <christian hergert me>, Georg Vienna <georg vienna himbarsoft com>
-Copyright=Copyright © 2017 Christian Hergert, Georg Vienna <georg vienna himbarsoft com>
 Builtin=true
+Copyright=Copyright © 2017 Christian Hergert, Georg Vienna <georg vienna himbarsoft com>
+Description=Helps keep your rust installation up to date!
+Loader=python3
+Module=rustup_plugin
+Name=RustUp
+X-Builder-ABI=@PACKAGE_ABI@
+X-Has-Resources=true
diff --git a/src/plugins/rustup/rustup.sh b/src/plugins/rustup/rustup.sh
index 7e089a1fb..7df218a9f 100755
--- a/src/plugins/rustup/rustup.sh
+++ b/src/plugins/rustup/rustup.sh
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 # Copyright 2016 The Rust Project Developers. See the COPYRIGHT
 # file at the top-level directory of this distribution and at
 # http://rust-lang.org/COPYRIGHT.
@@ -9,8 +9,8 @@
 # option. This file may not be copied, modified, or distributed
 # except according to those terms.
 
-# This is just a little script that can be curled from the internet to
-# install rustup. It just does platform detection, curls the installer
+# This is just a little script that can be downloaded from the internet to
+# install rustup. It just does platform detection, downloads the installer
 # and runs it.
 
 set -u
@@ -41,8 +41,8 @@ EOF
 }
 
 main() {
+    downloader --check
     need_cmd uname
-    need_cmd curl
     need_cmd mktemp
     need_cmd chmod
     need_cmd mkdir
@@ -100,7 +100,7 @@ main() {
     fi
 
     ensure mkdir -p "$_dir"
-    ensure curl -sSfL "$_url" -o "$_file"
+    ensure downloader "$_url" "$_file"
     ensure chmod u+x "$_file"
     if [ ! -x "$_file" ]; then
         printf '%s\n' "Cannot execute $_file (likely because of mounting /tmp as noexec)." 1>&2
@@ -308,7 +308,7 @@ get_architecture() {
 
     # Detect armv7 but without the CPU features Rust needs in that build,
     # and fall back to arm.
-    # See https://github.com/rust-lang-nursery/rustup.rs/issues/587.
+    # See https://github.com/rust-lang/rustup.rs/issues/587.
     if [ $_ostype = "unknown-linux-gnueabihf" -a $_cputype = armv7 ]; then
         if ensure grep '^Features' /proc/cpuinfo | grep -q -v neon; then
             # At least one processor does not have NEON.
@@ -331,11 +331,16 @@ err() {
 }
 
 need_cmd() {
-    if ! command -v "$1" > /dev/null 2>&1
+    if ! check_cmd "$1"
     then err "need '$1' (command not found)"
     fi
 }
 
+check_cmd() {
+    command -v "$1" > /dev/null 2>&1
+    return $?
+}
+
 need_ok() {
     if [ $? != 0 ]; then err "$1"; fi
 }
@@ -359,4 +364,24 @@ ignore() {
     "$@"
 }
 
+# This wraps curl or wget. Try curl first, if not installed,
+# use wget instead.
+downloader() {
+    if check_cmd curl
+    then _dld=curl
+    elif check_cmd wget
+    then _dld=wget
+    else _dld='curl or wget' # to be used in error message of need_cmd
+    fi
+
+    if [ "$1" = --check ]
+    then need_cmd "$_dld"
+    elif [ "$_dld" = curl ]
+    then curl -sSfL "$1" -o "$2"
+    elif [ "$_dld" = wget ]
+    then wget "$1" -O "$2"
+    else err "Unknown downloader"   # should not reach here
+    fi
+}
+
 main "$@" || exit 1
diff --git a/src/plugins/rustup/rustup_plugin.py b/src/plugins/rustup/rustup_plugin.py
index 4a5d6719c..8b3157ac6 100644
--- a/src/plugins/rustup/rustup_plugin.py
+++ b/src/plugins/rustup/rustup_plugin.py
@@ -20,16 +20,11 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-import gi
 import os
 import re
 import pty
 import stat
 
-gi.require_version('Dazzle', '1.0')
-gi.require_version('Ide', '1.0')
-gi.require_version('Gtk', '3.0')
-
 from gi.repository import Dazzle
 from gi.repository import GLib
 from gi.repository import GObject
@@ -41,7 +36,7 @@ from gi.repository import Peas
 _ = Ide.gettext
 
 def get_resource(path):
-    full_path = os.path.join('/org/gnome/builder/plugins/rustup_plugin', path)
+    full_path = os.path.join('/plugins/rustup_plugin', path)
     return Gio.resources_lookup_data(full_path, 0).get_data()
 
 def get_module_data_path(name):
@@ -73,18 +68,14 @@ def looks_like_channel(channel):
 
 class RustUpWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
     """
-    The RustUpWorkbenchAddin is a helper to handle open workbenches.
-    It manages the set of open workbenches stored in the application addin.
+    The RustUpWorkbenchAddin is a helper to handle open workspaces.
+    It manages the set of open workspaces stored in the application addin.
     """
-    def do_load(self, workbench):
-        RustupApplicationAddin.instance.add_workbench(workbench)
-        def unload(workbench, context):
-            RustupApplicationAddin.instance.workbenches.discard(workbench)
-        workbench.connect('unload', unload)
+    def do_workspace_added(self, workspace):
+        RustupApplicationAddin.instance.add_workspace(workspace)
 
-    def do_unload(self, workbench):
-        if RustupApplicationAddin.instance:
-            RustupApplicationAddin.instance.workbenches.discard(workbench)
+    def do_workspace_removed(self, workspace):
+        RustupApplicationAddin.instance.workspaces.discard(workspace)
 
 _NO_RUSTUP = _('Rustup not installed')
 
@@ -112,7 +103,7 @@ class RustupApplicationAddin(GObject.Object, Ide.ApplicationAddin):
 
     def do_load(self, application):
         RustupApplicationAddin.instance = self
-        self.workbenches = set()
+        self.workspaces = set()
         self.active_transfer = None
         self.has_rustup = False
         self.rustup_version = _NO_RUSTUP
@@ -210,27 +201,27 @@ class RustupApplicationAddin(GObject.Object, Ide.ApplicationAddin):
             pass
         self.emit('rustup_changed')
 
-    def add_workbench(self, workbench):
+    def add_workspace(self, workspace):
         # recheck if rustup was installed outside of gnome-builder
-        def is_active(workbench, active):
-            if workbench.is_active ():
+        def is_active(workspace, active):
+            if workspace.is_active ():
                 if self.active_transfer is None:
                     RustupApplicationAddin.instance.check_rustup()
-        workbench.connect('notify::is-active', is_active)
+        workspace.connect('notify::is-active', is_active)
         # call us if a transfer completes (could be the active_transfer)
-        transfer_manager = Gio.Application.get_default().get_transfer_manager()
+        transfer_manager = Ide.TransferManager.get_default()
         transfer_manager.connect('transfer-completed', self.transfer_completed)
         transfer_manager.connect('transfer-failed', self.transfer_failed)
-        self.workbenches.add(workbench)
+        self.workspaces.add(workspace)
 
     def transfer_completed(self, transfer_manager, transfer):
-        # reset the active transfer on completion, ensures that new workbenches dont get an old transfer
+        # reset the active transfer on completion, ensures that new workspaces dont get an old transfer
         if self.active_transfer == transfer:
             self.active_transfer = None
             self.notify('busy')
 
     def transfer_failed(self, transfer_manager, transfer, error):
-        # reset the active transfer on error, ensures that new workbenches dont get an old transfer
+        # reset the active transfer on error, ensures that new workspaces dont get an old transfer
         if self.active_transfer == transfer:
             self.active_transfer = None
             self.notify('busy')
@@ -238,7 +229,7 @@ class RustupApplicationAddin(GObject.Object, Ide.ApplicationAddin):
     def run_transfer(self, transfer):
         self.active_transfer = transfer
         self.notify('busy')
-        transfer_manager = Gio.Application.get_default().get_transfer_manager()
+        transfer_manager = Ide.TransferManager.get_default()
         transfer_manager.execute_async(transfer)
 
     def install(self):
@@ -390,7 +381,8 @@ class RustupInstaller(Ide.Transfer):
                         try:
                             self.props.progress = float(percent)/100
                         except Exception as te:
-                            print('_read_line_cb', self.state, line, te)
+                            if type(te) is not ValueError:
+                                print('_read_line_cb', self.state, line, te)
                 elif self.state == _STATE_DOWN_COMP or self.state == _STATE_SYNC_UPDATE or self.state == 
_STATE_CHECK_UPDATE_SELF  or self.state == _STATE_DOWN_UPDATE_SELF:
                     # the first progress can be empty, skip it
                     if length > 0:
diff --git a/src/plugins/snippets/ide-snippet-completion-item.c 
b/src/plugins/snippets/ide-snippet-completion-item.c
index 939d26552..6beab904a 100644
--- a/src/plugins/snippets/ide-snippet-completion-item.c
+++ b/src/plugins/snippets/ide-snippet-completion-item.c
@@ -1,6 +1,6 @@
 /* ide-snippet-completion-item.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,12 +14,14 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-snippet-completion-item"
 
+#include "config.h"
+
 #include <glib/gi18n.h>
 
 #include "ide-snippet-completion-item.h"
diff --git a/src/plugins/snippets/ide-snippet-completion-item.h 
b/src/plugins/snippets/ide-snippet-completion-item.h
index bd475c36c..f8838cbda 100644
--- a/src/plugins/snippets/ide-snippet-completion-item.h
+++ b/src/plugins/snippets/ide-snippet-completion-item.h
@@ -1,6 +1,6 @@
 /* ide-snippet-completion-item.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-sourceview.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/snippets/ide-snippet-completion-provider.c 
b/src/plugins/snippets/ide-snippet-completion-provider.c
index e76609ee2..8c62bc568 100644
--- a/src/plugins/snippets/ide-snippet-completion-provider.c
+++ b/src/plugins/snippets/ide-snippet-completion-provider.c
@@ -1,6 +1,6 @@
 /* ide-snippet-completion-provider.c
  *
- * Copyright © 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,12 +14,14 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-snippet-completion-provider.h"
 
+#include "config.h"
+
 #include "ide-snippet-completion-provider.h"
 #include "ide-snippet-completion-item.h"
 #include "ide-snippet-model.h"
@@ -70,7 +72,7 @@ ide_snippet_completion_provider_load (IdeCompletionProvider *provider,
   g_assert (IDE_IS_SNIPPET_COMPLETION_PROVIDER (self));
   g_assert (IDE_IS_CONTEXT (context));
 
-  storage = ide_context_get_snippets (context);
+  storage = ide_snippet_storage_from_context (context);
   self->model = ide_snippet_model_new (storage);
 }
 
diff --git a/src/plugins/snippets/ide-snippet-completion-provider.h 
b/src/plugins/snippets/ide-snippet-completion-provider.h
index 77ead4081..74828e0ac 100644
--- a/src/plugins/snippets/ide-snippet-completion-provider.h
+++ b/src/plugins/snippets/ide-snippet-completion-provider.h
@@ -1,6 +1,6 @@
 /* ide-snippet-completion-provider.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-core.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/snippets/ide-snippet-model.c b/src/plugins/snippets/ide-snippet-model.c
index 015564925..9f167c3e1 100644
--- a/src/plugins/snippets/ide-snippet-model.c
+++ b/src/plugins/snippets/ide-snippet-model.c
@@ -1,6 +1,6 @@
 /* ide-snippet-model.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,12 +14,14 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-snippet-model"
 
+#include "config.h"
+
 #include "ide-snippet-model.h"
 #include "ide-snippet-completion-item.h"
 
diff --git a/src/plugins/snippets/ide-snippet-model.h b/src/plugins/snippets/ide-snippet-model.h
index e8180ea1e..575705507 100644
--- a/src/plugins/snippets/ide-snippet-model.h
+++ b/src/plugins/snippets/ide-snippet-model.h
@@ -1,6 +1,6 @@
 /* ide-snippet-model.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-sourceview.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/snippets/ide-snippet-preferences-addin.c 
b/src/plugins/snippets/ide-snippet-preferences-addin.c
index 600440610..c10a0c23d 100644
--- a/src/plugins/snippets/ide-snippet-preferences-addin.c
+++ b/src/plugins/snippets/ide-snippet-preferences-addin.c
@@ -1,6 +1,6 @@
 /* ide-snippet-preferences-addin.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,14 +14,16 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-snippet-preferences-addin"
 
+#include "config.h"
+
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-gui.h>
 
 #include "ide-snippet-preferences-addin.h"
 
diff --git a/src/plugins/snippets/ide-snippet-preferences-addin.h 
b/src/plugins/snippets/ide-snippet-preferences-addin.h
index 1b85786ff..fc2dc12fd 100644
--- a/src/plugins/snippets/ide-snippet-preferences-addin.h
+++ b/src/plugins/snippets/ide-snippet-preferences-addin.h
@@ -1,6 +1,6 @@
 /* ide-snippet-preferences-addin.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/snippets/meson.build b/src/plugins/snippets/meson.build
index e6652d71b..ab2e3fec2 100644
--- a/src/plugins/snippets/meson.build
+++ b/src/plugins/snippets/meson.build
@@ -1,20 +1,15 @@
-if get_option('with_snippets')
-
-snippets_resources = gnome.compile_resources(
-  'snippets-resources',
-  'snippets.gresource.xml',
-  c_name: 'gbp_snippets',
-)
-
-snippets_sources = [
+plugins_sources += files([
   'snippets-plugin.c',
   'ide-snippet-completion-provider.c',
   'ide-snippet-completion-item.c',
   'ide-snippet-model.c',
   'ide-snippet-preferences-addin.c',
-]
+])
 
-gnome_builder_plugins_sources += files(snippets_sources)
-gnome_builder_plugins_sources += snippets_resources[0]
+snippets_resources = gnome.compile_resources(
+  'snippets-resources',
+  'snippets.gresource.xml',
+  c_name: 'gbp_snippets',
+)
 
-endif
+plugins_sources += snippets_resources[0]
diff --git a/src/plugins/snippets/snippets-plugin.c b/src/plugins/snippets/snippets-plugin.c
index ba2698306..dbcc58643 100644
--- a/src/plugins/snippets/snippets-plugin.c
+++ b/src/plugins/snippets/snippets-plugin.c
@@ -1,6 +1,6 @@
 /* snippets-plugin.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,16 +14,21 @@
  *
  * 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
  */
 
-#include <ide.h>
+#include "config.h"
+
+#include <libide-gui.h>
+#include <libide-sourceview.h>
 #include <libpeas/peas.h>
 
 #include "ide-snippet-completion-provider.h"
 #include "ide-snippet-preferences-addin.h"
 
-void
-gbp_snippets_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_gbp_snippets_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_COMPLETION_PROVIDER,
diff --git a/src/plugins/snippets/snippets.gresource.xml b/src/plugins/snippets/snippets.gresource.xml
index 560b08dac..a4ec9a3a6 100644
--- a/src/plugins/snippets/snippets.gresource.xml
+++ b/src/plugins/snippets/snippets.gresource.xml
@@ -1,8 +1,23 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/snippets">
     <file>snippets.plugin</file>
   </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/snippets-plugin">
+  <gresource prefix="/org/gnome/builder">
+    <!-- compressing these would mean we waste heap to maintain internal
+         pointers, so we don't compress them. -->
+    <file>snippets/chdr.snippets</file>
+    <file>snippets/c.snippets</file>
+    <file>snippets/gobject.snippets</file>
+    <file>snippets/java.snippets</file>
+    <file>snippets/js.snippets</file>
+    <file>snippets/licenses.snippets</file>
+    <file>snippets/main.snippets</file>
+    <file>snippets/python.snippets</file>
+    <file>snippets/rpmspec.snippets</file>
+    <file>snippets/rust.snippets</file>
+    <file>snippets/shebang.snippets</file>
+    <file>snippets/vala.snippets</file>
+    <file>snippets/xml.snippets</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/snippets/snippets.plugin b/src/plugins/snippets/snippets.plugin
index 037dcec7f..a6f91aa75 100644
--- a/src/plugins/snippets/snippets.plugin
+++ b/src/plugins/snippets/snippets.plugin
@@ -1,11 +1,11 @@
 [Plugin]
-Module=snippets-plugin
-Name=Snippets
-Description=Support for snippets in a variety of languages
 Authors=Christian Hergert <christian hergert me>
+Builtin=true
 Copyright=Copyright © 2018 Christian Hergert
 Depends=editor;
-Builtin=true
+Description=Support for snippets in a variety of languages
+Embedded=_gbp_snippets_register_types
 Hidden=true
-Embedded=gbp_snippets_register_types
+Module=snippets
+Name=Snippets
 X-Completion-Provider-Languages=*
diff --git a/data/snippets/c.snippets b/src/plugins/snippets/snippets/c.snippets
similarity index 100%
rename from data/snippets/c.snippets
rename to src/plugins/snippets/snippets/c.snippets
diff --git a/data/snippets/chdr.snippets b/src/plugins/snippets/snippets/chdr.snippets
similarity index 100%
rename from data/snippets/chdr.snippets
rename to src/plugins/snippets/snippets/chdr.snippets
diff --git a/data/snippets/gobject.snippets b/src/plugins/snippets/snippets/gobject.snippets
similarity index 100%
rename from data/snippets/gobject.snippets
rename to src/plugins/snippets/snippets/gobject.snippets
diff --git a/data/snippets/java.snippets b/src/plugins/snippets/snippets/java.snippets
similarity index 100%
rename from data/snippets/java.snippets
rename to src/plugins/snippets/snippets/java.snippets
diff --git a/data/snippets/js.snippets b/src/plugins/snippets/snippets/js.snippets
similarity index 100%
rename from data/snippets/js.snippets
rename to src/plugins/snippets/snippets/js.snippets
diff --git a/data/snippets/licenses.snippets b/src/plugins/snippets/snippets/licenses.snippets
similarity index 100%
rename from data/snippets/licenses.snippets
rename to src/plugins/snippets/snippets/licenses.snippets
diff --git a/data/snippets/main.snippets b/src/plugins/snippets/snippets/main.snippets
similarity index 100%
rename from data/snippets/main.snippets
rename to src/plugins/snippets/snippets/main.snippets
diff --git a/data/snippets/python.snippets b/src/plugins/snippets/snippets/python.snippets
similarity index 100%
rename from data/snippets/python.snippets
rename to src/plugins/snippets/snippets/python.snippets
diff --git a/data/snippets/rpmspec.snippets b/src/plugins/snippets/snippets/rpmspec.snippets
similarity index 100%
rename from data/snippets/rpmspec.snippets
rename to src/plugins/snippets/snippets/rpmspec.snippets
diff --git a/data/snippets/rust.snippets b/src/plugins/snippets/snippets/rust.snippets
similarity index 100%
rename from data/snippets/rust.snippets
rename to src/plugins/snippets/snippets/rust.snippets
diff --git a/data/snippets/shebang.snippets b/src/plugins/snippets/snippets/shebang.snippets
similarity index 100%
rename from data/snippets/shebang.snippets
rename to src/plugins/snippets/snippets/shebang.snippets
diff --git a/data/snippets/vala.snippets b/src/plugins/snippets/snippets/vala.snippets
similarity index 100%
rename from data/snippets/vala.snippets
rename to src/plugins/snippets/snippets/vala.snippets
diff --git a/data/snippets/xml.snippets b/src/plugins/snippets/snippets/xml.snippets
similarity index 100%
rename from data/snippets/xml.snippets
rename to src/plugins/snippets/snippets/xml.snippets
diff --git a/src/plugins/spellcheck/gbp-spell-buffer-addin.c b/src/plugins/spellcheck/gbp-spell-buffer-addin.c
index 455007349..c5a8bb6e7 100644
--- a/src/plugins/spellcheck/gbp-spell-buffer-addin.c
+++ b/src/plugins/spellcheck/gbp-spell-buffer-addin.c
@@ -1,6 +1,6 @@
 /* gbp-spell-buffer-addin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-spell-buffer-addin"
diff --git a/src/plugins/spellcheck/gbp-spell-buffer-addin.h b/src/plugins/spellcheck/gbp-spell-buffer-addin.h
index c2ce380f5..37a3fbad7 100644
--- a/src/plugins/spellcheck/gbp-spell-buffer-addin.h
+++ b/src/plugins/spellcheck/gbp-spell-buffer-addin.h
@@ -1,6 +1,6 @@
 /* gbp-spell-buffer-addin.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-editor.h>
 #include <gspell/gspell.h>
 
 G_BEGIN_DECLS
diff --git a/src/plugins/spellcheck/gbp-spell-dict.c b/src/plugins/spellcheck/gbp-spell-dict.c
index 3b5466873..c56497a86 100644
--- a/src/plugins/spellcheck/gbp-spell-dict.c
+++ b/src/plugins/spellcheck/gbp-spell-dict.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 /* This GObject exists until Gspell handles managing the content of a dictionary */
@@ -22,7 +24,7 @@
 #include <enchant.h>
 #include <gspell/gspell.h>
 
-#include <ide.h>
+#include <libide-editor.h>
 
 typedef enum {
   INIT_NONE,
diff --git a/src/plugins/spellcheck/gbp-spell-dict.h b/src/plugins/spellcheck/gbp-spell-dict.h
index a4fcdd49a..10d3f2d84 100644
--- a/src/plugins/spellcheck/gbp-spell-dict.h
+++ b/src/plugins/spellcheck/gbp-spell-dict.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/spellcheck/gbp-spell-editor-addin.c b/src/plugins/spellcheck/gbp-spell-editor-addin.c
index c60652fac..8e0ef541b 100644
--- a/src/plugins/spellcheck/gbp-spell-editor-addin.c
+++ b/src/plugins/spellcheck/gbp-spell-editor-addin.c
@@ -1,6 +1,6 @@
 /* gbp-spell-editor-addin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-spell-editor-addin"
@@ -27,7 +29,7 @@ struct _GbpSpellEditorAddin
 {
   GObject               parent_instance;
 
-  IdeEditorPerspective *editor;
+  IdeEditorSurface *editor;
 
   DzlDockWidget        *dock;
   GbpSpellWidget       *widget;
@@ -35,17 +37,17 @@ struct _GbpSpellEditorAddin
 
 static void
 gbp_spell_editor_addin_load (IdeEditorAddin       *addin,
-                             IdeEditorPerspective *editor)
+                             IdeEditorSurface *editor)
 {
   GbpSpellEditorAddin *self = (GbpSpellEditorAddin *)addin;
-  IdeLayoutTransientSidebar *sidebar;
+  IdeTransientSidebar *sidebar;
 
   g_assert (GBP_IS_SPELL_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
 
   self->editor = editor;
 
-  sidebar = ide_editor_perspective_get_transient_sidebar (editor);
+  sidebar = ide_editor_surface_get_transient_sidebar (editor);
 
   self->dock = g_object_new (DZL_TYPE_DOCK_WIDGET,
                              "title", _("Spelling"),
@@ -70,12 +72,12 @@ gbp_spell_editor_addin_load (IdeEditorAddin       *addin,
 
 static void
 gbp_spell_editor_addin_unload (IdeEditorAddin       *addin,
-                               IdeEditorPerspective *editor)
+                               IdeEditorSurface *editor)
 {
   GbpSpellEditorAddin *self = (GbpSpellEditorAddin *)addin;
 
   g_assert (GBP_IS_SPELL_EDITOR_ADDIN (self));
-  g_assert (IDE_IS_EDITOR_PERSPECTIVE (editor));
+  g_assert (IDE_IS_EDITOR_SURFACE (editor));
 
   if (self->dock != NULL)
     gtk_widget_destroy (GTK_WIDGET (self->dock));
@@ -87,14 +89,14 @@ gbp_spell_editor_addin_unload (IdeEditorAddin       *addin,
 }
 
 static void
-gbp_spell_editor_addin_view_set (IdeEditorAddin *addin,
-                                 IdeLayoutView  *view)
+gbp_spell_editor_addin_page_set (IdeEditorAddin *addin,
+                                 IdePage  *view)
 {
   GbpSpellEditorAddin *self = (GbpSpellEditorAddin *)addin;
-  IdeEditorView *current;
+  IdeEditorPage *current;
 
   g_assert (GBP_IS_SPELL_EDITOR_ADDIN (self));
-  g_assert (!view || IDE_IS_LAYOUT_VIEW (view));
+  g_assert (!view || IDE_IS_PAGE (view));
 
   /* If there is currently a view attached, and this is
    * a new view, then we want to unset it so that the
@@ -105,7 +107,7 @@ gbp_spell_editor_addin_view_set (IdeEditorAddin *addin,
 
   if (current != NULL)
     {
-      if (view == IDE_LAYOUT_VIEW (current))
+      if (view == IDE_PAGE (current))
         return;
 
       gbp_spell_widget_set_editor (self->widget, NULL);
@@ -123,7 +125,7 @@ editor_addin_iface_init (IdeEditorAddinInterface *iface)
 {
   iface->load = gbp_spell_editor_addin_load;
   iface->unload = gbp_spell_editor_addin_unload;
-  iface->view_set = gbp_spell_editor_addin_view_set;
+  iface->page_set = gbp_spell_editor_addin_page_set;
 }
 
 G_DEFINE_TYPE_WITH_CODE (GbpSpellEditorAddin, gbp_spell_editor_addin, G_TYPE_OBJECT,
@@ -141,18 +143,18 @@ gbp_spell_editor_addin_init (GbpSpellEditorAddin *self)
 
 void
 _gbp_spell_editor_addin_begin (GbpSpellEditorAddin *self,
-                               IdeEditorView       *view)
+                               IdeEditorPage       *view)
 {
-  IdeLayoutTransientSidebar *sidebar;
+  IdeTransientSidebar *sidebar;
 
   g_return_if_fail (GBP_IS_SPELL_EDITOR_ADDIN (self));
-  g_return_if_fail (IDE_IS_EDITOR_VIEW (view));
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (view));
 
   gbp_spell_widget_set_editor (self->widget, view);
 
-  sidebar = ide_editor_perspective_get_transient_sidebar (self->editor);
-  ide_layout_transient_sidebar_set_view (sidebar, IDE_LAYOUT_VIEW (view));
-  ide_layout_transient_sidebar_set_panel (sidebar, GTK_WIDGET (self->dock));
+  sidebar = ide_editor_surface_get_transient_sidebar (self->editor);
+  ide_transient_sidebar_set_page (sidebar, IDE_PAGE (view));
+  ide_transient_sidebar_set_panel (sidebar, GTK_WIDGET (self->dock));
 
   /* TODO: This needs API via transient sidebar panel */
   g_object_set (self->editor, "right-visible", TRUE, NULL);
@@ -160,10 +162,10 @@ _gbp_spell_editor_addin_begin (GbpSpellEditorAddin *self,
 
 void
 _gbp_spell_editor_addin_cancel (GbpSpellEditorAddin *self,
-                                IdeEditorView       *view)
+                                IdeEditorPage       *view)
 {
   g_return_if_fail (GBP_IS_SPELL_EDITOR_ADDIN (self));
-  g_return_if_fail (IDE_IS_EDITOR_VIEW (view));
+  g_return_if_fail (IDE_IS_EDITOR_PAGE (view));
 
   gbp_spell_widget_set_editor (self->widget, NULL);
 
diff --git a/src/plugins/spellcheck/gbp-spell-editor-addin.h b/src/plugins/spellcheck/gbp-spell-editor-addin.h
index 64f6333a2..7d260ce9e 100644
--- a/src/plugins/spellcheck/gbp-spell-editor-addin.h
+++ b/src/plugins/spellcheck/gbp-spell-editor-addin.h
@@ -1,6 +1,6 @@
 /* gbp-spell-editor-addin.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-editor.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/spellcheck/gbp-spell-editor-page-addin.c 
b/src/plugins/spellcheck/gbp-spell-editor-page-addin.c
new file mode 100644
index 000000000..d28eaaed8
--- /dev/null
+++ b/src/plugins/spellcheck/gbp-spell-editor-page-addin.c
@@ -0,0 +1,394 @@
+/* gbp-spell-editor-page-addin.c
+ *
+ * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
+ * Copyright 2017-2019 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-spell-editor-page-addin"
+
+#include "config.h"
+
+#include <dazzle.h>
+
+#include <gspell/gspell.h>
+#include <glib/gi18n.h>
+
+#include "gbp-spell-buffer-addin.h"
+#include "gbp-spell-editor-addin.h"
+#include "gbp-spell-editor-page-addin.h"
+#include "gbp-spell-navigator.h"
+#include "gbp-spell-private.h"
+#include "gbp-spell-utils.h"
+
+#define SPELLCHECKER_SUBREGION_LENGTH 500
+
+#define I_(s) g_intern_static_string(s)
+
+struct _GbpSpellEditorPageAddin
+{
+  GObject          parent_instance;
+
+  /* Borrowed references */
+  IdeEditorPage   *view;
+  GtkTextMark     *word_begin;
+  GtkTextMark     *word_end;
+  GtkTextMark     *start_boundary;
+  GtkTextMark     *end_boundary;
+
+  /* Owned references */
+  DzlBindingGroup *buffer_addin_bindings;
+  GspellNavigator *navigator;
+
+  gint             checking_count;
+};
+
+static void
+gbp_spell_editor_page_addin_begin (GSimpleAction *action,
+                                   GVariant      *variant,
+                                   gpointer       user_data)
+{
+  GbpSpellEditorPageAddin *self = user_data;
+  IdeEditorAddin *addin;
+  GtkWidget *editor;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self));
+
+  editor = gtk_widget_get_ancestor (GTK_WIDGET (self->view), IDE_TYPE_EDITOR_SURFACE);
+  addin = ide_editor_addin_find_by_module_name (IDE_EDITOR_SURFACE (editor), "spellcheck");
+  _gbp_spell_editor_addin_begin (GBP_SPELL_EDITOR_ADDIN (addin), self->view);
+}
+
+static void
+gbp_spell_editor_page_addin_cancel (GSimpleAction *action,
+                                    GVariant      *variant,
+                                    gpointer       user_data)
+{
+  GbpSpellEditorPageAddin *self = user_data;
+  IdeEditorAddin *addin;
+  GtkWidget *editor;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self));
+
+  editor = gtk_widget_get_ancestor (GTK_WIDGET (self->view), IDE_TYPE_EDITOR_SURFACE);
+  addin = ide_editor_addin_find_by_module_name (IDE_EDITOR_SURFACE (editor), "spellcheck");
+  _gbp_spell_editor_addin_cancel (GBP_SPELL_EDITOR_ADDIN (addin), self->view);
+}
+
+static const GActionEntry actions[] = {
+  { "spellcheck", gbp_spell_editor_page_addin_begin },
+  { "cancel-spellcheck", gbp_spell_editor_page_addin_cancel },
+};
+
+  static const DzlShortcutEntry spellchecker_shortcut_entry[] = {
+    { "org.gnome.builder.editor-page.spellchecker",
+      0, NULL,
+      NC_("shortcut window", "Editor shortcuts"),
+      NC_("shortcut window", "Editing"),
+      NC_("shortcut window", "Show the spellchecker panel") },
+  };
+
+static void
+gbp_spell_editor_page_addin_load (IdeEditorPageAddin *addin,
+                                  IdeEditorPage      *view)
+{
+  GbpSpellEditorPageAddin *self = (GbpSpellEditorPageAddin *)addin;
+  g_autoptr(GSimpleActionGroup) group = NULL;
+  g_autoptr(GPropertyAction) enabled_action = NULL;
+  DzlShortcutController *controller;
+  IdeBufferAddin *buffer_addin;
+  GspellTextView *wrapper;
+  IdeSourceView *source_view;
+  IdeBuffer *buffer;
+
+  g_assert (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  self->view = view;
+
+  source_view = ide_editor_page_get_view (view);
+  g_assert (source_view != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (source_view));
+
+  buffer = ide_editor_page_get_buffer (view);
+  g_assert (buffer != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  buffer_addin = ide_buffer_addin_find_by_module_name (buffer, "spellcheck");
+
+  if (!GBP_IS_SPELL_BUFFER_ADDIN (buffer_addin))
+    {
+      /* We might find ourselves in a race here and the buffer
+       * addins are already in destruction. Therefore, silently
+       * fail any further setup.
+       */
+      ide_widget_warning (source_view, _("Failed to initialize spellchecking, disabling"));
+      return;
+    }
+
+  wrapper = gspell_text_view_get_from_gtk_text_view (GTK_TEXT_VIEW (source_view));
+  g_assert (wrapper != NULL);
+  g_assert (GSPELL_IS_TEXT_VIEW (wrapper));
+
+  self->buffer_addin_bindings = dzl_binding_group_new ();
+  dzl_binding_group_bind (self->buffer_addin_bindings, "enabled",
+                          wrapper, "enable-language-menu",
+                          G_BINDING_SYNC_CREATE);
+  dzl_binding_group_bind (self->buffer_addin_bindings, "enabled",
+                          wrapper, "inline-spell-checking",
+                          G_BINDING_SYNC_CREATE);
+  dzl_binding_group_set_source (self->buffer_addin_bindings, buffer_addin);
+
+  group = g_simple_action_group_new ();
+  enabled_action = g_property_action_new ("enabled", buffer_addin, "enabled");
+  g_action_map_add_action (G_ACTION_MAP (group), G_ACTION (enabled_action));
+  g_action_map_add_action_entries (G_ACTION_MAP (group), actions, G_N_ELEMENTS (actions), self);
+  gtk_widget_insert_action_group (GTK_WIDGET (view), "spellcheck", G_ACTION_GROUP (group));
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (view));
+  dzl_shortcut_controller_add_command_action (controller,
+                                              "org.gnome.builder.editor-page.spellchecker",
+                                              I_("<shift>F7"),
+                                              DZL_SHORTCUT_PHASE_CAPTURE,
+                                              "spellcheck.spellcheck");
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             spellchecker_shortcut_entry,
+                                             G_N_ELEMENTS (spellchecker_shortcut_entry),
+                                             GETTEXT_PACKAGE);
+}
+
+static void
+gbp_spell_editor_page_addin_unload (IdeEditorPageAddin *addin,
+                                    IdeEditorPage      *view)
+{
+  GbpSpellEditorPageAddin *self = (GbpSpellEditorPageAddin *)addin;
+
+  g_assert (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_EDITOR_PAGE (view));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (view), "spellcheck", NULL);
+
+  if (self->buffer_addin_bindings != NULL)
+    {
+      dzl_binding_group_set_source (self->buffer_addin_bindings, NULL);
+      g_clear_object (&self->buffer_addin_bindings);
+    }
+
+  g_clear_object (&self->navigator);
+
+  self->view = NULL;
+}
+
+static void
+editor_page_addin_iface_init (IdeEditorPageAddinInterface *iface)
+{
+  iface->load = gbp_spell_editor_page_addin_load;
+  iface->unload = gbp_spell_editor_page_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpSpellEditorPageAddin, gbp_spell_editor_page_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_PAGE_ADDIN, editor_page_addin_iface_init))
+
+static void
+gbp_spell_editor_page_addin_class_init (GbpSpellEditorPageAddinClass *klass)
+{
+}
+
+static void
+gbp_spell_editor_page_addin_init (GbpSpellEditorPageAddin *self)
+{
+}
+
+/**
+ * gbp_spell_editor_page_addin_begin_checking:
+ * @self: a #GbpSpellEditorPageAddin
+ *
+ * This function should be called by the #GbpSpellWidget to enable
+ * spellchecking on the textview and underlying buffer. Doing so allows the
+ * inline-spellchecking and language-menu to be dynamically enabled even if
+ * spellchecking is typically disabled in the buffer.
+ *
+ * The caller should call gbp_spell_editor_page_addin_end_checking() when they
+ * have completed the spellchecking process.
+ *
+ * Since: 3.26
+ */
+void
+gbp_spell_editor_page_addin_begin_checking (GbpSpellEditorPageAddin *self)
+{
+  GObject *buffer_addin;
+
+  g_return_if_fail (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self));
+  g_return_if_fail (self->view != NULL);
+  g_return_if_fail (self->checking_count >= 0);
+
+  self->checking_count++;
+
+  buffer_addin = dzl_binding_group_get_source (self->buffer_addin_bindings);
+
+  if (buffer_addin == NULL)
+    {
+      ide_widget_warning (self->view, _("Failed to initialize spellchecking, disabling"));
+      return;
+    }
+
+  if (self->checking_count == 1)
+    {
+      IdeSourceView *view;
+      GtkTextBuffer *buffer;
+      GtkTextIter begin;
+      GtkTextIter end;
+
+      gbp_spell_buffer_addin_begin_checking (GBP_SPELL_BUFFER_ADDIN (buffer_addin));
+
+      view = ide_editor_page_get_view (self->view);
+      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
+
+      /* Use the selected range, otherwise whole buffer */
+      if (!gtk_text_buffer_get_selection_bounds (buffer, &begin, &end))
+        gtk_text_buffer_get_bounds (buffer, &begin, &end);
+
+      /* The selection might begin in the middle of a word */
+      if (gbp_spell_utils_text_iter_inside_word (&begin) &&
+          !gbp_spell_utils_text_iter_starts_word (&begin))
+        gbp_spell_utils_text_iter_backward_word_start (&begin);
+
+      /* And also at the end */
+      if (gbp_spell_utils_text_iter_inside_word (&end))
+        gbp_spell_utils_text_iter_forward_word_end (&end);
+
+      /* Place current position at the beginning of the selection */
+      self->word_begin = gtk_text_buffer_create_mark (buffer, NULL, &begin, TRUE);
+      self->word_end = gtk_text_buffer_create_mark (buffer, NULL, &begin, FALSE);
+
+      /* Setup our acceptable range for checking */
+      self->start_boundary = gtk_text_buffer_create_mark (buffer, NULL, &begin, TRUE);
+      self->end_boundary = gtk_text_buffer_create_mark (buffer, NULL, &end, FALSE);
+    }
+}
+
+/**
+ * gbp_spell_editor_page_addin_end_checking:
+ * @self: a #GbpSpellEditorPageAddin
+ *
+ * Completes a spellcheck operation and potentially restores the buffer to
+ * the visual state before spellchecking started.
+ *
+ * Since: 3.26
+ */
+void
+gbp_spell_editor_page_addin_end_checking (GbpSpellEditorPageAddin *self)
+{
+  g_return_if_fail (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self));
+  g_return_if_fail (self->checking_count >= 0);
+
+  self->checking_count--;
+
+  if (self->checking_count == 0)
+    {
+      GObject *buffer_addin;
+
+      buffer_addin = dzl_binding_group_get_source (self->buffer_addin_bindings);
+
+      if (GBP_IS_SPELL_BUFFER_ADDIN (buffer_addin))
+        gbp_spell_buffer_addin_end_checking (GBP_SPELL_BUFFER_ADDIN (buffer_addin));
+
+      if (self->view != NULL)
+        {
+          IdeBuffer *buffer = ide_editor_page_get_buffer (self->view);
+
+          /*
+           * We could be in disposal here, so its possible the buffer has
+           * already been cleared and released.
+           */
+
+          if (buffer != NULL)
+            {
+              gtk_text_buffer_delete_mark (GTK_TEXT_BUFFER (buffer), self->word_begin);
+              gtk_text_buffer_delete_mark (GTK_TEXT_BUFFER (buffer), self->word_end);
+              gtk_text_buffer_delete_mark (GTK_TEXT_BUFFER (buffer), self->start_boundary);
+              gtk_text_buffer_delete_mark (GTK_TEXT_BUFFER (buffer), self->end_boundary);
+            }
+        }
+
+      self->word_begin = NULL;
+      self->word_end = NULL;
+      self->start_boundary = NULL;
+      self->end_boundary = NULL;
+
+      g_clear_object (&self->navigator);
+    }
+}
+
+/**
+ * gbp_spell_editor_page_addin_get_checker:
+ * @self: a #GbpSpellEditorPageAddin
+ *
+ * This function may return %NULL before
+ * gbp_spell_editor_page_addin_begin_checking() has been called.
+ *
+ * Returns: (nullable) (transfer none): a #GspellChecker or %NULL
+ *
+ * Since: 3.26
+ */
+GspellChecker *
+gbp_spell_editor_page_addin_get_checker (GbpSpellEditorPageAddin *self)
+{
+  GObject *buffer_addin;
+
+  g_return_val_if_fail (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self), NULL);
+
+  buffer_addin = dzl_binding_group_get_source (self->buffer_addin_bindings);
+  if (GBP_IS_SPELL_BUFFER_ADDIN (buffer_addin))
+    return gbp_spell_buffer_addin_get_checker (GBP_SPELL_BUFFER_ADDIN (buffer_addin));
+
+  return NULL;
+}
+
+/**
+ * gbp_spell_editor_page_addin_get_navigator:
+ * @self: a #GbpSpellEditorPageAddin
+ *
+ * This function may return %NULL before
+ * gbp_spell_editor_page_addin_begin_checking() has been called.
+ *
+ * Returns: (nullable) (transfer none): a #GspellNavigator or %NULL
+ *
+ * Since: 3.26
+ */
+GspellNavigator *
+gbp_spell_editor_page_addin_get_navigator (GbpSpellEditorPageAddin *self)
+{
+  g_return_val_if_fail (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self), NULL);
+
+  if (self->navigator == NULL)
+    {
+      if (self->view != NULL)
+        {
+          IdeSourceView *view = ide_editor_page_get_view (self->view);
+
+          self->navigator = gbp_spell_navigator_new (GTK_TEXT_VIEW (view));
+          if (self->navigator)
+            g_object_ref_sink (self->navigator);
+        }
+    }
+
+  return self->navigator;
+}
diff --git a/src/plugins/spellcheck/gbp-spell-editor-page-addin.h 
b/src/plugins/spellcheck/gbp-spell-editor-page-addin.h
new file mode 100644
index 000000000..0ead81e51
--- /dev/null
+++ b/src/plugins/spellcheck/gbp-spell-editor-page-addin.h
@@ -0,0 +1,40 @@
+/* gbp-spell-editor-page-addin.h
+ *
+ * Copyright 2017-2019 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 <gspell/gspell.h>
+#include <libide-editor.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SPELL_EDITOR_PAGE_ADDIN (gbp_spell_editor_page_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSpellEditorPageAddin, gbp_spell_editor_page_addin, GBP, SPELL_EDITOR_PAGE_ADDIN, 
GObject)
+
+void             gbp_spell_editor_page_addin_begin_checking     (GbpSpellEditorPageAddin *self);
+void             gbp_spell_editor_page_addin_end_checking       (GbpSpellEditorPageAddin *self);
+GspellChecker   *gbp_spell_editor_page_addin_get_checker        (GbpSpellEditorPageAddin *self);
+GspellNavigator *gbp_spell_editor_page_addin_get_navigator      (GbpSpellEditorPageAddin *self);
+guint            gbp_spell_editor_page_addin_get_count          (GbpSpellEditorPageAddin *self,
+                                                                 const gchar             *word);
+gboolean         gbp_spell_editor_page_addin_move_to_word_start (GbpSpellEditorPageAddin *self);
+
+G_END_DECLS
diff --git a/src/plugins/spellcheck/gbp-spell-language-popover.c 
b/src/plugins/spellcheck/gbp-spell-language-popover.c
index cb9b84360..32a83bf69 100644
--- a/src/plugins/spellcheck/gbp-spell-language-popover.c
+++ b/src/plugins/spellcheck/gbp-spell-language-popover.c
@@ -17,14 +17,18 @@
  *
  * Adaptation of GspellLanguageChooserButton to show a popover
  * https://wiki.gnome.org/Projects/gspell
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include "gbp-spell-language-popover.h"
+#define G_LOG_DOMAIN "gbp-spell-language-popover"
+
+#include "config.h"
 
 #include <glib/gi18n.h>
+#include <libide-gui.h>
 
-#include "util/ide-gtk.h"
-#include "workbench/ide-workbench.h"
+#include "gbp-spell-language-popover.h"
 
 struct _GbpSpellLanguagePopover
 {
diff --git a/src/plugins/spellcheck/gbp-spell-language-popover.h 
b/src/plugins/spellcheck/gbp-spell-language-popover.h
index 4f30fcbf2..35458fbe6 100644
--- a/src/plugins/spellcheck/gbp-spell-language-popover.h
+++ b/src/plugins/spellcheck/gbp-spell-language-popover.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/spellcheck/gbp-spell-navigator.c b/src/plugins/spellcheck/gbp-spell-navigator.c
index ca380ede4..bf9bb80f9 100644
--- a/src/plugins/spellcheck/gbp-spell-navigator.c
+++ b/src/plugins/spellcheck/gbp-spell-navigator.c
@@ -17,10 +17,12 @@
 
  * This code is a modification of:
  * https://git.gnome.org/browse/gspell/tree/gspell/gspell-navigator-text-view.c
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-editor.h>
 
 #include "gbp-spell-buffer-addin.h"
 #include "gbp-spell-navigator.h"
@@ -84,7 +86,7 @@ get_misspelled_tag (GbpSpellNavigator *self)
   g_assert (self->buffer != NULL);
   g_assert (IDE_IS_BUFFER (self->buffer));
 
-  buffer_addin = ide_buffer_addin_find_by_module_name (IDE_BUFFER (self->buffer), "spellcheck-plugin");
+  buffer_addin = ide_buffer_addin_find_by_module_name (IDE_BUFFER (self->buffer), "spellcheck");
   if (buffer_addin != NULL)
     return gbp_spell_buffer_addin_get_misspelled_tag (GBP_SPELL_BUFFER_ADDIN (buffer_addin));
 
diff --git a/src/plugins/spellcheck/gbp-spell-navigator.h b/src/plugins/spellcheck/gbp-spell-navigator.h
index 4c32c75a1..1964e579f 100644
--- a/src/plugins/spellcheck/gbp-spell-navigator.h
+++ b/src/plugins/spellcheck/gbp-spell-navigator.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/spellcheck/gbp-spell-private.h b/src/plugins/spellcheck/gbp-spell-private.h
index 62b38a96c..15d844af5 100644
--- a/src/plugins/spellcheck/gbp-spell-private.h
+++ b/src/plugins/spellcheck/gbp-spell-private.h
@@ -1,7 +1,7 @@
 /* gbp-spell-widget-private.h
  *
  * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -15,17 +15,19 @@
  *
  * 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 <gspell/gspell.h>
-#include <ide.h>
+#include <libide-editor.h>
 
 #include "gbp-spell-dict.h"
 #include "gbp-spell-widget.h"
 #include "gbp-spell-editor-addin.h"
-#include "gbp-spell-editor-view-addin.h"
+#include "gbp-spell-editor-page-addin.h"
 
 G_BEGIN_DECLS
 
@@ -41,9 +43,9 @@ struct _GbpSpellWidget
   GtkBin                   parent_instance;
 
   /* Owned references */
-  IdeEditorView           *editor;
-  GbpSpellEditorViewAddin *editor_view_addin;
-  DzlSignalGroup          *editor_view_addin_signals;
+  IdeEditorPage           *editor;
+  GbpSpellEditorPageAddin *editor_page_addin;
+  DzlSignalGroup          *editor_page_addin_signals;
   GPtrArray               *words_array;
   GbpSpellDict            *dict;
 
@@ -91,8 +93,8 @@ void       _gbp_spell_widget_change         (GbpSpellWidget *self,
                                              gboolean        change_all);
 
 void       _gbp_spell_editor_addin_begin    (GbpSpellEditorAddin *self,
-                                             IdeEditorView       *view);
+                                             IdeEditorPage       *view);
 void       _gbp_spell_editor_addin_cancel   (GbpSpellEditorAddin *self,
-                                             IdeEditorView       *view);
+                                             IdeEditorPage       *view);
 
 G_END_DECLS
diff --git a/src/plugins/spellcheck/gbp-spell-utils.c b/src/plugins/spellcheck/gbp-spell-utils.c
index 7cb2f78e2..a074011bb 100644
--- a/src/plugins/spellcheck/gbp-spell-utils.c
+++ b/src/plugins/spellcheck/gbp-spell-utils.c
@@ -18,6 +18,8 @@
  * This code is mostly from:
  * https://git.gnome.org/browse/gspell/tree/gspell/gspell-utils.c
  * https://git.gnome.org/browse/gspell/tree/gspell/gspell-text-iter.c
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gbp-spell-utils"
diff --git a/src/plugins/spellcheck/gbp-spell-utils.h b/src/plugins/spellcheck/gbp-spell-utils.h
index 2690ec66d..98b647ded 100644
--- a/src/plugins/spellcheck/gbp-spell-utils.h
+++ b/src/plugins/spellcheck/gbp-spell-utils.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/spellcheck/gbp-spell-widget-actions.c 
b/src/plugins/spellcheck/gbp-spell-widget-actions.c
index 50d0e6222..16594cb48 100644
--- a/src/plugins/spellcheck/gbp-spell-widget-actions.c
+++ b/src/plugins/spellcheck/gbp-spell-widget-actions.c
@@ -1,7 +1,7 @@
 /* gbp-spell-widget-actions.c
  *
  * Copyright 2016 Sebastien Lafargue <slafargue gnome org>
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -15,6 +15,8 @@
  *
  * 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-spell-widget-actions"
@@ -72,12 +74,12 @@ gbp_spell_widget_actions_ignore_all (GSimpleAction *action,
   g_assert (G_IS_SIMPLE_ACTION (action));
   g_assert (GBP_IS_SPELL_WIDGET (self));
 
-  if (self->editor_view_addin != NULL)
+  if (self->editor_page_addin != NULL)
     {
       GspellChecker *checker;
       const gchar *word;
 
-      checker = gbp_spell_editor_view_addin_get_checker (self->editor_view_addin);
+      checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
       word = gtk_label_get_text (self->word_label);
 
       if (!dzl_str_empty0 (word))
@@ -134,19 +136,19 @@ _gbp_spell_widget_update_actions (GbpSpellWidget *self)
 
   g_return_if_fail (GBP_IS_SPELL_WIDGET (self));
 
-  if (IDE_IS_EDITOR_VIEW (self->editor) &&
-      GBP_IS_SPELL_EDITOR_VIEW_ADDIN (self->editor_view_addin) &&
+  if (IDE_IS_EDITOR_PAGE (self->editor) &&
+      GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self->editor_page_addin) &&
       self->spellchecking_status == TRUE)
     {
-      g_assert (IDE_IS_EDITOR_VIEW_ADDIN (self->editor_view_addin));
+      g_assert (IDE_IS_EDITOR_PAGE_ADDIN (self->editor_page_addin));
 
       can_change = TRUE;
       can_change_all = TRUE;
       can_move_next_word = TRUE;
 
-      if (self->editor_view_addin != NULL)
+      if (self->editor_page_addin != NULL)
         {
-          if (NULL != (navigator = gbp_spell_editor_view_addin_get_navigator (self->editor_view_addin)))
+          if (NULL != (navigator = gbp_spell_editor_page_addin_get_navigator (self->editor_page_addin)))
             word_counted = gbp_spell_navigator_get_is_words_counted (GBP_SPELL_NAVIGATOR (navigator));
         }
 
diff --git a/src/plugins/spellcheck/gbp-spell-widget.c b/src/plugins/spellcheck/gbp-spell-widget.c
index 6ab0c6e3a..117bec728 100644
--- a/src/plugins/spellcheck/gbp-spell-widget.c
+++ b/src/plugins/spellcheck/gbp-spell-widget.c
@@ -14,12 +14,14 @@
  *
  * 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-spell-widget"
 
 #include <dazzle.h>
-#include <ide.h>
+#include <libide-editor.h>
 #include <glib/gi18n.h>
 #include <gspell/gspell.h>
 
@@ -108,9 +110,9 @@ fill_suggestions_box (GbpSpellWidget  *self,
       return;
     }
 
-  if (self->editor_view_addin != NULL)
+  if (self->editor_page_addin != NULL)
     {
-      checker = gbp_spell_editor_view_addin_get_checker (self->editor_view_addin);
+      checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
       suggestions = gspell_checker_get_suggestions (checker, word, -1);
     }
 
@@ -145,10 +147,10 @@ update_count_label (GbpSpellWidget *self)
 
   g_assert (GBP_IS_SPELL_WIDGET (self));
 
-  if (self->editor_view_addin == NULL)
+  if (self->editor_page_addin == NULL)
     return;
 
-  navigator = gbp_spell_editor_view_addin_get_navigator (self->editor_view_addin);
+  navigator = gbp_spell_editor_page_addin_get_navigator (self->editor_page_addin);
   word = gtk_label_get_text (self->word_label);
   count = gbp_spell_navigator_get_count (GBP_SPELL_NAVIGATOR (navigator), word);
 
@@ -184,10 +186,10 @@ _gbp_spell_widget_move_next_word (GbpSpellWidget *self)
 
   g_assert (GBP_IS_SPELL_WIDGET (self));
 
-  if (self->editor_view_addin == NULL)
+  if (self->editor_page_addin == NULL)
     return FALSE;
 
-  navigator = gbp_spell_editor_view_addin_get_navigator (self->editor_view_addin);
+  navigator = gbp_spell_editor_page_addin_get_navigator (self->editor_page_addin);
 
   if ((ret = gspell_navigator_goto_next (navigator, &word, NULL, &error)))
     {
@@ -228,9 +230,9 @@ check_word_timeout_cb (GbpSpellWidget *self)
   gboolean ret = TRUE;
 
   g_assert (GBP_IS_SPELL_WIDGET (self));
-  g_assert (self->editor_view_addin != NULL);
+  g_assert (self->editor_page_addin != NULL);
 
-  checker = gbp_spell_editor_view_addin_get_checker (self->editor_view_addin);
+  checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
 
   self->check_word_state = CHECK_WORD_CHECKING;
 
@@ -308,7 +310,7 @@ gbp_spell_widget__word_entry_changed_cb (GbpSpellWidget *self,
 
   dzl_clear_source (&self->check_word_timeout_id);
 
-  if (self->editor_view_addin != NULL)
+  if (self->editor_page_addin != NULL)
     {
       self->check_word_timeout_id = g_timeout_add_full (G_PRIORITY_LOW,
                                                         CHECK_WORD_INTERVAL_MIN,
@@ -408,14 +410,14 @@ dict_check_word_timeout_cb (GbpSpellWidget *self)
 
   g_assert (GBP_IS_SPELL_WIDGET (self));
 
-  if (self->editor_view_addin == NULL)
+  if (self->editor_page_addin == NULL)
     {
       /* lost our chance */
       self->dict_check_word_timeout_id = 0;
       return G_SOURCE_REMOVE;
     }
 
-  checker = gbp_spell_editor_view_addin_get_checker (self->editor_view_addin);
+  checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
 
   self->dict_check_word_state = CHECK_WORD_CHECKING;
 
@@ -609,7 +611,7 @@ check_dict_available (GbpSpellWidget *self)
 {
   g_assert (GBP_IS_SPELL_WIDGET (self));
 
-  return (self->editor_view_addin != NULL && self->language != NULL);
+  return (self->editor_page_addin != NULL && self->language != NULL);
 }
 
 static void
@@ -698,11 +700,11 @@ gbp_spell_widget__language_notify_cb (GbpSpellWidget *self,
   g_assert (GBP_IS_SPELL_WIDGET (self));
   g_assert (GTK_IS_BUTTON (language_chooser_button));
 
-  if (self->editor_view_addin == NULL)
+  if (self->editor_page_addin == NULL)
     return;
 
-  checker = gbp_spell_editor_view_addin_get_checker (self->editor_view_addin);
-  navigator = gbp_spell_editor_view_addin_get_navigator (self->editor_view_addin);
+  checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
+  navigator = gbp_spell_editor_page_addin_get_navigator (self->editor_page_addin);
 
   current_language = gspell_checker_get_language (checker);
   spell_language = gspell_language_chooser_get_language (GSPELL_LANGUAGE_CHOOSER (language_chooser_button));
@@ -771,10 +773,10 @@ gbp_spell_widget__populate_popup_cb (GbpSpellWidget *self,
   g_assert (GTK_IS_WIDGET (popup));
   g_assert (GTK_IS_ENTRY (entry));
 
-  if (self->editor_view_addin == NULL)
+  if (self->editor_page_addin == NULL)
     return;
 
-  checker = gbp_spell_editor_view_addin_get_checker (self->editor_view_addin);
+  checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
   text = gtk_entry_get_text (entry);
 
   if (!self->is_word_entry_valid && !dzl_str_empty0 (text))
@@ -920,21 +922,21 @@ gbp_spell_widget_constructed (GObject *object)
 
 static void
 gbp_spell_widget_bind_addin (GbpSpellWidget          *self,
-                             GbpSpellEditorViewAddin *editor_view_addin,
-                             DzlSignalGroup          *editor_view_addin_signals)
+                             GbpSpellEditorPageAddin *editor_page_addin,
+                             DzlSignalGroup          *editor_page_addin_signals)
 {
   GspellChecker *checker;
 
   g_assert (GBP_IS_SPELL_WIDGET (self));
-  g_assert (GBP_IS_SPELL_EDITOR_VIEW_ADDIN (editor_view_addin));
-  g_assert (DZL_IS_SIGNAL_GROUP (editor_view_addin_signals));
-  g_assert (self->editor_view_addin == NULL);
+  g_assert (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (editor_page_addin));
+  g_assert (DZL_IS_SIGNAL_GROUP (editor_page_addin_signals));
+  g_assert (self->editor_page_addin == NULL);
 
-  self->editor_view_addin = g_object_ref (editor_view_addin);
+  self->editor_page_addin = g_object_ref (editor_page_addin);
 
-  gbp_spell_editor_view_addin_begin_checking (editor_view_addin);
+  gbp_spell_editor_page_addin_begin_checking (editor_page_addin);
 
-  checker = gbp_spell_editor_view_addin_get_checker (editor_view_addin);
+  checker = gbp_spell_editor_page_addin_get_checker (editor_page_addin);
   gbp_spell_dict_set_checker (self->dict, checker);
 
   self->language = gspell_checker_get_language (checker);
@@ -947,19 +949,19 @@ gbp_spell_widget_bind_addin (GbpSpellWidget          *self,
 
 static void
 gbp_spell_widget_unbind_addin (GbpSpellWidget *self,
-                               DzlSignalGroup *editor_view_addin_signals)
+                               DzlSignalGroup *editor_page_addin_signals)
 {
   g_assert (GBP_IS_SPELL_WIDGET (self));
-  g_assert (DZL_IS_SIGNAL_GROUP (editor_view_addin_signals));
+  g_assert (DZL_IS_SIGNAL_GROUP (editor_page_addin_signals));
 
-  if (self->editor_view_addin != NULL)
+  if (self->editor_page_addin != NULL)
     {
-      gbp_spell_editor_view_addin_end_checking (self->editor_view_addin);
+      gbp_spell_editor_page_addin_end_checking (self->editor_page_addin);
       gbp_spell_dict_set_checker (self->dict, NULL);
       self->language = NULL;
       gspell_language_chooser_set_language (GSPELL_LANGUAGE_CHOOSER (self->language_chooser_button), NULL);
 
-      g_clear_object (&self->editor_view_addin);
+      g_clear_object (&self->editor_page_addin);
 
       _gbp_spell_widget_update_actions (self);
     }
@@ -982,8 +984,8 @@ gbp_spell_widget_destroy (GtkWidget *widget)
 
   /* Ensure reference holding things are released */
   g_clear_object (&self->editor);
-  g_clear_object (&self->editor_view_addin);
-  g_clear_object (&self->editor_view_addin_signals);
+  g_clear_object (&self->editor_page_addin);
+  g_clear_object (&self->editor_page_addin_signals);
   g_clear_object (&self->dict);
   g_clear_pointer (&self->words_array, g_ptr_array_unref);
 
@@ -1042,12 +1044,12 @@ gbp_spell_widget_class_init (GbpSpellWidgetClass *klass)
 
   properties [PROP_EDITOR] =
     g_param_spec_object ("editor", NULL, NULL,
-                         IDE_TYPE_EDITOR_VIEW,
+                         IDE_TYPE_EDITOR_PAGE,
                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/spellcheck-plugin/gbp-spell-widget.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/spellcheck/gbp-spell-widget.ui");
 
   gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, word_label);
   gtk_widget_class_bind_template_child (widget_class, GbpSpellWidget, count_label);
@@ -1075,14 +1077,14 @@ gbp_spell_widget_init (GbpSpellWidget *self)
                             G_CALLBACK (dict_row_key_pressed_event_cb),
                             self);
 
-  self->editor_view_addin_signals = dzl_signal_group_new (GBP_TYPE_SPELL_EDITOR_VIEW_ADDIN);
+  self->editor_page_addin_signals = dzl_signal_group_new (GBP_TYPE_SPELL_EDITOR_PAGE_ADDIN);
 
-  g_signal_connect_swapped (self->editor_view_addin_signals,
+  g_signal_connect_swapped (self->editor_page_addin_signals,
                             "bind",
                             G_CALLBACK (gbp_spell_widget_bind_addin),
                             self);
 
-  g_signal_connect_swapped (self->editor_view_addin_signals,
+  g_signal_connect_swapped (self->editor_page_addin_signals,
                             "unbind",
                             G_CALLBACK (gbp_spell_widget_unbind_addin),
                             self);
@@ -1094,11 +1096,11 @@ gbp_spell_widget_init (GbpSpellWidget *self)
  *
  * Gets the editor that is currently being spellchecked.
  *
- * Returns: (nullable) (transfer none): An #IdeEditorView or %NULL
+ * Returns: (nullable) (transfer none): An #IdeEditorPage or %NULL
  *
  * Since: 3.26
  */
-IdeEditorView *
+IdeEditorPage *
 gbp_spell_widget_get_editor (GbpSpellWidget *self)
 {
   g_return_val_if_fail (GBP_IS_SPELL_WIDGET (self), NULL);
@@ -1108,21 +1110,21 @@ gbp_spell_widget_get_editor (GbpSpellWidget *self)
 
 void
 gbp_spell_widget_set_editor (GbpSpellWidget *self,
-                             IdeEditorView  *editor)
+                             IdeEditorPage  *editor)
 {
   GspellNavigator *navigator;
 
   g_return_if_fail (GBP_IS_SPELL_WIDGET (self));
-  g_return_if_fail (!editor || IDE_IS_EDITOR_VIEW (editor));
+  g_return_if_fail (!editor || IDE_IS_EDITOR_PAGE (editor));
 
   if (g_set_object (&self->editor, editor))
     {
-      IdeEditorViewAddin *addin = NULL;
+      IdeEditorPageAddin *addin = NULL;
 
       if (editor != NULL)
         {
-          addin = ide_editor_view_addin_find_by_module_name (editor, "spellcheck-plugin");
-          navigator = gbp_spell_editor_view_addin_get_navigator (GBP_SPELL_EDITOR_VIEW_ADDIN (addin));
+          addin = ide_editor_page_addin_find_by_module_name (editor, "spellcheck");
+          navigator = gbp_spell_editor_page_addin_get_navigator (GBP_SPELL_EDITOR_PAGE_ADDIN (addin));
           g_signal_connect_object (navigator,
                                    "notify::words-counted",
                                    G_CALLBACK (gbp_spell_widget__words_counted_cb),
@@ -1130,16 +1132,16 @@ gbp_spell_widget_set_editor (GbpSpellWidget *self,
                                    G_CONNECT_SWAPPED);
         }
 
-      dzl_signal_group_set_target (self->editor_view_addin_signals, addin);
+      dzl_signal_group_set_target (self->editor_page_addin_signals, addin);
 
       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_EDITOR]);
     }
 }
 
 GtkWidget *
-gbp_spell_widget_new (IdeEditorView *editor)
+gbp_spell_widget_new (IdeEditorPage *editor)
 {
-  g_return_val_if_fail (!editor || IDE_IS_EDITOR_VIEW (editor), NULL);
+  g_return_val_if_fail (!editor || IDE_IS_EDITOR_PAGE (editor), NULL);
 
   return g_object_new (GBP_TYPE_SPELL_WIDGET,
                        "editor", editor,
@@ -1156,10 +1158,10 @@ _gbp_spell_widget_change (GbpSpellWidget *self,
   const gchar *word;
 
   g_assert (GBP_IS_SPELL_WIDGET (self));
-  g_assert (IDE_IS_EDITOR_VIEW (self->editor));
-  g_assert (GBP_IS_SPELL_EDITOR_VIEW_ADDIN (self->editor_view_addin));
+  g_assert (IDE_IS_EDITOR_PAGE (self->editor));
+  g_assert (GBP_IS_SPELL_EDITOR_PAGE_ADDIN (self->editor_page_addin));
 
-  checker = gbp_spell_editor_view_addin_get_checker (self->editor_view_addin);
+  checker = gbp_spell_editor_page_addin_get_checker (self->editor_page_addin);
   g_assert (GSPELL_IS_CHECKER (checker));
 
   word = gtk_label_get_text (self->word_label);
@@ -1168,7 +1170,7 @@ _gbp_spell_widget_change (GbpSpellWidget *self,
   change_to = g_strdup (gtk_entry_get_text (self->word_entry));
   g_assert (!dzl_str_empty0 (change_to));
 
-  navigator = gbp_spell_editor_view_addin_get_navigator (self->editor_view_addin);
+  navigator = gbp_spell_editor_page_addin_get_navigator (self->editor_page_addin);
   g_assert (navigator != NULL);
 
   gspell_checker_set_correction (checker, word, -1, change_to, -1);
diff --git a/src/plugins/spellcheck/gbp-spell-widget.h b/src/plugins/spellcheck/gbp-spell-widget.h
index 86c9f9d36..74372159c 100644
--- a/src/plugins/spellcheck/gbp-spell-widget.h
+++ b/src/plugins/spellcheck/gbp-spell-widget.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-editor.h>
 
 G_BEGIN_DECLS
 
@@ -26,9 +28,9 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpSpellWidget, gbp_spell_widget, GBP, SPELL_WIDGET, GtkBin)
 
-GtkWidget     *gbp_spell_widget_new        (IdeEditorView  *editor);
-IdeEditorView *gbp_spell_widget_get_editor (GbpSpellWidget *self);
+GtkWidget     *gbp_spell_widget_new        (IdeEditorPage  *editor);
+IdeEditorPage *gbp_spell_widget_get_editor (GbpSpellWidget *self);
 void           gbp_spell_widget_set_editor (GbpSpellWidget *self,
-                                            IdeEditorView  *editor);
+                                            IdeEditorPage  *editor);
 
 G_END_DECLS
diff --git a/src/plugins/spellcheck/meson.build b/src/plugins/spellcheck/meson.build
index 4cf50e130..659d12621 100644
--- a/src/plugins/spellcheck/meson.build
+++ b/src/plugins/spellcheck/meson.build
@@ -1,38 +1,29 @@
-if get_option('with_spellcheck')
+if get_option('plugin_spellcheck')
 
-spellcheck_resources = gnome.compile_resources(
-  'gbp-spell-resources',
-  'spellcheck.gresource.xml',
-  c_name: 'gbp_spell',
-)
-
-spellcheck_sources = [
-  'spellcheck-plugin.c',
+plugins_sources += files([
   'gbp-spell-buffer-addin.c',
-  'gbp-spell-buffer-addin.h',
   'gbp-spell-dict.c',
-  'gbp-spell-dict.h',
   'gbp-spell-editor-addin.c',
-  'gbp-spell-editor-addin.h',
-  'gbp-spell-editor-view-addin.c',
-  'gbp-spell-editor-view-addin.h',
+  'gbp-spell-editor-page-addin.c',
   'gbp-spell-language-popover.c',
-  'gbp-spell-language-popover.h',
   'gbp-spell-navigator.c',
-  'gbp-spell-navigator.h',
   'gbp-spell-utils.c',
-  'gbp-spell-utils.h',
-  'gbp-spell-widget.c',
-  'gbp-spell-widget.h',
   'gbp-spell-widget-actions.c',
-]
+  'gbp-spell-widget.c',
+  'spellcheck-plugin.c',
+])
+
+plugin_spellcheck_resources = gnome.compile_resources(
+  'spellcheck-resources',
+  'spellcheck.gresource.xml',
+  c_name: 'gbp_spellcheck',
+)
 
-gnome_builder_plugins_deps += [
+plugins_deps += [
   dependency('gspell-1', version: '>= 1.2.0'),
   dependency('enchant-2'),
 ]
 
-gnome_builder_plugins_sources += files(spellcheck_sources)
-gnome_builder_plugins_sources += spellcheck_resources[0]
+plugins_sources += plugin_spellcheck_resources[0]
 
 endif
diff --git a/src/plugins/spellcheck/spellcheck-plugin.c b/src/plugins/spellcheck/spellcheck-plugin.c
index c808d545f..95247f9bc 100644
--- a/src/plugins/spellcheck/spellcheck-plugin.c
+++ b/src/plugins/spellcheck/spellcheck-plugin.c
@@ -1,6 +1,6 @@
 /* spellcheck-plugin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,19 +14,29 @@
  *
  * 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
  */
 
+#include "config.h"
+
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-editor.h>
 
 #include "gbp-spell-buffer-addin.h"
 #include "gbp-spell-editor-addin.h"
-#include "gbp-spell-editor-view-addin.h"
+#include "gbp-spell-editor-page-addin.h"
 
-void
-gbp_spellcheck_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_gbp_spellcheck_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module, IDE_TYPE_BUFFER_ADDIN, GBP_TYPE_SPELL_BUFFER_ADDIN);
-  peas_object_module_register_extension_type (module, IDE_TYPE_EDITOR_ADDIN, GBP_TYPE_SPELL_EDITOR_ADDIN);
-  peas_object_module_register_extension_type (module, IDE_TYPE_EDITOR_VIEW_ADDIN, 
GBP_TYPE_SPELL_EDITOR_VIEW_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUFFER_ADDIN,
+                                              GBP_TYPE_SPELL_BUFFER_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_ADDIN,
+                                              GBP_TYPE_SPELL_EDITOR_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                              GBP_TYPE_SPELL_EDITOR_PAGE_ADDIN);
 }
diff --git a/src/plugins/spellcheck/spellcheck.gresource.xml b/src/plugins/spellcheck/spellcheck.gresource.xml
index 7ca7d4ba2..26aa92b65 100644
--- a/src/plugins/spellcheck/spellcheck.gresource.xml
+++ b/src/plugins/spellcheck/spellcheck.gresource.xml
@@ -1,11 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/spellcheck">
     <file>spellcheck.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/spellcheck-plugin">
-    <file compressed="true" preprocess="xml-stripblanks">gtk/menus.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks">gbp-spell-widget.ui</file>
-    <file compressed="true">themes/shared.css</file>
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+    <file preprocess="xml-stripblanks">gbp-spell-widget.ui</file>
+    <file>themes/shared.css</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/spellcheck/spellcheck.plugin b/src/plugins/spellcheck/spellcheck.plugin
index 6ab54b75a..54a0d7a31 100644
--- a/src/plugins/spellcheck/spellcheck.plugin
+++ b/src/plugins/spellcheck/spellcheck.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=spellcheck-plugin
-Name=Spellcheck
-Description=Provides spellchecking for documents
 Authors=Sébastien Lafargue <slafargue gnome org>
-Copyright=Copyright © 2016 Sébastien Lafargue
-Depends=editor
 Builtin=true
-Embedded=gbp_spellcheck_register_types
+Copyright=Copyright © 2016 Sébastien Lafargue
+Depends=editor;
+Description=Provides spellchecking for documents
+Embedded=_gbp_spellcheck_register_types
+Hidden=true
+Module=spellcheck
+Name=Spellcheck
diff --git a/src/plugins/sublime/gbp-sublime-preferences-addin.c 
b/src/plugins/sublime/gbp-sublime-preferences-addin.c
new file mode 100644
index 000000000..c99435ae7
--- /dev/null
+++ b/src/plugins/sublime/gbp-sublime-preferences-addin.c
@@ -0,0 +1,89 @@
+/* gbp-sublime-preferences-addin.c
+ *
+ * Copyright 2018-2019 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-sublime-preferences-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+
+#include "gbp-sublime-preferences-addin.h"
+
+struct _GbpSublimePreferencesAddin
+{
+  GObject parent_instance;
+  guint   keybinding_id;
+};
+
+static void
+gbp_sublime_preferences_addin_load (IdePreferencesAddin *addin,
+                                    DzlPreferences      *preferences)
+{
+  GbpSublimePreferencesAddin *self = (GbpSublimePreferencesAddin *)addin;
+
+  g_assert (GBP_IS_SUBLIME_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  self->keybinding_id = dzl_preferences_add_radio (preferences,
+                                                   "keyboard",
+                                                   "mode",
+                                                   "org.gnome.builder.editor",
+                                                   "keybindings",
+                                                   NULL,
+                                                   "\"sublime\"",
+                                                   _("Sublime Text"),
+                                                   _("Emulates the Sublime Text editor"),
+                                                   NULL,
+                                                   20);
+}
+
+static void
+gbp_sublime_preferences_addin_unload (IdePreferencesAddin *addin,
+                                      DzlPreferences      *preferences)
+{
+  GbpSublimePreferencesAddin *self = (GbpSublimePreferencesAddin *)addin;
+
+  g_assert (GBP_IS_SUBLIME_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  dzl_preferences_remove_id (preferences, self->keybinding_id);
+}
+
+static void
+preferences_addin_iface_init (IdePreferencesAddinInterface *iface)
+{
+  iface->load = gbp_sublime_preferences_addin_load;
+  iface->unload = gbp_sublime_preferences_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpSublimePreferencesAddin, gbp_sublime_preferences_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_PREFERENCES_ADDIN,
+                                                preferences_addin_iface_init))
+
+static void
+gbp_sublime_preferences_addin_class_init (GbpSublimePreferencesAddinClass *klass)
+{
+}
+
+static void
+gbp_sublime_preferences_addin_init (GbpSublimePreferencesAddin *self)
+{
+}
diff --git a/src/plugins/sublime/gbp-sublime-preferences-addin.h 
b/src/plugins/sublime/gbp-sublime-preferences-addin.h
new file mode 100644
index 000000000..68dea4975
--- /dev/null
+++ b/src/plugins/sublime/gbp-sublime-preferences-addin.h
@@ -0,0 +1,31 @@
+/* gbp-sublime-preferences-addin.h
+ *
+ * Copyright 2018-2019 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_SUBLIME_PREFERENCES_ADDIN (gbp_sublime_preferences_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSublimePreferencesAddin, gbp_sublime_preferences_addin, GBP, 
SUBLIME_PREFERENCES_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/sublime/keybindings/sublime.css b/src/plugins/sublime/keybindings/sublime.css
new file mode 100644
index 000000000..dc33250ee
--- /dev/null
+++ b/src/plugins/sublime/keybindings/sublime.css
@@ -0,0 +1,314 @@
+@import url("resource:///org/gnome/builder/keybindings/shared.css");
+
+@binding-set sublime-ide-source-view
+{
+  bind "<ctrl>n" { "action" ("editor", "new-file", "") };
+  bind "<ctrl>o" { "action" ("win", "open-with-dialog", "") };
+  bind "<ctrl>s" { "action" ("editor-page", "save", "") };
+  bind "<ctrl><shift>s" { "action" ("editor-page", "save-as", "") };
+  bind "<ctrl>w" { "action" ("frame", "close-page", "") };
+  bind "<ctrl>q" { "action" ("app", "quit", "") };
+
+  bind "<ctrl>z" { "clear-count" ()
+                   "clear-selection" ()
+                   "remove-cursors" ()
+                   "undo" () };
+  bind "<ctrl><shift>z" { "clear-count" ()
+                          "clear-selection" ()
+                          "remove-cursors" ()
+                          "redo" () };
+
+  bind "<ctrl>v" { "paste-clipboard-extended" (1, 0, 0) };
+  bind "<alt>slash" { "cycle-completion" (down) };
+
+  /* This should be F9, but that is hardcoded to hide the sidebar instead */
+  bind "<alt>F9" { "sort" (1, 0) };
+  bind "<shift>F9" { "sort" (0, 0) };
+
+  bind "<ctrl><alt>i" { "reindent" () };
+  bind "<ctrl><shift>Up" { "move-lines" (0, -1) };
+  bind "<ctrl><shift>Down" { "move-lines" (0, 1) };
+  bind "<ctrl><shift>d" { "duplicate-entire-line" () };
+  bind "<ctrl><shift>k" { "delete-from-cursor" (paragraphs, 1) };
+  bind "<ctrl>j" { "join-lines" () };
+
+  /* Insert line above */
+  bind "<ctrl><shift>Return" { "begin-user-action" ()
+                               "movement" (first-char, 0, 0, 0)
+                               "insert-at-cursor" ("\n")
+                               "move-cursor" (display-lines, -1, 0)
+                               "reindent" ()
+                               "end-user-action" () };
+  bind "<ctrl><shift>KP_Enter" { "begin-user-action" ()
+                                 "movement" (first-char, 0, 0, 0)
+                                 "insert-at-cursor" ("\n")
+                                 "move-cursor" (display-lines, -1, 0)
+                                 "reindent" ()
+                                 "end-user-action" () };
+  /* Insert line below. This should be Ctrl+Enter but that is hardcoded to
+   * open the command bar. So we add Alt+Enter instead, but also accept
+   * Ctrl+Enter on the keypad */
+  bind "<alt>Return" { "begin-user-action" ()
+                        "movement" (line-end, 0, 1, 0)
+                        "insert-at-cursor" ("\n")
+                        "reindent" ()
+                        "end-user-action" () };
+  bind "<alt>KP_Enter" { "begin-user-action" ()
+                         "movement" (line-end, 0, 1, 0)
+                         "insert-at-cursor" ("\n")
+                         "reindent" ()
+                         "end-user-action" () };
+  bind "<ctrl>KP_Enter" { "begin-user-action" ()
+                          "movement" (line-end, 0, 1, 0)
+                          "insert-at-cursor" ("\n")
+                          "reindent" ()
+                          "end-user-action" () };
+
+  bind "<ctrl>Delete" { "delete-from-cursor" (words, 1) };
+  bind "<ctrl>KP_Delete" { "delete-from-cursor" (words, 1) };
+  bind "<ctrl>BackSpace" { "delete-from-cursor" (words, -1) };
+  bind "<ctrl>t" { "move-words" (1) };
+
+  bind "<alt><shift>Down" { "add-cursor" (column) };
+  bind "Escape" { "reset" () };
+  bind "<ctrl>a" { "select-all" (1) };
+  /* Expand selection to brackets: this should be Ctrl+Shift+M, but that's
+   * hardcoded to uncomment the current line */
+  bind "<alt><shift>m" { "movement" (match-special, 1, 0, 0) };
+  bind "<ctrl><shift>a" { "select-tag" (1) };
+
+  bind "<ctrl>f" { "action" ("editor-page", "find", "") };
+  /* Make "Incremental search" and "Use selection for search" do the same thing
+   * as Ctrl+F, in Builder they are all the same anyway */
+  bind "<ctrl>i" { "action" ("editor-page", "find", "") };
+  bind "<ctrl>e" { "action" ("editor-page", "find", "") };
+  bind "F3" { "action" ("editor-page", "move-next-search-result", "") };
+  bind "<shift>F3" { "action" ("editor-page", "move-previous-search-result", "") };
+  bind "<ctrl>h" { "action" ("editor-page", "find-replace", "") };
+  bind "<ctrl><shift>e" { "action" ("editor-page", "find-replace", "") };
+
+  bind "<ctrl>quoteleft" { "action" ("dockbin", "bottom-visible", "") };
+  bind "<shift>F11" { "action" ("win", "fullscreen", "") };
+  bind "F6" { "action" ("spellcheck", "spellcheck", "") };
+
+  bind "<alt>exclam" { "action" ("frame", "split-page", "''") };
+  bind "<alt><shift>KP_1" { "action" ("frame", "split-page", "''") };
+
+  bind "<ctrl>r" { "action" ("symbol-tree", "search", "") };
+  bind "F12" { "goto-definition" () };
+  bind "<ctrl>g" { "action" ("editor-page", "goto-line", "") };
+  bind "<alt>minus" { "action" ("history", "move-previous-edit", "") };
+  bind "<alt>underscore" { "action" ("history", "move-next-edit", "") };
+  /* Goto matching bracket; should be Ctrl+M but that is hardcoded to comment
+   * out the current line */
+  bind "<alt>m" { "movement" (match-special, 0, 1, 1) };
+  bind "<super><shift>h" { "clear-selection" ()
+                           "movement" (previous-word-end, 0, 1, 1)
+                           "movement" (next-word-start, 0, 1, 0)
+                           "movement" (next-word-end, 1, 0, 1)
+                           "request-documentation" ()
+                           "clear-count" ()
+                           "clear-selection" () };
+  bind "<ctrl><super>e" { "move-error" (down) };
+  bind "<ctrl><super><shift>e" { "move-error" (up) };
+
+  bind "<ctrl>Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl>KP_Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl>Page_Down" { "action" ("frame", "next-page", "") };
+  bind "<ctrl>KP_Page_Down" { "action" ("frame", "next-page", "") };
+  bind "<ctrl>Tab" { "action" ("frame", "next-page", "") };
+  bind "<ctrl><shift>Tab" { "action" ("frame", "previous-page", "") };
+  bind "<alt>o" { "action" ("win", "find-other-file", "") };
+
+  bind "<ctrl>Up" { "movement" (screen-up, 0, 0, 0) };
+  bind "<ctrl>KP_Up" { "movement" (screen-up, 0, 0, 0) };
+  bind "<ctrl>Down" { "movement" (screen-down, 0, 0, 0) };
+  bind "<ctrl>KP_Down" { "movement" (screen-down, 0, 0, 0) };
+
+  bind "<ctrl><shift>p" { "action" ("win", "show-command-bar", "") };
+  bind "<ctrl>b" { "action" ("build-manager", "build", "") };
+  /* Ctrl+Shift+B is Build with Build System in Sublime Text; Builder doesn't
+   * have this concept. Instead you configure the build system in the Build
+   * Preferences perspective, so let's have that keybinding take us there. */
+  bind "<ctrl><shift>b" { "action" ("buildui", "configure", "''") };
+
+  /* Sublime has a "Toggle macro recording" key. Builder has begin/end
+   * recording, so we add an extra shortcut for end */
+  bind "<ctrl><alt>q" { "begin-macro" () };
+  bind "<ctrl><super>q" { "end-macro" () };
+  bind "<ctrl><alt><shift>q" { "replay-macro" (1) };
+
+  bind "<ctrl>minus" { "decrease-font-size" () };
+  bind "<ctrl>KP_Subtract" { "decrease-font-size" () };
+  bind "<ctrl>plus" { "increase-font-size" () };
+  bind "<ctrl>equal" { "increase-font-size" () };
+  bind "<ctrl>KP_Add" { "increase-font-size" () };
+
+  /* These don't originally have bindings in Sublime */
+  bind "<ctrl>k" { "action" ("frame", "show-list", "") };
+  bind "<ctrl>0" { "reset-font-size" () };
+  bind "<ctrl>KP_0" { "reset-font-size" () };
+
+  /* Not in Sublime Text by default, but nice macros */
+  bind "<ctrl>semicolon" { "save-insert-mark" ()
+                           "movement" (line-end, 0, 1, 0)
+                           "insert-at-cursor" (";")
+                           "restore-insert-mark" () };
+  bind "<ctrl>comma" { "save-insert-mark" ()
+                       "movement" (line-end, 0, 1, 0)
+                       "insert-at-cursor" (",")
+                       "restore-insert-mark" () };
+  /* Add back emoji with a different shortcut */
+  bind "<ctrl>colon" { "insert-emoji" () };
+
+  /* Not bound:
+   *   Ctrl+Shift+Space Expand selection to scope; Highlighting scope is a
+   *     concept that doesn't exist in Builder
+   *   Ctrl+Alt+Shift+P Show scope name; ditto
+   *   F4, Shift+F4 Previous/next search result in results panel; Builder
+   *     doesn't have a results panel
+   *   Ctrl+1, Ctrl+Shift+1, etc; Builder doesn't have multiple frames
+   *   Alt+Shift+2, etc; Builder can open vertical splits and close them, which
+   *     is already plenty of flexibility for splitting windows
+   *
+   * Not yet bound, but probably could be with some extra code:
+   *   Ctrl+Shift+T Reopen last closed file
+   *   Ctrl+Shift+V Paste and indent; needs extra parameter to
+   *     paste-clipboard-extended, to select pasted text so we can reindent it
+   *   Ctrl+Shift+J Expand selection to indentation; selects every contiguous
+   *     line at the current indendation level or deeper. Needs movement type
+   *   Alt+1, Alt+2, etc. Go to position in stack; needs a goto-page action in
+   *     ide-frame-actions.c
+   *   Alt+. Close Tag; nice to have for editing HTML
+   *   Alt+Shift+W Wrap Selection with Tag; inserts <p> and </p> around the
+   *     selection, and selects both "p"'s
+   *   Ctrl+Alt+Shift+K Goto Previous Git Difference
+   *   Ctrl+Shift+F Find in Files, Ctrl+Shift+R Goto Symbol in Project; Builder
+   *     doesn't have global search yet
+   *   Ctrl+F2 Toggle Bookmark, etc; GtkSourceView doesn't have actions for this
+   *   Ctrl+/, Ctrl+Shift+/ Toggle Line/Block Comment; comment/uncomment seems
+   *     to be hardcoded to Ctrl+M and Ctrl+Shift+M
+   *   Ctrl+Shift+L Split Selection into Lines
+   *   Alt+Shift+Up/Down Add Cursor on Previous/Next Line; currently
+   *     add_cursor(column) only works if there's a selection.
+   *   Ctrl+F3 Quick Find; expands selection to current word if no selection,
+   *     and jumps immediately to next match
+   *   Alt+F3 Quick Find All; expands selection to current word if no selection,
+   *     and selects all matches
+   *   F4 Build Results; needs an action to go to the Build Issues pane of the
+   *     sidebar
+   *   Alt+Q Wrap Paragraph At Ruler; needs an action for this
+   *
+   * Known issues:
+   *   Ctrl+D and Ctrl+L: The transition to the 'has-selection' state doesn't
+   *     happen when the selection is initiated from a keybinding, so pressing
+   *     Ctrl+D twice doesn't select a word and its next occurrence, and
+   *     likewise pressing Ctrl+L twice doesn't select two lines.
+   *   Ctrl+[, Ctrl+] Indent and Unindent; These should preserve the place of
+   *     the insert mark relative to the text, instead of the line offset.
+   *   Some shortcuts (Ctrl+Enter, Ctrl+M, Ctrl+Shift+M, Ctrl+Shift+U, F9)
+   *     apparently can't be overridden.
+   *   Ctrl+Alt+Q doesn't toggle macro recording. Instead, use Ctrl+Super+Q to
+   *     stop recording.
+   *   Ctrl+Alt+P, Switch Project, doesn't work when the editor page is focused.
+   *   Previous Error and Next Error were originally bound to Ctrl+K, P and
+   *     Ctrl+K, N on SublimeLinter for Linux. We use the shortcuts from macOS
+   *     in order to avoid a separate Ctrl+K state.
+   */
+}
+
+/* Keys that work differently depending on whether there is text selected */
+@binding-set sublime-ide-source-view-no-selection
+{
+  /* Ctrl+C without anything selected copies the entire line incl. newline */
+  bind "<ctrl>c" { "save-insert-mark" ()
+                   "movement" (first-char, 0, 0, 0)
+                   "movement" (line-end, 1, 0, 0)
+                   "copy-clipboard-extended" ()
+                   "clear-selection" ()
+                   "restore-insert-mark" () };
+  /* Ditto Ctrl+X for cut */
+  bind "<ctrl>x" { "movement" (first-char, 0, 0, 0)
+                   "movement" (line-end, 1, 0, 0)
+                   "copy-clipboard-extended" ()
+                   "delete-selection" () };
+  /* Expand selection to word - is Quick Add Next when there's a selection */
+  bind "<ctrl>d" { "movement" (previous-word-end, 0, 1, 1)
+                   "movement" (next-word-start, 0, 1, 0)
+                   "movement" (next-word-end, 1, 0, 1) };
+  bind "<ctrl>bracketleft" { "save-insert-mark" ()
+                             "movement" (first-char, 0, 1, 0)
+                             "movement" (line-end, 1, 1, 0)
+                             "indent-selection" (-1)
+                             "clear-selection" ()
+                             "restore-insert-mark" () };
+  bind "<ctrl>bracketright" { "save-insert-mark" ()
+                              "movement" (first-char, 0, 1, 0)
+                              "movement" (line-end, 1, 1, 0)
+                              "indent-selection" (1)
+                              "clear-selection" ()
+                              "restore-insert-mark" () };
+  /* Expand selection to line */
+  bind "<ctrl>l" { "movement" (first-char, 0, 0, 0)
+                   "movement" (line-end, 1, 0, 0) };
+
+  /* These don't originally have bindings in Sublime */
+  bind "<ctrl>u" { "save-insert-mark" ()
+                   "movement" (previous-word-end, 0, 1, 1)
+                   "movement" (next-word-start, 0, 1, 0)
+                   "movement" (next-word-end, 1, 0, 1)
+                   "change-case" (upper)
+                   "restore-insert-mark" () };
+  bind "<alt>u" { "save-insert-mark" ()
+                  "movement" (previous-word-end, 0, 1, 1)
+                  "movement" (next-word-start, 0, 1, 0)
+                  "movement" (next-word-end, 1, 0, 1)
+                  "change-case" (lower)
+                  "restore-insert-mark" () };
+  bind "<ctrl>asciitilde" { "save-insert-mark" ()
+                            "movement" (previous-word-end, 0, 1, 1)
+                            "movement" (next-word-start, 0, 1, 0)
+                            "movement" (next-word-end, 1, 0, 1)
+                            "change-case" (toggle)
+                            "restore-insert-mark" () };
+}
+
+@binding-set sublime-ide-source-view-has-selection
+{
+  bind "<ctrl>c" { "copy-clipboard" () };
+  bind "<ctrl>x" { "cut-clipboard" () };
+  bind "<ctrl>d" { "add-cursor" (match) };
+  bind "<ctrl>bracketleft" { "indent-selection" (-1) };
+  bind "<ctrl>bracketright" { "indent-selection" (1) };
+  bind "<ctrl>l" { "movement" (line-end, 1, 0, 0) };
+
+  bind "<ctrl>u" { "change-case" (upper) };
+  bind "<alt>u" { "change-case" (lower) };
+  bind "<ctrl>asciitilde" { "change-case" (toggle) };
+}
+
+
+@binding-set sublime-workbench-bindings
+{
+  bind "<ctrl>less" { "action" ("app", "preferences", "") };
+  bind "<ctrl>p" { "action" ("win", "global-search", "") };
+  /* No quick project switcher in Builder, but we bind this to Open Project */
+  bind "<ctrl><alt>p" { "action" ("app", "open-project", "") };
+}
+
+window.workbench {
+  -gtk-key-bindings: sublime-workbench-bindings;
+}
+
+.sourceview,
+idesourceviewmode.default {
+  -gtk-key-bindings: sublime-ide-source-view-no-selection,
+                     sublime-ide-source-view,
+                     sublime-workbench-bindings;
+}
+
+idesourceviewmode.default.has-selection {
+  -gtk-key-bindings: sublime-ide-source-view-has-selection,
+                     sublime-ide-source-view,
+                     sublime-workbench-bindings;
+}
diff --git a/src/plugins/sublime/meson.build b/src/plugins/sublime/meson.build
new file mode 100644
index 000000000..3272cfc5f
--- /dev/null
+++ b/src/plugins/sublime/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'sublime-plugin.c',
+  'gbp-sublime-preferences-addin.c',
+])
+
+plugin_sublime_resources = gnome.compile_resources(
+  'sublime-resources',
+  'sublime.gresource.xml',
+  c_name: 'gbp_sublime',
+)
+
+plugins_sources += plugin_sublime_resources[0]
diff --git a/src/plugins/sublime/sublime-plugin.c b/src/plugins/sublime/sublime-plugin.c
new file mode 100644
index 000000000..f77e4a0fe
--- /dev/null
+++ b/src/plugins/sublime/sublime-plugin.c
@@ -0,0 +1,36 @@
+/* sublime-plugin.c
+ *
+ * Copyright 2018-2019 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 "sublime-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+
+#include "gbp-sublime-preferences-addin.h"
+
+_IDE_EXTERN void
+_gbp_sublime_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_SUBLIME_PREFERENCES_ADDIN);
+}
diff --git a/src/plugins/sublime/sublime.gresource.xml b/src/plugins/sublime/sublime.gresource.xml
new file mode 100644
index 000000000..699dc4a8f
--- /dev/null
+++ b/src/plugins/sublime/sublime.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/sublime">
+    <file>sublime.plugin</file>
+    <file>keybindings/sublime.css</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/sublime/sublime.plugin b/src/plugins/sublime/sublime.plugin
new file mode 100644
index 000000000..a9fa502ff
--- /dev/null
+++ b/src/plugins/sublime/sublime.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Philip Chimento <philip endlessm com>
+Builtin=true
+Copyright=Copyright © 2018 Philip Chimento
+Description=Emulation of various Sublime Text features
+Embedded=_gbp_sublime_register_types
+Hidden=true
+Module=sublime
+Name=Sublime Text Emulation
diff --git a/src/plugins/support/gtk/menus.ui b/src/plugins/support/gtk/menus.ui
index 710acf348..26bfa30af 100644
--- a/src/plugins/support/gtk/menus.ui
+++ b/src/plugins/support/gtk/menus.ui
@@ -1,8 +1,8 @@
 <?xml version="1.0"?>
 <interface>
   <!-- interface-requires gtk+ 3.0 -->
-  <menu id="gear-menu">
-    <section id="gear-menu-placeholder-1">
+  <menu id="ide-primary-workspace-menu">
+    <section id="ide-primary-workspace-menu-placeholder3">
       <item>
         <attribute name="label" translatable="yes">Generate Support Log</attribute>
         <attribute name="action">app.generate-support</attribute>
diff --git a/src/plugins/support/ide-support-application-addin.c 
b/src/plugins/support/ide-support-application-addin.c
index b7aed8f53..4304dbd8c 100644
--- a/src/plugins/support/ide-support-application-addin.c
+++ b/src/plugins/support/ide-support-application-addin.c
@@ -1,6 +1,6 @@
 /* ide-support-application-addin.c
  *
- * Copyright 2015 Christian Hergert <chergert redhat com>
+ * Copyright 2015-2019 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
@@ -14,13 +14,15 @@
  *
  * 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
  */
 
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-gui.h>
 
-#include "ide-support-application-addin.h"
 #include "ide-support.h"
+#include "ide-support-application-addin.h"
 
 struct _IdeSupportApplicationAddin
 {
@@ -51,6 +53,7 @@ generate_support_activate (GSimpleAction              *action,
                            GVariant                   *variant,
                            IdeSupportApplicationAddin *self)
 {
+  g_autoptr(GFile) file = NULL;
   GtkWidget *dialog;
   gchar *text = NULL;
   GList *windows;
@@ -66,6 +69,8 @@ generate_support_activate (GSimpleAction              *action,
   log_path = g_build_filename (g_get_home_dir (), name, NULL);
   g_free (name);
 
+  file = g_file_new_for_path (log_path);
+
   windows = gtk_application_get_windows (GTK_APPLICATION (IDE_APPLICATION_DEFAULT));
 
   str = ide_get_support_log ();
@@ -92,6 +97,8 @@ generate_support_activate (GSimpleAction              *action,
   g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
   gtk_window_present (GTK_WINDOW (dialog));
 
+  dzl_file_manager_show (file, NULL);
+
 cleanup:
   g_free (text);
   g_clear_error (&error);
diff --git a/src/plugins/support/ide-support-application-addin.h 
b/src/plugins/support/ide-support-application-addin.h
index 36210d110..34abaf494 100644
--- a/src/plugins/support/ide-support-application-addin.h
+++ b/src/plugins/support/ide-support-application-addin.h
@@ -1,6 +1,6 @@
 /* ide-support-application-addin.h
  *
- * Copyright 2015 Christian Hergert <chergert redhat com>
+ * Copyright 2015-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/support/ide-support.c b/src/plugins/support/ide-support.c
index a1a95d523..aa2bcb4e6 100644
--- a/src/plugins/support/ide-support.c
+++ b/src/plugins/support/ide-support.c
@@ -1,6 +1,6 @@
 /* ide-support.c
  *
- * Copyright 2014 Christian Hergert <christian hergert me>
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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 "ide-support"
@@ -22,7 +24,7 @@
 
 #include <dazzle.h>
 #include <gtk/gtk.h>
-#include <ide.h>
+#include <libide-gui.h>
 #include <ide-build-ident.h>
 #include <libpeas/peas.h>
 #include <string.h>
diff --git a/src/plugins/support/ide-support.h b/src/plugins/support/ide-support.h
index 231892770..3c905137f 100644
--- a/src/plugins/support/ide-support.h
+++ b/src/plugins/support/ide-support.h
@@ -1,6 +1,6 @@
 /* ide-support.h
  *
- * Copyright 2014 Christian Hergert <christian hergert me>
+ * Copyright 2014-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/support/meson.build b/src/plugins/support/meson.build
index 1e1d9fba7..23db282d9 100644
--- a/src/plugins/support/meson.build
+++ b/src/plugins/support/meson.build
@@ -1,20 +1,13 @@
-if get_option('with_support')
+plugins_sources += files([
+  'ide-support-application-addin.c',
+  'ide-support.c',
+  'support-plugin.c',
+])
 
-support_resources = gnome.compile_resources(
+plugin_support_resources = gnome.compile_resources(
   'support-resources',
   'support.gresource.xml',
-  c_name: 'ide_support',
+  c_name: 'gbp_support',
 )
 
-support_sources = [
-  'ide-support-application-addin.c',
-  'ide-support-application-addin.h',
-  'ide-support.c',
-  'ide-support.h',
-  'ide-support-plugin.c',
-]
-
-gnome_builder_plugins_sources += files(support_sources)
-gnome_builder_plugins_sources += support_resources[0]
-
-endif
+plugins_sources += plugin_support_resources[0]
diff --git a/src/plugins/support/support-plugin.c b/src/plugins/support/support-plugin.c
new file mode 100644
index 000000000..88ccd0be2
--- /dev/null
+++ b/src/plugins/support/support-plugin.c
@@ -0,0 +1,32 @@
+/* support-plugin.c
+ *
+ * Copyright 2015-2019 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
+ */
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+
+#include "ide-support-application-addin.h"
+
+_IDE_EXTERN void
+_ide_support_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_APPLICATION_ADDIN,
+                                              IDE_TYPE_SUPPORT_APPLICATION_ADDIN);
+}
diff --git a/src/plugins/support/support.gresource.xml b/src/plugins/support/support.gresource.xml
index 03ae36934..e9dd9595c 100644
--- a/src/plugins/support/support.gresource.xml
+++ b/src/plugins/support/support.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/support">
     <file>support.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/support-plugin">
     <file>gtk/menus.ui</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/support/support.plugin b/src/plugins/support/support.plugin
index 5ce7621e9..fb42e627e 100644
--- a/src/plugins/support/support.plugin
+++ b/src/plugins/support/support.plugin
@@ -1,9 +1,9 @@
 [Plugin]
-Module=support-plugin
+Module=support
 Name=Support
 Description=Generate support logs for assistance
 Authors=Christian Hergert <christian hergert me>
 Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
 Hidden=true
-Embedded=ide_support_register_types
+Embedded=_ide_support_register_types
diff --git a/src/plugins/symbol-tree/gbp-symbol-frame-addin.c 
b/src/plugins/symbol-tree/gbp-symbol-frame-addin.c
new file mode 100644
index 000000000..ce25848ee
--- /dev/null
+++ b/src/plugins/symbol-tree/gbp-symbol-frame-addin.c
@@ -0,0 +1,563 @@
+/* gbp-symbol-frame-addin.c
+ *
+ * Copyright 2017-2019 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-symbol-frame-addin"
+
+#include "config.h"
+
+#include <libide-editor.h>
+#include <glib/gi18n.h>
+
+#include "gbp-symbol-frame-addin.h"
+#include "gbp-symbol-menu-button.h"
+
+#define CURSOR_MOVED_DELAY_MSEC 500
+#define I_(s) (g_intern_static_string(s))
+
+struct _GbpSymbolFrameAddin {
+  GObject              parent_instance;
+
+  GbpSymbolMenuButton *button;
+  GCancellable        *cancellable;
+  GCancellable        *scope_cancellable;
+  DzlSignalGroup      *buffer_signals;
+
+  guint                cursor_moved_handler;
+};
+
+typedef struct
+{
+  GPtrArray         *resolvers;
+  IdeBuffer         *buffer;
+  IdeLocation *location;
+} SymbolResolverTaskData;
+
+static DzlShortcutEntry symbol_tree_shortcuts[] = {
+  { "org.gnome.builder.symbol-tree.search",
+    0, NULL,
+    NC_("shortcut window", "Editor shortcuts"),
+    NC_("shortcut window", "Symbols"),
+    NC_("shortcut window", "Search symbols within document") },
+};
+
+static void
+symbol_resolver_task_data_free (SymbolResolverTaskData *data)
+{
+  g_assert (data != NULL);
+  g_assert (data->resolvers != NULL);
+  g_assert (data->buffer != NULL);
+  g_assert (IDE_IS_BUFFER (data->buffer));
+
+  g_clear_pointer (&data->resolvers, g_ptr_array_unref);
+  g_clear_object (&data->buffer);
+  g_clear_object (&data->location);
+  g_slice_free (SymbolResolverTaskData, data);
+}
+
+static void
+gbp_symbol_frame_addin_find_scope_cb (GObject      *object,
+                                      GAsyncResult *result,
+                                      gpointer      user_data)
+{
+  IdeSymbolResolver *symbol_resolver = (IdeSymbolResolver *)object;
+  GbpSymbolFrameAddin *self;
+  g_autoptr(IdeSymbol) symbol = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  SymbolResolverTaskData *data;
+
+  g_assert (IDE_IS_SYMBOL_RESOLVER (symbol_resolver));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  symbol = ide_symbol_resolver_find_nearest_scope_finish (symbol_resolver, result, &error);
+  g_assert (symbol != NULL || error != NULL);
+
+  self = ide_task_get_source_object (task);
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+
+  data = ide_task_get_task_data (task);
+  g_assert (data != NULL);
+  g_assert (IDE_IS_BUFFER (data->buffer));
+  g_assert (data->resolvers != NULL);
+  g_assert (data->resolvers->len > 0);
+
+  g_ptr_array_remove_index (data->resolvers, data->resolvers->len - 1);
+
+  /* If symbol is not found and symbol resolvers are left try those */
+  if (symbol == NULL && data->resolvers->len > 0)
+    {
+      IdeSymbolResolver *resolver;
+
+      resolver = g_ptr_array_index (data->resolvers, data->resolvers->len - 1);
+      ide_symbol_resolver_find_nearest_scope_async (resolver,
+                                                    data->location,
+                                                    self->scope_cancellable,
+                                                    gbp_symbol_frame_addin_find_scope_cb,
+                                                    g_steal_pointer (&task));
+
+      return;
+    }
+
+  if (error != NULL)
+    g_debug ("Failed to find nearest scope: %s", error->message);
+
+  if (self->button != NULL)
+    gbp_symbol_menu_button_set_symbol (self->button, symbol);
+
+  /* We don't use this, but we should return a value anyway */
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_symbol_frame_addin_cursor_moved_cb (gpointer user_data)
+{
+  GbpSymbolFrameAddin *self = user_data;
+  IdeBuffer *buffer;
+
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+
+  g_cancellable_cancel (self->scope_cancellable);
+  g_clear_object (&self->scope_cancellable);
+
+  buffer = dzl_signal_group_get_target (self->buffer_signals);
+
+  if (buffer != NULL)
+    {
+      g_autoptr(GPtrArray) resolvers = NULL;
+
+      resolvers = ide_buffer_get_symbol_resolvers (buffer);
+      IDE_PTR_ARRAY_SET_FREE_FUNC (resolvers, g_object_unref);
+
+      if (resolvers->len > 0)
+        {
+          g_autoptr(IdeTask) task = NULL;
+          SymbolResolverTaskData *data;
+          IdeSymbolResolver *resolver;
+
+          self->scope_cancellable = g_cancellable_new ();
+
+          task = ide_task_new (self, self->scope_cancellable, NULL, NULL);
+          ide_task_set_source_tag (task, gbp_symbol_frame_addin_cursor_moved_cb);
+          ide_task_set_priority (task, G_PRIORITY_LOW);
+
+          data = g_slice_new0 (SymbolResolverTaskData);
+          data->resolvers = g_steal_pointer (&resolvers);
+          data->location = ide_buffer_get_insert_location (buffer);
+          data->buffer = g_object_ref (buffer);
+          ide_task_set_task_data (task, data, symbol_resolver_task_data_free);
+
+          resolver = g_ptr_array_index (data->resolvers, data->resolvers->len - 1);
+          /* Go through symbol resolvers one by one to find nearest scope */
+          ide_symbol_resolver_find_nearest_scope_async (resolver,
+                                                        data->location,
+                                                        self->scope_cancellable,
+                                                        gbp_symbol_frame_addin_find_scope_cb,
+                                                        g_steal_pointer (&task));
+        }
+    }
+
+  self->cursor_moved_handler = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+gbp_symbol_frame_addin_cursor_moved (GbpSymbolFrameAddin *self,
+                                     const GtkTextIter   *location,
+                                     IdeBuffer           *buffer)
+{
+  GSource *source;
+  gint64 ready_time;
+
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+  g_assert (location != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  if (self->cursor_moved_handler == 0)
+    {
+      self->cursor_moved_handler =
+        gdk_threads_add_timeout_full (G_PRIORITY_LOW,
+                                      CURSOR_MOVED_DELAY_MSEC,
+                                      gbp_symbol_frame_addin_cursor_moved_cb,
+                                      g_object_ref (self),
+                                      g_object_unref);
+      return;
+    }
+
+  /* Try to reuse our existing GSource if we can */
+  ready_time = g_get_monotonic_time () + (CURSOR_MOVED_DELAY_MSEC * 1000);
+  source = g_main_context_find_source_by_id (NULL, self->cursor_moved_handler);
+  g_source_set_ready_time (source, ready_time);
+}
+
+static void
+gbp_symbol_frame_addin_get_symbol_tree_cb (GObject      *object,
+                                           GAsyncResult *result,
+                                           gpointer      user_data)
+{
+  IdeSymbolResolver *symbol_resolver = (IdeSymbolResolver *)object;
+  GbpSymbolFrameAddin *self;
+  g_autoptr(IdeSymbolTree) tree = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autoptr(IdeTask) task = user_data;
+  SymbolResolverTaskData *data;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (IDE_IS_SYMBOL_RESOLVER (symbol_resolver));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  tree = ide_symbol_resolver_get_symbol_tree_finish (symbol_resolver, result, &error);
+
+  self = ide_task_get_source_object (task);
+  data = ide_task_get_task_data (task);
+
+  g_ptr_array_remove_index (data->resolvers, data->resolvers->len - 1);
+
+  /* Ignore empty trees, in favor of next symbol resovler */
+  if (tree != NULL && ide_symbol_tree_get_n_children (tree, NULL) == 0)
+    g_clear_object (&tree);
+
+  /* If tree is not fetched and symbol resolvers are left then try those */
+  if (tree == NULL && data->resolvers->len > 0)
+    {
+      g_autoptr(GBytes) content = NULL;
+      IdeSymbolResolver *resolver;
+      GFile *file;
+
+      file = ide_buffer_get_file (data->buffer);
+      resolver = g_ptr_array_index (data->resolvers, data->resolvers->len - 1);
+      content = ide_buffer_dup_content (data->buffer);
+
+      ide_symbol_resolver_get_symbol_tree_async (resolver,
+                                                 file,
+                                                 content,
+                                                 self->cancellable,
+                                                 gbp_symbol_frame_addin_get_symbol_tree_cb,
+                                                 g_steal_pointer (&task));
+      return;
+    }
+
+  if (error != NULL)
+    g_debug ("Failed to get symbol tree: %s", error->message);
+
+  /* If we were destroyed, short-circuit */
+  if (self->button != NULL)
+    {
+      /* Only override if we got a new value (this helps with situations
+       * where the parse tree breaks intermittently.
+       */
+      if (tree != NULL)
+        gbp_symbol_menu_button_set_symbol_tree (self->button, tree);
+    }
+
+  /* We don't use this, but we should return a value anyway */
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_symbol_frame_addin_update_tree (GbpSymbolFrameAddin *self,
+                                    IdeBuffer           *buffer)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) resolvers = NULL;
+  g_autoptr(GBytes) content = NULL;
+  SymbolResolverTaskData *data;
+  IdeSymbolResolver *resolver;
+  GFile *file;
+
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /* Cancel any in-flight work */
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+
+  resolvers = ide_buffer_get_symbol_resolvers (buffer);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (resolvers, g_object_unref);
+
+  if (resolvers->len == 0)
+    {
+      gtk_widget_hide (GTK_WIDGET (self->button));
+      return;
+    }
+
+  gtk_widget_show (GTK_WIDGET (self->button));
+
+  file = ide_buffer_get_file (buffer);
+  g_assert (G_IS_FILE (file));
+
+  content = ide_buffer_dup_content (buffer);
+
+  self->cancellable = g_cancellable_new ();
+
+  task = ide_task_new (self, self->cancellable, NULL, NULL);
+  ide_task_set_source_tag (task, gbp_symbol_frame_addin_update_tree);
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  data = g_slice_new0 (SymbolResolverTaskData);
+  data->resolvers = g_steal_pointer (&resolvers);
+  data->buffer = g_object_ref (buffer);
+  ide_task_set_task_data (task, data, symbol_resolver_task_data_free);
+
+  g_assert (data->resolvers->len > 0);
+
+  resolver = g_ptr_array_index (data->resolvers, data->resolvers->len - 1);
+  ide_symbol_resolver_get_symbol_tree_async (resolver,
+                                             file,
+                                             content,
+                                             self->cancellable,
+                                             gbp_symbol_frame_addin_get_symbol_tree_cb,
+                                             g_steal_pointer (&task));
+}
+
+static void
+gbp_symbol_frame_addin_change_settled (GbpSymbolFrameAddin *self,
+                                       IdeBuffer           *buffer)
+{
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  /* Ignore this request unless the button is active */
+  if (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->button)))
+    return;
+
+  gbp_symbol_frame_addin_update_tree (self, buffer);
+}
+
+static void
+gbp_symbol_frame_addin_button_toggled (GbpSymbolFrameAddin *self,
+                                       GtkMenuButton       *button)
+{
+  IdeBuffer *buffer;
+
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+  g_assert (GTK_IS_MENU_BUTTON (button));
+
+  buffer = dzl_signal_group_get_target (self->buffer_signals);
+  g_assert (!buffer || IDE_IS_BUFFER (buffer));
+
+  if (buffer != NULL && gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (button)))
+    gbp_symbol_frame_addin_update_tree (self, buffer);
+}
+
+static void
+gbp_symbol_frame_addin_notify_has_symbol_resolvers (GbpSymbolFrameAddin *self,
+                                                    GParamSpec          *pspec,
+                                                    IdeBuffer           *buffer)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+
+  gtk_widget_set_visible (GTK_WIDGET (self->button),
+                          ide_buffer_has_symbol_resolvers (buffer));
+  gbp_symbol_frame_addin_update_tree (self, buffer);
+}
+
+static void
+gbp_symbol_frame_addin_bind (GbpSymbolFrameAddin *self,
+                             IdeBuffer           *buffer,
+                             DzlSignalGroup      *buffer_signals)
+{
+  g_autoptr(GPtrArray) resolvers = NULL;
+
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (DZL_IS_SIGNAL_GROUP (buffer_signals));
+
+  self->cancellable = g_cancellable_new ();
+
+  gbp_symbol_menu_button_set_symbol (self->button, NULL);
+  gbp_symbol_frame_addin_notify_has_symbol_resolvers (self, NULL, buffer);
+}
+
+static void
+gbp_symbol_frame_addin_unbind (GbpSymbolFrameAddin *self,
+                               DzlSignalGroup      *buffer_signals)
+{
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+  g_assert (DZL_IS_SIGNAL_GROUP (buffer_signals));
+
+  dzl_clear_source (&self->cursor_moved_handler);
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+
+  g_cancellable_cancel (self->scope_cancellable);
+  g_clear_object (&self->scope_cancellable);
+
+  gtk_widget_hide (GTK_WIDGET (self->button));
+}
+
+static void
+search_action_cb (GSimpleAction *action,
+                  GVariant      *param,
+                  gpointer       user_data)
+{
+  GbpSymbolFrameAddin *self = user_data;
+
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+
+  if (gtk_widget_get_visible (GTK_WIDGET (self->button)))
+    gtk_widget_activate (GTK_WIDGET (self->button));
+}
+
+static void
+gbp_symbol_frame_addin_load (IdeFrameAddin *addin,
+                             IdeFrame      *stack)
+{
+  GbpSymbolFrameAddin *self = (GbpSymbolFrameAddin *)addin;
+  g_autoptr(GSimpleActionGroup) actions = NULL;
+  DzlShortcutController *controller;
+  GtkWidget *header;
+  static const GActionEntry entries[] = {
+    { "search", search_action_cb },
+  };
+
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+  g_assert (IDE_IS_FRAME (stack));
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (stack));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              I_("org.gnome.builder.symbol-tree.search"),
+                                              "<Primary><Shift>k",
+                                              DZL_SHORTCUT_PHASE_BUBBLE,
+                                              I_("symbol-tree.search"));
+
+  actions = g_simple_action_group_new ();
+  g_action_map_add_action_entries (G_ACTION_MAP (actions),
+                                   entries,
+                                   G_N_ELEMENTS (entries),
+                                   self);
+  gtk_widget_insert_action_group (GTK_WIDGET (stack),
+                                  "symbol-tree",
+                                  G_ACTION_GROUP (actions));
+
+  /* Add our menu button to the header */
+  header = ide_frame_get_titlebar (stack);
+  self->button = g_object_new (GBP_TYPE_SYMBOL_MENU_BUTTON, NULL);
+  g_signal_connect (self->button,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->button);
+  g_signal_connect_swapped (self->button,
+                            "toggled",
+                            G_CALLBACK (gbp_symbol_frame_addin_button_toggled),
+                            self);
+  ide_frame_header_add_custom_title (IDE_FRAME_HEADER (header),
+                                            GTK_WIDGET (self->button),
+                                            100);
+
+  /* Setup our signals to the buffer */
+  self->buffer_signals = dzl_signal_group_new (IDE_TYPE_BUFFER);
+
+  g_signal_connect_swapped (self->buffer_signals,
+                            "bind",
+                            G_CALLBACK (gbp_symbol_frame_addin_bind),
+                            self);
+
+  g_signal_connect_swapped (self->buffer_signals,
+                            "unbind",
+                            G_CALLBACK (gbp_symbol_frame_addin_unbind),
+                            self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "cursor-moved",
+                                    G_CALLBACK (gbp_symbol_frame_addin_cursor_moved),
+                                    self);
+
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "change-settled",
+                                    G_CALLBACK (gbp_symbol_frame_addin_change_settled),
+                                    self);
+  dzl_signal_group_connect_swapped (self->buffer_signals,
+                                    "notify::has-symbol-resolvers",
+                                    G_CALLBACK (gbp_symbol_frame_addin_notify_has_symbol_resolvers),
+                                    self);
+}
+
+static void
+gbp_symbol_frame_addin_unload (IdeFrameAddin *addin,
+                               IdeFrame      *stack)
+{
+  GbpSymbolFrameAddin *self = (GbpSymbolFrameAddin *)addin;
+
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+  g_assert (IDE_IS_FRAME (stack));
+
+  gtk_widget_insert_action_group (GTK_WIDGET (stack), "symbol-tree", NULL);
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+  g_clear_object (&self->buffer_signals);
+
+  if (self->button != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->button));
+}
+
+static void
+gbp_symbol_frame_addin_set_page (IdeFrameAddin *addin,
+                                 IdePage       *page)
+{
+  GbpSymbolFrameAddin *self = (GbpSymbolFrameAddin *)addin;
+  IdeBuffer *buffer = NULL;
+
+  g_assert (GBP_IS_SYMBOL_FRAME_ADDIN (self));
+  g_assert (!page || IDE_IS_PAGE (page));
+
+  /* First clear any old symbol tree */
+  gbp_symbol_menu_button_set_symbol_tree (self->button, NULL);
+
+  if (IDE_IS_EDITOR_PAGE (page))
+    buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (page));
+
+  dzl_signal_group_set_target (self->buffer_signals, buffer);
+}
+
+static void
+frame_addin_iface_init (IdeFrameAddinInterface *iface)
+{
+  iface->load = gbp_symbol_frame_addin_load;
+  iface->unload = gbp_symbol_frame_addin_unload;
+  iface->set_page = gbp_symbol_frame_addin_set_page;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpSymbolFrameAddin,
+                         gbp_symbol_frame_addin,
+                         G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_FRAME_ADDIN,
+                                                frame_addin_iface_init))
+
+static void
+gbp_symbol_frame_addin_class_init (GbpSymbolFrameAddinClass *klass)
+{
+}
+
+static void
+gbp_symbol_frame_addin_init (GbpSymbolFrameAddin *self)
+{
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             symbol_tree_shortcuts,
+                                             G_N_ELEMENTS (symbol_tree_shortcuts),
+                                             GETTEXT_PACKAGE);
+}
diff --git a/src/plugins/symbol-tree/gbp-symbol-frame-addin.h 
b/src/plugins/symbol-tree/gbp-symbol-frame-addin.h
new file mode 100644
index 000000000..db160ac53
--- /dev/null
+++ b/src/plugins/symbol-tree/gbp-symbol-frame-addin.h
@@ -0,0 +1,31 @@
+/* gbp-symbol-frame-addin.h
+ *
+ * Copyright 2017-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SYMBOL_FRAME_ADDIN (gbp_symbol_frame_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSymbolFrameAddin, gbp_symbol_frame_addin, GBP, SYMBOL_FRAME_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/symbol-tree/gbp-symbol-hover-provider.c 
b/src/plugins/symbol-tree/gbp-symbol-hover-provider.c
index 0af699874..60f1387a1 100644
--- a/src/plugins/symbol-tree/gbp-symbol-hover-provider.c
+++ b/src/plugins/symbol-tree/gbp-symbol-hover-provider.c
@@ -1,6 +1,6 @@
 /* gbp-symbol-hover-provider.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,12 +14,17 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-symbol-hover-provider"
 
+#include "config.h"
+
+#include <libide-code.h>
+#include <libide-gui.h>
+#include <libide-editor.h>
 #include <glib/gi18n.h>
 
 #include "gbp-symbol-hover-provider.h"
@@ -32,28 +37,24 @@ struct _GbpSymbolHoverProvider
 };
 
 static gboolean
-on_activate_link (GbpSymbolHoverProvider *self,
-                  const gchar            *uristr,
-                  GtkLabel               *label)
+on_activate_link (GtkLabel    *label,
+                  const gchar *uristr,
+                  IdeLocation *location)
 {
-  IdeWorkbench *workbench;
-  g_autoptr(IdeUri) uri = NULL;
+  IdeWorkspace *workspace;
+  IdeSurface *surface;
 
-  g_assert (GBP_IS_SYMBOL_HOVER_PROVIDER (self));
   g_assert (uristr != NULL);
   g_assert (GTK_IS_LABEL (label));
+  g_assert (IDE_IS_LOCATION (location));
 
-  workbench = ide_widget_get_workbench (GTK_WIDGET (label));
-  uri = ide_uri_new (uristr, 0, NULL);
+  if (!(workspace = ide_widget_get_workspace (GTK_WIDGET (label))))
+    return FALSE;
 
-  if (!uri || !workbench)
+  if (!(surface = ide_workspace_get_surface_by_name (workspace, "editor")))
     return FALSE;
 
-  ide_workbench_open_uri_async (workbench,
-                                uri,
-                                "editor",
-                                IDE_WORKBENCH_OPEN_FLAGS_NONE,
-                                NULL, NULL, NULL);
+  ide_editor_surface_focus_location (IDE_EDITOR_SURFACE (surface), location);
 
   return TRUE;
 }
@@ -75,12 +76,11 @@ gbp_symbol_hover_provider_get_symbol_cb (GObject      *object,
   const gchar *name;
   GtkWidget *box;
   struct {
-    const gchar       *kind;
-    IdeSourceLocation *loc;
-  } loc[3] = {
+    const gchar *kind;
+    IdeLocation *loc;
+  } loc[] = {
+    { _("Location"), NULL },
     { _("Declaration"), NULL },
-    { _("Definition"), NULL },
-    { _("Canonical"), NULL },
   };
 
   g_assert (IDE_IS_BUFFER (buffer));
@@ -100,11 +100,10 @@ gbp_symbol_hover_provider_get_symbol_cb (GObject      *object,
   g_assert (context != NULL);
   g_assert (IDE_IS_HOVER_CONTEXT (context));
 
-  loc[0].loc = ide_symbol_get_declaration_location (symbol);
-  loc[1].loc = ide_symbol_get_definition_location (symbol);
-  loc[2].loc = ide_symbol_get_canonical_location (symbol);
+  loc[0].loc = ide_symbol_get_location (symbol);
+  loc[1].loc = ide_symbol_get_header_location (symbol);
 
-  if (!loc[0].loc && !loc[1].loc && !loc[2].loc)
+  if (!loc[0].loc && !loc[1].loc)
     {
       ide_task_return_boolean (task, TRUE);
       return;
@@ -136,13 +135,10 @@ gbp_symbol_hover_provider_get_symbol_cb (GObject      *object,
       if (loc[i].loc != NULL)
         {
           GtkWidget *label;
-          g_autoptr(IdeUri) uri = ide_source_location_get_uri (loc[i].loc);
-          g_autoptr(GFile) file = ide_uri_to_file (uri);
-          g_autofree gchar *uristr = ide_uri_to_string (uri, 0);
+          GFile *file = ide_location_get_file (loc[i].loc);
           g_autofree gchar *base = g_file_get_basename (file);
-          g_autofree gchar *escaped = g_markup_escape_text (uristr, -1);
-          g_autofree gchar *markup = g_strdup_printf ("<span size='smaller'>%s: <a href='%s'>%s</a></span>",
-                                                      loc[i].kind, escaped, base);
+          g_autofree gchar *markup = g_strdup_printf ("<span size='smaller'>%s: <a href='#'>%s</a></span>",
+                                                      loc[i].kind, base);
 
           label = g_object_new (GTK_TYPE_LABEL,
                                 "visible", TRUE,
@@ -150,11 +146,12 @@ gbp_symbol_hover_provider_get_symbol_cb (GObject      *object,
                                 "use-markup", TRUE,
                                 "label", markup,
                                 NULL);
-          g_signal_connect_object (label,
-                                   "activate-link",
-                                   G_CALLBACK (on_activate_link),
-                                   self,
-                                   G_CONNECT_SWAPPED);
+          g_signal_connect_data (label,
+                                 "activate-link",
+                                 G_CALLBACK (on_activate_link),
+                                 g_object_ref (loc[i].loc),
+                                 (GClosureNotify)g_object_unref,
+                                 0);
           gtk_container_add (GTK_CONTAINER (box), label);
         }
     }
diff --git a/src/plugins/symbol-tree/gbp-symbol-hover-provider.h 
b/src/plugins/symbol-tree/gbp-symbol-hover-provider.h
index 8ff472a9d..a550e5a04 100644
--- a/src/plugins/symbol-tree/gbp-symbol-hover-provider.h
+++ b/src/plugins/symbol-tree/gbp-symbol-hover-provider.h
@@ -1,6 +1,6 @@
 /* gbp-symbol-hover-provider.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/symbol-tree/gbp-symbol-menu-button.c 
b/src/plugins/symbol-tree/gbp-symbol-menu-button.c
index 5610a323b..1f6a18aaf 100644
--- a/src/plugins/symbol-tree/gbp-symbol-menu-button.c
+++ b/src/plugins/symbol-tree/gbp-symbol-menu-button.c
@@ -1,6 +1,6 @@
 /* gbp-symbol-menu-button.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,10 +14,13 @@
  *
  * 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-symbol-menu-button"
 
+#include <libide-sourceview.h>
 #include <glib/gi18n.h>
 
 #include "gbp-symbol-menu-button.h"
@@ -205,7 +208,7 @@ gbp_symbol_menu_button_class_init (GbpSymbolMenuButtonClass *klass)
 
   widget_class->destroy = gbp_symbol_menu_button_destroy;
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/symbol-tree-plugin/gbp-symbol-menu-button.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/symbol-tree/gbp-symbol-menu-button.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpSymbolMenuButton, popover);
   gtk_widget_class_bind_template_child (widget_class, GbpSymbolMenuButton, search_entry);
   gtk_widget_class_bind_template_child (widget_class, GbpSymbolMenuButton, symbol_icon);
@@ -250,7 +253,7 @@ gbp_symbol_menu_button_init (GbpSymbolMenuButton *self)
  *
  * Returns: (transfer none) (nullable): An #IdeSymbolTree or %NULL
  *
- * Since: 3.26
+ * Since: 3.32
  */
 IdeSymbolTree *
 gbp_symbol_menu_button_get_symbol_tree (GbpSymbolMenuButton *self)
@@ -266,7 +269,7 @@ gbp_symbol_menu_button_get_symbol_tree (GbpSymbolMenuButton *self)
  *
  * Sets the symbol tree to be displayed by the popover.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 gbp_symbol_menu_button_set_symbol_tree (GbpSymbolMenuButton *self,
diff --git a/src/plugins/symbol-tree/gbp-symbol-menu-button.h 
b/src/plugins/symbol-tree/gbp-symbol-menu-button.h
index 7a99a5e69..ebc5503f9 100644
--- a/src/plugins/symbol-tree/gbp-symbol-menu-button.h
+++ b/src/plugins/symbol-tree/gbp-symbol-menu-button.h
@@ -1,6 +1,6 @@
 /* gbp-symbol-menu-button.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/symbol-tree/gbp-symbol-tree-builder.c 
b/src/plugins/symbol-tree/gbp-symbol-tree-builder.c
index 21315765b..5f6c3900e 100644
--- a/src/plugins/symbol-tree/gbp-symbol-tree-builder.c
+++ b/src/plugins/symbol-tree/gbp-symbol-tree-builder.c
@@ -1,6 +1,6 @@
 /* gbp-symbol-tree-builder.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,12 +14,15 @@
  *
  * 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-symbol-tree-builder"
 
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
 
 #include "gbp-symbol-tree-builder.h"
 
@@ -93,10 +96,10 @@ gbp_symbol_tree_builder_get_location_cb (GObject      *object,
 {
   IdeSymbolNode *node = (IdeSymbolNode *)object;
   g_autoptr(GbpSymbolTreeBuilder) self = user_data;
-  g_autoptr(IdeSourceLocation) location = NULL;
+  g_autoptr(IdeLocation) location = NULL;
   g_autoptr(GError) error = NULL;
-  IdePerspective *editor;
-  IdeWorkbench *workbench;
+  IdeSurface *editor;
+  IdeWorkspace *workspace;
   DzlTree *tree;
 
   IDE_ENTRY;
@@ -115,10 +118,10 @@ gbp_symbol_tree_builder_get_location_cb (GObject      *object,
     }
 
   tree = dzl_tree_builder_get_tree (DZL_TREE_BUILDER (self));
-  workbench = ide_widget_get_workbench (GTK_WIDGET (tree));
-  editor = ide_workbench_get_perspective_by_name (workbench, "editor");
+  workspace = ide_widget_get_workspace (GTK_WIDGET (tree));
+  editor = ide_workspace_get_surface_by_name (workspace, "editor");
 
-  ide_editor_perspective_focus_location (IDE_EDITOR_PERSPECTIVE (editor), location);
+  ide_editor_surface_focus_location (IDE_EDITOR_SURFACE (editor), location);
 
   IDE_EXIT;
 }
diff --git a/src/plugins/symbol-tree/gbp-symbol-tree-builder.h 
b/src/plugins/symbol-tree/gbp-symbol-tree-builder.h
index 3d3031968..f999f822c 100644
--- a/src/plugins/symbol-tree/gbp-symbol-tree-builder.h
+++ b/src/plugins/symbol-tree/gbp-symbol-tree-builder.h
@@ -1,6 +1,6 @@
 /* gbp-symbol-tree-builder.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-gui.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/symbol-tree/meson.build b/src/plugins/symbol-tree/meson.build
index a085ab773..b40d3d4e6 100644
--- a/src/plugins/symbol-tree/meson.build
+++ b/src/plugins/symbol-tree/meson.build
@@ -1,23 +1,15 @@
-if get_option('with_symbol_tree')
-
-symbol_tree_resources = gnome.compile_resources(
-  'symbol-tree-resources',
-  'symbol-tree.gresource.xml',
-  c_name: 'symbol_tree',
-)
-
-symbol_tree_sources = [
+plugins_sources += files([
   'gbp-symbol-hover-provider.c',
-  'gbp-symbol-layout-stack-addin.c',
-  'gbp-symbol-layout-stack-addin.h',
+  'gbp-symbol-frame-addin.c',
   'gbp-symbol-menu-button.c',
-  'gbp-symbol-menu-button.h',
   'gbp-symbol-tree-builder.c',
-  'gbp-symbol-tree-builder.h',
   'symbol-tree-plugin.c',
-]
+])
 
-gnome_builder_plugins_sources += files(symbol_tree_sources)
-gnome_builder_plugins_sources += symbol_tree_resources[0]
+plugin_symbol_tree_resources = gnome.compile_resources(
+  'gbp-symbol-tree-resources',
+  'symbol-tree.gresource.xml',
+  c_name: 'gbp_symbol_tree',
+)
 
-endif
+plugins_sources += plugin_symbol_tree_resources[0]
diff --git a/src/plugins/symbol-tree/symbol-tree-plugin.c b/src/plugins/symbol-tree/symbol-tree-plugin.c
index 9b9d5cb92..0408a034e 100644
--- a/src/plugins/symbol-tree/symbol-tree-plugin.c
+++ b/src/plugins/symbol-tree/symbol-tree-plugin.c
@@ -1,6 +1,6 @@
 /* symbol-tree-plugin.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,20 +14,25 @@
  *
  * 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
  */
 
+#include "config.h"
+
 #include <libpeas/peas.h>
-#include <ide.h>
+#include <libide-sourceview.h>
+#include <libide-gui.h>
 
-#include "gbp-symbol-layout-stack-addin.h"
+#include "gbp-symbol-frame-addin.h"
 #include "gbp-symbol-hover-provider.h"
 
-void
-gbp_symbol_tree_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_gbp_symbol_tree_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
-                                              IDE_TYPE_LAYOUT_STACK_ADDIN,
-                                              GBP_TYPE_SYMBOL_LAYOUT_STACK_ADDIN);
+                                              IDE_TYPE_FRAME_ADDIN,
+                                              GBP_TYPE_SYMBOL_FRAME_ADDIN);
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_HOVER_PROVIDER,
                                               GBP_TYPE_SYMBOL_HOVER_PROVIDER);
diff --git a/src/plugins/symbol-tree/symbol-tree.gresource.xml 
b/src/plugins/symbol-tree/symbol-tree.gresource.xml
index 66a593430..44bbb417c 100644
--- a/src/plugins/symbol-tree/symbol-tree.gresource.xml
+++ b/src/plugins/symbol-tree/symbol-tree.gresource.xml
@@ -1,10 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/symbol-tree">
     <file>symbol-tree.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/symbol-tree-plugin">
-    <file>themes/shared.css</file>
     <file>gbp-symbol-menu-button.ui</file>
+    <file>themes/shared.css</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/symbol-tree/symbol-tree.plugin b/src/plugins/symbol-tree/symbol-tree.plugin
index 844edc75e..cc49c678d 100644
--- a/src/plugins/symbol-tree/symbol-tree.plugin
+++ b/src/plugins/symbol-tree/symbol-tree.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=symbol-tree-plugin
-Name=Symbol Tree
-Description=Provides a Symbol Tree for the currently focused document
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
-Depends=editor
 Builtin=true
-Embedded=gbp_symbol_tree_register_types
+Copyright=Copyright © 2015-2018 Christian Hergert
+Depends=editor;
+Description=Provides a Symbol Tree for the currently focused document
+Embedded=_gbp_symbol_tree_register_types
+Hidden=true
+Module=symbol-tree
+Name=Symbol Tree
diff --git a/src/plugins/sysprof/gbp-sysprof-surface.c b/src/plugins/sysprof/gbp-sysprof-surface.c
new file mode 100644
index 000000000..4f7da1d28
--- /dev/null
+++ b/src/plugins/sysprof/gbp-sysprof-surface.c
@@ -0,0 +1,264 @@
+/* gbp-sysprof-surface.c
+ *
+ * Copyright 2016-2019 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-sysprof-surface"
+
+#include <glib/gi18n.h>
+#include <sysprof.h>
+#include <sysprof-ui.h>
+
+#include "gbp-sysprof-surface.h"
+
+struct _GbpSysprofSurface
+{
+  IdeSurface            parent_instance;
+
+  SpCaptureReader      *reader;
+
+  GtkStack             *stack;
+  SpCallgraphView      *callgraph_view;
+  GtkLabel             *info_bar_label;
+  GtkButton            *info_bar_close;
+  GtkRevealer          *info_bar_revealer;
+  SpVisualizerView     *visualizers;
+  SpRecordingStateView *recording_view;
+  SpZoomManager        *zoom_manager;
+};
+
+static void gbp_sysprof_surface_reload (GbpSysprofSurface *self);
+
+G_DEFINE_TYPE (GbpSysprofSurface, gbp_sysprof_surface, IDE_TYPE_SURFACE)
+
+static void
+hide_info_bar (GbpSysprofSurface *self,
+               GtkButton             *button)
+{
+  g_assert (GBP_IS_SYSPROF_SURFACE (self));
+
+  gtk_revealer_set_reveal_child (self->info_bar_revealer, FALSE);
+}
+
+static void
+gbp_sysprof_surface_selection_changed (GbpSysprofSurface *self,
+                                           SpSelection           *selection)
+{
+  g_assert (GBP_IS_SYSPROF_SURFACE (self));
+  g_assert (SP_IS_SELECTION (selection));
+
+  gbp_sysprof_surface_reload (self);
+}
+
+static void
+gbp_sysprof_surface_finalize (GObject *object)
+{
+  GbpSysprofSurface *self = (GbpSysprofSurface *)object;
+
+  g_clear_pointer (&self->reader, sp_capture_reader_unref);
+
+  G_OBJECT_CLASS (gbp_sysprof_surface_parent_class)->finalize (object);
+}
+
+static void
+gbp_sysprof_surface_class_init (GbpSysprofSurfaceClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_sysprof_surface_finalize;
+
+  gtk_widget_class_set_template_from_resource (widget_class, "/plugins/sysprof/gbp-sysprof-surface.ui");
+  gtk_widget_class_bind_template_child (widget_class, GbpSysprofSurface, callgraph_view);
+  gtk_widget_class_bind_template_child (widget_class, GbpSysprofSurface, info_bar_label);
+  gtk_widget_class_bind_template_child (widget_class, GbpSysprofSurface, info_bar_close);
+  gtk_widget_class_bind_template_child (widget_class, GbpSysprofSurface, info_bar_revealer);
+  gtk_widget_class_bind_template_child (widget_class, GbpSysprofSurface, stack);
+  gtk_widget_class_bind_template_child (widget_class, GbpSysprofSurface, recording_view);
+  gtk_widget_class_bind_template_child (widget_class, GbpSysprofSurface, visualizers);
+  gtk_widget_class_bind_template_child (widget_class, GbpSysprofSurface, zoom_manager);
+
+  g_type_ensure (SP_TYPE_CALLGRAPH_VIEW);
+  g_type_ensure (SP_TYPE_CPU_VISUALIZER_ROW);
+  g_type_ensure (SP_TYPE_EMPTY_STATE_VIEW);
+  g_type_ensure (SP_TYPE_FAILED_STATE_VIEW);
+  g_type_ensure (SP_TYPE_RECORDING_STATE_VIEW);
+  g_type_ensure (SP_TYPE_VISUALIZER_VIEW);
+}
+
+static void
+gbp_sysprof_surface_init (GbpSysprofSurface *self)
+{
+  SpSelection *selection;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_widget_set_name (GTK_WIDGET (self), "profiler");
+  ide_surface_set_icon_name (IDE_SURFACE (self), "utilities-system-monitor-symbolic");
+  ide_surface_set_title (IDE_SURFACE (self), _("Profiler"));
+
+  g_signal_connect_object (self->info_bar_close,
+                           "clicked",
+                           G_CALLBACK (hide_info_bar),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  selection = sp_visualizer_view_get_selection (self->visualizers);
+
+  g_signal_connect_object (selection,
+                           "changed",
+                           G_CALLBACK (gbp_sysprof_surface_selection_changed),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+generate_cb (GObject      *object,
+             GAsyncResult *result,
+             gpointer      user_data)
+{
+  SpCallgraphProfile *profile = (SpCallgraphProfile *)object;
+  g_autoptr(GbpSysprofSurface) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (SP_IS_CALLGRAPH_PROFILE (profile));
+  g_assert (GBP_IS_SYSPROF_SURFACE (self));
+
+  if (!sp_profile_generate_finish (SP_PROFILE (profile), result, &error))
+    {
+      g_warning ("Failed to generate profile: %s", error->message);
+      return;
+    }
+
+  sp_callgraph_view_set_profile (self->callgraph_view, profile);
+}
+
+static void
+gbp_sysprof_surface_reload (GbpSysprofSurface *self)
+{
+  SpSelection *selection;
+  g_autoptr(SpProfile) profile = NULL;
+
+  g_assert (GBP_IS_SYSPROF_SURFACE (self));
+
+  if (self->reader == NULL)
+    return;
+
+  /* If we failed, ignore the (probably mostly empty) reader */
+  if (g_strcmp0 (gtk_stack_get_visible_child_name (self->stack), "failed") == 0)
+    return;
+
+  selection = sp_visualizer_view_get_selection (self->visualizers);
+  profile = sp_callgraph_profile_new_with_selection (selection);
+
+  sp_profile_set_reader (profile, self->reader);
+  sp_profile_generate (profile, NULL, generate_cb, g_object_ref (self));
+
+  sp_visualizer_view_set_reader (self->visualizers, self->reader);
+
+  gtk_stack_set_visible_child_name (self->stack, "results");
+}
+
+SpCaptureReader *
+gbp_sysprof_surface_get_reader (GbpSysprofSurface *self)
+{
+  g_return_val_if_fail (GBP_IS_SYSPROF_SURFACE (self), NULL);
+
+  return sp_visualizer_view_get_reader (self->visualizers);
+}
+
+void
+gbp_sysprof_surface_set_reader (GbpSysprofSurface *self,
+                                    SpCaptureReader       *reader)
+{
+  g_assert (GBP_IS_SYSPROF_SURFACE (self));
+
+  if (reader != self->reader)
+    {
+      SpSelection *selection;
+
+      if (self->reader != NULL)
+        {
+          g_clear_pointer (&self->reader, sp_capture_reader_unref);
+          sp_callgraph_view_set_profile (self->callgraph_view, NULL);
+          sp_visualizer_view_set_reader (self->visualizers, NULL);
+          gtk_stack_set_visible_child_name (self->stack, "empty");
+        }
+
+      selection = sp_visualizer_view_get_selection (self->visualizers);
+      sp_selection_unselect_all (selection);
+
+      if (reader != NULL)
+        {
+          self->reader = sp_capture_reader_ref (reader);
+          gbp_sysprof_surface_reload (self);
+        }
+    }
+}
+
+static void
+gbp_sysprof_surface_profiler_failed (GbpSysprofSurface *self,
+                                         const GError          *error,
+                                         SpProfiler            *profiler)
+{
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_SYSPROF_SURFACE (self));
+  g_assert (error != NULL);
+  g_assert (SP_IS_PROFILER (profiler));
+
+  gtk_stack_set_visible_child_name (self->stack, "failed");
+
+  gtk_label_set_label (self->info_bar_label, error->message);
+  gtk_revealer_set_reveal_child (self->info_bar_revealer, TRUE);
+
+  IDE_EXIT;
+}
+
+void
+gbp_sysprof_surface_set_profiler (GbpSysprofSurface *self,
+                                      SpProfiler            *profiler)
+{
+  g_return_if_fail (GBP_IS_SYSPROF_SURFACE (self));
+  g_return_if_fail (!profiler || SP_IS_PROFILER (profiler));
+
+  sp_recording_state_view_set_profiler (self->recording_view, profiler);
+
+  if (profiler != NULL)
+    {
+      gtk_stack_set_visible_child_name (self->stack, "recording");
+
+      g_signal_connect_object (profiler,
+                               "failed",
+                               G_CALLBACK (gbp_sysprof_surface_profiler_failed),
+                               self,
+                               G_CONNECT_SWAPPED);
+    }
+  else
+    {
+      gtk_stack_set_visible_child_name (self->stack, "empty");
+    }
+}
+
+SpZoomManager *
+gbp_sysprof_surface_get_zoom_manager (GbpSysprofSurface *self)
+{
+  g_return_val_if_fail (GBP_IS_SYSPROF_SURFACE (self), NULL);
+
+  return self->zoom_manager;
+}
diff --git a/src/plugins/sysprof/gbp-sysprof-surface.h b/src/plugins/sysprof/gbp-sysprof-surface.h
new file mode 100644
index 000000000..4ed48180a
--- /dev/null
+++ b/src/plugins/sysprof/gbp-sysprof-surface.h
@@ -0,0 +1,39 @@
+/* gbp-sysprof-surface.h
+ *
+ * Copyright 2016-2019 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-gui.h>
+#include <sysprof-ui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SYSPROF_SURFACE (gbp_sysprof_surface_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSysprofSurface, gbp_sysprof_surface, GBP, SYSPROF_SURFACE, IdeSurface)
+
+SpZoomManager   *gbp_sysprof_surface_get_zoom_manager (GbpSysprofSurface *self);
+void             gbp_sysprof_surface_set_profiler     (GbpSysprofSurface *self,
+                                                       SpProfiler        *profiler);
+SpCaptureReader *gbp_sysprof_surface_get_reader       (GbpSysprofSurface *self);
+void             gbp_sysprof_surface_set_reader       (GbpSysprofSurface *self,
+                                                       SpCaptureReader   *reader);
+
+G_END_DECLS
diff --git a/src/plugins/sysprof/gbp-sysprof-surface.ui b/src/plugins/sysprof/gbp-sysprof-surface.ui
new file mode 100644
index 000000000..bc2fcbe5c
--- /dev/null
+++ b/src/plugins/sysprof/gbp-sysprof-surface.ui
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="GbpSysprofSurface" parent="IdeSurface">
+    <child>
+      <object class="GtkBox">
+        <property name="orientation">vertical</property>
+        <property name="visible">true</property>
+        <child>
+          <object class="GtkRevealer" id="info_bar_revealer">
+            <property name="visible">true</property>
+            <property name="reveal-child">false</property>
+            <child>
+              <object class="GtkInfoBar" id="info_bar">
+                <property name="visible">true</property>
+                <child internal-child="content_area">
+                  <object class="GtkBox">
+                    <child>
+                      <object class="GtkLabel" id="info_bar_label">
+                        <property name="hexpand">true</property>
+                        <property name="label" translatable="yes">Failure</property>
+                        <property name="visible">true</property>
+                        <property name="wrap">true</property>
+                        <property name="xalign">0</property>
+                      </object>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="info_bar_close">
+                        <property name="label" translatable="yes">_Close</property>
+                        <property name="use-underline">true</property>
+                        <property name="visible">true</property>
+                        <property name="width-request">100</property>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+                <action-widgets>
+                  <action-widget response="0">info_bar_close</action-widget>
+                </action-widgets>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStack" id="stack">
+            <property name="homogeneous">false</property>
+            <property name="visible">true</property>
+            <child>
+              <object class="SpEmptyStateView">
+                <property name="subtitle" translatable="yes" comments="the action:// link is used to run the 
project">Select &lt;a href="action://run-manager.run-with-handler::profiler"&gt;Run with profiler&lt;/a&gt; 
from the run menu to begin</property>
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="name">empty</property>
+              </packing>
+            </child>
+            <child>
+              <object class="SpRecordingStateView" id="recording_view">
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="name">recording</property>
+              </packing>
+            </child>
+            <child>
+              <object class="SpFailedStateView" id="failed_view">
+                <property name="visible">true</property>
+              </object>
+              <packing>
+                <property name="name">failed</property>
+              </packing>
+            </child>
+            <child>
+              <object class="DzlMultiPaned">
+                <property name="visible">true</property>
+                <child>
+                  <object class="SpVisualizerView" id="visualizers">
+                    <property name="visible">true</property>
+                    <property name="zoom-manager">zoom_manager</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="SpCallgraphView" id="callgraph_view">
+                    <property name="vexpand">true</property>
+                    <property name="visible">true</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="name">results</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+  <object class="SpZoomManager" id="zoom_manager">
+  </object>
+</interface>
diff --git a/src/plugins/sysprof/gbp-sysprof-workspace-addin.c 
b/src/plugins/sysprof/gbp-sysprof-workspace-addin.c
new file mode 100644
index 000000000..2266cb377
--- /dev/null
+++ b/src/plugins/sysprof/gbp-sysprof-workspace-addin.c
@@ -0,0 +1,617 @@
+/* gbp-sysprof-workspace-addin.c
+ *
+ * Copyright 2016-2019 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
+ */
+
+#include <glib/gi18n.h>
+#include <sysprof.h>
+
+#include "gbp-sysprof-surface.h"
+#include "gbp-sysprof-workspace-addin.h"
+
+struct _GbpSysprofWorkspaceAddin
+{
+  GObject                parent_instance;
+
+  GSimpleActionGroup    *actions;
+  SpProfiler            *profiler;
+
+  GbpSysprofSurface     *surface;
+  IdeWorkspace          *workspace;
+
+  GtkBox                *zoom_controls;
+};
+
+static void workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GbpSysprofWorkspaceAddin, gbp_sysprof_workspace_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
+
+static void
+gbp_sysprof_workspace_addin_update_controls (GbpSysprofWorkspaceAddin *self)
+{
+  IdeSurface *surface;
+  gboolean visible;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
+
+  if (self->workspace == NULL)
+    return;
+
+  surface = ide_workspace_get_visible_surface (self->workspace);
+  visible = GBP_IS_SYSPROF_SURFACE (surface) &&
+            !!gbp_sysprof_surface_get_reader (GBP_SYSPROF_SURFACE (surface));
+
+  if (self->zoom_controls)
+    gtk_widget_set_visible (GTK_WIDGET (self->zoom_controls), visible);
+}
+
+static void
+profiler_stopped (GbpSysprofWorkspaceAddin *self,
+                  SpProfiler               *profiler)
+{
+  g_autoptr(SpCaptureReader) reader = NULL;
+  g_autoptr(GError) error = NULL;
+  SpCaptureWriter *writer;
+
+  IDE_ENTRY;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
+  g_assert (SP_IS_PROFILER (profiler));
+
+  if (self->profiler != profiler)
+    IDE_EXIT;
+
+  if (self->workspace == NULL)
+    IDE_EXIT;
+
+  writer = sp_profiler_get_writer (profiler);
+  reader = sp_capture_writer_create_reader (writer, &error);
+
+  if (reader == NULL)
+    {
+      /* TODO: Propagate error to an infobar or similar */
+      g_warning ("%s", error->message);
+      IDE_EXIT;
+    }
+
+  gbp_sysprof_surface_set_reader (self->surface, reader);
+
+  ide_workspace_set_visible_surface_name (self->workspace, "profiler");
+
+  gbp_sysprof_workspace_addin_update_controls (self);
+
+  IDE_EXIT;
+}
+
+static void
+profiler_child_spawned (GbpSysprofWorkspaceAddin *self,
+                        const gchar              *identifier,
+                        IdeRunner                *runner)
+{
+  GPid pid = 0;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
+  g_assert (identifier != NULL);
+  g_assert (IDE_IS_RUNNER (runner));
+
+  if (!SP_IS_PROFILER (self->profiler))
+    return;
+
+#ifdef G_OS_UNIX
+  pid = g_ascii_strtoll (identifier, NULL, 10);
+#endif
+
+  if G_UNLIKELY (pid == 0)
+    {
+      g_warning ("Failed to parse integer value from %s", identifier);
+      return;
+    }
+
+  IDE_TRACE_MSG ("Adding pid %s to profiler", identifier);
+
+  sp_profiler_add_pid (self->profiler, pid);
+  sp_profiler_start (self->profiler);
+}
+
+static gchar *
+get_runtime_sysroot (IdeContext  *context,
+                     const gchar *path)
+{
+  IdeConfigurationManager *config_manager;
+  IdeConfiguration *config;
+  IdeRuntime *runtime;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_CONTEXT (context));
+
+  config_manager = ide_configuration_manager_from_context (context);
+  config = ide_configuration_manager_get_current (config_manager);
+  runtime = ide_configuration_get_runtime (config);
+
+  if (runtime != NULL)
+    {
+      g_autoptr(GFile) base = g_file_new_for_path (path);
+      g_autoptr(GFile) translated = ide_runtime_translate_file (runtime, base);
+
+      if (translated != NULL)
+        return g_file_get_path (translated);
+    }
+
+  return NULL;
+}
+
+static void
+profiler_run_handler (IdeRunManager *run_manager,
+                      IdeRunner     *runner,
+                      gpointer       user_data)
+{
+  GbpSysprofWorkspaceAddin *self = user_data;
+  g_autoptr(SpSource) proc_source = NULL;
+  g_autoptr(SpSource) perf_source = NULL;
+  g_autoptr(SpSource) hostinfo_source = NULL;
+  g_autoptr(SpSource) memory_source = NULL;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_RUNNER (runner));
+  g_assert (IDE_IS_RUN_MANAGER (run_manager));
+
+  if (SP_IS_PROFILER (self->profiler))
+    {
+      if (sp_profiler_get_is_running (self->profiler))
+        sp_profiler_stop (self->profiler);
+      g_clear_object (&self->profiler);
+    }
+
+  /*
+   * First get a copy of the active runtime and find the root of it's
+   * translation path. That way we can adjust for the sysroot when
+   * resolving symbols.
+   *
+   * TODO: Hardcoding /usr and /app here sucks, we need a way to have
+   *       this in the flatpak plugin instead (and associated plumbing
+   *       to abstract it). We probably should just have a "get_debug_paths"
+   *       type helper from the runtime.
+   */
+  {
+    /* Put debug directories first so the resolve higher */
+    static const gchar *dirs[] = {
+      "/app/lib/debug",
+      "/usr/lib/debug",
+      "/app/bin",
+      "/app/lib",
+      "/usr/lib",
+      NULL
+    };
+
+    context = ide_object_get_context (IDE_OBJECT (run_manager));
+
+    for (guint i = 0; dirs[i]; i++)
+      {
+        g_autofree gchar *path = get_runtime_sysroot (context, dirs[i]);
+
+        if (path != NULL)
+          sp_symbol_dirs_add (path);
+      }
+  }
+
+  self->profiler = sp_local_profiler_new ();
+
+  g_signal_connect_object (self->profiler,
+                           "stopped",
+                           G_CALLBACK (gbp_sysprof_workspace_addin_update_controls),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_widget_hide (GTK_WIDGET (self->zoom_controls));
+
+  /*
+   * Currently we require whole-system because otherwise we can get a situation
+   * where we only watch the spawning process (say jhbuild, flatpak, etc).
+   * Longer term we either need a way to follow-children and/or limit to a
+   * cgroup/process-group.
+   */
+  sp_profiler_set_whole_system (SP_PROFILER (self->profiler), TRUE);
+
+  proc_source = sp_proc_source_new ();
+  sp_profiler_add_source (self->profiler, proc_source);
+
+  perf_source = sp_perf_source_new ();
+  sp_profiler_add_source (self->profiler, perf_source);
+
+  hostinfo_source = sp_hostinfo_source_new ();
+  sp_profiler_add_source (self->profiler, hostinfo_source);
+
+  memory_source = sp_memory_source_new ();
+  sp_profiler_add_source (self->profiler, memory_source);
+
+  /*
+   * TODO:
+   *
+   * We need to synchronize the inferior with the parent here. Ideally, we would
+   * prepend the application launch (to some degree) with the application we want
+   * to execute. In this case, we might want to add a "gnome-builder-sysprof"
+   * helper that will synchronize with the parent, and then block until we start
+   * the process (with the appropriate pid) before exec() otherwise we could
+   * miss the exit of the app and race to add the pid to the profiler.
+   */
+
+  g_signal_connect_object (runner,
+                           "spawned",
+                           G_CALLBACK (profiler_child_spawned),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (self->profiler,
+                           "stopped",
+                           G_CALLBACK (profiler_stopped),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gbp_sysprof_surface_set_profiler (self->surface, self->profiler);
+
+  ide_workspace_set_visible_surface (self->workspace, IDE_SURFACE (self->surface));
+}
+
+static void
+gbp_sysprof_workspace_addin_open_cb (GObject      *object,
+                                     GAsyncResult *result,
+                                     gpointer      user_data)
+{
+  GbpSysprofWorkspaceAddin *self = (GbpSysprofWorkspaceAddin *)object;
+  g_autoptr(SpCaptureReader) reader = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_TASK (result));
+
+  reader = ide_task_propagate_pointer (IDE_TASK (result), &error);
+
+  g_assert (reader || error != NULL);
+
+  if (reader == NULL)
+    {
+      g_message ("%s", error->message);
+      return;
+    }
+
+  gbp_sysprof_surface_set_profiler (self->surface, NULL);
+  gbp_sysprof_surface_set_reader (self->surface, reader);
+
+  gbp_sysprof_workspace_addin_update_controls (self);
+}
+
+static void
+gbp_sysprof_workspace_addin_open_worker (IdeTask      *task,
+                                         gpointer      source_object,
+                                         gpointer      task_data,
+                                         GCancellable *cancellable)
+{
+  g_autofree gchar *path = NULL;
+  g_autoptr(GError) error = NULL;
+  SpCaptureReader *reader;
+  GFile *file = task_data;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (source_object));
+  g_assert (G_IS_FILE (file));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  path = g_file_get_path (file);
+
+  if (NULL == (reader = sp_capture_reader_new (path, &error)))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_pointer (task, reader, (GDestroyNotify)sp_capture_reader_unref);
+}
+
+static void
+gbp_sysprof_workspace_addin_open (GbpSysprofWorkspaceAddin *self,
+                                  GFile                    *file)
+{
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
+  g_assert (G_IS_FILE (file));
+
+  if (!g_file_is_native (file))
+    {
+      g_warning ("Can only open local sysprof capture files.");
+      return;
+    }
+
+  task = ide_task_new (self, NULL, gbp_sysprof_workspace_addin_open_cb, NULL);
+  ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
+  ide_task_run_in_thread (task, gbp_sysprof_workspace_addin_open_worker);
+}
+
+static void
+open_profile_action (GSimpleAction *action,
+                     GVariant      *variant,
+                     gpointer       user_data)
+{
+  GbpSysprofWorkspaceAddin *self = user_data;
+  g_autoptr(GFile) workdir = NULL;
+  GtkFileChooserNative *native;
+  GtkFileFilter *filter;
+  IdeContext *context;
+  gint ret;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (self->workspace));
+  g_assert (GBP_IS_SYSPROF_SURFACE (self->surface));
+
+  ide_workspace_set_visible_surface (self->workspace, IDE_SURFACE (self->surface));
+
+  context = ide_workspace_get_context (self->workspace);
+  workdir = ide_context_ref_workdir (context);
+
+  native = gtk_file_chooser_native_new (_("Open Sysprof Capture…"),
+                                        GTK_WINDOW (self->workspace),
+                                        GTK_FILE_CHOOSER_ACTION_OPEN,
+                                        _("Open"),
+                                        _("Cancel"));
+  gtk_file_chooser_set_current_folder_file (GTK_FILE_CHOOSER (native), workdir, NULL);
+
+  /* Add our filter for sysprof capture files.  */
+  filter = gtk_file_filter_new ();
+  gtk_file_filter_set_name (filter, _("Sysprof Capture (*.syscap)"));
+  gtk_file_filter_add_pattern (filter, "*.syscap");
+  gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (native), filter);
+
+  /* And all files now */
+  filter = gtk_file_filter_new ();
+  gtk_file_filter_set_name (filter, _("All Files"));
+  gtk_file_filter_add_pattern (filter, "*");
+  gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (native), filter);
+
+  /* Unlike gtk_dialog_run(), this will handle processing
+   * various I/O events and so should be safe to use.
+   */
+  ret = gtk_native_dialog_run (GTK_NATIVE_DIALOG (native));
+
+  if (ret == GTK_RESPONSE_ACCEPT)
+    {
+      g_autoptr(GFile) file = NULL;
+
+      file = gtk_file_chooser_get_file (GTK_FILE_CHOOSER (native));
+      if (G_IS_FILE (file))
+        gbp_sysprof_workspace_addin_open (self, file);
+    }
+
+  gtk_native_dialog_hide (GTK_NATIVE_DIALOG (native));
+  gtk_native_dialog_destroy (GTK_NATIVE_DIALOG (native));
+}
+
+static void
+gbp_sysprof_workspace_addin_finalize (GObject *object)
+{
+  GbpSysprofWorkspaceAddin *self = (GbpSysprofWorkspaceAddin *)object;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  g_clear_object (&self->actions);
+
+  G_OBJECT_CLASS (gbp_sysprof_workspace_addin_parent_class)->finalize (object);
+}
+
+static void
+gbp_sysprof_workspace_addin_class_init (GbpSysprofWorkspaceAddinClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_sysprof_workspace_addin_finalize;
+}
+
+static void
+gbp_sysprof_workspace_addin_init (GbpSysprofWorkspaceAddin *self)
+{
+  static const GActionEntry entries[] = {
+    { "open-profile", open_profile_action },
+  };
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+
+  self->actions = g_simple_action_group_new ();
+
+  g_action_map_add_action_entries (G_ACTION_MAP (self->actions),
+                                   entries,
+                                   G_N_ELEMENTS (entries),
+                                   self);
+}
+
+static void
+run_manager_stopped (GbpSysprofWorkspaceAddin *self,
+                     IdeRunManager            *run_manager)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_RUN_MANAGER (run_manager));
+
+  if (self->profiler != NULL && sp_profiler_get_is_running (self->profiler))
+    sp_profiler_stop (self->profiler);
+}
+
+static gboolean
+zoom_level_to_string (GBinding     *binding,
+                      const GValue *from_value,
+                      GValue       *to_value,
+                      gpointer      user_data)
+{
+  gdouble level = g_value_get_double (from_value);
+  g_value_take_string (to_value, g_strdup_printf ("%d%%", (gint)(level * 100.0)));
+  return TRUE;
+}
+
+static void
+gbp_sysprof_workspace_addin_load (IdeWorkspaceAddin *addin,
+                                  IdeWorkspace      *workspace)
+{
+  GbpSysprofWorkspaceAddin *self = (GbpSysprofWorkspaceAddin *)addin;
+  SpZoomManager *zoom_manager;
+  IdeRunManager *run_manager;
+  IdeHeaderBar *header;
+  IdeContext *context;
+  GtkLabel *label;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace));
+
+  self->workspace = workspace;
+
+  context = ide_workspace_get_context (workspace);
+
+  /*
+   * Register our custom run handler to activate the profiler.
+   */
+  run_manager = ide_run_manager_from_context (context);
+  ide_run_manager_add_handler (run_manager,
+                               "profiler",
+                               _("Run with Profiler"),
+                               "utilities-system-monitor-symbolic",
+                               "<primary>F8",
+                               profiler_run_handler,
+                               self,
+                               NULL);
+  g_signal_connect_object (run_manager,
+                           "stopped",
+                           G_CALLBACK (run_manager_stopped),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  /* Add the surface to the workspace. */
+  self->surface = g_object_new (GBP_TYPE_SYSPROF_SURFACE,
+                                "visible", TRUE,
+                                NULL);
+  g_signal_connect (self->surface,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->surface);
+  ide_workspace_add_surface (workspace, IDE_SURFACE (self->surface));
+
+  zoom_manager = gbp_sysprof_surface_get_zoom_manager (self->surface);
+
+  /*
+   * Add our actions to the workspace so they can be activated via the
+   * headerbar or the surface.
+   */
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "profiler", G_ACTION_GROUP (self->actions));
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "profiler-zoom", G_ACTION_GROUP (zoom_manager));
+
+  /* Add our buttons to the header. */
+  header = ide_workspace_get_header_bar (workspace);
+  self->zoom_controls = g_object_new (GTK_TYPE_BOX,
+                                      "orientation", GTK_ORIENTATION_HORIZONTAL,
+                                      NULL);
+  g_signal_connect (self->zoom_controls,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->zoom_controls);
+  dzl_gtk_widget_add_style_class (GTK_WIDGET (self->zoom_controls), "linked");
+  gtk_container_add (GTK_CONTAINER (self->zoom_controls),
+                     g_object_new (GTK_TYPE_BUTTON,
+                                   "action-name", "profiler-zoom.zoom-out",
+                                   "can-focus", FALSE,
+                                   "child", g_object_new (GTK_TYPE_IMAGE,
+                                                          "icon-name", "zoom-out-symbolic",
+                                                          "visible", TRUE,
+                                                          NULL),
+                                   "visible", TRUE,
+                                   NULL));
+  label = g_object_new (GTK_TYPE_LABEL,
+                        "width-chars", 5,
+                        "visible", TRUE,
+                        NULL);
+  g_object_bind_property_full (zoom_manager, "zoom", label, "label", G_BINDING_SYNC_CREATE,
+                               zoom_level_to_string, NULL, NULL, NULL);
+  gtk_container_add (GTK_CONTAINER (self->zoom_controls),
+                     g_object_new (GTK_TYPE_BUTTON,
+                                   "action-name", "profiler-zoom.zoom-one",
+                                   "can-focus", FALSE,
+                                   "child", label,
+                                   "visible", TRUE,
+                                   NULL));
+  gtk_container_add (GTK_CONTAINER (self->zoom_controls),
+                     g_object_new (GTK_TYPE_BUTTON,
+                                   "action-name", "profiler-zoom.zoom-in",
+                                   "can-focus", FALSE,
+                                   "child", g_object_new (GTK_TYPE_IMAGE,
+                                                          "icon-name", "zoom-in-symbolic",
+                                                          "visible", TRUE,
+                                                          NULL),
+                                   "visible", TRUE,
+                                   NULL));
+  ide_header_bar_add_primary (header, GTK_WIDGET (self->zoom_controls));
+}
+
+static void
+gbp_sysprof_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                    IdeWorkspace      *workspace)
+{
+  GbpSysprofWorkspaceAddin *self = (GbpSysprofWorkspaceAddin *)addin;
+  IdeRunManager *run_manager;
+  IdeContext *context;
+
+  g_assert (GBP_IS_SYSPROF_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  context = ide_workspace_get_context (workspace);
+
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "profiler", NULL);
+  gtk_widget_insert_action_group (GTK_WIDGET (workspace), "profiler-zoom", NULL);
+
+  run_manager = ide_run_manager_from_context (context);
+  ide_run_manager_remove_handler (run_manager, "profiler");
+
+  if (self->surface)
+    gtk_widget_destroy (GTK_WIDGET (self->surface));
+
+  if (self->zoom_controls)
+    gtk_widget_destroy (GTK_WIDGET (self->zoom_controls));
+
+  self->zoom_controls = NULL;
+  self->surface = NULL;
+  self->workspace = NULL;
+}
+
+static void
+gbp_sysprof_workspace_addin_surface_set (IdeWorkspaceAddin *addin,
+                                         IdeSurface        *surface)
+{
+  GbpSysprofWorkspaceAddin *self = (GbpSysprofWorkspaceAddin *)addin;
+
+  g_assert (IDE_IS_WORKSPACE_ADDIN (addin));
+  g_assert (!surface || IDE_IS_SURFACE (surface));
+
+  gbp_sysprof_workspace_addin_update_controls (self);
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_sysprof_workspace_addin_load;
+  iface->unload = gbp_sysprof_workspace_addin_unload;
+  iface->surface_set = gbp_sysprof_workspace_addin_surface_set;
+}
diff --git a/src/plugins/sysprof/gbp-sysprof-workspace-addin.h 
b/src/plugins/sysprof/gbp-sysprof-workspace-addin.h
new file mode 100644
index 000000000..e43254ea1
--- /dev/null
+++ b/src/plugins/sysprof/gbp-sysprof-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-sysprof-workspace-addin.h
+ *
+ * Copyright 2016-2019 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-gui.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_SYSPROF_WORKSPACE_ADDIN (gbp_sysprof_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpSysprofWorkspaceAddin, gbp_sysprof_workspace_addin, GBP, SYSPROF_WORKSPACE_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/sysprof/gtk/menus.ui b/src/plugins/sysprof/gtk/menus.ui
index 5b2c5ad47..03ddad217 100644
--- a/src/plugins/sysprof/gtk/menus.ui
+++ b/src/plugins/sysprof/gtk/menus.ui
@@ -10,13 +10,13 @@
       </item>
     </section>
   </menu>
-  <menu id="perspectives-menu">
-    <section id="perspectives-menu-section">
+  <menu id="ide-primary-workspace-surfaces-menu">
+    <section id="ide-primary-workspace-surfaces-menu-section">
       <item>
         <attribute name="accel">&lt;alt&gt;3</attribute>
-        <attribute name="action">win.perspective</attribute>
-        <attribute name="after">perspective-menu-editor</attribute>
-        <attribute name="id">perspective-menu-profiler</attribute>
+        <attribute name="action">win.surface</attribute>
+        <attribute name="after">surface-menu-config</attribute>
+        <attribute name="id">surface-menu-profiler</attribute>
         <attribute name="label" translatable="yes">Profiler</attribute>
         <attribute name="role">normal</attribute>
         <attribute name="target">profiler</attribute>
@@ -37,4 +37,14 @@
       </item>
     </section>
   </menu>
+  <menu id="project-tree-run-with-submenu">
+    <section id="project-tree-menu-run-with-section">
+      <item>
+        <attribute name="id">project-tree-menu-profiler</attribute>
+        <attribute name="label" translatable="yes">Run with Profiler</attribute>
+        <attribute name="action">buildui.run-with-handler</attribute>
+        <attribute name="target" type="s">'profiler'</attribute>
+      </item>
+    </section>
+  </menu>
 </interface>
diff --git a/src/plugins/sysprof/meson.build b/src/plugins/sysprof/meson.build
index 31849c907..4930297d9 100644
--- a/src/plugins/sysprof/meson.build
+++ b/src/plugins/sysprof/meson.build
@@ -1,25 +1,24 @@
-if get_option('with_sysprof')
+if get_option('plugin_sysprof')
 
-sysprof_resources = gnome.compile_resources(
-  'gbp-sysprof-resources',
-  'sysprof.gresource.xml',
-  c_name: 'gbp_sysprof',
-)
-
-sysprof_sources = [
-  'gbp-sysprof-plugin.c',
-  'gbp-sysprof-perspective.c',
-  'gbp-sysprof-perspective.h',
-  'gbp-sysprof-workbench-addin.c',
-  'gbp-sysprof-workbench-addin.h',
-]
-
-gnome_builder_plugins_deps += [
+plugins_deps += [
   dependency('sysprof-2', version: '>= 3.31.1'),
   dependency('sysprof-ui-2', version: '>= 3.31.1'),
 ]
 
-gnome_builder_plugins_sources += files(sysprof_sources)
-gnome_builder_plugins_sources += sysprof_resources[0]
+plugins_sources += files([
+  'sysprof-plugin.c',
+  'gbp-sysprof-surface.c',
+  'gbp-sysprof-surface.h',
+  'gbp-sysprof-workspace-addin.c',
+  'gbp-sysprof-workspace-addin.h',
+])
+
+plugin_sysprof_resources = gnome.compile_resources(
+  'sysprof-resources',
+  'sysprof.gresource.xml',
+  c_name: 'gbp_sysprof',
+)
+
+plugins_sources += plugin_sysprof_resources[0]
 
 endif
diff --git a/src/plugins/sysprof/sysprof-plugin.c b/src/plugins/sysprof/sysprof-plugin.c
new file mode 100644
index 000000000..5654cc39a
--- /dev/null
+++ b/src/plugins/sysprof/sysprof-plugin.c
@@ -0,0 +1,39 @@
+/* sysprof-plugin.c
+ *
+ * Copyright 2016-2019 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 "sysprof-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+#include <sysprof.h>
+
+#include "gbp-sysprof-workspace-addin.h"
+
+_IDE_EXTERN void
+_gbp_sysprof_register_types (PeasObjectModule *module)
+{
+  sp_clock_init ();
+
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_SYSPROF_WORKSPACE_ADDIN);
+}
diff --git a/src/plugins/sysprof/sysprof.gresource.xml b/src/plugins/sysprof/sysprof.gresource.xml
index 3d6cdc782..1d23d1167 100644
--- a/src/plugins/sysprof/sysprof.gresource.xml
+++ b/src/plugins/sysprof/sysprof.gresource.xml
@@ -1,11 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/sysprof">
     <file>sysprof.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/sysprof-plugin">
     <file>gtk/menus.ui</file>
     <file>themes/shared.css</file>
-    <file>gbp-sysprof-perspective.ui</file>
+    <file>gbp-sysprof-surface.ui</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/sysprof/sysprof.plugin b/src/plugins/sysprof/sysprof.plugin
index 1fbd71991..602e998a0 100644
--- a/src/plugins/sysprof/sysprof.plugin
+++ b/src/plugins/sysprof/sysprof.plugin
@@ -1,8 +1,10 @@
 [Plugin]
-Module=sysprof-plugin
-Name=Sysprof
-Description=Integration with the Sysprof system profiler
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2016 Christian Hergert
 Builtin=true
-Embedded=gbp_sysprof_register_types
+Copyright=Copyright © 2016-2018 Christian Hergert
+Description=Integration with the Sysprof system profiler
+Embedded=_gbp_sysprof_register_types
+Hidden=true
+Module=sysprof
+Name=Sysprof
+X-Workspace-Kind=primary;
diff --git a/src/plugins/sysroot/gbp-sysroot-manager.c b/src/plugins/sysroot/gbp-sysroot-manager.c
index 0732667a2..eeb0d90ce 100644
--- a/src/plugins/sysroot/gbp-sysroot-manager.c
+++ b/src/plugins/sysroot/gbp-sysroot-manager.c
@@ -1,7 +1,7 @@
 /* gbp-sysroot-manager.c
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,6 +15,8 @@
  *
  * 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-sysroot-manager"
diff --git a/src/plugins/sysroot/gbp-sysroot-manager.h b/src/plugins/sysroot/gbp-sysroot-manager.h
index 5da6e30db..dda3c7eb3 100644
--- a/src/plugins/sysroot/gbp-sysroot-manager.h
+++ b/src/plugins/sysroot/gbp-sysroot-manager.h
@@ -1,7 +1,7 @@
 /* gbp-sysroot-manager.h
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/sysroot/gbp-sysroot-preferences-addin.c 
b/src/plugins/sysroot/gbp-sysroot-preferences-addin.c
index bce87b950..6e27005e6 100644
--- a/src/plugins/sysroot/gbp-sysroot-preferences-addin.c
+++ b/src/plugins/sysroot/gbp-sysroot-preferences-addin.c
@@ -1,7 +1,7 @@
 /* gbp-sysroot-preferences.c
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,14 @@
  *
  * 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-sysroot-preferences-addin"
 
 #include <glib/gi18n.h>
+#include <libide-gui.h>
 
 #include "gbp-sysroot-preferences-addin.h"
 #include "gbp-sysroot-preferences-row.h"
diff --git a/src/plugins/sysroot/gbp-sysroot-preferences-addin.h 
b/src/plugins/sysroot/gbp-sysroot-preferences-addin.h
index e8a4072cc..ab888f912 100644
--- a/src/plugins/sysroot/gbp-sysroot-preferences-addin.h
+++ b/src/plugins/sysroot/gbp-sysroot-preferences-addin.h
@@ -1,7 +1,7 @@
 /* gbp-sysroot-preferences.h
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/sysroot/gbp-sysroot-preferences-row.c 
b/src/plugins/sysroot/gbp-sysroot-preferences-row.c
index 9eb3a37d0..778ddcbce 100644
--- a/src/plugins/sysroot/gbp-sysroot-preferences-row.c
+++ b/src/plugins/sysroot/gbp-sysroot-preferences-row.c
@@ -1,7 +1,7 @@
 /* gbp-sysroot-preferences-row.c
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,6 +15,8 @@
  *
  * 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-sysroot-preferences-row"
@@ -303,7 +305,7 @@ gbp_sysroot_preferences_row_class_init (GbpSysrootPreferencesRowClass *klass)
 
   g_object_class_install_properties (object_class, N_PROPS, properties);
 
-  gtk_widget_class_set_template_from_resource (widget_class, 
"/org/gnome/builder/plugins/sysroot-plugin/gbp-sysroot-preferences-row.ui");
+  gtk_widget_class_set_template_from_resource (widget_class, 
"/plugins/sysroot/gbp-sysroot-preferences-row.ui");
   gtk_widget_class_bind_template_child (widget_class, GbpSysrootPreferencesRow, display_name);
   gtk_widget_class_bind_template_child (widget_class, GbpSysrootPreferencesRow, popover);
   gtk_widget_class_bind_template_child (widget_class, GbpSysrootPreferencesRow, name_entry);
diff --git a/src/plugins/sysroot/gbp-sysroot-preferences-row.h 
b/src/plugins/sysroot/gbp-sysroot-preferences-row.h
index ac3e08206..4d397084b 100644
--- a/src/plugins/sysroot/gbp-sysroot-preferences-row.h
+++ b/src/plugins/sysroot/gbp-sysroot-preferences-row.h
@@ -1,7 +1,7 @@
 /* gbp-sysroot-preferences-row.h
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/sysroot/gbp-sysroot-runtime-provider.c 
b/src/plugins/sysroot/gbp-sysroot-runtime-provider.c
index f89ff5992..8a30570ea 100644
--- a/src/plugins/sysroot/gbp-sysroot-runtime-provider.c
+++ b/src/plugins/sysroot/gbp-sysroot-runtime-provider.c
@@ -1,7 +1,7 @@
 /* gbp-sysroot-runtime-provider.c
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,6 +15,8 @@
  *
  * 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-sysroot-runtime-provider"
@@ -67,10 +69,13 @@ sysroot_runtime_provider_add_target (GbpSysrootRuntimeProvider *self,
                                      const gchar               *target)
 {
   g_autoptr(GbpSysrootRuntime) runtime = NULL;
-  IdeContext *context = NULL;
 
-  context = ide_object_get_context (IDE_OBJECT (self->runtime_manager));
-  runtime = gbp_sysroot_runtime_new (context, target);
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_SYSROOT_RUNTIME_PROVIDER (self));
+  g_assert (target != NULL);
+
+  runtime = gbp_sysroot_runtime_new (target);
+  ide_object_append (IDE_OBJECT (self), IDE_OBJECT (runtime));
 
   ide_runtime_manager_add (self->runtime_manager, IDE_RUNTIME (runtime));
   g_ptr_array_add (self->runtimes, g_steal_pointer (&runtime));
@@ -93,13 +98,11 @@ sysroot_runtime_provider_target_changed (GbpSysrootRuntimeProvider
 static void
 gbp_sysroot_runtime_provider_class_init (GbpSysrootRuntimeProviderClass *klass)
 {
-  
 }
 
 static void
 gbp_sysroot_runtime_provider_init (GbpSysrootRuntimeProvider *self)
 {
-  
 }
 
 static void
diff --git a/src/plugins/sysroot/gbp-sysroot-runtime-provider.h 
b/src/plugins/sysroot/gbp-sysroot-runtime-provider.h
index bb20f477e..688bc2479 100644
--- a/src/plugins/sysroot/gbp-sysroot-runtime-provider.h
+++ b/src/plugins/sysroot/gbp-sysroot-runtime-provider.h
@@ -1,7 +1,7 @@
 /* gbp-sysroot-runtime-provider.h
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/sysroot/gbp-sysroot-runtime.c b/src/plugins/sysroot/gbp-sysroot-runtime.c
index 58d3eaeba..86bf89eaa 100644
--- a/src/plugins/sysroot/gbp-sysroot-runtime.c
+++ b/src/plugins/sysroot/gbp-sysroot-runtime.c
@@ -1,7 +1,7 @@
 /* gbp-sysroot-runtime.c
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,6 +15,8 @@
  *
  * 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-sysroot-runtime"
@@ -36,19 +38,16 @@ struct _GbpSysrootRuntime
 G_DEFINE_TYPE (GbpSysrootRuntime, gbp_sysroot_runtime, IDE_TYPE_RUNTIME)
 
 GbpSysrootRuntime *
-gbp_sysroot_runtime_new (IdeContext  *context,
-                         const gchar *sysroot_id)
+gbp_sysroot_runtime_new (const gchar *sysroot_id)
 {
   g_autoptr(GbpSysrootRuntime) runtime = NULL;
   g_autofree gchar *built_id = NULL;
 
-  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
   g_return_val_if_fail (sysroot_id != NULL, NULL);
 
   built_id = g_strconcat (RUNTIME_PREFIX, sysroot_id, NULL);
   runtime = g_object_new (GBP_TYPE_SYSROOT_RUNTIME,
                           "id", built_id,
-                          "context", context,
                           "display-name", "",
                           NULL);
 
@@ -71,7 +70,7 @@ gbp_sysroot_runtime_get_sysroot_id (GbpSysrootRuntime *self)
   if (!g_str_has_prefix (runtime_id, RUNTIME_PREFIX))
     return runtime_id;
 
-  return runtime_id + strlen(RUNTIME_PREFIX);
+  return runtime_id + strlen (RUNTIME_PREFIX);
 }
 
 static IdeSubprocessLauncher *
@@ -227,10 +226,10 @@ gbp_sysroot_runtime_constructed (GObject *object)
   ide_runtime_set_display_name (IDE_RUNTIME (object), display_name);
 
   g_signal_connect_object (sysroot_manager,
-                            "target-name-changed",
-                            G_CALLBACK (sysroot_runtime_target_name_changed),
-                            object,
-                            G_CONNECT_SWAPPED);
+                           "target-name-changed",
+                           G_CALLBACK (sysroot_runtime_target_name_changed),
+                           object,
+                           G_CONNECT_SWAPPED);
 
   G_OBJECT_CLASS (gbp_sysroot_runtime_parent_class)->constructed (object);
 }
diff --git a/src/plugins/sysroot/gbp-sysroot-runtime.h b/src/plugins/sysroot/gbp-sysroot-runtime.h
index 11504a3c1..a8c225938 100644
--- a/src/plugins/sysroot/gbp-sysroot-runtime.h
+++ b/src/plugins/sysroot/gbp-sysroot-runtime.h
@@ -1,7 +1,7 @@
 /* gbp-sysroot-runtime.h
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
@@ -27,8 +29,7 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (GbpSysrootRuntime, gbp_sysroot_runtime, GBP, SYSROOT_RUNTIME, IdeRuntime)
 
-GbpSysrootRuntime  *gbp_sysroot_runtime_new            (IdeContext        *context,
-                                                        const gchar       *sysroot_id);
+GbpSysrootRuntime  *gbp_sysroot_runtime_new            (const gchar       *sysroot_id);
 const gchar        *gbp_sysroot_runtime_get_sysroot_id (GbpSysrootRuntime *self);
 
 G_END_DECLS
diff --git a/src/plugins/sysroot/gbp-sysroot-subprocess-launcher.c 
b/src/plugins/sysroot/gbp-sysroot-subprocess-launcher.c
index dc2b155ea..4d0e23a58 100644
--- a/src/plugins/sysroot/gbp-sysroot-subprocess-launcher.c
+++ b/src/plugins/sysroot/gbp-sysroot-subprocess-launcher.c
@@ -1,7 +1,7 @@
 /* gbp-sysroot-subprocess-launcher.c
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,6 +15,8 @@
  *
  * 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-sysroot-subprocess-launcher"
diff --git a/src/plugins/sysroot/gbp-sysroot-subprocess-launcher.h 
b/src/plugins/sysroot/gbp-sysroot-subprocess-launcher.h
index 3c766da71..166db6eae 100644
--- a/src/plugins/sysroot/gbp-sysroot-subprocess-launcher.h
+++ b/src/plugins/sysroot/gbp-sysroot-subprocess-launcher.h
@@ -1,7 +1,7 @@
 /* gbp-sysroot-subprocess-launcher.h
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,11 +15,13 @@
  *
  * 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 <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/sysroot/gbp-sysroot-toolchain-provider.c 
b/src/plugins/sysroot/gbp-sysroot-toolchain-provider.c
index f12532e6d..89c858ba6 100644
--- a/src/plugins/sysroot/gbp-sysroot-toolchain-provider.c
+++ b/src/plugins/sysroot/gbp-sysroot-toolchain-provider.c
@@ -16,6 +16,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #define G_LOG_DOMAIN "gbp-sysroot-toolchain-provider"
@@ -139,14 +141,12 @@ gbp_sysroot_toolchain_provider_try_poky (GbpSysrootToolchainProvider *self,
       g_autofree gchar *sdk_pkg_config_path = NULL;
       g_autofree gchar *qemu_static_name = NULL;
       g_autofree gchar *qemu_static_path = NULL;
-      IdeContext *context;
 
       sdk_file = g_file_new_for_path (sdk_path);
       sdk_canonical_path = g_file_get_path (sdk_file);
       toolchain_id = g_strdup_printf ("sysroot:%s", sdk_canonical_path);
       display_name = g_strdup_printf (_("%s (Sysroot SDK)"), sdk_canonical_path);
-      context = ide_object_get_context (IDE_OBJECT (self));
-      toolchain = ide_simple_toolchain_new (context, toolchain_id, display_name);
+      toolchain = ide_simple_toolchain_new (toolchain_id, display_name);
       ide_toolchain_set_host_triplet (IDE_TOOLCHAIN (toolchain), sysroot_triplet);
 
       sdk_tools_path = g_build_filename (sdk_canonical_path, "usr", "bin", sysroot_basename, NULL);
@@ -314,5 +314,5 @@ gbp_sysroot_toolchain_provider_class_init (GbpSysrootToolchainProviderClass *kla
 static void
 gbp_sysroot_toolchain_provider_init (GbpSysrootToolchainProvider *self)
 {
-  
+
 }
diff --git a/src/plugins/sysroot/gbp-sysroot-toolchain-provider.h 
b/src/plugins/sysroot/gbp-sysroot-toolchain-provider.h
index d64c811ea..652faecc3 100644
--- a/src/plugins/sysroot/gbp-sysroot-toolchain-provider.h
+++ b/src/plugins/sysroot/gbp-sysroot-toolchain-provider.h
@@ -16,11 +16,13 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *
  * Authors: Corentin Noël <corentin noel collabora com>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
 #pragma once
 
-#include <ide.h>
+#include <libide-foundry.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/sysroot/meson.build b/src/plugins/sysroot/meson.build
index 01c2b4d72..aad3d6229 100644
--- a/src/plugins/sysroot/meson.build
+++ b/src/plugins/sysroot/meson.build
@@ -1,30 +1,22 @@
-if get_option('with_sysroot')
+if get_option('plugin_sysroot')
 
-sysroot_resources = gnome.compile_resources(
-  'sysroot-resources',
-  'sysroot.gresource.xml',
-  c_name: 'gbp_sysroot',
-)
-
-sysroot_sources = [
+plugins_sources += files([
   'sysroot-plugin.c',
   'gbp-sysroot-manager.c',
-  'gbp-sysroot-manager.h',
   'gbp-sysroot-preferences-addin.c',
-  'gbp-sysroot-preferences-addin.h',
   'gbp-sysroot-preferences-row.c',
-  'gbp-sysroot-preferences-row.h',
   'gbp-sysroot-runtime.c',
-  'gbp-sysroot-runtime.h',
   'gbp-sysroot-runtime-provider.c',
-  'gbp-sysroot-runtime-provider.h',
   'gbp-sysroot-subprocess-launcher.c',
-  'gbp-sysroot-subprocess-launcher.h',
   'gbp-sysroot-toolchain-provider.c',
-  'gbp-sysroot-toolchain-provider.h'
-]
+])
+
+plugin_sysroot_resources = gnome.compile_resources(
+  'sysroot-resources',
+  'sysroot.gresource.xml',
+  c_name: 'gbp_sysroot',
+)
 
-gnome_builder_plugins_sources += files(sysroot_sources)
-gnome_builder_plugins_sources += sysroot_resources[0]
+plugins_sources += plugin_sysroot_resources[0]
 
 endif
diff --git a/src/plugins/sysroot/sysroot-plugin.c b/src/plugins/sysroot/sysroot-plugin.c
index b84f91669..4da9b9cc3 100644
--- a/src/plugins/sysroot/sysroot-plugin.c
+++ b/src/plugins/sysroot/sysroot-plugin.c
@@ -1,7 +1,7 @@
 /* sysroot-plugin.c
  *
- * Copyright (C) 2018 Corentin Noël <corentin noel collabora com>
- * Copyright (C) 2018 Collabora Ltd.
+ * Copyright 2018 Corentin Noël <corentin noel collabora com>
+ * Copyright 2018 Collabora Ltd.
  *
  * 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
@@ -15,18 +15,30 @@
  *
  * 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
  */
 
+#include "config.h"
+
 #include <libpeas/peas.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
 
 #include "gbp-sysroot-runtime-provider.h"
 #include "gbp-sysroot-preferences-addin.h"
 #include "gbp-sysroot-toolchain-provider.h"
 
-void
-gbp_sysroot_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_gbp_sysroot_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module, IDE_TYPE_RUNTIME_PROVIDER, 
GBP_TYPE_SYSROOT_RUNTIME_PROVIDER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_PREFERENCES_ADDIN, 
GBP_TYPE_SYSROOT_PREFERENCES_ADDIN);
-  peas_object_module_register_extension_type (module, IDE_TYPE_TOOLCHAIN_PROVIDER, 
GBP_TYPE_SYSROOT_TOOLCHAIN_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_RUNTIME_PROVIDER,
+                                              GBP_TYPE_SYSROOT_RUNTIME_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_SYSROOT_PREFERENCES_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_TOOLCHAIN_PROVIDER,
+                                              GBP_TYPE_SYSROOT_TOOLCHAIN_PROVIDER);
 }
diff --git a/src/plugins/sysroot/sysroot.gresource.xml b/src/plugins/sysroot/sysroot.gresource.xml
index 298d19cef..58b98e0c8 100644
--- a/src/plugins/sysroot/sysroot.gresource.xml
+++ b/src/plugins/sysroot/sysroot.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/sysroot">
     <file>sysroot.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/sysroot-plugin">
     <file>gbp-sysroot-preferences-row.ui</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/sysroot/sysroot.plugin b/src/plugins/sysroot/sysroot.plugin
index e98638bc7..64c2d23a5 100644
--- a/src/plugins/sysroot/sysroot.plugin
+++ b/src/plugins/sysroot/sysroot.plugin
@@ -1,8 +1,10 @@
 [Plugin]
-Module=sysroot-plugin
-Name=Sysroot Support
-Description=Provides sysroot support
 Authors=Corentin Noël <corentin noel collabora com>
-Copyright=Copyright © 2018 Collabora Ltd.
 Builtin=true
-Embedded=gbp_sysroot_register_types
+Copyright=Copyright © 2018 Collabora Ltd.
+Depends=buildui;
+Description=Provides sysroot support
+Embedded=_gbp_sysroot_register_types
+Hidden=true
+Module=sysroot
+Name=Sysroot Support
diff --git a/src/plugins/terminal/gbp-terminal-application-addin.c 
b/src/plugins/terminal/gbp-terminal-application-addin.c
new file mode 100644
index 000000000..9964b74cc
--- /dev/null
+++ b/src/plugins/terminal/gbp-terminal-application-addin.c
@@ -0,0 +1,88 @@
+/* gbp-terminal-application-addin.c
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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-terminal-application-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+#include <libide-terminal.h>
+
+#include "gbp-terminal-application-addin.h"
+
+struct _GbpTerminalApplicationAddin
+{
+  GObject parent_instance;
+};
+
+static void
+gbp_terminal_application_addin_handle_command_line (IdeApplicationAddin     *addin,
+                                                    IdeApplication          *application,
+                                                    GApplicationCommandLine *cmdline)
+{
+  IdeApplication *app = (IdeApplication *)application;
+  GVariantDict *options;
+
+  g_assert (IDE_IS_APPLICATION_ADDIN (addin));
+  g_assert (IDE_IS_APPLICATION (app));
+  g_assert (G_IS_APPLICATION_COMMAND_LINE (cmdline));
+
+  if ((options = g_application_command_line_get_options_dict (cmdline)) &&
+      g_variant_dict_contains (options, "terminal"))
+    ide_application_set_workspace_type (application, IDE_TYPE_TERMINAL_WORKSPACE);
+}
+
+static void
+gbp_terminal_application_addin_add_option_entries (IdeApplicationAddin *addin,
+                                                   IdeApplication      *app)
+{
+  g_assert (GBP_IS_TERMINAL_APPLICATION_ADDIN (addin));
+  g_assert (G_IS_APPLICATION (app));
+
+  g_application_add_main_option (G_APPLICATION (app),
+                                 "terminal",
+                                 't',
+                                 G_OPTION_FLAG_IN_MAIN,
+                                 G_OPTION_ARG_NONE,
+                                 _("Use terminal interface"),
+                                 NULL);
+}
+
+static void
+application_addin_iface_init (IdeApplicationAddinInterface *iface)
+{
+  iface->add_option_entries = gbp_terminal_application_addin_add_option_entries;
+  iface->handle_command_line = gbp_terminal_application_addin_handle_command_line;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpTerminalApplicationAddin, gbp_terminal_application_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_APPLICATION_ADDIN,
+                                                application_addin_iface_init))
+
+static void
+gbp_terminal_application_addin_class_init (GbpTerminalApplicationAddinClass *klass)
+{
+}
+
+static void
+gbp_terminal_application_addin_init (GbpTerminalApplicationAddin *self)
+{
+}
diff --git a/src/plugins/terminal/gbp-terminal-application-addin.h 
b/src/plugins/terminal/gbp-terminal-application-addin.h
new file mode 100644
index 000000000..383ec54cb
--- /dev/null
+++ b/src/plugins/terminal/gbp-terminal-application-addin.h
@@ -0,0 +1,31 @@
+/* gbp-terminal-application-addin.h
+ *
+ * Copyright 2018 Christian Hergert <unknown domain org>
+ *
+ * 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_TERMINAL_APPLICATION_ADDIN (gbp_terminal_application_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpTerminalApplicationAddin, gbp_terminal_application_addin, GBP, 
TERMINAL_APPLICATION_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/terminal/gbp-terminal-workspace-addin.c 
b/src/plugins/terminal/gbp-terminal-workspace-addin.c
new file mode 100644
index 000000000..9d7b79c35
--- /dev/null
+++ b/src/plugins/terminal/gbp-terminal-workspace-addin.c
@@ -0,0 +1,460 @@
+/* gbp-terminal-workspace-addin.c
+ *
+ * Copyright 2018-2019 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-terminal-workspace-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-editor.h>
+#include <libide-foundry.h>
+#include <libide-terminal.h>
+#include <libide-gui.h>
+
+#include "gbp-terminal-workspace-addin.h"
+
+struct _GbpTerminalWorkspaceAddin
+{
+  GObject          parent_instance;
+
+  IdeWorkspace    *workspace;
+
+  DzlDockWidget   *bottom_dock;
+  IdeTerminalPage *bottom;
+
+  DzlDockWidget   *run_panel;
+  IdeTerminalPage *run_terminal;
+};
+
+static IdeRuntime *
+find_runtime (IdeWorkspace *workspace)
+{
+  IdeContext *context;
+  IdeConfigurationManager *config_manager;
+  IdeConfiguration *config;
+
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  context = ide_workspace_get_context (workspace);
+  config_manager = ide_configuration_manager_from_context (context);
+  config = ide_configuration_manager_get_current (config_manager);
+
+  return ide_configuration_get_runtime (config);
+}
+
+static gchar *
+find_builddir (IdeWorkspace *workspace)
+{
+  IdeContext *context;
+  IdeBuildManager *build_manager;
+  IdeBuildPipeline *pipeline;
+  const gchar *builddir = NULL;
+
+  if ((context = ide_workspace_get_context (workspace)) &&
+      (build_manager = ide_build_manager_from_context (context)) &&
+      (pipeline = ide_build_manager_get_pipeline (build_manager)) &&
+      (builddir = ide_build_pipeline_get_builddir (pipeline)) &&
+      g_file_test (builddir, G_FILE_TEST_IS_DIR))
+    return g_strdup (builddir);
+
+  return NULL;
+}
+
+static void
+new_terminal_activate (GSimpleAction *action,
+                       GVariant      *param,
+                       gpointer       user_data)
+{
+  GbpTerminalWorkspaceAddin *self = user_data;
+  g_autofree gchar *cwd = NULL;
+  IdeTerminalPage *page;
+  IdeSurface *surface;
+  IdeRuntime *runtime = NULL;
+  const gchar *name;
+  gboolean run_on_host = TRUE;
+  gboolean use_runner = FALSE;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (G_IS_SIMPLE_ACTION (action));
+  g_assert (GBP_IS_TERMINAL_WORKSPACE_ADDIN (self));
+
+  name = g_action_get_name (G_ACTION (action));
+
+  if (ide_str_equal0 (name, "new-terminal-in-runtime"))
+    {
+      runtime = find_runtime (self->workspace);
+      cwd = find_builddir (self->workspace);
+    }
+  else if (ide_str_equal0 (name, "debug-terminal"))
+    run_on_host = FALSE;
+
+  if (ide_str_equal0 (name, "new-terminal-in-runner"))
+    {
+      runtime = find_runtime (self->workspace);
+      use_runner = TRUE;
+    }
+
+  if (!(surface = ide_workspace_get_surface_by_name (self->workspace, "editor")) &&
+      !(surface = ide_workspace_get_surface_by_name (self->workspace, "terminal")))
+    return;
+
+  ide_workspace_set_visible_surface (self->workspace, surface);
+
+  if (IDE_IS_EDITOR_SURFACE (surface) && ide_str_equal0 (name, "new-terminal-in-dir"))
+    {
+      IdePage *editor = ide_editor_surface_get_active_page (IDE_EDITOR_SURFACE (surface));
+
+      if (IDE_IS_EDITOR_PAGE (editor))
+        {
+          IdeBuffer *buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (editor));
+
+          if (buffer != NULL)
+            {
+              GFile *file = ide_buffer_get_file (buffer);
+              g_autoptr(GFile) parent = g_file_get_parent (file);
+
+              cwd = g_file_get_path (parent);
+            }
+        }
+    }
+
+  page = g_object_new (IDE_TYPE_TERMINAL_PAGE,
+                       "cwd", cwd,
+                       "run-on-host", run_on_host,
+                       "runtime", runtime,
+                       "use-runner", use_runner,
+                       "visible", TRUE,
+                       NULL);
+  gtk_container_add (GTK_CONTAINER (surface), GTK_WIDGET (page));
+
+  ide_widget_reveal_and_grab (GTK_WIDGET (page));
+}
+
+static void
+on_run_manager_run (GbpTerminalWorkspaceAddin *self,
+                    IdeRunner                *runner,
+                    IdeRunManager            *run_manager)
+{
+  IdeEnvironment *env;
+  VtePty *pty = NULL;
+  int tty_fd;
+  g_autoptr(GDateTime) now = NULL;
+  g_autofree gchar *formatted = NULL;
+  g_autofree gchar *tmp = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (GBP_IS_TERMINAL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_RUNNER (runner));
+  g_assert (IDE_IS_RUN_MANAGER (run_manager));
+
+  /*
+   * We need to create a new or re-use our existing terminal page
+   * for run output. Additionally, we need to override the stdin,
+   * stdout, and stderr file-descriptors to our pty master for the
+   * terminal instance.
+   */
+
+  pty = vte_pty_new_sync (VTE_PTY_DEFAULT, NULL, NULL);
+
+  if (pty == NULL)
+    {
+      g_warning ("Failed to allocate PTY for run output");
+      IDE_GOTO (failure);
+    }
+
+  if (self->run_terminal == NULL)
+    {
+      IdeSurface *surface;
+      GtkWidget *bottom_pane;
+
+      self->run_terminal = g_object_new (IDE_TYPE_TERMINAL_PAGE,
+                                         "manage-spawn", FALSE,
+                                         "pty", pty,
+                                         "visible", TRUE,
+                                         NULL);
+      g_signal_connect (self->run_terminal,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        &self->run_terminal);
+
+      self->run_panel = g_object_new (DZL_TYPE_DOCK_WIDGET,
+                                      "child", self->run_terminal,
+                                      "expand", TRUE,
+                                      "icon-name", "system-run-symbolic",
+                                      "title", _("Application Output"),
+                                      "visible", TRUE,
+                                      NULL);
+      g_signal_connect (self->run_panel,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        &self->run_panel);
+
+      surface = ide_workspace_get_surface_by_name (self->workspace, "editor");
+      g_assert (IDE_IS_EDITOR_SURFACE (surface));
+
+      bottom_pane = ide_editor_surface_get_utilities (IDE_EDITOR_SURFACE (surface));
+      gtk_container_add (GTK_CONTAINER (bottom_pane), GTK_WIDGET (self->run_panel));
+    }
+  else
+    {
+      ide_terminal_page_set_pty (self->run_terminal, pty);
+    }
+
+  if (-1 != (tty_fd = ide_vte_pty_create_slave (pty)))
+    {
+      ide_runner_set_tty (runner, tty_fd);
+      close (tty_fd);
+    }
+
+  env = ide_runner_get_environment (runner);
+  ide_environment_setenv (env, "TERM", "xterm-256color");
+  ide_environment_setenv (env, "INSIDE_GNOME_BUILDER", PACKAGE_VERSION);
+
+  now = g_date_time_new_now_local ();
+  tmp = g_date_time_format (now, "%X");
+
+  /* translators: %s is replaced with the current local time of day */
+  formatted = g_strdup_printf (_("Application started at %s\r\n"), tmp);
+
+  ide_terminal_page_feed (self->run_terminal, formatted);
+
+  dzl_dock_item_present (DZL_DOCK_ITEM (self->run_panel));
+
+failure:
+
+  g_clear_object (&pty);
+
+  IDE_EXIT;
+}
+
+static void
+on_run_manager_stopped (GbpTerminalWorkspaceAddin *self,
+                        IdeRunManager             *run_manager)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_TERMINAL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_RUN_MANAGER (run_manager));
+
+  ide_terminal_page_feed (self->run_terminal, _("Application exited\r\n"));
+}
+
+static const GActionEntry terminal_actions[] = {
+  { "new-terminal", new_terminal_activate },
+  { "new-terminal-in-runner", new_terminal_activate },
+  { "new-terminal-in-runtime", new_terminal_activate },
+  { "new-terminal-in-dir", new_terminal_activate },
+  { "debug-terminal", new_terminal_activate },
+};
+
+#define I_ g_intern_string
+
+static const DzlShortcutEntry gbp_terminal_shortcut_entries[] = {
+  { "org.gnome.builder.workspace.new-terminal",
+    0, NULL,
+    NC_("shortcut window", "Workspace shortcuts"),
+    NC_("shortcut window", "General"),
+    NC_("shortcut window", "Terminal") },
+
+  { "org.gnome.builder.workspace.new-terminal-in-runtime",
+    0, NULL,
+    NC_("shortcut window", "Workspace shortcuts"),
+    NC_("shortcut window", "General"),
+    NC_("shortcut window", "Terminal in Build Runtime") },
+
+  { "org.gnome.builder.workspace.new-terminal-in-runner",
+    0, NULL,
+    NC_("shortcut window", "Workspace shortcuts"),
+    NC_("shortcut window", "General"),
+    NC_("shortcut window", "Terminal in Runtime") },
+};
+
+static void
+gbp_terminal_workspace_addin_setup_shortcuts (GbpTerminalWorkspaceAddin *self,
+                                              IdeWorkspace              *workspace)
+{
+  DzlShortcutController *controller;
+
+  g_assert (GBP_IS_TERMINAL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  controller = dzl_shortcut_controller_find (GTK_WIDGET (workspace));
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              "org.gnome.builder.workspace.new-terminal",
+                                              I_("<primary><shift>t"),
+                                              DZL_SHORTCUT_PHASE_DISPATCH,
+                                              "win.new-terminal");
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              "org.gnome.builder.workspace.new-terminal-in-runtime",
+                                              I_("<primary><alt><shift>t"),
+                                              DZL_SHORTCUT_PHASE_DISPATCH,
+                                              "win.new-terminal-in-runtime");
+
+  dzl_shortcut_controller_add_command_action (controller,
+                                              "org.gnome.builder.workspace.new-terminal-in-runner",
+                                              I_("<primary><alt>t"),
+                                              DZL_SHORTCUT_PHASE_DISPATCH,
+                                              "win.new-terminal-in-runner");
+
+  dzl_shortcut_manager_add_shortcut_entries (NULL,
+                                             gbp_terminal_shortcut_entries,
+                                             G_N_ELEMENTS (gbp_terminal_shortcut_entries),
+                                             GETTEXT_PACKAGE);
+}
+
+static void
+gbp_terminal_workspace_addin_load (IdeWorkspaceAddin *addin,
+                                   IdeWorkspace      *workspace)
+{
+  GbpTerminalWorkspaceAddin *self = (GbpTerminalWorkspaceAddin *)addin;
+  IdeWorkbench *workbench;
+  IdeSurface *surface;
+  GtkWidget *utilities;
+
+  g_assert (GBP_IS_TERMINAL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace) ||
+            IDE_IS_EDITOR_WORKSPACE (workspace) ||
+            IDE_IS_TERMINAL_WORKSPACE (workspace));
+
+  self->workspace = workspace;
+
+  gbp_terminal_workspace_addin_setup_shortcuts (self, workspace);
+  g_action_map_add_action_entries (G_ACTION_MAP (workspace),
+                                   terminal_actions,
+                                   G_N_ELEMENTS (terminal_actions),
+                                   self);
+
+  if ((surface = ide_workspace_get_surface_by_name (workspace, "editor")) &&
+      IDE_IS_EDITOR_SURFACE (surface) &&
+      (utilities = ide_editor_surface_get_utilities (IDE_EDITOR_SURFACE (surface))))
+    {
+      IdeRunManager *run_manager;
+      IdeContext *context;
+
+      self->bottom_dock = g_object_new (DZL_TYPE_DOCK_WIDGET,
+                                        "title", _("Terminal"),
+                                        "icon-name", "utilities-terminal-symbolic",
+                                        "visible", TRUE,
+                                        NULL);
+      g_signal_connect (self->bottom_dock,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        &self->bottom_dock);
+      gtk_container_add (GTK_CONTAINER (utilities), GTK_WIDGET (self->bottom_dock));
+
+      self->bottom = g_object_new (IDE_TYPE_TERMINAL_PAGE,
+                                   "visible", TRUE,
+                                   NULL);
+      g_signal_connect (self->bottom,
+                        "destroy",
+                        G_CALLBACK (gtk_widget_destroyed),
+                        &self->bottom);
+      gtk_container_add (GTK_CONTAINER (self->bottom_dock), GTK_WIDGET (self->bottom));
+
+      workbench = ide_widget_get_workbench (GTK_WIDGET (workspace));
+
+      if (ide_workbench_has_project (workbench))
+        {
+          /* Setup terminals when a project is run */
+          context = ide_widget_get_context (GTK_WIDGET (workspace));
+          run_manager = ide_run_manager_from_context (context);
+          g_signal_connect_object (run_manager,
+                                   "run",
+                                   G_CALLBACK (on_run_manager_run),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+          g_signal_connect_object (run_manager,
+                                   "stopped",
+                                   G_CALLBACK (on_run_manager_stopped),
+                                   self,
+                                   G_CONNECT_SWAPPED);
+        }
+    }
+}
+
+static void
+gbp_terminal_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                     IdeWorkspace      *workspace)
+{
+  GbpTerminalWorkspaceAddin *self = (GbpTerminalWorkspaceAddin *)addin;
+  IdeWorkbench *workbench;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_TERMINAL_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_PRIMARY_WORKSPACE (workspace) ||
+            IDE_IS_EDITOR_WORKSPACE (workspace) ||
+            IDE_IS_TERMINAL_WORKSPACE (workspace));
+
+  workbench = ide_widget_get_workbench (GTK_WIDGET (workspace));
+
+  if (ide_workbench_has_project (workbench))
+    {
+      IdeRunManager *run_manager;
+      IdeContext *context;
+
+      context = ide_widget_get_context (GTK_WIDGET (workspace));
+      run_manager = ide_run_manager_from_context (context);
+      g_signal_handlers_disconnect_by_func (run_manager,
+                                            G_CALLBACK (on_run_manager_run),
+                                            self);
+      g_signal_handlers_disconnect_by_func (run_manager,
+                                            G_CALLBACK (on_run_manager_stopped),
+                                            self);
+    }
+
+  for (guint i = 0; i < G_N_ELEMENTS (terminal_actions); i++)
+    g_action_map_remove_action (G_ACTION_MAP (workspace), terminal_actions[i].name);
+
+  if (self->bottom_dock != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->bottom_dock));
+
+  if (self->run_panel != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->run_panel));
+
+  g_assert (self->bottom == NULL);
+  g_assert (self->bottom_dock == NULL);
+
+  g_assert (self->run_terminal == NULL);
+  g_assert (self->run_panel == NULL);
+
+  self->workspace = NULL;
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_terminal_workspace_addin_load;
+  iface->unload = gbp_terminal_workspace_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpTerminalWorkspaceAddin, gbp_terminal_workspace_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN, workspace_addin_iface_init))
+
+static void
+gbp_terminal_workspace_addin_class_init (GbpTerminalWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_terminal_workspace_addin_init (GbpTerminalWorkspaceAddin *self)
+{
+}
diff --git a/src/plugins/terminal/gbp-terminal-workspace-addin.h 
b/src/plugins/terminal/gbp-terminal-workspace-addin.h
new file mode 100644
index 000000000..aceef9a47
--- /dev/null
+++ b/src/plugins/terminal/gbp-terminal-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-terminal-workspace-addin.h
+ *
+ * Copyright 2018-2019 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_TERMINAL_WORKSPACE_ADDIN (gbp_terminal_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpTerminalWorkspaceAddin, gbp_terminal_workspace_addin, GBP, 
TERMINAL_WORKSPACE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/terminal/gtk/menus.ui b/src/plugins/terminal/gtk/menus.ui
index 9e4ef30aa..7405b8307 100644
--- a/src/plugins/terminal/gtk/menus.ui
+++ b/src/plugins/terminal/gtk/menus.ui
@@ -1,32 +1,5 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <menu id="terminal-view-document-menu">
-    <section id="terminal-document-section">
-      <attribute name="label" translatable="yes">Terminal</attribute>
-      <item>
-        <attribute name="label" translatable="yes">Split</attribute>
-        <attribute name="action">layoutstack.split-view</attribute>
-        <attribute name="target" type="s">''</attribute>
-        <attribute name="verb-icon-name">builder-split-tab-symbolic</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">Reset</attribute>
-        <attribute name="action">terminal-view.reset</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">Reset and Clear</attribute>
-        <attribute name="action">terminal-view.reset-and-clear</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">Save As</attribute>
-        <attribute name="action">terminal-view.save-as</attribute>
-      </item>
-      <item>
-        <attribute name="label" translatable="yes">Close</attribute>
-        <attribute name="action">layoutstack.close-view</attribute>
-      </item>
-    </section>
-  </menu>
   <menu id="new-document-menu">
     <section id="new-document-section">
       <item>
@@ -53,13 +26,4 @@
       </item>
     </section>
   </menu>
-  <menu id="ide-source-view-popup-menu">
-    <section id="ide-source-view-popup-menu-terminal-section">
-      <attribute name="after">ide-source-view-popup-menu-zoom-section</attribute>
-      <item>
-        <attribute name="label" translatable="yes">New terminal in directory</attribute>
-        <attribute name="action">win.new-terminal-in-dir</attribute>
-      </item>
-    </section>
-  </menu>
 </interface>
diff --git a/src/plugins/terminal/meson.build b/src/plugins/terminal/meson.build
index e697221d1..f13204f81 100644
--- a/src/plugins/terminal/meson.build
+++ b/src/plugins/terminal/meson.build
@@ -1,22 +1,13 @@
-terminal_resources = gnome.compile_resources(
-  'terminal-resources',
+plugins_sources += files([
+  'gbp-terminal-application-addin.c',
+  'gbp-terminal-workspace-addin.c',
+  'terminal-plugin.c',
+])
+
+plugin_terminal_resources = gnome.compile_resources(
+  'gbp-terminal-resources',
   'terminal.gresource.xml',
-  c_name: 'gb_terminal',
+  c_name: 'gbp_terminal',
 )
 
-terminal_sources = [
-  'gb-terminal-plugin.c',
-  'gb-terminal-private.h',
-  'gb-terminal-view.c',
-  'gb-terminal-view.h',
-  'gb-terminal-view-private.h',
-  'gb-terminal-view-actions.c',
-  'gb-terminal-view-actions.h',
-  'gb-terminal-workbench-addin.c',
-  'gb-terminal-workbench-addin.h',
-]
-
-gnome_builder_plugins_deps += [libvte_dep]
-
-gnome_builder_plugins_sources += files(terminal_sources)
-gnome_builder_plugins_sources += terminal_resources[0]
+plugins_sources += plugin_terminal_resources[0]
diff --git a/src/plugins/terminal/terminal-plugin.c b/src/plugins/terminal/terminal-plugin.c
new file mode 100644
index 000000000..06a97dc19
--- /dev/null
+++ b/src/plugins/terminal/terminal-plugin.c
@@ -0,0 +1,41 @@
+/* terminal-plugin.c
+ *
+ * Copyright 2018-2019 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 "terminal-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+#include <libide-terminal.h>
+
+#include "gbp-terminal-application-addin.h"
+#include "gbp-terminal-workspace-addin.h"
+
+_IDE_EXTERN void
+_gbp_terminal_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_APPLICATION_ADDIN,
+                                              GBP_TYPE_TERMINAL_APPLICATION_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_TERMINAL_WORKSPACE_ADDIN);
+}
diff --git a/src/plugins/terminal/terminal.gresource.xml b/src/plugins/terminal/terminal.gresource.xml
index 2cdd9c33c..e6e7bcefa 100644
--- a/src/plugins/terminal/terminal.gresource.xml
+++ b/src/plugins/terminal/terminal.gresource.xml
@@ -1,10 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/terminal">
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
     <file>terminal.plugin</file>
   </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/terminal">
-    <file>gb-terminal-view.ui</file>
-    <file>gtk/menus.ui</file>
-  </gresource>
 </gresources>
diff --git a/src/plugins/terminal/terminal.plugin b/src/plugins/terminal/terminal.plugin
index 0a81faf5d..fa450841f 100644
--- a/src/plugins/terminal/terminal.plugin
+++ b/src/plugins/terminal/terminal.plugin
@@ -1,9 +1,12 @@
 [Plugin]
-Module=terminal
-Name=Terminal
-Description=A terminal for Builder
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
-Depends=editor
 Builtin=true
-Embedded=gb_terminal_register_types
+Copyright=Copyright © 2014-2018 Christian Hergert
+Depends=editor;
+Description=Builder's terminal support
+Embedded=_gbp_terminal_register_types
+Hidden=true
+Module=terminal
+Name=Terminal
+X-At-Startup=true
+X-Workspace-Kind=editor;primary;terminal;
diff --git a/src/plugins/testui/gbp-test-path.c b/src/plugins/testui/gbp-test-path.c
new file mode 100644
index 000000000..e9d97eb39
--- /dev/null
+++ b/src/plugins/testui/gbp-test-path.c
@@ -0,0 +1,181 @@
+/* gbp-test-path.c
+ *
+ * Copyright 2018-2019 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-test-path"
+
+#include "config.h"
+
+#include "gbp-test-path.h"
+
+struct _GbpTestPath
+{
+  GObject         parent_instance;
+  IdeTestManager *test_manager;
+  gchar          *path;
+  gchar          *name;
+};
+
+enum {
+  PROP_0,
+  PROP_PATH,
+  PROP_TEST_MANAGER,
+  N_PROPS
+};
+
+G_DEFINE_TYPE (GbpTestPath, gbp_test_path, G_TYPE_OBJECT)
+
+static GParamSpec *properties [N_PROPS];
+
+static void
+gbp_test_path_dispose (GObject *object)
+{
+  GbpTestPath *self = (GbpTestPath *)object;
+
+  g_clear_object (&self->test_manager);
+  g_clear_pointer (&self->path, g_free);
+  g_clear_pointer (&self->name, g_free);
+
+  G_OBJECT_CLASS (gbp_test_path_parent_class)->dispose (object);
+}
+
+static void
+gbp_test_path_get_property (GObject    *object,
+                            guint       prop_id,
+                            GValue     *value,
+                            GParamSpec *pspec)
+{
+  GbpTestPath *self = GBP_TEST_PATH (object);
+
+  switch (prop_id)
+    {
+    case PROP_PATH:
+      g_value_set_string (value, self->path);
+      break;
+
+    case PROP_TEST_MANAGER:
+      g_value_set_object (value, self->test_manager);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_test_path_set_property (GObject      *object,
+                            guint         prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  GbpTestPath *self = GBP_TEST_PATH (object);
+
+  switch (prop_id)
+    {
+    case PROP_PATH:
+      if ((self->path = g_value_dup_string (value)))
+        self->name = g_path_get_basename (self->path);
+      break;
+
+    case PROP_TEST_MANAGER:
+      self->test_manager = g_value_dup_object (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+gbp_test_path_class_init (GbpTestPathClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = gbp_test_path_dispose;
+  object_class->get_property = gbp_test_path_get_property;
+  object_class->set_property = gbp_test_path_set_property;
+
+  properties [PROP_PATH] =
+    g_param_spec_string ("path", NULL, NULL, NULL,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  properties [PROP_TEST_MANAGER] =
+    g_param_spec_object ("test-manager", NULL, NULL, IDE_TYPE_TEST_MANAGER,
+                         (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_properties (object_class, N_PROPS, properties);
+}
+
+static void
+gbp_test_path_init (GbpTestPath *self)
+{
+}
+
+GbpTestPath *
+gbp_test_path_new (IdeTestManager *test_manager,
+                   const gchar    *path)
+{
+  return g_object_new (GBP_TYPE_TEST_PATH,
+                       "test-manager", test_manager,
+                       "path", path,
+                       NULL);
+}
+
+const gchar *
+gbp_test_path_get_name (GbpTestPath *self)
+{
+  g_return_val_if_fail (GBP_IS_TEST_PATH (self), NULL);
+
+  return self->name;
+}
+
+GPtrArray *
+gbp_test_path_get_folders (GbpTestPath *self)
+{
+  GPtrArray *folders;
+  g_auto(GStrv) dirs = NULL;
+
+  g_return_val_if_fail (GBP_IS_TEST_PATH (self), NULL);
+
+  folders = g_ptr_array_new ();
+
+  dirs = ide_test_manager_get_folders (self->test_manager, self->path);
+
+  for (guint i = 0; dirs[i]; i++)
+    {
+      g_autofree gchar *subdir = NULL;
+
+      if (self->path == NULL)
+        subdir = g_strdup (dirs[i]);
+      else
+        subdir = g_strjoin ("/", self->path, dirs[i], NULL);
+
+      g_ptr_array_add (folders, gbp_test_path_new (self->test_manager, subdir));
+    }
+
+  return g_steal_pointer (&folders);
+}
+
+GPtrArray *
+gbp_test_path_get_tests (GbpTestPath *self)
+{
+  g_return_val_if_fail (GBP_IS_TEST_PATH (self), NULL);
+
+  return ide_test_manager_get_tests (self->test_manager, self->path);
+}
diff --git a/src/plugins/testui/gbp-test-path.h b/src/plugins/testui/gbp-test-path.h
new file mode 100644
index 000000000..8e1b24dff
--- /dev/null
+++ b/src/plugins/testui/gbp-test-path.h
@@ -0,0 +1,37 @@
+/* gbp-test-path.h
+ *
+ * Copyright 2018-2019 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-foundry.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_TEST_PATH (gbp_test_path_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpTestPath, gbp_test_path, GBP, TEST_PATH, GObject)
+
+GbpTestPath *gbp_test_path_new         (IdeTestManager *test_manager,
+                                        const gchar    *path);
+const gchar *gbp_test_path_get_name    (GbpTestPath    *self);
+GPtrArray   *gbp_test_path_get_folders (GbpTestPath    *self);
+GPtrArray   *gbp_test_path_get_tests   (GbpTestPath    *self);
+
+G_END_DECLS
diff --git a/src/plugins/testui/gbp-test-tree-addin.c b/src/plugins/testui/gbp-test-tree-addin.c
new file mode 100644
index 000000000..f6a541566
--- /dev/null
+++ b/src/plugins/testui/gbp-test-tree-addin.c
@@ -0,0 +1,394 @@
+/* gbp-test-tree-addin.c
+ *
+ * Copyright 2018-2019 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-test-tree-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+#include <libide-tree.h>
+#include <libide-threading.h>
+
+#include "ide-tree-private.h"
+
+#include "gbp-test-path.h"
+#include "gbp-test-tree-addin.h"
+
+struct _GbpTestTreeAddin
+{
+  GObject       parent_instance;
+  IdeTreeModel *model;
+  IdeTree      *tree;
+};
+
+typedef struct
+{
+  IdeTreeNode     *node;
+  IdeTest         *test;
+  IdeNotification *notif;
+} RunTest;
+
+static void
+run_test_free (RunTest *state)
+{
+  g_clear_object (&state->node);
+  g_clear_object (&state->test);
+  g_clear_object (&state->notif);
+  g_slice_free (RunTest, state);
+}
+
+static void
+gbp_test_tree_addin_build_paths_cb (GObject      *object,
+                                    GAsyncResult *result,
+                                    gpointer      user_data)
+{
+  IdeTestManager *test_manager = (IdeTestManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GPtrArray) dirs = NULL;
+  g_autoptr(GPtrArray) tests = NULL;
+  IdeTreeNode *node;
+  GbpTestPath *path;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TEST_MANAGER (test_manager));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  ide_test_manager_ensure_loaded_finish (test_manager, result, NULL);
+
+  node = ide_task_get_task_data (task);
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (ide_tree_node_holds (node, GBP_TYPE_TEST_PATH));
+
+  path = ide_tree_node_get_item (node);
+  g_assert (GBP_IS_TEST_PATH (path));
+
+  dirs = gbp_test_path_get_folders (path);
+  tests = gbp_test_path_get_tests (path);
+
+  IDE_PTR_ARRAY_SET_FREE_FUNC (dirs, g_object_unref);
+  IDE_PTR_ARRAY_SET_FREE_FUNC (tests, g_object_unref);
+
+  for (guint i = 0; i < dirs->len; i++)
+    {
+      GbpTestPath *child_path = g_ptr_array_index (dirs, i);
+      g_autoptr(IdeTreeNode) child = NULL;
+
+      child = ide_tree_node_new ();
+      ide_tree_node_set_children_possible (child, TRUE);
+      ide_tree_node_set_display_name (child, gbp_test_path_get_name (child_path));
+      ide_tree_node_set_icon_name (child, "folder-symbolic");
+      ide_tree_node_set_expanded_icon_name (child, "folder-open-symbolic");
+      ide_tree_node_set_item (child, child_path);
+      ide_tree_node_append (node, child);
+    }
+
+  for (guint i = 0; i < tests->len; i++)
+    {
+      IdeTest *test = g_ptr_array_index (tests, i);
+      g_autoptr(IdeTreeNode) child = NULL;
+
+      child = ide_tree_node_new ();
+      ide_tree_node_set_children_possible (child, FALSE);
+      ide_tree_node_set_display_name (child, ide_test_get_display_name (test));
+      ide_tree_node_set_icon_name (child, ide_test_get_icon_name (test));
+      ide_tree_node_set_item (child, test);
+      ide_tree_node_append (node, child);
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+gbp_test_tree_addin_build_children_async (IdeTreeAddin        *addin,
+                                          IdeTreeNode         *node,
+                                          GCancellable        *cancellbale,
+                                          GAsyncReadyCallback  callback,
+                                          gpointer             user_data)
+{
+  GbpTestTreeAddin *self = (GbpTestTreeAddin *)addin;
+  g_autoptr(IdeTask) task = NULL;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_TEST_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (!cancellbale || G_IS_CANCELLABLE (cancellbale));
+
+  task = ide_task_new (addin, cancellbale, callback, user_data);
+  ide_task_set_source_tag (task, gbp_test_tree_addin_build_children_async);
+  ide_task_set_task_data (task, g_object_ref (node), g_object_unref);
+
+  if (ide_tree_node_holds (node, IDE_TYPE_CONTEXT))
+    {
+      g_autoptr(IdeTreeNode) child = NULL;
+      g_autoptr(GbpTestPath) path = NULL;
+      IdeTestManager *test_manager;
+      IdeContext *context;
+
+      context = ide_tree_node_get_item (node);
+      test_manager = ide_test_manager_from_context (context);
+      path = gbp_test_path_new (test_manager, NULL);
+
+      child = ide_tree_node_new ();
+      ide_tree_node_set_children_possible (child, TRUE);
+      ide_tree_node_set_display_name (child, _("Unit Tests"));
+      ide_tree_node_set_icon_name (child, "builder-unit-tests-symbolic");
+      ide_tree_node_set_item (child, path);
+      ide_tree_node_prepend (node, child);
+    }
+  else if (ide_tree_node_holds (node, GBP_TYPE_TEST_PATH))
+    {
+      IdeContext *context = ide_widget_get_context (GTK_WIDGET (self->tree));
+      IdeTestManager *test_manager = ide_test_manager_from_context (context);
+
+      ide_test_manager_ensure_loaded_async (test_manager,
+                                            NULL,
+                                            gbp_test_tree_addin_build_paths_cb,
+                                            g_steal_pointer (&task));
+      return;
+    }
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_test_tree_addin_build_children_finish (IdeTreeAddin  *addin,
+                                           GAsyncResult  *result,
+                                           GError       **error)
+{
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_TEST_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static IdeTreeNodeVisit
+locate_unit_tests (IdeTreeNode *node,
+                   gpointer     user_data)
+{
+  IdeTreeNode **out_node = user_data;
+
+  if (ide_tree_node_holds (node, GBP_TYPE_TEST_PATH))
+    {
+      *out_node = node;
+      return IDE_TREE_NODE_VISIT_BREAK;
+    }
+
+  return IDE_TREE_NODE_VISIT_CONTINUE;
+}
+
+static void
+gbp_test_tree_addin_notify_loading (GbpTestTreeAddin *self,
+                                    GParamSpec       *pspec,
+                                    IdeTestManager   *test_manager)
+{
+  IdeTreeNode *root;
+  IdeTreeNode *node = NULL;
+  gint64 loading_time;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_TEST_TREE_ADDIN (self));
+  g_assert (IDE_IS_TEST_MANAGER (test_manager));
+
+  root = ide_tree_model_get_root (self->model);
+
+  ide_tree_node_traverse (root,
+                          G_PRE_ORDER,
+                          G_TRAVERSE_ALL,
+                          1,
+                          locate_unit_tests,
+                          &node);
+
+  if (node != NULL &&
+      ide_tree_node_expanded (self->tree, node) &&
+      !_ide_tree_node_get_loading (node, &loading_time))
+    {
+      ide_tree_collapse_node (self->tree, node);
+      ide_tree_expand_node (self->tree, node);
+    }
+}
+
+static void
+gbp_test_tree_addin_load (IdeTreeAddin *addin,
+                          IdeTree      *tree,
+                          IdeTreeModel *model)
+{
+  GbpTestTreeAddin *self = (GbpTestTreeAddin *)addin;
+  IdeTestManager *test_manager;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  self->tree = tree;
+  self->model = model;
+
+  context = ide_object_get_context (IDE_OBJECT (model));
+  test_manager = ide_test_manager_from_context (context);
+
+  g_signal_connect_object (test_manager,
+                           "notify::loading",
+                           G_CALLBACK (gbp_test_tree_addin_notify_loading),
+                           self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+gbp_test_tree_addin_unload (IdeTreeAddin *addin,
+                            IdeTree      *tree,
+                            IdeTreeModel *model)
+{
+  GbpTestTreeAddin *self = (GbpTestTreeAddin *)addin;
+  IdeTestManager *test_manager;
+  IdeContext *context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  context = ide_object_get_context (IDE_OBJECT (model));
+  test_manager = ide_test_manager_from_context (context);
+  g_signal_handlers_disconnect_by_func (test_manager,
+                                        G_CALLBACK (gbp_test_tree_addin_notify_loading),
+                                        self);
+
+  self->tree = NULL;
+  self->model = NULL;
+}
+
+static void
+gbp_test_tree_addin_run_cb (GObject      *object,
+                            GAsyncResult *result,
+                            gpointer      user_data)
+{
+  IdeTestManager *test_manager = (IdeTestManager *)object;
+  g_autoptr(IdeTask) task = user_data;
+  g_autoptr(GError) error = NULL;
+  const gchar *icon_name = NULL;
+  RunTest *state;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_TEST_MANAGER (test_manager));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (IDE_IS_TASK (task));
+
+  if (!ide_test_manager_run_finish (test_manager, result, &error))
+    {
+      /* TODO: Plumb more errors into test-manager */
+      if (g_error_matches (error, IDE_RUNTIME_ERROR, IDE_RUNTIME_ERROR_BUILD_FAILED) ||
+          error->domain == G_SPAWN_ERROR)
+        icon_name = "dialog-warning-symbolic";
+    }
+
+  state = ide_task_get_task_data (task);
+
+  g_assert (state != NULL);
+  g_assert (IDE_IS_TREE_NODE (state->node));
+  g_assert (IDE_IS_TEST (state->test));
+  g_assert (IDE_IS_NOTIFICATION (state->notif));
+
+  if (icon_name == NULL)
+    icon_name = ide_test_get_icon_name (state->test);
+  ide_tree_node_set_icon_name (state->node, icon_name);
+
+  ide_notification_withdraw_in_seconds (state->notif, 1);
+
+  ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_test_tree_addin_node_activated (IdeTreeAddin *addin,
+                                    IdeTree      *tree,
+                                    IdeTreeNode  *node)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autofree gchar *title = NULL;
+  IdeTestManager *test_manager;
+  IdeContext *context;
+  RunTest *state;
+  IdeTest *test;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_TEST_TREE_ADDIN (addin));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (IDE_IS_TREE_NODE (node));
+
+  if (!ide_tree_node_holds (node, IDE_TYPE_TEST))
+    return FALSE;
+
+  context = ide_widget_get_context (GTK_WIDGET (tree));
+  test_manager = ide_test_manager_from_context (context);
+  test = ide_tree_node_get_item (node);
+
+  state = g_slice_new0 (RunTest);
+  state->node = g_object_ref (node);
+  state->test = g_object_ref (test);
+  state->notif = ide_notification_new ();
+
+  /* translators: %s is replaced with the name of the unit test */
+  title = g_strdup_printf (_("Running test “%s”…"),
+                           ide_test_get_display_name (test));
+  ide_notification_set_title (state->notif, title);
+  ide_notification_set_urgent (state->notif, TRUE);
+  ide_notification_attach (state->notif, IDE_OBJECT (context));
+
+  task = ide_task_new (addin, NULL, NULL, NULL);
+  ide_task_set_source_tag (task, gbp_test_tree_addin_node_activated);
+  ide_task_set_task_data (task, state, run_test_free);
+
+  ide_tree_node_set_icon_name (node, "content-loading-symbolic");
+
+  ide_test_manager_run_async (test_manager,
+                              test,
+                              NULL,
+                              gbp_test_tree_addin_run_cb,
+                              g_steal_pointer (&task));
+
+  return TRUE;
+}
+
+static void
+tree_addin_iface_init (IdeTreeAddinInterface *iface)
+{
+  iface->load = gbp_test_tree_addin_load;
+  iface->unload = gbp_test_tree_addin_unload;
+  iface->build_children_async = gbp_test_tree_addin_build_children_async;
+  iface->build_children_finish = gbp_test_tree_addin_build_children_finish;
+  iface->node_activated = gbp_test_tree_addin_node_activated;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpTestTreeAddin, gbp_test_tree_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_TREE_ADDIN, tree_addin_iface_init))
+
+static void
+gbp_test_tree_addin_class_init (GbpTestTreeAddinClass *klass)
+{
+}
+
+static void
+gbp_test_tree_addin_init (GbpTestTreeAddin *self)
+{
+}
diff --git a/src/plugins/testui/gbp-test-tree-addin.h b/src/plugins/testui/gbp-test-tree-addin.h
new file mode 100644
index 000000000..af28845ce
--- /dev/null
+++ b/src/plugins/testui/gbp-test-tree-addin.h
@@ -0,0 +1,31 @@
+/* gbp-test-tree-addin.h
+ *
+ * Copyright 2018-2019 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_TEST_TREE_ADDIN (gbp_test_tree_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpTestTreeAddin, gbp_test_tree_addin, GBP, TEST_TREE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/testui/meson.build b/src/plugins/testui/meson.build
new file mode 100644
index 000000000..2c4fad914
--- /dev/null
+++ b/src/plugins/testui/meson.build
@@ -0,0 +1,13 @@
+plugins_sources += files([
+  'testui-plugin.c',
+  'gbp-test-path.c',
+  'gbp-test-tree-addin.c',
+])
+
+plugin_testui_resources = gnome.compile_resources(
+  'testui-resources',
+  'testui.gresource.xml',
+  c_name: 'gbp_testui',
+)
+
+plugins_sources += plugin_testui_resources[0]
diff --git a/src/plugins/testui/testui-plugin.c b/src/plugins/testui/testui-plugin.c
new file mode 100644
index 000000000..4b8199fab
--- /dev/null
+++ b/src/plugins/testui/testui-plugin.c
@@ -0,0 +1,36 @@
+/* testui-plugin.c
+ *
+ * Copyright 2017-2019 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 "testui-plugin"
+
+#include "config.h"
+
+#include <libide-tree.h>
+#include <libpeas/peas.h>
+
+#include "gbp-test-tree-addin.h"
+
+_IDE_EXTERN void
+_gbp_testui_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_TREE_ADDIN,
+                                              GBP_TYPE_TEST_TREE_ADDIN);
+}
diff --git a/src/plugins/testui/testui.gresource.xml b/src/plugins/testui/testui.gresource.xml
new file mode 100644
index 000000000..1c245e26e
--- /dev/null
+++ b/src/plugins/testui/testui.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/testui">
+    <file>testui.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/testui/testui.plugin b/src/plugins/testui/testui.plugin
new file mode 100644
index 000000000..967ac6dde
--- /dev/null
+++ b/src/plugins/testui/testui.plugin
@@ -0,0 +1,11 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2017-2018 Christian Hergert
+Depends=editor;
+Description=Unit testing for Builder
+Embedded=_gbp_testui_register_types
+Hidden=true
+Module=testui
+Name=Unit Testing
+X-Tree-Kind=project-tree;
diff --git a/src/plugins/todo/gbp-todo-item.c b/src/plugins/todo/gbp-todo-item.c
index 0197c42aa..8ebfead37 100644
--- a/src/plugins/todo/gbp-todo-item.c
+++ b/src/plugins/todo/gbp-todo-item.c
@@ -1,6 +1,6 @@
 /* gbp-todo-item.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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-todo-item"
@@ -73,7 +75,7 @@ gbp_todo_item_init (GbpTodoItem *self)
  *
  * Returns: (transfer full): A newly allocated #GbpTodoItem
  *
- * Since: 3.26
+ * Since: 3.32
  */
 GbpTodoItem *
 gbp_todo_item_new (GBytes *bytes)
diff --git a/src/plugins/todo/gbp-todo-item.h b/src/plugins/todo/gbp-todo-item.h
index f01e604e7..4ef12a786 100644
--- a/src/plugins/todo/gbp-todo-item.h
+++ b/src/plugins/todo/gbp-todo-item.h
@@ -1,6 +1,6 @@
 /* gbp-todo-item.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/todo/gbp-todo-model.c b/src/plugins/todo/gbp-todo-model.c
index cbfeac887..438db3091 100644
--- a/src/plugins/todo/gbp-todo-model.c
+++ b/src/plugins/todo/gbp-todo-model.c
@@ -1,6 +1,6 @@
 /* gbp-todo-model.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,14 @@
  *
  * 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-todo-model"
 
-#include <ide.h>
+#include <libide-code.h>
+#include <libide-gui.h>
 #include <string.h>
 
 #include "gbp-todo-model.h"
@@ -297,7 +300,7 @@ gbp_todo_model_init (GbpTodoModel *self)
  *
  * Returns: (transfer full): A newly created #GbpTodoModel.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 GbpTodoModel *
 gbp_todo_model_new (IdeVcs *vcs)
@@ -635,7 +638,7 @@ is_typed (IdeVcs      *vcs,
  * If @file is not a native file (meaning it is accessable on the
  * normal, mounted, local file-system) this operation will fail.
  *
- * Since: 3.26
+ * Since: 3.32
  */
 void
 gbp_todo_model_mine_async (GbpTodoModel        *self,
@@ -666,7 +669,7 @@ gbp_todo_model_mine_async (GbpTodoModel        *self,
       return;
     }
 
-  workdir = ide_vcs_get_working_directory (self->vcs);
+  workdir = ide_vcs_get_workdir (self->vcs);
 
   m = g_slice_new0 (Mine);
   m->file = g_object_ref (file);
@@ -686,6 +689,8 @@ gbp_todo_model_mine_async (GbpTodoModel        *self,
  * Completes an asynchronous request to gbp_todo_model_mine_async().
  *
  * Returns: %TRUE if successful; otherwise %FALSE and @error is set.
+ *
+ * Since: 3.32
  */
 gboolean
 gbp_todo_model_mine_finish (GbpTodoModel  *self,
diff --git a/src/plugins/todo/gbp-todo-model.h b/src/plugins/todo/gbp-todo-model.h
index 8314cd1d9..95e04ea79 100644
--- a/src/plugins/todo/gbp-todo-model.h
+++ b/src/plugins/todo/gbp-todo-model.h
@@ -1,6 +1,6 @@
 /* gbp-todo-model.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,14 @@
  *
  * 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 <ide.h>
+#include <gtk/gtk.h>
+#include <libide-vcs.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/todo/gbp-todo-panel.c b/src/plugins/todo/gbp-todo-panel.c
index 7a9639aff..02bb1fc06 100644
--- a/src/plugins/todo/gbp-todo-panel.c
+++ b/src/plugins/todo/gbp-todo-panel.c
@@ -1,6 +1,6 @@
 /* gbp-todo-panel.c
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,12 +14,15 @@
  *
  * 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-todo-panel"
 
 #include <glib/gi18n.h>
-#include <ide.h>
+#include <libide-code.h>
+#include <libide-gui.h>
 
 #include "gbp-todo-item.h"
 #include "gbp-todo-panel.h"
@@ -94,7 +97,6 @@ gbp_todo_panel_row_activated (GbpTodoPanel      *self,
 {
   g_autoptr(GbpTodoItem) item = NULL;
   g_autoptr(GFile) file = NULL;
-  g_autoptr(IdeUri) uri = NULL;
   g_autofree gchar *fragment = NULL;
   IdeWorkbench *workbench;
   GtkTreeModel *model;
@@ -128,26 +130,26 @@ gbp_todo_panel_row_activated (GbpTodoPanel      *self,
       GFile *workdir;
 
       context = ide_workbench_get_context (workbench);
-      vcs = ide_context_get_vcs (context);
-      workdir = ide_vcs_get_working_directory (vcs);
+      vcs = ide_vcs_from_context (context);
+      workdir = ide_vcs_get_workdir (vcs);
       file = g_file_get_child (workdir, path);
     }
 
-  uri = ide_uri_new_from_file (file);
-
-  /* Set lineno info so that the editor can jump
-   * to the location of the TODO item. Our line number
-   * from the model is 1-based, and we need 0-based for
+  /* Set lineno info so that the editor can jump to the location of the TODO
+   * item. Our line number from the model is 1-based, and we need 0-based for
    * our API to open files.
    */
   lineno = gbp_todo_item_get_lineno (item);
   if (lineno > 0)
     lineno--;
 
-  fragment = g_strdup_printf ("L%u", lineno);
-  ide_uri_set_fragment (uri, fragment);
-
-  ide_workbench_open_uri_async (workbench, uri, "editor", 0, NULL, NULL, NULL);
+  ide_workbench_open_at_async (workbench,
+                               file,
+                               "editor",
+                               lineno,
+                               -1,
+                               IDE_BUFFER_OPEN_FLAGS_NONE,
+                               NULL, NULL, NULL);
 }
 
 static gboolean
@@ -345,6 +347,8 @@ gbp_todo_panel_init (GbpTodoPanel *self)
  * Gets the model being displayed by the treeview.
  *
  * Returns: (transfer none) (nullable): a #GbpTodoModel.
+ *
+ * Since: 3.32
  */
 GbpTodoModel *
 gbp_todo_panel_get_model (GbpTodoPanel *self)
diff --git a/src/plugins/todo/gbp-todo-panel.h b/src/plugins/todo/gbp-todo-panel.h
index 6637d2406..5da1addce 100644
--- a/src/plugins/todo/gbp-todo-panel.h
+++ b/src/plugins/todo/gbp-todo-panel.h
@@ -1,6 +1,6 @@
 /* gbp-todo-panel.h
  *
- * Copyright 2017 Christian Hergert <chergert redhat com>
+ * Copyright 2017-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <dazzle.h>
 
 #include "gbp-todo-model.h"
 
diff --git a/src/plugins/todo/gbp-todo-workspace-addin.c b/src/plugins/todo/gbp-todo-workspace-addin.c
new file mode 100644
index 000000000..3931eeffd
--- /dev/null
+++ b/src/plugins/todo/gbp-todo-workspace-addin.c
@@ -0,0 +1,213 @@
+/* gbp-todo-workspace-addin.c
+ *
+ * Copyright 2017-2019 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-todo-workspace-addin"
+
+#include <libide-editor.h>
+#include <glib/gi18n.h>
+
+#include "gbp-todo-workspace-addin.h"
+#include "gbp-todo-panel.h"
+
+struct _GbpTodoWorkspaceAddin
+{
+  GObject       parent_instance;
+
+  GbpTodoPanel *panel;
+  GbpTodoModel *model;
+  GCancellable *cancellable;
+  GFile        *workdir;
+
+  guint         has_presented : 1;
+  guint         is_global_mining : 1;
+};
+
+static void
+gbp_todo_workspace_addin_mine_cb (GObject      *object,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  GbpTodoModel *model = (GbpTodoModel *)object;
+  g_autoptr(GbpTodoWorkspaceAddin) self = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_TODO_WORKSPACE_ADDIN (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+  g_assert (GBP_IS_TODO_MODEL (model));
+
+  /* We only do this once, safe to re-clear on per-file mining */
+  self->is_global_mining = FALSE;
+
+  if (!gbp_todo_model_mine_finish (model, result, &error))
+    g_warning ("todo: %s", error->message);
+
+  if (self->panel != NULL)
+    gbp_todo_panel_make_ready (self->panel);
+}
+
+static void
+gbp_todo_workspace_addin_presented_cb (GbpTodoWorkspaceAddin *self,
+                                       GbpTodoPanel          *panel)
+{
+  g_assert (GBP_IS_TODO_WORKSPACE_ADDIN (self));
+  g_assert (GBP_IS_TODO_PANEL (panel));
+
+  if (self->has_presented)
+    return;
+
+  self->has_presented = TRUE;
+  self->is_global_mining = TRUE;
+
+  gbp_todo_model_mine_async (self->model,
+                             self->workdir,
+                             self->cancellable,
+                             gbp_todo_workspace_addin_mine_cb,
+                             g_object_ref (self));
+}
+
+static void
+gbp_todo_workspace_addin_buffer_saved (GbpTodoWorkspaceAddin *self,
+                                       IdeBuffer             *buffer,
+                                       IdeBufferManager      *bufmgr)
+{
+  GFile *file;
+
+  g_assert (GBP_IS_TODO_WORKSPACE_ADDIN (self));
+  g_assert (self->model != NULL);
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (IDE_IS_BUFFER_MANAGER (bufmgr));
+
+  if (!self->has_presented || self->is_global_mining)
+    return;
+
+  file = ide_buffer_get_file (buffer);
+  gbp_todo_model_mine_async (self->model,
+                             file,
+                             self->cancellable,
+                             gbp_todo_workspace_addin_mine_cb,
+                             g_object_ref (self));
+}
+
+static void
+gbp_todo_workspace_addin_load (IdeWorkspaceAddin *addin,
+                               IdeWorkspace      *workspace)
+{
+  GbpTodoWorkspaceAddin *self = (GbpTodoWorkspaceAddin *)addin;
+  IdeEditorSidebar *sidebar;
+  IdeBufferManager *bufmgr;
+  IdeSurface *editor;
+  IdeContext *context;
+  IdeVcs *vcs;
+  GFile *workdir;
+
+  g_assert (GBP_IS_TODO_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  self->cancellable = g_cancellable_new ();
+
+  context = ide_workspace_get_context (workspace);
+  vcs = ide_vcs_from_context (context);
+  workdir = ide_vcs_get_workdir (vcs);
+  bufmgr = ide_buffer_manager_from_context (context);
+  editor = ide_workspace_get_surface_by_name (workspace, "editor");
+  sidebar = ide_editor_surface_get_sidebar (IDE_EDITOR_SURFACE (editor));
+
+  self->workdir = g_object_ref (workdir);
+
+  g_signal_connect_object (bufmgr,
+                           "buffer-saved",
+                           G_CALLBACK (gbp_todo_workspace_addin_buffer_saved),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  self->model = gbp_todo_model_new (vcs);
+
+  self->panel = g_object_new (GBP_TYPE_TODO_PANEL,
+                              "model", self->model,
+                              "visible", TRUE,
+                              NULL);
+  g_signal_connect_object (self->panel,
+                           "presented",
+                           G_CALLBACK (gbp_todo_workspace_addin_presented_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect (self->panel,
+                    "destroy",
+                    G_CALLBACK (gtk_widget_destroyed),
+                    &self->panel);
+  ide_editor_sidebar_add_section (sidebar,
+                                  "todo",
+                                  _("TODO/FIXMEs"),
+                                  "emblem-ok-symbolic",
+                                  NULL, NULL,
+                                  GTK_WIDGET (self->panel),
+                                  200);
+}
+
+static void
+gbp_todo_workspace_addin_unload (IdeWorkspaceAddin *addin,
+                                 IdeWorkspace      *workspace)
+{
+  GbpTodoWorkspaceAddin *self = (GbpTodoWorkspaceAddin *)addin;
+  IdeBufferManager *bufmgr;
+  IdeContext *context;
+
+  g_assert (GBP_IS_TODO_WORKSPACE_ADDIN (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+
+  g_cancellable_cancel (self->cancellable);
+  g_clear_object (&self->cancellable);
+
+  context = ide_widget_get_context (GTK_WIDGET (workspace));
+  bufmgr = ide_buffer_manager_from_context (context);
+
+  g_signal_handlers_disconnect_by_func (bufmgr,
+                                        G_CALLBACK (gbp_todo_workspace_addin_buffer_saved),
+                                        self);
+
+  if (self->panel != NULL)
+    gtk_widget_destroy (GTK_WIDGET (self->panel));
+
+  g_assert (self->panel == NULL);
+
+  g_clear_object (&self->model);
+  g_clear_object (&self->workdir);
+}
+
+static void
+workspace_addin_iface_init (IdeWorkspaceAddinInterface *iface)
+{
+  iface->load = gbp_todo_workspace_addin_load;
+  iface->unload = gbp_todo_workspace_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpTodoWorkspaceAddin, gbp_todo_workspace_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_WORKSPACE_ADDIN,
+                                                workspace_addin_iface_init))
+
+static void
+gbp_todo_workspace_addin_class_init (GbpTodoWorkspaceAddinClass *klass)
+{
+}
+
+static void
+gbp_todo_workspace_addin_init (GbpTodoWorkspaceAddin *self)
+{
+}
diff --git a/src/plugins/todo/gbp-todo-workspace-addin.h b/src/plugins/todo/gbp-todo-workspace-addin.h
new file mode 100644
index 000000000..ea1b8f930
--- /dev/null
+++ b/src/plugins/todo/gbp-todo-workspace-addin.h
@@ -0,0 +1,31 @@
+/* gbp-todo-workspace-addin.h
+ *
+ * Copyright 2017-2019 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_TODO_WORKSPACE_ADDIN (gbp_todo_workspace_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpTodoWorkspaceAddin, gbp_todo_workspace_addin, GBP, TODO_WORKSPACE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/todo/meson.build b/src/plugins/todo/meson.build
index cc76a72fa..0e8b6f67a 100644
--- a/src/plugins/todo/meson.build
+++ b/src/plugins/todo/meson.build
@@ -1,24 +1,19 @@
-if get_option('with_todo')
+if get_option('plugin_todo')
 
-todo_resources = gnome.compile_resources(
+plugins_sources += files([
+  'gbp-todo-item.c',
+  'gbp-todo-model.c',
+  'gbp-todo-panel.c',
+  'gbp-todo-workspace-addin.c',
+  'todo-plugin.c',
+])
+
+plugin_todo_resources = gnome.compile_resources(
   'todo-resources',
   'todo.gresource.xml',
   c_name: 'gbp_todo',
 )
 
-todo_sources = [
-  'gbp-todo-item.c',
-  'gbp-todo-item.h',
-  'gbp-todo-model.c',
-  'gbp-todo-model.h',
-  'gbp-todo-plugin.c',
-  'gbp-todo-panel.c',
-  'gbp-todo-panel.h',
-  'gbp-todo-workbench-addin.c',
-  'gbp-todo-workbench-addin.h',
-]
-
-gnome_builder_plugins_sources += files(todo_sources)
-gnome_builder_plugins_sources += todo_resources[0]
+plugins_sources += plugin_todo_resources[0]
 
 endif
diff --git a/src/plugins/todo/todo-plugin.c b/src/plugins/todo/todo-plugin.c
new file mode 100644
index 000000000..7c93c5de7
--- /dev/null
+++ b/src/plugins/todo/todo-plugin.c
@@ -0,0 +1,34 @@
+/* todo-plugin.c
+ *
+ * Copyright 2015-2019 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
+ */
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-gui.h>
+
+#include "gbp-todo-workspace-addin.h"
+
+_IDE_EXTERN void
+_gbp_todo_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_WORKSPACE_ADDIN,
+                                              GBP_TYPE_TODO_WORKSPACE_ADDIN);
+}
diff --git a/src/plugins/todo/todo.gresource.xml b/src/plugins/todo/todo.gresource.xml
index 793c8cebd..991ecb4a8 100644
--- a/src/plugins/todo/todo.gresource.xml
+++ b/src/plugins/todo/todo.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/todo">
     <file>todo.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/todo/todo.plugin b/src/plugins/todo/todo.plugin
index 580dc8c70..a249e9b6f 100644
--- a/src/plugins/todo/todo.plugin
+++ b/src/plugins/todo/todo.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=todo-plugin
-Name=To-Do Tracker
-Description=Find and present To-Do items from source code
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015-2017 Christian Hergert
 Builtin=true
-Depends=editor
-Embedded=gbp_todo_register_types
+Copyright=Copyright © 2015-2018 Christian Hergert
+Depends=editor;
+Description=Find and present To-Do items from source code
+Embedded=_gbp_todo_register_types
+Module=todo
+Name=To-Do Tracker
+X-Workspace-Kind=primary;
diff --git a/src/plugins/trim-spaces/gbp-trim-spaces-buffer-addin.c 
b/src/plugins/trim-spaces/gbp-trim-spaces-buffer-addin.c
new file mode 100644
index 000000000..57cffb35f
--- /dev/null
+++ b/src/plugins/trim-spaces/gbp-trim-spaces-buffer-addin.c
@@ -0,0 +1,77 @@
+/* gbp-trim-spaces-buffer-addin.c
+ *
+ * Copyright 2018-2019 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-trim-spaces-buffer-addin"
+
+#include "config.h"
+
+#include <libide-code.h>
+
+#include "ide-buffer-private.h"
+
+#include "gbp-trim-spaces-buffer-addin.h"
+
+struct _GbpTrimSpacesBufferAddin
+{
+  GObject parent_instance;
+};
+
+static void
+gbp_trim_spaces_buffer_addin_save_file (IdeBufferAddin *addin,
+                                        IdeBuffer      *buffer,
+                                        GFile          *file)
+{
+  IdeFileSettings *file_settings;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_TRIM_SPACES_BUFFER_ADDIN (addin));
+  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
+
+  if (!(file_settings = ide_buffer_get_file_settings (buffer)) ||
+      !ide_file_settings_get_trim_trailing_whitespace (file_settings))
+    return;
+
+  /*
+   * If file-settings dictate that we should trim trailing whitespace, trim it
+   * from the modified lines in the IdeBuffer. This is performed automatically
+   * based on line state within ide_buffer_trim_trailing_whitespace().
+   */
+  ide_buffer_trim_trailing_whitespace (buffer);
+}
+
+static void
+buffer_addin_iface_init (IdeBufferAddinInterface *iface)
+{
+  iface->save_file = gbp_trim_spaces_buffer_addin_save_file;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpTrimSpacesBufferAddin, gbp_trim_spaces_buffer_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_BUFFER_ADDIN, buffer_addin_iface_init))
+
+static void
+gbp_trim_spaces_buffer_addin_class_init (GbpTrimSpacesBufferAddinClass *klass)
+{
+}
+
+static void
+gbp_trim_spaces_buffer_addin_init (GbpTrimSpacesBufferAddin *self)
+{
+}
diff --git a/src/plugins/trim-spaces/gbp-trim-spaces-buffer-addin.h 
b/src/plugins/trim-spaces/gbp-trim-spaces-buffer-addin.h
new file mode 100644
index 000000000..44d39332e
--- /dev/null
+++ b/src/plugins/trim-spaces/gbp-trim-spaces-buffer-addin.h
@@ -0,0 +1,31 @@
+/* gbp-trim-spaces-buffer-addin.h
+ *
+ * Copyright 2018-2019 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_TRIM_SPACES_BUFFER_ADDIN (gbp_trim_spaces_buffer_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpTrimSpacesBufferAddin, gbp_trim_spaces_buffer_addin, GBP, TRIM_SPACES_BUFFER_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/trim-spaces/meson.build b/src/plugins/trim-spaces/meson.build
new file mode 100644
index 000000000..e366b0e69
--- /dev/null
+++ b/src/plugins/trim-spaces/meson.build
@@ -0,0 +1,12 @@
+plugins_sources += files([
+  'trim-spaces-plugin.c',
+  'gbp-trim-spaces-buffer-addin.c',
+])
+
+plugin_trim_spaces_resources = gnome.compile_resources(
+  'gbp-trim-spaces-resources',
+  'trim-spaces.gresource.xml',
+  c_name: 'gbp_trim_spaces',
+)
+
+plugins_sources += plugin_trim_spaces_resources[0]
diff --git a/src/plugins/trim-spaces/trim-spaces-plugin.c b/src/plugins/trim-spaces/trim-spaces-plugin.c
new file mode 100644
index 000000000..8fbbe1024
--- /dev/null
+++ b/src/plugins/trim-spaces/trim-spaces-plugin.c
@@ -0,0 +1,36 @@
+/* trim-spaces-plugin.c
+ *
+ * Copyright 2018-2019 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 "trim-spaces-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-code.h>
+
+#include "gbp-trim-spaces-buffer-addin.h"
+
+_IDE_EXTERN void
+_gbp_trim_spaces_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_BUFFER_ADDIN,
+                                              GBP_TYPE_TRIM_SPACES_BUFFER_ADDIN);
+}
diff --git a/src/plugins/trim-spaces/trim-spaces.gresource.xml 
b/src/plugins/trim-spaces/trim-spaces.gresource.xml
new file mode 100644
index 000000000..d492fd495
--- /dev/null
+++ b/src/plugins/trim-spaces/trim-spaces.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/trim-spaces">
+    <file>trim-spaces.plugin</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/trim-spaces/trim-spaces.plugin b/src/plugins/trim-spaces/trim-spaces.plugin
new file mode 100644
index 000000000..d97ccde0c
--- /dev/null
+++ b/src/plugins/trim-spaces/trim-spaces.plugin
@@ -0,0 +1,10 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2018 Christian Hergert
+Depends=editor;
+Description=Trim trailing whitespace when saving buffers
+Embedded=_gbp_trim_spaces_register_types
+Hidden=true
+Module=trim-spaces
+Name=Trim Spaces
diff --git a/src/plugins/vala-pack/ide-vala-code-indexer.vala 
b/src/plugins/vala-pack/ide-vala-code-indexer.vala
index ba24c64aa..0ad3ea95b 100644
--- a/src/plugins/vala-pack/ide-vala-code-indexer.vala
+++ b/src/plugins/vala-pack/ide-vala-code-indexer.vala
@@ -34,7 +34,7 @@ namespace Ide
                        throws GLib.Error
                {
                        var context = this.get_context ();
-                       var service = (Ide.ValaService)context.get_service_typed (typeof (Ide.ValaService));
+                       var service = Ide.ValaService.from_context (context);
                        var index = service.index;
                        var tree = index.get_symbol_tree_sync (file, cancellable);
 
@@ -55,18 +55,18 @@ namespace Ide
                        return ret;
                }
 
-               public async string generate_key_async (Ide.SourceLocation location,
+               public async string generate_key_async (Ide.Location location,
                                                        string[]? build_flags,
                                                        GLib.Cancellable? cancellable)
                        throws GLib.Error
                {
                        var context = this.get_context ();
-                       var service = (Ide.ValaService)context.get_service_typed (typeof (Ide.ValaService));
+                       var service = Ide.ValaService.from_context (context);
                        var index = service.index;
                        var file = location.get_file ();
                        var line = location.get_line () + 1;
                        var column = location.get_line_offset () + 1;
-                       Vala.Symbol? symbol = yield index.find_symbol_at (file.get_file (), (int)line, 
(int)column);
+                       Vala.Symbol? symbol = yield index.find_symbol_at (file, (int)line, (int)column);
 
                        if (symbol == null)
                                throw new GLib.IOError.FAILED ("failed to locate symbol");
diff --git a/src/plugins/vala-pack/ide-vala-completion-provider.vala 
b/src/plugins/vala-pack/ide-vala-completion-provider.vala
index 2174f0306..510c81841 100644
--- a/src/plugins/vala-pack/ide-vala-completion-provider.vala
+++ b/src/plugins/vala-pack/ide-vala-completion-provider.vala
@@ -29,7 +29,7 @@ namespace Ide
 
                public void load (Ide.Context context)
                {
-                       this.service = context.get_service_typed (typeof (Ide.ValaService)) as 
Ide.ValaService;
+                       this.service = Ide.ValaService.from_context (context);
                }
 
                public bool is_trigger (Gtk.TextIter iter, unichar ch)
@@ -61,11 +61,12 @@ namespace Ide
                        var file = buffer.get_file ();
                        Gtk.TextIter iter, begin;
 
-                       if (file.is_temporary) {
+                       if (buffer.is_temporary) {
                                throw new GLib.IOError.NOT_SUPPORTED ("Cannot complete on temporary files");
                        }
 
-                       buffer.sync_to_unsaved_files ();
+                       /* force buffer sync */
+                       buffer.dup_content ();
 
                        context.get_bounds (out iter, null);
 
@@ -77,7 +78,7 @@ namespace Ide
                        var line_offset = iter.get_line_offset ();
 
                        var index = this.service.index;
-                       var unsaved_files = this.get_context ().get_unsaved_files ();
+                       var unsaved_files = Ide.UnsavedFiles.from_context (this.get_context ());
 
                        /* make a copy for threaded access */
                        var unsaved_files_copy = unsaved_files.to_array ();
@@ -85,7 +86,7 @@ namespace Ide
                        Ide.ThreadPool.push (Ide.ThreadPoolKind.COMPILER, () => {
                                int res_line = -1;
                                int res_column = -1;
-                               results = index.code_complete (file.file,
+                               results = index.code_complete (file,
                                                               line + 1,
                                                               line_offset + 1,
                                                               line_text,
diff --git a/src/plugins/vala-pack/ide-vala-diagnostic-provider.vala 
b/src/plugins/vala-pack/ide-vala-diagnostic-provider.vala
index de7f0bbbe..7771c34dd 100644
--- a/src/plugins/vala-pack/ide-vala-diagnostic-provider.vala
+++ b/src/plugins/vala-pack/ide-vala-diagnostic-provider.vala
@@ -24,17 +24,20 @@ namespace Ide
 {
        public class ValaDiagnosticProvider: Ide.Object, Ide.DiagnosticProvider
        {
-               public async Ide.Diagnostics? diagnose_async (Ide.File file,
-                                                             Ide.Buffer buffer,
-                                                             GLib.Cancellable? cancellable)
+               public async Ide.Diagnostics diagnose_async (GLib.File file,
+                                                            GLib.Bytes? contents,
+                                                            string? language_id,
+                                                            GLib.Cancellable? cancellable)
                        throws GLib.Error
                {
-                       var service = (Ide.ValaService)get_context ().get_service_typed (typeof 
(Ide.ValaService));
-                       yield service.index.parse_file (file.file, get_context ().unsaved_files, cancellable);
-                       var results = yield service.index.get_diagnostics (file.file, cancellable);
+                       var service = Ide.ValaService.from_context (this.get_context ());
+                       var unsaved_files = Ide.UnsavedFiles.from_context (this.get_context ());
+                       yield service.index.parse_file (file, unsaved_files, cancellable);
+                       var results = yield service.index.get_diagnostics (file, cancellable);
                        return results;
                }
 
                public void load () {}
+               public void unload () {}
        }
 }
diff --git a/src/plugins/vala-pack/ide-vala-index.vala b/src/plugins/vala-pack/ide-vala-index.vala
index 14f5ec00f..f9b8bd82d 100644
--- a/src/plugins/vala-pack/ide-vala-index.vala
+++ b/src/plugins/vala-pack/ide-vala-index.vala
@@ -41,8 +41,7 @@ namespace Ide
 
                public ValaIndex (Ide.Context context)
                {
-                       var vcs = context.get_vcs();
-                       var workdir = vcs.get_working_directory();
+                       var workdir = context.ref_workdir ();
 
                        this.source_files = new HashMap<GLib.File,Ide.ValaSourceFile> (GLib.File.hash, 
(GLib.EqualFunc)GLib.File.equal);
 
@@ -83,20 +82,6 @@ namespace Ide
 
                        this.code_context.run_output = false;
 
-                       int minor = 36;
-                       var tokens = Config.VALA_VERSION.split(".", 2);
-                       if (tokens[1] != null) {
-                               minor = int.parse(tokens[1]);
-                       }
-
-                       for (var i = 2; i <= minor; i += 2) {
-                               this.code_context.add_define ("VALA_0_%d".printf (i));
-                       }
-
-                       for (var i = 16; i < GLib.Version.minor; i+= 2) {
-                               this.code_context.add_define ("GLIB_2_%d".printf (i));
-                       }
-
                        this.code_context.vapi_directories = {};
 
                        /* $prefix/share/vala-0.32/vapi */
@@ -246,8 +231,7 @@ namespace Ide
                                        }
                                        else if (param.has_suffix (".vapi")) {
                                                if (!GLib.Path.is_absolute (param)) {
-                                                       var vcs = this.context.get_vcs ();
-                                                       var workdir = vcs.get_working_directory ();
+                                                       var workdir = context.ref_workdir ();
                                                        var child = workdir.get_child (param);
                                                        this.add_file (child);
                                                } else {
@@ -268,11 +252,10 @@ namespace Ide
                async void update_build_flags (GLib.File file,
                                               GLib.Cancellable? cancellable)
                {
-                       var ifile = new Ide.File (this.context, file);
-                       var build_system = this.context.get_build_system ();
+                       var build_system = Ide.BuildSystem.from_context (this.context);
 
                        try {
-                               var flags = yield build_system.get_build_flags_async (ifile, cancellable);
+                               var flags = yield build_system.get_build_flags_async (file, cancellable);
                                load_build_flags (flags);
                        } catch (GLib.Error err) {
                                warning ("%s", err.message);
diff --git a/src/plugins/vala-pack/ide-vala-service.vala b/src/plugins/vala-pack/ide-vala-service.vala
index c0cb4ee05..1223abf6c 100644
--- a/src/plugins/vala-pack/ide-vala-service.vala
+++ b/src/plugins/vala-pack/ide-vala-service.vala
@@ -22,10 +22,14 @@ using Vala;
 
 namespace Ide
 {
-       public class ValaService: Ide.Object, Ide.Service
+       public class ValaService: Ide.Object
        {
                Ide.ValaIndex _index;
 
+               public static Ide.ValaService from_context (Ide.Context context) {
+                       return (Ide.ValaService)context.ensure_child_typed (typeof (ValaService));
+               }
+
                public unowned string get_name () {
                        return typeof (Ide.ValaService).name ();
                }
@@ -36,10 +40,10 @@ namespace Ide
                                        this._index = new Ide.ValaIndex (this.get_context ());
 
                                        Ide.ThreadPool.push (Ide.ThreadPoolKind.INDEXER, () => {
-                                               Ide.Vcs vcs = this.get_context ().get_vcs ();
+                                               var workdir = this.ref_context ().ref_workdir ();
                                                var files = new ArrayList<GLib.File> ();
 
-                                               load_directory (vcs.get_working_directory (), null, files);
+                                               load_directory (workdir, null, files);
 
                                                if (files.size > 0) {
                                                        this._index.add_files.begin (files, null, () => {
@@ -53,12 +57,6 @@ namespace Ide
                        }
                }
 
-               public void start () {
-               }
-
-               public void stop () {
-               }
-
                public void load_directory (GLib.File directory,
                                            GLib.Cancellable? cancellable,
                                            ArrayList<GLib.File> files)
diff --git a/src/plugins/vala-pack/ide-vala-source-file.vala b/src/plugins/vala-pack/ide-vala-source-file.vala
index 9572fb7ab..f82d580c5 100644
--- a/src/plugins/vala-pack/ide-vala-source-file.vala
+++ b/src/plugins/vala-pack/ide-vala-source-file.vala
@@ -45,7 +45,7 @@ namespace Ide
        public class ValaSourceFile: Vala.SourceFile
        {
                ArrayList<Ide.Diagnostic> diagnostics;
-               internal Ide.File file;
+               internal GLib.File file;
 
                public ValaSourceFile (Vala.CodeContext context,
                                       Vala.SourceFileType type,
@@ -55,7 +55,7 @@ namespace Ide
                {
                        base (context, type, filename, content, cmdline);
 
-                       this.file = new Ide.File (null, GLib.File.new_for_path (filename));
+                       this.file = GLib.File.new_for_path (filename);
                        this.diagnostics = new ArrayList<Ide.Diagnostic> ();
 
                        this.add_default_namespace ();
@@ -66,12 +66,18 @@ namespace Ide
 
                public GLib.File get_file ()
                {
-                       return this.file.file;
+                       return this.file;
                }
 
                public void reset ()
                {
-                       this.diagnostics.clear ();
+                       /* clear diagnostics on main thread */
+                       var old_diags = this.diagnostics;
+                       this.diagnostics = new ArrayList<Ide.Diagnostic> ();
+                       GLib.Idle.add(() => {
+                               old_diags.clear ();
+                               return false;
+                       });
 
                        /* Copy the node list since we will be mutating while iterating */
                        var copy = new ArrayList<Vala.CodeNode> ();
@@ -101,9 +107,8 @@ namespace Ide
 
                public void sync (GenericArray<Ide.UnsavedFile> unsaved_files)
                {
-                       var gfile = this.file.file;
                        unsaved_files.foreach((unsaved_file) => {
-                               if (unsaved_file.get_file ().equal (gfile)) {
+                               if (unsaved_file.get_file ().equal (this.file)) {
                                        var bytes = unsaved_file.get_content ();
 
                                        if (bytes.get_data () != (uint8[]) this.content) {
@@ -119,27 +124,25 @@ namespace Ide
                                    string message,
                                    Ide.DiagnosticSeverity severity)
                {
-                       var begin = new Ide.SourceLocation (this.file,
-                                                           source_reference.begin.line - 1,
-                                                           source_reference.begin.column - 1,
-                                                           0);
-                       var end = new Ide.SourceLocation (this.file,
-                                                         source_reference.end.line - 1,
-                                                         source_reference.end.column - 1,
-                                                         0);
+                       var begin = new Ide.Location (this.file,
+                                                     source_reference.begin.line - 1,
+                                                     source_reference.begin.column - 1);
+                       var end = new Ide.Location (this.file,
+                                                   source_reference.end.line - 1,
+                                                   source_reference.end.column - 1);
 
                        var diag = new Ide.Diagnostic (severity, message, begin);
-                       diag.take_range (new Ide.SourceRange (begin, end));
+                       diag.take_range (new Ide.Range (begin, end));
                        this.diagnostics.add (diag);
                }
 
                public Ide.Diagnostics? diagnose ()
                {
-                       var ar = new GLib.GenericArray<Ide.Diagnostic> ();
+                       var ret = new Ide.Diagnostics ();
                        foreach (var diag in this.diagnostics) {
-                               ar.add (diag);
+                               ret.add (diag);
                        }
-                       return new Ide.Diagnostics (ar);
+                       return ret;
                }
 
                void add_default_namespace ()
diff --git a/src/plugins/vala-pack/ide-vala-symbol-resolver.vala 
b/src/plugins/vala-pack/ide-vala-symbol-resolver.vala
index 4d98a3b1c..2a8febfae 100644
--- a/src/plugins/vala-pack/ide-vala-symbol-resolver.vala
+++ b/src/plugins/vala-pack/ide-vala-symbol-resolver.vala
@@ -25,19 +25,19 @@ namespace Ide
        public class ValaSymbolResolver: Ide.Object, Ide.SymbolResolver
        {
                public async Ide.SymbolTree? get_symbol_tree_async (GLib.File file,
-                                                                   Ide.Buffer buffer,
+                                                                   GLib.Bytes? contents,
                                                                    GLib.Cancellable? cancellable)
                        throws GLib.Error
                {
                        var context = this.get_context ();
-                       var service = (Ide.ValaService)context.get_service_typed (typeof (Ide.ValaService));
+                       var service = Ide.ValaService.from_context (context);
                        var index = service.index;
                        var symbol_tree = yield index.get_symbol_tree (file, cancellable);
 
                        return symbol_tree;
                }
 
-               Ide.Symbol? create_symbol (Ide.File file, Vala.Symbol symbol)
+               Ide.Symbol? create_symbol (GLib.File file, Vala.Symbol symbol)
                {
                        var kind = Ide.SymbolKind.NONE;
                        if (symbol is Vala.Class)
@@ -69,28 +69,27 @@ namespace Ide
                        var source_reference = symbol.source_reference;
 
                        if (source_reference != null) {
-                               var loc = new Ide.SourceLocation (file,
-                                                                                                 
source_reference.begin.line - 1,
-                                                                                                 
source_reference.begin.column - 1,
-                                                                                                 0);
-                               return new Ide.Symbol (symbol.name, kind, flags, loc, loc, loc);
+                               var loc = new Ide.Location (file,
+                                                           source_reference.begin.line - 1,
+                                                           source_reference.begin.column - 1);
+                               return new Ide.Symbol (symbol.name, kind, flags, loc, loc);
                        }
 
                        return null;
                }
 
-               public async Ide.Symbol? lookup_symbol_async (Ide.SourceLocation location,
+               public async Ide.Symbol? lookup_symbol_async (Ide.Location location,
                                                              GLib.Cancellable? cancellable)
                        throws GLib.Error
                {
                        var context = this.get_context ();
-                       var service = (Ide.ValaService)context.get_service_typed (typeof (Ide.ValaService));
+                       var service = Ide.ValaService.from_context (context);
                        var index = service.index;
                        var file = location.get_file ();
                        var line = (int)location.get_line () + 1;
                        var column = (int)location.get_line_offset () + 1;
 
-                       Vala.Symbol? symbol = yield index.find_symbol_at (file.get_file (), line, column);
+                       Vala.Symbol? symbol = yield index.find_symbol_at (file, line, column);
 
                        if (symbol != null)
                                return create_symbol (file, symbol);
@@ -117,25 +116,26 @@ namespace Ide
                public void load () {}
                public void unload () {}
 
-               public async GLib.GenericArray<Ide.SourceRange> find_references_async (Ide.SourceLocation 
location,
-                                                                                      GLib.Cancellable? 
cancellable)
+               public async GLib.GenericArray<Ide.Range> find_references_async (Ide.Location location,
+                                                                                string? language_id,
+                                                                                GLib.Cancellable? 
cancellable)
                        throws GLib.Error
                {
-                       return new GLib.GenericArray<Ide.SourceRange> ();
+                       return new GLib.GenericArray<Ide.Range> ();
                }
 
-               public async Ide.Symbol? find_nearest_scope_async (Ide.SourceLocation location,
+               public async Ide.Symbol? find_nearest_scope_async (Ide.Location location,
                                                                   GLib.Cancellable? cancellable)
                        throws GLib.Error
                {
                        var context = this.get_context ();
-                       var service = (Ide.ValaService)context.get_service_typed (typeof (Ide.ValaService));
+                       var service = Ide.ValaService.from_context (context);
                        var index = service.index;
                        var file = location.get_file ();
                        var line = (int)location.get_line () + 1;
                        var column = (int)location.get_line_offset () + 1;
 
-                       var symbol = yield index.find_symbol_at (file.get_file (), line, column);
+                       var symbol = yield index.find_symbol_at (file, line, column);
 
                        while (symbol != null) {
                                if (symbol is Vala.Class ||
diff --git a/src/plugins/vala-pack/ide-vala-symbol-tree.vala b/src/plugins/vala-pack/ide-vala-symbol-tree.vala
index d5140a11a..9edfde2ad 100644
--- a/src/plugins/vala-pack/ide-vala-symbol-tree.vala
+++ b/src/plugins/vala-pack/ide-vala-symbol-tree.vala
@@ -142,15 +142,14 @@ namespace Ide
                                this.kind = Ide.SymbolKind.FIELD;
                }
 
-               public override async Ide.SourceLocation? get_location_async (GLib.Cancellable? cancellable)
+               public override async Ide.Location? get_location_async (GLib.Cancellable? cancellable)
                {
                        var source_reference = this.node.source_reference;
                        var file = (source_reference.file as Ide.ValaSourceFile).file;
 
-                       return new Ide.SourceLocation (file,
-                                                      source_reference.begin.line - 1,
-                                                      source_reference.begin.column - 1,
-                                                      0);
+                       return new Ide.Location (file,
+                                                source_reference.begin.line - 1,
+                                                source_reference.begin.column - 1);
                }
        }
 }
diff --git a/src/plugins/vala-pack/meson.build b/src/plugins/vala-pack/meson.build
index a8c53c6e5..bc3be9d63 100644
--- a/src/plugins/vala-pack/meson.build
+++ b/src/plugins/vala-pack/meson.build
@@ -1,8 +1,4 @@
-if get_option('with_vala_pack')
-
-if not get_option('with_vapi')
-#  error('You must enable VAPI generation to build the Vala pack')
-endif
+if get_option('plugin_vala')
 
 add_languages('vala')
 
@@ -10,7 +6,7 @@ valac = meson.get_compiler('vala')
 libvala_version = run_command(valac.cmd_array()[0], '--api-version').stdout().strip()
 libvala = dependency('libvala-@0@'.format(libvala_version))
 
-vala_pack_sources = [
+vala_sources = [
   'config.vapi',
   'ide-vala-service.vala',
   'ide-vala-code-indexer.vala',
@@ -30,41 +26,45 @@ vala_pack_sources = [
   'vala-pack-plugin.vala',
 ]
 
-vala_pack_deps = [
+vala_deps = [
   libvala,
-  libide_vapi,
   libpeas_dep,
-  libide_plugin_dep,
+  libide_vapi,
+  libgtksource_dep,
+  libvte_dep,
+  libdazzle_dep,
+  libtemplate_glib_dep,
 ]
 
-shared_module('vala-pack-plugin', vala_pack_sources,
-     link_args: gnome_builder_plugins_link_args,
-  link_depends: gnome_builder_plugins_link_deps,
-  dependencies: vala_pack_deps,
-       install: true,
-   install_dir: plugindir,
- install_rpath: pkglibdir_abs,
+shared_module('plugin-vala-pack', vala_sources,
+         dependencies: vala_deps,
+              install: true,
+          install_dir: plugindir,
+        install_rpath: pkglibdir_abs,
+  include_directories: [include_directories('.')] + libide_include_directories,
 
-     vala_args: [ '--target-glib=2.52',
-                  '--pkg=posix',
-                  '--pkg=libpeas-1.0',
-                  '--pkg=gtksourceview-4',
-                  '--pkg=gio-2.0',
-                  '--pkg=libvala-' + libvala_version,
-                  '--pkg=libdazzle-1.0',
-                ],
-        c_args: [ '-DVALA_VERSION="@0@"'.format(libvala_version),
-                  '-DLOG_DOMAIN="vala-pack-plugin"',
-                  '-DGETTEXT_PACKAGE="gnome-builder"',
-                  '-DPACKAGE_DATADIR="@0@"'.format(join_paths(get_option('prefix'), get_option('datadir'), 
'gnome-builder')),
-                 '-Wno-incompatible-pointer-types',
-                ],
+            vala_args: [ '--target-glib=2.52',
+                         '--pkg=posix',
+                         '--pkg=libpeas-1.0',
+                         '--pkg=gtksourceview-4',
+                         '--pkg=gio-2.0',
+                         '--pkg=libvala-' + libvala_version,
+                         '--pkg=libdazzle-1.0',
+                         '--pkg=template-glib-1.0',
+                         '--pkg=vte-2.91',
+                       ],
+               c_args: [ '-DVALA_VERSION="@0@"'.format(libvala_version),
+                         '-DLOG_DOMAIN="vala-pack"',
+                         '-DGETTEXT_PACKAGE="gnome-builder"',
+                         '-DPACKAGE_DATADIR="@0@"'.format(join_paths(get_option('prefix'), 
get_option('datadir'), 'gnome-builder')),
+                        '-Wno-incompatible-pointer-types',
+                       ],
 )
 
 configure_file(
           input: 'vala-pack.plugin',
          output: 'vala-pack.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/vala-pack/vala-pack-plugin.vala b/src/plugins/vala-pack/vala-pack-plugin.vala
index df206e6bb..5a1b78e21 100644
--- a/src/plugins/vala-pack/vala-pack-plugin.vala
+++ b/src/plugins/vala-pack/vala-pack-plugin.vala
@@ -31,6 +31,5 @@ public void peas_register_types (GLib.TypeModule module)
        peas.register_extension_type (typeof (Ide.DiagnosticProvider), typeof (Ide.ValaDiagnosticProvider));
        peas.register_extension_type (typeof (Ide.Indenter), typeof (Ide.ValaIndenter));
        peas.register_extension_type (typeof (Ide.PreferencesAddin), typeof (Ide.ValaPreferencesAddin));
-       peas.register_extension_type (typeof (Ide.Service), typeof (Ide.ValaService));
        peas.register_extension_type (typeof (Ide.SymbolResolver), typeof (Ide.ValaSymbolResolver));
 }
diff --git a/src/plugins/vala-pack/vala-pack.plugin b/src/plugins/vala-pack/vala-pack.plugin
index ee625b166..42eb787ea 100644
--- a/src/plugins/vala-pack/vala-pack.plugin
+++ b/src/plugins/vala-pack/vala-pack.plugin
@@ -1,15 +1,16 @@
 [Plugin]
-Module=vala-pack-plugin
-Name=Vala Language Pack
-Description=Provides an auto-indenter, auto-completion, diagnostics, and symbol resolver for Vala
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
+Copyright=Copyright © 2015 Christian Hergert
+Description=Provides an auto-indenter, auto-completion, diagnostics, and symbol resolver for Vala
+Module=plugin-vala-pack
+Name=Vala Language Pack
+X-Builder-ABI=@PACKAGE_ABI@
+X-Code-Indexer-Languages-Priority=100
+X-Code-Indexer-Languages=vala
 X-Completion-Provider-Languages=vala
 X-Diagnostic-Provider-Languages=vala
-X-Indenter-Languages=vala
 X-Indenter-Languages-Priority=0
-X-Symbol-Resolver-Languages=vala
+X-Indenter-Languages=vala
 X-Symbol-Resolver-Languages-Priority=100
-X-Code-Indexer-Languages=vala
-X-Code-Indexer-Languages-Priority=100
+X-Symbol-Resolver-Languages=vala
diff --git a/src/plugins/valgrind/gtk/menus.ui b/src/plugins/valgrind/gtk/menus.ui
index 598a29e41..4a2e8122a 100644
--- a/src/plugins/valgrind/gtk/menus.ui
+++ b/src/plugins/valgrind/gtk/menus.ui
@@ -13,4 +13,14 @@
       </item>
     </section>
   </menu>
+  <menu id="project-tree-run-with-submenu">
+    <section id="project-tree-menu-run-with-section">
+      <item>
+        <attribute name="id">project-tree-menu-valgrind</attribute>
+        <attribute name="label" translatable="yes">Run with Valgrind</attribute>
+        <attribute name="action">buildui.run-with-handler</attribute>
+        <attribute name="target" type="s">'valgrind'</attribute>
+      </item>
+    </section>
+  </menu>
 </interface>
diff --git a/src/plugins/valgrind/meson.build b/src/plugins/valgrind/meson.build
index 7684f9f0c..240671f1f 100644
--- a/src/plugins/valgrind/meson.build
+++ b/src/plugins/valgrind/meson.build
@@ -1,20 +1,19 @@
-if get_option('with_valgrind')
-
-install_data('valgrind_plugin.py', install_dir: plugindir)
+if get_option('plugin_valgrind')
 
 valgrind_resources = gnome.compile_resources(
   'valgrind_plugin',
-  'valgrind-plugin.gresource.xml',
-
+  'valgrind.gresource.xml',
   gresource_bundle: true,
            install: true,
        install_dir: plugindir,
 )
 
+install_data('valgrind_plugin.py', install_dir: plugindir)
+
 configure_file(
           input: 'valgrind.plugin',
          output: 'valgrind.plugin',
-           copy: true,
+  configuration: config_h,
         install: true,
     install_dir: plugindir,
 )
diff --git a/src/plugins/valgrind/valgrind.gresource.xml b/src/plugins/valgrind/valgrind.gresource.xml
new file mode 100644
index 000000000..7fc84b65d
--- /dev/null
+++ b/src/plugins/valgrind/valgrind.gresource.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/valgrind_plugin">
+    <file>gtk/menus.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/valgrind/valgrind.plugin b/src/plugins/valgrind/valgrind.plugin
index 0cf8c8c6a..e8b8cc63b 100644
--- a/src/plugins/valgrind/valgrind.plugin
+++ b/src/plugins/valgrind/valgrind.plugin
@@ -1,8 +1,11 @@
 [Plugin]
-Module=valgrind_plugin
-Loader=python3
-Name=Valgrind
-Description=Provides integration with valgrind
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2017 Christian Hergert
 Builtin=true
+Copyright=Copyright © 2017-2018 Christian Hergert
+Depends=buildui;editor;
+Description=Provides integration with valgrind
+Loader=python3
+Module=valgrind_plugin
+Name=Valgrind
+X-Builder-ABI=@PACKAGE_ABI@
+X-Has-Resources=true
diff --git a/src/plugins/valgrind/valgrind_plugin.py b/src/plugins/valgrind/valgrind_plugin.py
index 2b922f70f..d067c4d44 100644
--- a/src/plugins/valgrind/valgrind_plugin.py
+++ b/src/plugins/valgrind/valgrind_plugin.py
@@ -1,7 +1,7 @@
 #
-# __init__.py
+# valgrind_plugin.py
 #
-# Copyright 2017 Christian Hergert <chergert redhat com>
+# Copyright 2017-2018 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
@@ -20,8 +20,6 @@
 import gi
 import os
 
-gi.require_version('Ide', '1.0')
-
 from gi.repository import Ide
 from gi.repository import GLib
 from gi.repository import GObject
@@ -29,6 +27,7 @@ from gi.repository import GObject
 _ = Ide.gettext
 
 class ValgrindWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
+    build_manager = None
     workbench = None
     has_handler = False
     notify_handler = None
@@ -36,12 +35,13 @@ class ValgrindWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
     def do_load(self, workbench):
         self.workbench = workbench
 
-        build_manager = workbench.get_context().get_build_manager()
-        self.notify_handler = build_manager.connect('notify::pipeline', self.notify_pipeline)
-        self.notify_pipeline(build_manager, None)
+    def do_project_loaded(self, project_info):
+        self.build_manager = Ide.BuildManager.from_context(self.workbench.get_context())
+        self.notify_handler = self.build_manager.connect('notify::pipeline', self.notify_pipeline)
+        self.notify_pipeline(self.build_manager, None)
 
     def notify_pipeline(self, build_manager, pspec):
-        run_manager = self.workbench.get_context().get_run_manager()
+        run_manager = Ide.RunManager.from_context(self.workbench.get_context())
 
         # When the pipeline changes, we need to check to see if we can find
         # valgrind inside the runtime environment.
@@ -58,12 +58,13 @@ class ValgrindWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
             run_manager.remove_handler('valgrind')
 
     def do_unload(self, workbench):
-        build_manager = workbench.get_context().get_build_manager()
-        build_manager.disconnect(self.notify_handler)
-        self.notify_handler = None
+        if self.build_manager is not None:
+            if self.notify_handler is not None:
+                self.build_manager.disconnect(self.notify_handler)
+                self.notify_handler = None
 
         if self.has_handler:
-            run_manager = workbench.get_context().get_run_manager()
+            run_manager = Ide.RunManager.from_context(workbench.get_context())
             run_manager.remove_handler('valgrind')
 
         self.workbench = None
@@ -83,5 +84,5 @@ class ValgrindWorkbenchAddin(GObject.Object, Ide.WorkbenchAddin):
         # If we weren't unloaded in the meantime, we can open the file using
         # the "editor" hint to ensure the editor opens the file.
         if self.workbench:
-            uri = Ide.Uri.new('file://'+name, 0)
-            self.workbench.open_uri_async(uri, 'editor', 0, None, None, None)
+            gfile = Gio.File.new_for_path(name)
+            self.workbench.open_async(gfile, 'editor', 0, None, None, None)
diff --git a/src/plugins/vcsui/gbp-vcsui-editor-page-addin.c b/src/plugins/vcsui/gbp-vcsui-editor-page-addin.c
new file mode 100644
index 000000000..f952b7582
--- /dev/null
+++ b/src/plugins/vcsui/gbp-vcsui-editor-page-addin.c
@@ -0,0 +1,137 @@
+/* gbp-vcsui-editor-page-addin.c
+ *
+ * Copyright 2018-2019 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-vcsui-editor-page-addin"
+
+#include "config.h"
+
+#include <libide-editor.h>
+
+#include "gbp-vcsui-editor-page-addin.h"
+
+struct _GbpVcsuiEditorPageAddin
+{
+  GObject parent_instance;
+};
+
+static void
+on_push_snippet_cb (GbpVcsuiEditorPageAddin *self,
+                    IdeSnippet              *snippet,
+                    GtkTextIter             *iter,
+                    IdeSourceView           *source_view)
+{
+  g_autoptr(IdeVcsConfig) vcs_config = NULL;
+  g_autoptr(IdeContext) ide_context = NULL;
+  IdeSnippetContext *context;
+  IdeBuffer *buffer;
+  IdeVcs *vcs;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_VCSUI_EDITOR_PAGE_ADDIN (self));
+  g_assert (IDE_IS_SNIPPET (snippet));
+  g_assert (iter != NULL);
+  g_assert (IDE_IS_SOURCE_VIEW (source_view));
+
+  buffer = IDE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view)));
+  ide_context = ide_buffer_ref_context (buffer);
+  context = ide_snippet_get_context (snippet);
+
+  if ((vcs = ide_vcs_from_context (ide_context)) &&
+       (vcs_config = ide_vcs_get_config (vcs)))
+    {
+      GValue value = G_VALUE_INIT;
+
+      g_value_init (&value, G_TYPE_STRING);
+
+      ide_vcs_config_get_config (vcs_config, IDE_VCS_CONFIG_FULL_NAME, &value);
+
+      if (!ide_str_empty0 (g_value_get_string (&value)))
+        {
+          ide_snippet_context_add_shared_variable (context, "author", g_value_get_string (&value));
+          ide_snippet_context_add_shared_variable (context, "fullname", g_value_get_string (&value));
+          ide_snippet_context_add_shared_variable (context, "username", g_value_get_string (&value));
+        }
+
+      g_value_reset (&value);
+
+      ide_vcs_config_get_config (vcs_config, IDE_VCS_CONFIG_EMAIL, &value);
+
+      if (!ide_str_empty0 (g_value_get_string (&value)))
+        ide_snippet_context_add_shared_variable (context, "email", g_value_get_string (&value));
+
+      g_value_unset (&value);
+    }
+}
+
+static void
+gbp_vcsui_editor_page_addin_load (IdeEditorPageAddin *addin,
+                                  IdeEditorPage      *page)
+{
+  IdeSourceView *source_view;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (page));
+
+  source_view = ide_editor_page_get_view (page);
+
+  g_signal_connect_object (source_view,
+                           "push-snippet",
+                           G_CALLBACK (on_push_snippet_cb),
+                           addin,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+gbp_vcsui_editor_page_addin_unload (IdeEditorPageAddin *addin,
+                                    IdeEditorPage      *page)
+{
+  IdeSourceView *source_view;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (IDE_IS_EDITOR_PAGE_ADDIN (addin));
+  g_assert (IDE_IS_EDITOR_PAGE (page));
+
+  source_view = ide_editor_page_get_view (page);
+
+  g_signal_handlers_disconnect_by_func (source_view,
+                                        G_CALLBACK (on_push_snippet_cb),
+                                        addin);
+}
+
+static void
+editor_page_addin_iface_init (IdeEditorPageAddinInterface *iface)
+{
+  iface->load = gbp_vcsui_editor_page_addin_load;
+  iface->unload = gbp_vcsui_editor_page_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpVcsuiEditorPageAddin, gbp_vcsui_editor_page_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_EDITOR_PAGE_ADDIN, editor_page_addin_iface_init))
+
+static void
+gbp_vcsui_editor_page_addin_class_init (GbpVcsuiEditorPageAddinClass *klass)
+{
+}
+
+static void
+gbp_vcsui_editor_page_addin_init (GbpVcsuiEditorPageAddin *self)
+{
+}
diff --git a/src/plugins/vcsui/gbp-vcsui-editor-page-addin.h b/src/plugins/vcsui/gbp-vcsui-editor-page-addin.h
new file mode 100644
index 000000000..d69bcd0d6
--- /dev/null
+++ b/src/plugins/vcsui/gbp-vcsui-editor-page-addin.h
@@ -0,0 +1,31 @@
+/* gbp-vcsui-editor-page-addin.h
+ *
+ * Copyright 2018-2019 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_VCSUI_EDITOR_PAGE_ADDIN (gbp_vcsui_editor_page_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpVcsuiEditorPageAddin, gbp_vcsui_editor_page_addin, GBP, VCSUI_EDITOR_PAGE_ADDIN, 
GObject)
+
+G_END_DECLS
diff --git a/src/plugins/vcsui/gbp-vcsui-tree-addin.c b/src/plugins/vcsui/gbp-vcsui-tree-addin.c
new file mode 100644
index 000000000..37171f618
--- /dev/null
+++ b/src/plugins/vcsui/gbp-vcsui-tree-addin.c
@@ -0,0 +1,209 @@
+/* gbp-vcsui-tree-addin.c
+ *
+ * Copyright 2018-2019 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-vcsui-tree-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libpeas/peas.h>
+#include <libide-foundry.h>
+#include <libide-gui.h>
+#include <libide-plugins.h>
+#include <libide-threading.h>
+#include <libide-tree.h>
+#include <libide-vcs.h>
+
+#include "gbp-vcsui-tree-addin.h"
+
+struct _GbpVcsuiTreeAddin
+{
+  GObject        parent_instance;
+
+  IdeTree       *tree;
+  IdeTreeModel  *model;
+  IdeVcs        *vcs;
+  IdeVcsMonitor *monitor;
+
+  GdkRGBA        added_color;
+  GdkRGBA        changed_color;
+};
+
+static void
+get_foreground_for_class (GtkStyleContext   *style_context,
+                          const gchar       *name,
+                          GdkRGBA           *rgba)
+{
+  GtkStateFlags state;
+
+  g_assert (GTK_IS_STYLE_CONTEXT (style_context));
+  g_assert (name != NULL);
+  g_assert (rgba != NULL);
+
+  state = gtk_style_context_get_state (style_context);
+  gtk_style_context_save (style_context);
+  gtk_style_context_add_class (style_context, name);
+  gtk_style_context_get_color (style_context, state, rgba);
+  gtk_style_context_restore (style_context);
+}
+
+static void
+on_tree_style_changed_cb (GbpVcsuiTreeAddin *self,
+                          GtkStyleContext   *context)
+{
+  g_assert (GBP_IS_VCSUI_TREE_ADDIN (self));
+  g_assert (GTK_IS_STYLE_CONTEXT (context));
+
+  get_foreground_for_class (context, "vcs-added", &self->added_color);
+  get_foreground_for_class (context, "vcs-changed", &self->changed_color);
+}
+
+static void
+gbp_vcsui_tree_addin_load (IdeTreeAddin *addin,
+                           IdeTree      *tree,
+                           IdeTreeModel *model)
+{
+  GbpVcsuiTreeAddin *self = (GbpVcsuiTreeAddin *)addin;
+  GtkStyleContext *style_context;
+  IdeWorkbench *workbench;
+  IdeVcsMonitor *monitor;
+  IdeVcs *vcs;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_VCSUI_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  self->model = model;
+  self->tree = tree;
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (tree));
+  g_signal_connect_object (style_context,
+                           "changed",
+                           G_CALLBACK (on_tree_style_changed_cb),
+                           self,
+                           G_CONNECT_SWAPPED);
+  on_tree_style_changed_cb (self, style_context);
+
+  if ((workbench = ide_widget_get_workbench (GTK_WIDGET (tree))) &&
+      (vcs = ide_workbench_get_vcs (workbench)) &&
+      (monitor = ide_workbench_get_vcs_monitor (workbench)))
+    {
+      self->vcs = g_object_ref (vcs);
+      self->monitor = g_object_ref (monitor);
+      g_signal_connect_object (self->monitor,
+                               "changed",
+                               G_CALLBACK (gtk_widget_queue_draw),
+                               tree,
+                               G_CONNECT_SWAPPED);
+    }
+}
+
+static void
+gbp_vcsui_tree_addin_unload (IdeTreeAddin *addin,
+                             IdeTree      *tree,
+                             IdeTreeModel *model)
+{
+  GbpVcsuiTreeAddin *self = (GbpVcsuiTreeAddin *)addin;
+  GtkStyleContext *style_context;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_VCSUI_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE (tree));
+  g_assert (IDE_IS_TREE_MODEL (model));
+
+  style_context = gtk_widget_get_style_context (GTK_WIDGET (tree));
+  g_signal_handlers_disconnect_by_func (style_context,
+                                        G_CALLBACK (on_tree_style_changed_cb),
+                                        self);
+
+  g_clear_object (&self->monitor);
+  g_clear_object (&self->vcs);
+  self->model = NULL;
+  self->tree = NULL;
+}
+
+static void
+gbp_vcsui_tree_addin_selection_changed (IdeTreeAddin *addin,
+                                        IdeTreeNode  *node)
+{
+  GbpVcsuiTreeAddin *self = (GbpVcsuiTreeAddin *)addin;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_VCSUI_TREE_ADDIN (self));
+  g_assert (!node || IDE_IS_TREE_NODE (node));
+
+}
+
+static void
+gbp_vcsui_tree_addin_cell_data_func (IdeTreeAddin    *addin,
+                                     IdeTreeNode     *node,
+                                     GtkCellRenderer *cell)
+{
+  GbpVcsuiTreeAddin *self = (GbpVcsuiTreeAddin *)addin;
+  g_autoptr(IdeVcsFileInfo) info = NULL;
+  g_autoptr(GFile) file = NULL;
+  IdeProjectFile *project_file;
+
+  g_assert (GBP_IS_VCSUI_TREE_ADDIN (self));
+  g_assert (IDE_IS_TREE_NODE (node));
+  g_assert (GTK_IS_CELL_RENDERER (cell));
+
+  if (self->monitor == NULL)
+    return;
+
+  if (!ide_tree_node_holds (node, IDE_TYPE_PROJECT_FILE))
+    return;
+
+  project_file = ide_tree_node_get_item (node);
+  file = ide_project_file_ref_file (project_file);
+
+  if ((info = ide_vcs_monitor_ref_info (self->monitor, file)))
+    {
+      IdeVcsFileStatus status = ide_vcs_file_info_get_status (info);
+
+      if (status == IDE_VCS_FILE_STATUS_ADDED)
+        g_object_set (cell, "foreground-rgba", &self->added_color, NULL);
+      else if (status == IDE_VCS_FILE_STATUS_CHANGED)
+        g_object_set (cell, "foreground-rgba", &self->changed_color, NULL);
+    }
+}
+
+static void
+tree_addin_iface_init (IdeTreeAddinInterface *iface)
+{
+  iface->cell_data_func = gbp_vcsui_tree_addin_cell_data_func;
+  iface->load = gbp_vcsui_tree_addin_load;
+  iface->selection_changed = gbp_vcsui_tree_addin_selection_changed;
+  iface->unload = gbp_vcsui_tree_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpVcsuiTreeAddin, gbp_vcsui_tree_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_TREE_ADDIN, tree_addin_iface_init))
+
+static void
+gbp_vcsui_tree_addin_class_init (GbpVcsuiTreeAddinClass *klass)
+{
+}
+
+static void
+gbp_vcsui_tree_addin_init (GbpVcsuiTreeAddin *self)
+{
+}
diff --git a/src/plugins/vcsui/gbp-vcsui-tree-addin.h b/src/plugins/vcsui/gbp-vcsui-tree-addin.h
new file mode 100644
index 000000000..09a48ded1
--- /dev/null
+++ b/src/plugins/vcsui/gbp-vcsui-tree-addin.h
@@ -0,0 +1,31 @@
+/* gbp-vcsui-tree-addin.h
+ *
+ * Copyright 2018-2019 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_VCSUI_TREE_ADDIN (gbp_vcsui_tree_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpVcsuiTreeAddin, gbp_vcsui_tree_addin, GBP, VCSUI_TREE_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/vcsui/gtk/menus.ui b/src/plugins/vcsui/gtk/menus.ui
new file mode 100644
index 000000000..6d37a1313
--- /dev/null
+++ b/src/plugins/vcsui/gtk/menus.ui
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!--
+  <menu id="project-tree-menu">
+    <section id="project-tree-menu-placeholder2">
+      <submenu id="project-tree-menu-version-control">
+        <attribute name="label" translatable="yes">Version Control</attribute>
+        <item>
+          <attribute name="label" translatable="yes">Restore File</attribute>
+          <attribute name="action">vcsui.restore-file</attribute>
+        </item>
+        <item>
+          <attribute name="label" translatable="yes">Browse History</attribute>
+          <attribute name="action">vcsui.browse-history</attribute>
+        </item>
+      </submenu>
+    </section>
+  </menu>
+  -->
+</interface>
diff --git a/src/plugins/vcsui/meson.build b/src/plugins/vcsui/meson.build
new file mode 100644
index 000000000..0d198a0af
--- /dev/null
+++ b/src/plugins/vcsui/meson.build
@@ -0,0 +1,13 @@
+plugins_sources += files([
+  'vcsui-plugin.c',
+  'gbp-vcsui-tree-addin.c',
+  'gbp-vcsui-editor-page-addin.c',
+])
+
+plugin_vcsui_resources = gnome.compile_resources(
+  'vcsui-resources',
+  'vcsui.gresource.xml',
+  c_name: 'gbp_vcsui',
+)
+
+plugins_sources += plugin_vcsui_resources[0]
diff --git a/src/plugins/vcsui/vcsui-plugin.c b/src/plugins/vcsui/vcsui-plugin.c
new file mode 100644
index 000000000..3d9d2f601
--- /dev/null
+++ b/src/plugins/vcsui/vcsui-plugin.c
@@ -0,0 +1,42 @@
+/* vcsui-plugin.c
+ *
+ * Copyright 2018-2019 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 "vcsui-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <libide-tree.h>
+
+#include "gbp-vcsui-editor-page-addin.h"
+#include "gbp-vcsui-tree-addin.h"
+
+_IDE_EXTERN void
+_gbp_vcsui_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_EDITOR_PAGE_ADDIN,
+                                              GBP_TYPE_VCSUI_EDITOR_PAGE_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_TREE_ADDIN,
+                                              GBP_TYPE_VCSUI_TREE_ADDIN);
+}
diff --git a/src/plugins/vcsui/vcsui.gresource.xml b/src/plugins/vcsui/vcsui.gresource.xml
new file mode 100644
index 000000000..3dd0a29b3
--- /dev/null
+++ b/src/plugins/vcsui/vcsui.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/vcsui">
+    <file>vcsui.plugin</file>
+    <file preprocess="xml-stripblanks">gtk/menus.ui</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/vcsui/vcsui.plugin b/src/plugins/vcsui/vcsui.plugin
new file mode 100644
index 000000000..8772dac74
--- /dev/null
+++ b/src/plugins/vcsui/vcsui.plugin
@@ -0,0 +1,11 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2015-2018 Christian Hergert
+Depends=editor;project-tree;
+Description=Provides user interface components to display VCS
+Embedded=_gbp_vcsui_register_types
+Hidden=true
+Module=vcsui
+Name=VCS interface extensions
+X-Workspace-Kind=primary;
diff --git a/src/plugins/vim/gb-vim.c b/src/plugins/vim/gb-vim.c
new file mode 100644
index 000000000..5ad7bdf9a
--- /dev/null
+++ b/src/plugins/vim/gb-vim.c
@@ -0,0 +1,1661 @@
+/* gb-vim.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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 "gb-vim"
+
+#include <errno.h>
+#include <glib/gi18n.h>
+#include <gtksourceview/gtksource.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+
+#include "gb-vim.h"
+
+G_DEFINE_QUARK (gb-vim-error-quark, gb_vim_error)
+
+typedef gboolean (*GbVimSetFunc)     (GtkSourceView  *source_view,
+                                      const gchar    *key,
+                                      const gchar    *value,
+                                      GError        **error);
+typedef gboolean (*GbVimCommandFunc) (GtkWidget      *active_widget,
+                                      const gchar    *command,
+                                      const gchar    *options,
+                                      GError        **error);
+
+typedef struct
+{
+  const gchar  *name;
+  GbVimSetFunc  func;
+} GbVimSet;
+
+typedef struct
+{
+  const gchar *name;
+  const gchar *alias;
+} GbVimSetAlias;
+
+typedef struct
+{
+  const gchar      *name;
+  GbVimCommandFunc  func;
+  const gchar      *description;
+} GbVimCommand;
+
+typedef struct
+{
+  GtkWidget   *active_widget;
+  gchar *file_path;
+} SplitCallbackData;
+
+static GFile *
+find_workdir (GtkWidget *active_widget)
+{
+  g_autoptr(GFile) workdir = NULL;
+  IdeWorkbench *workbench;
+  IdeContext *context;
+
+  if (!(workbench = ide_widget_get_workbench (active_widget)) ||
+      !(context = ide_workbench_get_context (workbench)) ||
+      !(workdir = ide_context_ref_workdir (context)))
+    return NULL;
+
+  return g_steal_pointer (&workdir);
+}
+
+static gboolean
+int32_parse (gint         *value,
+             const gchar  *str,
+             gint          lower,
+             gint          upper,
+             const gchar  *param_name,
+             GError      **error)
+{
+  gint64 v64;
+  gchar *v64_str;
+
+  g_assert (value);
+  g_assert (str);
+  g_assert (lower <= upper);
+  g_assert (param_name);
+
+  v64 = g_ascii_strtoll (str, NULL, 10);
+
+  if (((v64 == G_MININT64) || (v64 == G_MAXINT64)) && (errno == ERANGE))
+    {
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_NOT_NUMBER,
+                   _("Number required"));
+      return FALSE;
+    }
+
+  if ((v64 < lower) || (v64 > upper))
+    {
+      v64_str = g_strdup_printf ("%"G_GINT64_FORMAT, v64);
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_NUMBER_OUT_OF_RANGE,
+                   _("%s is invalid for %s"),
+                   v64_str, param_name);
+      g_free (v64_str);
+      return FALSE;
+    }
+
+  *value = v64;
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_autoindent (GtkSourceView  *source_view,
+                       const gchar    *key,
+                       const gchar    *value,
+                       GError        **error)
+{
+  g_object_set (source_view, "auto-indent", TRUE, NULL);
+  return TRUE;
+}
+
+
+static gboolean
+gb_vim_set_expandtab (GtkSourceView  *source_view,
+                      const gchar    *key,
+                      const gchar    *value,
+                      GError        **error)
+{
+  g_object_set (source_view, "insert-spaces-instead-of-tabs", TRUE, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_filetype (GtkSourceView  *source_view,
+                     const gchar    *key,
+                     const gchar    *value,
+                     GError        **error)
+{
+  GtkSourceLanguageManager *manager;
+  GtkSourceLanguage *language;
+  GtkTextBuffer *buffer;
+
+  if (0 == g_strcmp0 (value, "cs"))
+    value = "c-sharp";
+  else if (0 == g_strcmp0 (value, "xhmtl"))
+    value = "html";
+  else if (0 == g_strcmp0 (value, "javascript"))
+    value = "js";
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));
+  manager = gtk_source_language_manager_get_default ();
+  language = gtk_source_language_manager_get_language (manager, value);
+
+  if (language == NULL)
+    {
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_UNKNOWN_OPTION,
+                   _("Cannot find language “%s”"),
+                   value);
+      return FALSE;
+    }
+
+  g_object_set (buffer, "language", language, NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_noautoindent (GtkSourceView  *source_view,
+                         const gchar    *key,
+                         const gchar    *value,
+                         GError        **error)
+{
+  g_object_set (source_view, "auto-indent", FALSE, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_noexpandtab (GtkSourceView  *source_view,
+                        const gchar    *key,
+                        const gchar    *value,
+                        GError        **error)
+{
+  g_object_set (source_view, "insert-spaces-instead-of-tabs", FALSE, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_nonumber (GtkSourceView  *source_view,
+                     const gchar    *key,
+                     const gchar    *value,
+                     GError        **error)
+{
+  g_object_set (source_view, "show-line-numbers", FALSE, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_number (GtkSourceView  *source_view,
+                   const gchar    *key,
+                   const gchar    *value,
+                   GError        **error)
+{
+  g_object_set (source_view, "show-line-numbers", TRUE, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_scrolloff (GtkSourceView  *source_view,
+                      const gchar    *key,
+                      const gchar    *value,
+                      GError        **error)
+{
+  gint scroll_offset = 0;
+
+  if (!int32_parse (&scroll_offset, value, 0, G_MAXINT32, "scroll size", error))
+    return FALSE;
+  if (IDE_IS_SOURCE_VIEW (source_view))
+    g_object_set (source_view, "scroll-offset", scroll_offset, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_shiftwidth (GtkSourceView  *source_view,
+                       const gchar    *key,
+                       const gchar    *value,
+                       GError        **error)
+{
+  gint shiftwidth = 0;
+
+  if (!int32_parse (&shiftwidth, value, 0, G_MAXINT32, "shift width", error))
+    return FALSE;
+
+  if (shiftwidth == 0)
+    shiftwidth = -1;
+
+  g_object_set (source_view, "indent-width", shiftwidth, NULL);
+  return TRUE;
+}
+
+static gboolean
+gb_vim_set_tabstop (GtkSourceView  *source_view,
+                    const gchar    *key,
+                    const gchar    *value,
+                    GError        **error)
+{
+  gint tabstop  = 0;
+
+  if (!int32_parse (&tabstop , value, 1, 32, "tab stop", error))
+    return FALSE;
+
+  g_object_set (source_view, "tab-width", tabstop, NULL);
+  return TRUE;
+}
+
+static const GbVimSet vim_sets [] = {
+  { "autoindent",    gb_vim_set_autoindent },
+  { "expandtab",     gb_vim_set_expandtab },
+  { "filetype",      gb_vim_set_filetype },
+  { "noautoindent",  gb_vim_set_noautoindent },
+  { "noexpandtab",   gb_vim_set_noexpandtab },
+  { "nonumber",      gb_vim_set_nonumber },
+  { "number",        gb_vim_set_number },
+  { "scrolloff",     gb_vim_set_scrolloff },
+  { "shiftwidth",    gb_vim_set_shiftwidth },
+  { "tabstop",       gb_vim_set_tabstop },
+  { NULL }
+};
+
+static const GbVimSetAlias vim_set_aliases[] = {
+  { "ai",   "autoindent" },
+  { "et",   "expandtab" },
+  { "ft",   "filetype" },
+  { "noet", "noexpandtab" },
+  { "nu",   "number" },
+  { "noai", "noautoindent" },
+  { "nonu", "nonumber" },
+  { "so",   "scrolloff" },
+  { "sw",   "shiftwidth" },
+  { "ts",   "tabstop" },
+  { NULL }
+};
+
+static const GbVimSet *
+lookup_set (const gchar *key)
+{
+  gsize i;
+
+  g_assert (key);
+
+  for (i = 0; vim_set_aliases [i].name; i++)
+    {
+      if (g_str_equal (vim_set_aliases [i].name, key))
+        {
+          key = vim_set_aliases [i].alias;
+          break;
+        }
+    }
+
+  for (i = 0; vim_sets [i].name; i++)
+    {
+      if (g_str_equal (vim_sets [i].name, key))
+        return &vim_sets [i];
+    }
+
+  return NULL;
+}
+
+static gboolean
+gb_vim_set_source_view_error (GError **error)
+{
+  g_set_error (error,
+               GB_VIM_ERROR,
+               GB_VIM_ERROR_NOT_SOURCE_VIEW,
+               _("This command requires a GtkSourceView to be focused"));
+
+  return FALSE;
+}
+
+static gboolean
+gb_vim_set_no_view_error (GError **error)
+{
+  g_set_error (error,
+               GB_VIM_ERROR,
+               GB_VIM_ERROR_NO_VIEW,
+               _("This command requires a view to be focused"));
+
+  return FALSE;
+}
+
+static gboolean
+gb_vim_command_set (GtkWidget      *active_widget,
+                    const gchar    *command,
+                    const gchar    *options,
+                    GError        **error)
+{
+  IdeSourceView *source_view;
+  gboolean ret = FALSE;
+  gchar **parts;
+  gsize i;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+  g_assert (command);
+  g_assert (options);
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+  else
+    return gb_vim_set_source_view_error (error);
+
+  parts = g_strsplit (options, " ", 0);
+
+  if (parts [0] == NULL)
+    {
+      ret = TRUE;
+      goto cleanup;
+    }
+
+  for (i = 0; parts [i]; i++)
+    {
+      const GbVimSet *set;
+      const gchar *value = "";
+      gchar *key = parts [i];
+      gchar *tmp;
+
+      for (tmp = key; *tmp; tmp = g_utf8_next_char (tmp))
+        {
+          if (g_utf8_get_char (tmp) == '=')
+            {
+              *tmp = '\0';
+              value = ++tmp;
+              break;
+            }
+        }
+
+      set = lookup_set (key);
+
+      if (set == NULL)
+        {
+          g_set_error (error,
+                       GB_VIM_ERROR,
+                       GB_VIM_ERROR_UNKNOWN_OPTION,
+                       _("Unknown option: %s"),
+                       key);
+          goto cleanup;
+        }
+
+      if (!set->func (GTK_SOURCE_VIEW (source_view), key, value, error))
+        goto cleanup;
+    }
+
+  ret = TRUE;
+
+cleanup:
+  g_strfreev (parts);
+
+  return ret;
+}
+
+static gboolean
+gb_vim_command_colorscheme (GtkWidget      *active_widget,
+                            const gchar    *command,
+                            const gchar    *options,
+                            GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      GtkSourceStyleSchemeManager *manager;
+      GtkSourceStyleScheme *style_scheme;
+      GtkTextBuffer *buffer;
+      g_autofree gchar *trimmed = NULL;
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      trimmed = g_strstrip (g_strdup (options));
+      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));
+      manager = gtk_source_style_scheme_manager_get_default ();
+      style_scheme = gtk_source_style_scheme_manager_get_scheme (manager, trimmed);
+
+      if (style_scheme == NULL)
+        {
+          g_set_error (error,
+                       GB_VIM_ERROR,
+                       GB_VIM_ERROR_UNKNOWN_OPTION,
+                       _("Cannot find colorscheme “%s”"),
+                       options);
+          return FALSE;
+        }
+
+      g_object_set (buffer, "style-scheme", style_scheme, NULL);
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_edit (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GFile) file = NULL;
+  IdeWorkbench *workbench;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (ide_str_empty0 (options))
+    {
+      dzl_gtk_widget_action (GTK_WIDGET (active_widget), "win", "open-with-dialog", NULL);
+      return TRUE;
+    }
+
+  if (!(workdir = find_workdir (active_widget)))
+    {
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_NOT_SOURCE_VIEW,
+                   _("Failed to locate working directory"));
+      return FALSE;
+    }
+
+  if (g_path_is_absolute (options))
+    file = g_file_new_for_path (options);
+  else
+    file = g_file_get_child (workdir, options);
+
+  workbench = ide_widget_get_workbench (active_widget);
+  ide_workbench_open_async (workbench, file, "editor", 0, NULL, NULL, NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_tabe (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (!ide_str_empty0 (options))
+    return gb_vim_command_edit (active_widget, command, options, error);
+
+  dzl_gtk_widget_action (GTK_WIDGET (active_widget), "editor", "new-file", NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_quit (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      dzl_gtk_widget_action (GTK_WIDGET (source_view), "editor-page", "save", NULL);
+    }
+
+  dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "close-page", NULL);
+
+  return TRUE;
+}
+
+static void
+gb_vim_command_split_cb (GObject      *object,
+                         GAsyncResult *result,
+                         gpointer      user_data)
+{
+  IdeWorkbench *workbench = (IdeWorkbench *)object;
+  SplitCallbackData *split_callback_data = user_data;
+  GtkWidget *active_widget;
+  const gchar *file_path;
+  GVariant *variant;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (split_callback_data != NULL);
+
+  if (ide_workbench_open_finish (workbench, result, &error))
+    {
+      active_widget = split_callback_data->active_widget;
+      file_path = split_callback_data->file_path;
+      variant = g_variant_new_string (file_path);
+
+      dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "split-page", variant);
+    }
+
+  g_clear_object (&split_callback_data->active_widget);
+  g_clear_pointer (&split_callback_data->file_path, g_free);
+  g_slice_free (SplitCallbackData, split_callback_data);
+}
+
+static void
+gb_vim_command_vsplit_cb (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  IdeWorkbench *workbench = (IdeWorkbench *)object;
+  SplitCallbackData *split_callback_data = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (IDE_IS_WORKBENCH (workbench));
+  g_assert (split_callback_data != NULL);
+
+  if (ide_workbench_open_finish (workbench,result, &error))
+    dzl_gtk_widget_action (split_callback_data->active_widget,
+                           "frame",
+                           "open-in-new-frame",
+                           g_variant_new_string (split_callback_data->file_path));
+
+  g_clear_object (&split_callback_data->active_widget);
+  g_clear_pointer (&split_callback_data->file_path, g_free);
+  g_slice_free (SplitCallbackData, split_callback_data);
+}
+
+static gboolean
+load_split_async (GtkWidget            *active_widget,
+                  const gchar          *options,
+                  GAsyncReadyCallback   callback,
+                  GError              **error)
+{
+  g_autoptr(GFile) workdir = NULL;
+  g_autoptr(GFile) file = NULL;
+  g_autofree gchar *file_path = NULL;
+  SplitCallbackData *split_callback_data;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+  g_assert (options != NULL);
+  g_assert (callback != NULL);
+
+  if (!(workdir = find_workdir (active_widget)))
+    {
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_NOT_SOURCE_VIEW,
+                   _("Failed to locate working directory"));
+      return FALSE;
+    }
+
+  if (!g_path_is_absolute (options))
+    {
+      g_autofree gchar *workdir_path = NULL;
+      workdir_path = g_file_get_path (workdir);
+      file_path = g_build_filename (workdir_path, options, NULL);
+    }
+  else
+    file_path = g_strdup (options);
+
+  file = g_file_new_for_path (file_path);
+
+  split_callback_data = g_slice_new0 (SplitCallbackData);
+  split_callback_data->active_widget = g_object_ref (active_widget);
+  split_callback_data->file_path = g_steal_pointer (&file_path);
+
+  ide_workbench_open_async (ide_widget_get_workbench (active_widget),
+                            file,
+                            "editor",
+                            IDE_BUFFER_OPEN_FLAGS_NO_VIEW,
+                            NULL,
+                            callback,
+                            split_callback_data);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_split (GtkWidget    *active_widget,
+                      const gchar  *command,
+                      const gchar  *options,
+                      GError      **error)
+{
+  GVariant *variant;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (!IDE_IS_PAGE (active_widget))
+    return gb_vim_set_no_view_error (error);
+
+  if (ide_str_empty0 (options))
+    {
+      variant = g_variant_new_string ("");
+      dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "split-page", variant);
+
+      return TRUE;
+    }
+  else
+    return load_split_async (active_widget, options, gb_vim_command_split_cb, error);
+}
+
+static gboolean
+gb_vim_command_vsplit (GtkWidget    *active_widget,
+                       const gchar  *command,
+                       const gchar  *options,
+                       GError      **error)
+{
+  GVariant *variant;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (!IDE_IS_PAGE (active_widget))
+    return gb_vim_set_no_view_error (error);
+
+  if (ide_str_empty0 (options))
+    {
+      variant = g_variant_new_string ("");
+      dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "open-in-new-frame", variant);
+
+      return TRUE;
+    }
+  else
+    return load_split_async (active_widget, options, gb_vim_command_vsplit_cb, error);
+}
+
+static gboolean
+gb_vim_command_write (GtkWidget      *active_widget,
+                      const gchar    *command,
+                      const gchar    *options,
+                      GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      dzl_gtk_widget_action (GTK_WIDGET (source_view), "editor-page", "save", NULL);
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_wq (GtkWidget      *active_widget,
+                   const gchar    *command,
+                   const gchar    *options,
+                   GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    return (gb_vim_command_write (active_widget, command, options, error) &&
+            gb_vim_command_quit (active_widget, command, options, error));
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_nohl (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeEditorSearch *search = ide_editor_page_get_search (IDE_EDITOR_PAGE (active_widget));
+      ide_editor_search_set_visible (search, FALSE);
+      return TRUE;
+    }
+
+  return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_make (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  /* TODO: check for an open project */
+  dzl_gtk_widget_action (GTK_WIDGET (active_widget), "build-manager", "build", NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_syntax (GtkWidget      *active_widget,
+                       const gchar    *command,
+                       const gchar    *options,
+                       GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeBuffer *buffer = ide_editor_page_get_buffer (IDE_EDITOR_PAGE (active_widget));
+
+      if (g_str_equal (options, "enable") || g_str_equal (options, "on"))
+        gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (buffer), TRUE);
+      else if (g_str_equal (options, "off"))
+        gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (buffer), FALSE);
+      else
+        {
+          g_set_error (error,
+                       GB_VIM_ERROR,
+                       GB_VIM_ERROR_UNKNOWN_OPTION,
+                       _("Invalid :syntax subcommand: %s"),
+                       options);
+          return FALSE;
+        }
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_sort (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      g_signal_emit_by_name (source_view, "sort", FALSE, FALSE);
+      g_signal_emit_by_name (source_view, "clear-selection");
+      g_signal_emit_by_name (source_view, "set-mode", NULL,
+                             IDE_SOURCE_VIEW_MODE_TYPE_PERMANENT);
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_bnext (GtkWidget      *active_widget,
+                      const gchar    *command,
+                      const gchar    *options,
+                      GError        **error)
+{
+  IdeFrame *frame_;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if ((frame_ = (IdeFrame *)gtk_widget_get_ancestor (active_widget, IDE_TYPE_FRAME)) &&
+      g_list_model_get_n_items (G_LIST_MODEL (frame_)) > 0)
+    dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "next-page", NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_bprevious (GtkWidget      *active_widget,
+                          const gchar    *command,
+                          const gchar    *options,
+                          GError        **error)
+{
+  IdeFrame *frame_;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if ((frame_ = (IdeFrame *)gtk_widget_get_ancestor (active_widget, IDE_TYPE_FRAME)) &&
+      g_list_model_get_n_items (G_LIST_MODEL (frame_)) > 0)
+    dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "previous-page", NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_command_cnext (GtkWidget      *active_widget,
+                      const gchar    *command,
+                      const gchar    *options,
+                      GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      g_signal_emit_by_name (source_view, "move-error", GTK_DIR_DOWN);
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_cprevious (GtkWidget      *active_widget,
+                          const gchar    *command,
+                          const gchar    *options,
+                          GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+
+      g_signal_emit_by_name (source_view, "move-error", GTK_DIR_UP);
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static gboolean
+gb_vim_command_buffers (GtkWidget      *active_widget,
+                        const gchar    *command,
+                        const gchar    *options,
+                        GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  dzl_gtk_widget_action (GTK_WIDGET (active_widget), "frame", "show-list", NULL);
+
+  return TRUE;
+}
+
+static gboolean
+gb_vim_jump_to_line (GtkWidget      *active_widget,
+                     const gchar    *command,
+                     const gchar    *options,
+                     GError        **error)
+{
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    {
+      IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+      GtkTextBuffer *buffer;
+      gboolean extend_selection;
+      gint line;
+
+      if (!int32_parse (&line, options, 0, G_MAXINT32, "line number", error))
+        return FALSE;
+
+      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));
+      extend_selection = gtk_text_buffer_get_has_selection (buffer);
+      ide_source_view_set_count (IDE_SOURCE_VIEW (source_view), line);
+
+      if (line == 0)
+        {
+          GtkTextIter iter;
+
+          /*
+           * Zero is not a valid line number, and IdeSourceView treats
+           * that as a move to the end of the buffer. Instead, we want
+           * line 1 to be like vi/vim.
+           */
+          gtk_text_buffer_get_start_iter (buffer, &iter);
+          gtk_text_buffer_select_range (buffer, &iter, &iter);
+          gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (source_view),
+                                        gtk_text_buffer_get_insert (buffer),
+                                        0.0, FALSE, 0.0, 0.0);
+        }
+      else
+        {
+          g_signal_emit_by_name (source_view,
+                                 "movement",
+                                 IDE_SOURCE_VIEW_MOVEMENT_NTH_LINE,
+                                 extend_selection, TRUE, TRUE);
+        }
+
+      ide_source_view_set_count (IDE_SOURCE_VIEW (source_view), 0);
+
+      g_signal_emit_by_name (source_view, "save-insert-mark");
+
+      return TRUE;
+    }
+  else
+    return gb_vim_set_source_view_error (error);
+}
+
+static void
+gb_vim_do_substitute_line (GtkTextBuffer *buffer,
+                           GtkTextIter   *begin,
+                           const gchar   *search_text,
+                           const gchar   *replace_text,
+                           gboolean       is_global)
+{
+  GtkSourceSearchContext *search_context;
+  GtkSourceSearchSettings *search_settings;
+  GtkTextIter match_begin;
+  GtkTextIter match_end;
+  gboolean has_wrapped = FALSE;
+  GError *error = NULL;
+  gint line_number;
+
+  g_assert(buffer);
+  g_assert(begin);
+  g_assert(search_text);
+  g_assert(replace_text);
+
+  search_settings = gtk_source_search_settings_new ();
+  search_context =  gtk_source_search_context_new (GTK_SOURCE_BUFFER (buffer), search_settings);
+  line_number = gtk_text_iter_get_line(begin);
+  gtk_text_iter_set_line_offset(begin, 0);
+
+  gtk_source_search_settings_set_search_text (search_settings, search_text);
+  gtk_source_search_settings_set_case_sensitive (search_settings, TRUE);
+
+  while (gtk_source_search_context_forward (search_context, begin, &match_begin, &match_end, &has_wrapped) 
&& !has_wrapped)
+    {
+      if (gtk_text_iter_get_line (&match_end) != line_number)
+        break;
+
+      if (!gtk_source_search_context_replace (search_context, &match_begin, &match_end, replace_text, -1, 
&error))
+        {
+          g_warning ("%s", error->message);
+          g_clear_error (&error);
+          break;
+        }
+
+      *begin = match_end;
+
+      if (!is_global)
+        break;
+    }
+
+  g_clear_object (&search_settings);
+  g_clear_object (&search_context);
+}
+
+static void
+gb_vim_do_substitute (GtkTextBuffer *buffer,
+                      GtkTextIter   *begin,
+                      GtkTextIter   *end,
+                      const gchar   *search_text,
+                      const gchar   *replace_text,
+                      gboolean       is_global,
+                      gboolean       should_search_all_lines)
+{
+  GtkTextIter begin_tmp;
+  GtkTextIter end_tmp;
+  GtkTextMark *last_line;
+  GtkTextIter *current_line;
+  GtkTextMark *end_mark;
+  GtkTextMark *insert;
+
+  g_assert (search_text);
+  g_assert (replace_text);
+  g_assert ((!begin && !end) || (begin && end));
+
+  insert = gtk_text_buffer_get_insert (buffer);
+
+  if (!begin)
+    {
+      if (should_search_all_lines)
+        gtk_text_buffer_get_start_iter (buffer, &begin_tmp);
+      else
+        gtk_text_buffer_get_iter_at_mark (buffer, &begin_tmp, insert);
+      begin = &begin_tmp;
+    }
+
+  if (!end)
+    {
+      if (should_search_all_lines)
+        gtk_text_buffer_get_end_iter (buffer, &end_tmp);
+      else
+        gtk_text_buffer_get_iter_at_mark (buffer, &end_tmp, insert);
+      end = &end_tmp;
+    }
+
+  current_line = begin;
+  last_line = gtk_text_buffer_create_mark (buffer, NULL, current_line, FALSE);
+  end_mark = gtk_text_buffer_create_mark (buffer, NULL, end, FALSE);
+
+  for (guint line = gtk_text_iter_get_line (current_line);
+       line <= gtk_text_iter_get_line (end);
+       line++)
+    {
+      gb_vim_do_substitute_line (buffer, current_line, search_text, replace_text, is_global);
+      gtk_text_buffer_get_iter_at_mark (buffer, current_line, last_line);
+      gtk_text_buffer_get_iter_at_mark (buffer, end, end_mark);
+      gtk_text_iter_set_line (current_line, line + 1);
+    }
+
+  gtk_text_buffer_delete_mark (buffer, last_line);
+  gtk_text_buffer_delete_mark (buffer, end_mark);
+}
+
+static gboolean
+gb_vim_command_substitute (GtkWidget    *active_widget,
+                           const gchar  *command,
+                           const gchar  *options,
+                           GError      **error)
+{
+  IdeSourceView  *source_view;
+  GtkTextBuffer *buffer;
+  const gchar *search_begin = NULL;
+  const gchar *search_end = NULL;
+  const gchar *replace_begin = NULL;
+  const gchar *replace_end = NULL;
+  g_autofree gchar *search_text = NULL;
+  g_autofree gchar *replace_text = NULL;
+  GtkTextIter *substitute_begin = NULL;
+  GtkTextIter *substitute_end = NULL;
+  gunichar separator;
+  gboolean replace_in_every_line = FALSE;
+  gboolean replace_every_occurence_in_line = FALSE;
+  gboolean replace_ask_for_confirmation = FALSE;
+  GtkTextIter selection_begin, selection_end;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+  g_assert (g_str_has_prefix (command, "%s") || g_str_has_prefix (command, "s"));
+
+  if (IDE_IS_EDITOR_PAGE (active_widget))
+    source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (active_widget));
+  else
+    return gb_vim_set_source_view_error (error);
+
+  if (command[0] == '%')
+    {
+      replace_in_every_line = TRUE;
+      command++;
+    }
+
+  command++;
+
+  separator = g_utf8_get_char (command);
+  if (!separator)
+    goto invalid_request;
+
+  search_begin = command = g_utf8_next_char (command);
+
+  for (; *command; command = g_utf8_next_char (command))
+    {
+      if (*command == '\\')
+        {
+          command = g_utf8_next_char (command);
+          if (!*command)
+            goto invalid_request;
+          continue;
+        }
+
+      if (g_utf8_get_char (command) == separator)
+        {
+          search_end = command;
+          break;
+        }
+    }
+
+  if (search_end == NULL)
+    {
+      search_text = g_strdup (search_begin);
+      replace_text = g_strdup ("");
+    }
+  else
+    {
+      search_text = g_strndup (search_begin, search_end - search_begin);
+
+      replace_begin = command = g_utf8_next_char (command);
+
+      for (; *command; command = g_utf8_next_char (command))
+        {
+          if (*command == '\\')
+            {
+              command = g_utf8_next_char (command);
+              if (!*command)
+                goto invalid_request;
+              continue;
+            }
+
+          if (g_utf8_get_char (command) == separator)
+            {
+              replace_end = command;
+              break;
+            }
+        }
+
+      if (replace_end == NULL)
+        replace_text = g_strdup (replace_begin);
+      else
+        {
+          replace_text = g_strndup (replace_begin, replace_end - replace_begin);
+          command = g_utf8_next_char (command);
+        }
+
+      if (*command)
+        {
+          for (; *command; command++)
+            {
+              switch (*command)
+                {
+                case 'c':
+                  replace_ask_for_confirmation = TRUE;
+                  break;
+
+                case 'g':
+                  replace_every_occurence_in_line = TRUE;
+                  break;
+
+                /* what other options are supported? */
+                default:
+                  break;
+                }
+            }
+        }
+    }
+
+  if (replace_ask_for_confirmation)
+    {
+      GVariant *variant;
+      GVariantBuilder builder;
+
+      g_variant_builder_init (&builder, G_VARIANT_TYPE_STRING_ARRAY);
+      g_variant_builder_add (&builder, "s", search_text);
+      g_variant_builder_add (&builder, "s", replace_text);
+      variant = g_variant_builder_end (&builder);
+
+      dzl_gtk_widget_action (active_widget, "editor-page", "replace-confirm", variant);
+
+      return TRUE;
+    }
+
+  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));
+
+  if (gtk_text_buffer_get_has_selection (buffer))
+    {
+      gtk_text_buffer_get_selection_bounds (buffer, &selection_begin, &selection_end);
+      substitute_begin = &selection_begin;
+      substitute_end = &selection_end;
+    }
+
+  gtk_text_buffer_begin_user_action (buffer);
+  gb_vim_do_substitute (buffer, substitute_begin, substitute_end, search_text, replace_text, 
replace_every_occurence_in_line, replace_in_every_line);
+  gtk_text_buffer_end_user_action (buffer);
+
+  return TRUE;
+
+invalid_request:
+  g_set_error (error,
+               GB_VIM_ERROR,
+               GB_VIM_ERROR_UNKNOWN_OPTION,
+               _("Invalid search and replace request"));
+  return FALSE;
+}
+
+static const GbVimCommand vim_commands[] = {
+  { "bdelete",     gb_vim_command_quit },
+  { "bnext",       gb_vim_command_bnext },
+  { "bprevious",   gb_vim_command_bprevious },
+  { "buffers",     gb_vim_command_buffers },
+  { "cnext",       gb_vim_command_cnext },
+  { "colorscheme", gb_vim_command_colorscheme, N_("Change the pages colorscheme") },
+  { "cprevious",   gb_vim_command_cprevious },
+  { "edit",        gb_vim_command_edit },
+  { "ls",          gb_vim_command_buffers },
+  { "make",        gb_vim_command_make, N_("Build the project") },
+  { "nohl",        gb_vim_command_nohl, N_("Clear search highlighting") },
+  { "open",        gb_vim_command_edit, N_("Open a file by path") },
+  { "quit",        gb_vim_command_quit, N_("Close the page") },
+  { "set",         gb_vim_command_set, N_("Set various buffer options") },
+  { "sort",        gb_vim_command_sort, N_("Sort the selected lines") },
+  { "split",       gb_vim_command_split, N_("Create a split page below the current page") },
+  { "syntax",      gb_vim_command_syntax, N_("Toggle syntax highlighting") },
+  { "tabe",        gb_vim_command_tabe },
+  { "vsplit",      gb_vim_command_vsplit },
+  { "w",           gb_vim_command_write },
+  { "wq",          gb_vim_command_wq, N_("Save and close the current page") },
+  { "write",       gb_vim_command_write, N_("Save the current page") },
+  { NULL }
+};
+
+static gboolean
+looks_like_substitute (const gchar *line)
+{
+  g_assert (line);
+
+  if (g_str_has_prefix (line, "%s"))
+    return TRUE;
+  return *line == 's';
+}
+
+static const GbVimCommand *
+lookup_command (const gchar  *name,
+                gchar       **options_sup)
+{
+  static GbVimCommand line_command = { "__line__", gb_vim_jump_to_line };
+  gint line;
+  gsize i;
+
+  g_assert (name);
+  g_assert (options_sup);
+
+  *options_sup = NULL;
+
+  for (i = 0; vim_commands [i].name; i++)
+    {
+      if (g_str_has_prefix (vim_commands [i].name, name))
+        return &vim_commands [i];
+    }
+
+  if (g_ascii_isdigit (*name) && int32_parse (&line, name, 0, G_MAXINT32, "line", NULL))
+    {
+      *options_sup = g_strdup (name);
+      return &line_command;
+    }
+
+  return NULL;
+}
+
+gboolean
+gb_vim_execute (GtkWidget      *active_widget,
+                const gchar    *line,
+                GError        **error)
+{
+  g_autofree gchar *name_slice = NULL;
+  g_autofree gchar *options_sup = NULL;
+  const GbVimCommand *command;
+  const gchar *command_name = line;
+  const gchar *options;
+  g_autofree gchar *all_options = NULL;
+  gboolean result;
+
+  g_return_val_if_fail (GTK_IS_WIDGET (active_widget), FALSE);
+  g_return_val_if_fail (line, FALSE);
+
+  for (options = line; *options; options = g_utf8_next_char (options))
+    {
+      gunichar ch;
+
+      ch = g_utf8_get_char (options);
+
+      if (g_unichar_isspace (ch))
+        break;
+    }
+
+  if (g_unichar_isspace (g_utf8_get_char (options)))
+    {
+      command_name = name_slice = g_strndup (line, options - line);
+      options = g_utf8_next_char (options);
+    }
+
+  command = lookup_command (command_name, &options_sup);
+
+  if (command == NULL)
+    {
+      if (looks_like_substitute (line))
+        return gb_vim_command_substitute (active_widget, line, "", error);
+
+      g_set_error (error,
+                   GB_VIM_ERROR,
+                   GB_VIM_ERROR_NOT_FOUND,
+                   _("Not a command: %s"),
+                   command_name);
+
+      return FALSE;
+    }
+
+  if (options_sup)
+    all_options = g_strconcat (options, " ", options_sup, NULL);
+  else
+    all_options = g_strdup (options);
+
+  result = command->func (active_widget, command_name, all_options, error);
+
+  return result;
+}
+
+static gchar *
+joinv_and_add (gchar       **parts,
+               gsize         len,
+               const gchar  *delim,
+               const gchar  *str)
+{
+  GString *gstr;
+  gsize i;
+
+  gstr = g_string_new (parts [0]);
+  for (i = 1; i < len; i++)
+    g_string_append_printf (gstr, "%s%s", delim, parts [i]);
+  g_string_append_printf (gstr, "%s%s", delim, str);
+
+  return g_string_free (gstr, FALSE);
+}
+
+static void
+gb_vim_complete_set (const gchar *line,
+                     GPtrArray   *ar)
+{
+  const gchar *key;
+  gchar **parts;
+  guint len;
+  gsize i;
+
+  parts = g_strsplit (line, " ", 0);
+  len = g_strv_length (parts);
+
+  if (len < 2)
+    {
+      g_strfreev (parts);
+
+      return;
+    }
+
+  key = parts [len - 1];
+
+  for (i = 0; vim_sets [i].name; i++)
+    {
+      if (g_str_has_prefix (vim_sets [i].name, key))
+        g_ptr_array_add (ar, joinv_and_add (parts, len - 1, " ", vim_sets [i].name));
+    }
+
+  for (i = 0; vim_set_aliases [i].name; i++)
+    {
+      if (g_str_has_prefix (vim_set_aliases [i].name, key))
+        g_ptr_array_add (ar, joinv_and_add (parts, len - 1, " ", vim_set_aliases [i].name));
+    }
+
+  g_strfreev (parts);
+}
+
+static void
+gb_vim_complete_command (const gchar *line,
+                         GPtrArray   *ar)
+{
+  gsize i;
+
+  for (i = 0; vim_commands [i].name; i++)
+    {
+      if (g_str_has_prefix (vim_commands [i].name, line))
+        g_ptr_array_add (ar, g_strdup (vim_commands [i].name));
+    }
+}
+
+static void
+gb_vim_complete_edit_files (GtkWidget   *active_widget,
+                            const gchar *command,
+                            GPtrArray   *ar,
+                            const gchar *prefix)
+{
+  g_autoptr(GFile) child = NULL;
+  g_autoptr(GFile) parent = NULL;
+  g_autoptr(GFile) workdir = NULL;
+
+  IDE_ENTRY;
+
+  g_assert (command);
+  g_assert (ar);
+  g_assert (prefix);
+
+  if (!(workdir = find_workdir (active_widget)))
+    IDE_EXIT;
+
+  child = g_file_get_child (workdir, prefix);
+
+  if (g_file_query_exists (child, NULL))
+    {
+      if (g_file_query_file_type (child, 0, NULL) == G_FILE_TYPE_DIRECTORY)
+        {
+          g_autoptr(GFileEnumerator) fe = NULL;
+          GFileInfo *descendent;
+
+          if (!g_str_has_suffix (prefix, "/"))
+            {
+              g_ptr_array_add (ar, g_strdup_printf ("%s %s/", command, prefix));
+              IDE_EXIT;
+            }
+
+          fe = g_file_enumerate_children (child,
+                                          G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
+                                          G_FILE_QUERY_INFO_NONE,
+                                          NULL, NULL);
+
+          if (fe == NULL)
+            IDE_EXIT;
+
+          while ((descendent = g_file_enumerator_next_file (fe, NULL, NULL)))
+            {
+              const gchar *name;
+
+              name = g_file_info_get_display_name (descendent);
+              g_ptr_array_add (ar, g_strdup_printf ("%s %s%s", command, prefix, name));
+              g_object_unref (descendent);
+            }
+
+          IDE_EXIT;
+        }
+    }
+
+  parent = g_file_get_parent (child);
+
+  if (parent != NULL)
+    {
+      g_autoptr(GFileEnumerator) fe = NULL;
+      GFileInfo *descendent;
+      const gchar *slash;
+      const gchar *partial_name;
+      g_autofree gchar *prefix_dir = NULL;
+
+#ifdef IDE_ENABLE_TRACE
+      {
+        g_autofree gchar *parent_path = g_file_get_path (parent);
+        IDE_TRACE_MSG ("parent_path: %s", parent_path);
+      }
+#endif
+
+      if ((slash = strrchr (prefix, G_DIR_SEPARATOR)))
+        {
+          partial_name = slash + 1;
+          prefix_dir = g_strndup (prefix, slash - prefix + 1);
+        }
+      else
+        {
+          partial_name = prefix;
+        }
+
+      fe = g_file_enumerate_children (parent,
+                                      G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME","
+                                      G_FILE_ATTRIBUTE_STANDARD_TYPE,
+                                      G_FILE_QUERY_INFO_NONE,
+                                      NULL, NULL);
+
+      if (fe == NULL)
+        IDE_EXIT;
+
+      while ((descendent = g_file_enumerator_next_file (fe, NULL, NULL)))
+        {
+          const gchar *name;
+          GFileType file_type;
+
+          name = g_file_info_get_display_name (descendent);
+          file_type = g_file_info_get_file_type (descendent);
+
+          IDE_TRACE_MSG ("name=%s prefix=%s", name, prefix);
+
+          if (name && g_str_has_prefix (name, partial_name))
+            {
+              gchar *completed_command;
+              const gchar *descendent_name;
+              g_autofree gchar *full_path = NULL;
+              g_autofree gchar *parent_path = NULL;
+
+              parent_path = g_file_get_path (parent);
+              descendent_name = g_file_info_get_name (descendent);
+              full_path = g_build_filename (parent_path, descendent_name, NULL);
+
+              if (prefix[0] == G_DIR_SEPARATOR)
+                completed_command = g_strdup_printf ("%s %s%s", command, full_path,
+                                                     file_type == G_FILE_TYPE_DIRECTORY ? G_DIR_SEPARATOR_S 
: "");
+              else if (strchr (prefix, G_DIR_SEPARATOR) == NULL)
+                completed_command = g_strdup_printf ("%s %s%s", command, descendent_name,
+                                                     file_type == G_FILE_TYPE_DIRECTORY ? G_DIR_SEPARATOR_S 
: "");
+              else
+                completed_command = g_strdup_printf ("%s %s%s%s", command, prefix_dir, descendent_name,
+                                                     file_type == G_FILE_TYPE_DIRECTORY ? G_DIR_SEPARATOR_S 
: "");
+
+              IDE_TRACE_MSG ("edit completion: %s", completed_command);
+
+              g_ptr_array_add (ar, completed_command);
+            }
+          g_object_unref (descendent);
+        }
+
+      IDE_EXIT;
+    }
+
+  IDE_EXIT;
+}
+
+static void
+gb_vim_complete_edit (GtkWidget *active_widget,
+                      const gchar   *line,
+                      GPtrArray     *ar)
+{
+  gchar **parts;
+
+  parts = g_strsplit (line, " ", 2);
+  if (parts [0] == NULL || parts [1] == NULL)
+    {
+      g_strfreev (parts);
+      return;
+    }
+
+  gb_vim_complete_edit_files (active_widget, parts [0], ar, parts [1]);
+
+  g_strfreev (parts);
+}
+
+static void
+gb_vim_complete_colorscheme (const gchar *line,
+                             GPtrArray   *ar)
+{
+  GtkSourceStyleSchemeManager *manager;
+  const gchar * const *scheme_ids;
+  const gchar *tmp;
+  g_autofree gchar *prefix = NULL;
+  gsize i;
+
+  manager = gtk_source_style_scheme_manager_get_default ();
+  scheme_ids = gtk_source_style_scheme_manager_get_scheme_ids (manager);
+
+  for (tmp = strchr (line, ' ');
+       tmp && *tmp && g_unichar_isspace (g_utf8_get_char (tmp));
+       tmp = g_utf8_next_char (tmp))
+    {
+      /* do nothing */
+    }
+
+  if (!tmp)
+    return;
+
+  prefix = g_strndup (line, tmp - line);
+
+  for (i = 0; scheme_ids [i]; i++)
+    {
+      const gchar *scheme_id = scheme_ids [i];
+
+      if (g_str_has_prefix (scheme_id, tmp))
+        {
+          gchar *item;
+
+          item = g_strdup_printf ("%s%s", prefix, scheme_id);
+          IDE_TRACE_MSG ("colorscheme: %s", item);
+          g_ptr_array_add (ar, item);
+        }
+    }
+}
+
+gchar **
+gb_vim_complete (GtkWidget   *active_widget,
+                 const gchar *line)
+{
+  GPtrArray *ar;
+
+  g_assert (GTK_IS_WIDGET (active_widget));
+
+  ar = g_ptr_array_new ();
+
+  if (line != NULL)
+    {
+      if (IDE_IS_EDITOR_PAGE (active_widget))
+        {
+          if (g_str_has_prefix (line, "set "))
+            gb_vim_complete_set (line, ar);
+          else if (g_str_has_prefix (line, "colorscheme "))
+            gb_vim_complete_colorscheme (line, ar);
+        }
+
+      if (g_str_has_prefix (line, "e ") ||
+          g_str_has_prefix (line, "edit ") ||
+          g_str_has_prefix (line, "o ") ||
+          g_str_has_prefix (line, "open ") ||
+          g_str_has_prefix (line, "sp ") ||
+          g_str_has_prefix (line, "split ") ||
+          g_str_has_prefix (line, "vsp ") ||
+          g_str_has_prefix (line, "vsplit ") ||
+          g_str_has_prefix (line, "tabe "))
+          gb_vim_complete_edit (active_widget, line, ar);
+      else
+          gb_vim_complete_command (line, ar);
+    }
+
+  g_ptr_array_add (ar, NULL);
+
+  return (gchar **)g_ptr_array_free (ar, FALSE);
+}
+
+const gchar **
+gb_vim_commands (const gchar   *typed_text,
+                 const gchar ***descriptions)
+{
+  GPtrArray *ar = NULL;
+  GPtrArray *desc_ar = NULL;
+  g_auto(GStrv) parts = NULL;
+
+  g_assert (typed_text);
+  g_assert (descriptions);
+
+  parts = g_strsplit (typed_text, " ", 2);
+  ar = g_ptr_array_new ();
+  desc_ar = g_ptr_array_new ();
+
+  g_assert (parts != NULL);
+  g_assert (parts[0] != NULL);
+
+  for (guint i = 0; vim_commands[i].name; i++)
+    {
+      if (g_str_has_prefix (vim_commands[i].name, parts[0]))
+        {
+          g_ptr_array_add (ar, (gchar *)vim_commands[i].name);
+          g_ptr_array_add (desc_ar, (gchar *)vim_commands[i].description);
+        }
+    }
+
+  g_ptr_array_add (ar, NULL);
+  g_ptr_array_add (desc_ar, NULL);
+
+  *descriptions = (const gchar **)g_ptr_array_free (desc_ar, FALSE);
+
+  return (const gchar **)g_ptr_array_free (ar, FALSE);
+}
diff --git a/src/plugins/vim/gb-vim.h b/src/plugins/vim/gb-vim.h
new file mode 100644
index 000000000..49994fdd9
--- /dev/null
+++ b/src/plugins/vim/gb-vim.h
@@ -0,0 +1,48 @@
+/* gb-vim.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+
+G_BEGIN_DECLS
+
+#define GB_VIM_ERROR (gb_vim_error_quark())
+
+typedef enum
+{
+  GB_VIM_ERROR_NOT_IMPLEMENTED,
+  GB_VIM_ERROR_NOT_FOUND,
+  GB_VIM_ERROR_NOT_NUMBER,
+  GB_VIM_ERROR_NUMBER_OUT_OF_RANGE,
+  GB_VIM_ERROR_CANNOT_FIND_COLORSCHEME,
+  GB_VIM_ERROR_UNKNOWN_OPTION,
+  GB_VIM_ERROR_NOT_SOURCE_VIEW,
+  GB_VIM_ERROR_NO_VIEW
+} IdeVimError;
+
+GQuark        gb_vim_error_quark (void);
+gboolean      gb_vim_execute     (GtkWidget      *active_widget,
+                                  const gchar    *line,
+                                  GError        **error);
+gchar       **gb_vim_complete    (GtkWidget      *active_widget,
+                                  const gchar    *line);
+const gchar **gb_vim_commands    (const gchar    *typed_text,
+                                  const gchar  ***descriptions);
+
+G_END_DECLS
diff --git a/src/plugins/vim/gbp-vim-command-provider.c b/src/plugins/vim/gbp-vim-command-provider.c
new file mode 100644
index 000000000..e55d95aab
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-command-provider.c
@@ -0,0 +1,122 @@
+/* gbp-vim-command-provider.c
+ *
+ * Copyright 2018-2019 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-vim-command-provider"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-editor.h>
+#include <libide-gui.h>
+#include <libide-threading.h>
+
+#include "gb-vim.h"
+#include "gbp-vim-command.h"
+#include "gbp-vim-command-provider.h"
+
+struct _GbpVimCommandProvider
+{
+  GObject parent_instance;
+};
+
+static void
+gbp_vim_command_provider_query_async (IdeCommandProvider  *provider,
+                                      IdeWorkspace        *workspace,
+                                      const gchar         *typed_text,
+                                      GCancellable        *cancellable,
+                                      GAsyncReadyCallback  callback,
+                                      gpointer             user_data)
+{
+  GbpVimCommandProvider *self = (GbpVimCommandProvider *)provider;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GPtrArray) results = NULL;
+  g_autofree const gchar **commands = NULL;
+  g_autofree const gchar **descriptions = NULL;
+  IdePage *page;
+
+  g_assert (GBP_IS_VIM_COMMAND_PROVIDER (self));
+  g_assert (IDE_IS_WORKSPACE (workspace));
+  g_assert (typed_text != NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_vim_command_provider_query_async);
+
+  results = g_ptr_array_new_with_free_func (g_object_unref);
+  page = ide_workspace_get_most_recent_page (workspace);
+
+  if (!IDE_IS_EDITOR_PAGE (page))
+    goto no_active_widget;
+
+  commands = gb_vim_commands (typed_text, &descriptions);
+
+  for (guint i = 0; commands[i]; i++)
+    {
+      g_autoptr(GbpVimCommand) command = NULL;
+
+      command = gbp_vim_command_new (GTK_WIDGET (page),
+                                     typed_text,
+                                     g_dgettext (GETTEXT_PACKAGE, commands[i]),
+                                     g_dgettext (GETTEXT_PACKAGE, descriptions[i]));
+      g_ptr_array_add (results, g_steal_pointer (&command));
+    }
+
+no_active_widget:
+  ide_task_return_pointer (task,
+                           g_steal_pointer (&results),
+                           (GDestroyNotify)g_ptr_array_unref);
+}
+
+static GPtrArray *
+gbp_vim_command_provider_query_finish (IdeCommandProvider  *provider,
+                                       GAsyncResult        *result,
+                                       GError             **error)
+{
+  GbpVimCommandProvider *self = (GbpVimCommandProvider *)provider;
+  GPtrArray *ret;
+
+  g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (GBP_IS_VIM_COMMAND_PROVIDER (self));
+  g_assert (G_IS_ASYNC_RESULT (result));
+
+  ret = ide_task_propagate_pointer (IDE_TASK (result), error);
+
+  return IDE_PTR_ARRAY_STEAL_FULL (&ret);
+}
+
+static void
+command_provider_iface_init (IdeCommandProviderInterface *iface)
+{
+  iface->query_async = gbp_vim_command_provider_query_async;
+  iface->query_finish = gbp_vim_command_provider_query_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpVimCommandProvider, gbp_vim_command_provider, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_COMMAND_PROVIDER, command_provider_iface_init))
+
+static void
+gbp_vim_command_provider_class_init (GbpVimCommandProviderClass *klass)
+{
+}
+
+static void
+gbp_vim_command_provider_init (GbpVimCommandProvider *self)
+{
+}
diff --git a/src/plugins/vim/gbp-vim-command-provider.h b/src/plugins/vim/gbp-vim-command-provider.h
new file mode 100644
index 000000000..25a4bc03e
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-command-provider.h
@@ -0,0 +1,31 @@
+/* gbp-vim-command-provider.h
+ *
+ * Copyright 2018-2019 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_VIM_COMMAND_PROVIDER (gbp_vim_command_provider_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpVimCommandProvider, gbp_vim_command_provider, GBP, VIM_COMMAND_PROVIDER, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/vim/gbp-vim-command.c b/src/plugins/vim/gbp-vim-command.c
new file mode 100644
index 000000000..5fb01cbe0
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-command.c
@@ -0,0 +1,141 @@
+/* gbp-vim-command.c
+ *
+ * Copyright 2018-2019 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-vim-command"
+
+#include "config.h"
+
+#include <libide-gui.h>
+
+#include "gbp-vim-command.h"
+#include "gb-vim.h"
+
+struct _GbpVimCommand
+{
+  IdeObject  parent_instance;
+  GtkWidget *active_widget;
+  gchar     *typed_text;
+  gchar     *command;
+  gchar     *description;
+};
+
+static gchar *
+gbp_vim_command_get_title (IdeCommand *command)
+{
+  GbpVimCommand *self = (GbpVimCommand *)command;
+
+  return g_strdup (self->command);
+}
+
+static gchar *
+gbp_vim_command_get_subtitle (IdeCommand *command)
+{
+  GbpVimCommand *self = (GbpVimCommand *)command;
+
+  return g_strdup (self->description);
+}
+
+static void
+gbp_vim_command_run_async (IdeCommand          *command,
+                           GCancellable        *cancellable,
+                           GAsyncReadyCallback  callback,
+                           gpointer             user_data)
+{
+  GbpVimCommand *self = (GbpVimCommand *)command;
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (GBP_IS_VIM_COMMAND (self));
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  task = ide_task_new (self, cancellable, callback, user_data);
+  ide_task_set_source_tag (task, gbp_vim_command_run_async);
+
+  if (!gb_vim_execute (self->active_widget, self->typed_text, &error))
+    ide_task_return_error (task, g_steal_pointer (&error));
+  else
+    ide_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gbp_vim_command_run_finish (IdeCommand    *command,
+                            GAsyncResult  *result,
+                            GError       **error)
+{
+  g_assert (GBP_IS_VIM_COMMAND (command));
+  g_assert (IDE_IS_TASK (result));
+
+  return ide_task_propagate_boolean (IDE_TASK (result), error);
+}
+
+static void
+command_iface_init (IdeCommandInterface *iface)
+{
+  iface->get_title = gbp_vim_command_get_title;
+  iface->get_subtitle = gbp_vim_command_get_subtitle;
+  iface->run_async = gbp_vim_command_run_async;
+  iface->run_finish = gbp_vim_command_run_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpVimCommand, gbp_vim_command, IDE_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_COMMAND, command_iface_init))
+
+static void
+gbp_vim_command_finalize (GObject *object)
+{
+  GbpVimCommand *self = (GbpVimCommand *)object;
+
+  g_clear_pointer (&self->typed_text, g_free);
+  g_clear_pointer (&self->command, g_free);
+  g_clear_pointer (&self->description, g_free);
+  g_clear_object (&self->active_widget);
+
+  G_OBJECT_CLASS (gbp_vim_command_parent_class)->finalize (object);
+}
+
+static void
+gbp_vim_command_class_init (GbpVimCommandClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->finalize = gbp_vim_command_finalize;
+}
+
+static void
+gbp_vim_command_init (GbpVimCommand *self)
+{
+}
+
+GbpVimCommand *
+gbp_vim_command_new (GtkWidget   *active_widget,
+                     const gchar *typed_text,
+                     const gchar *command,
+                     const gchar *description)
+{
+  g_autoptr(GbpVimCommand) ret = NULL;
+
+  ret = g_object_new (GBP_TYPE_VIM_COMMAND, NULL);
+  ret->active_widget = g_object_ref (active_widget);
+  ret->typed_text = g_strdup (typed_text);
+  ret->command = g_strdup (command);
+  ret->description = g_strdup (description);
+
+  return g_steal_pointer (&ret);
+}
diff --git a/src/plugins/vim/gbp-vim-command.h b/src/plugins/vim/gbp-vim-command.h
new file mode 100644
index 000000000..79e5994d1
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-command.h
@@ -0,0 +1,36 @@
+/* gbp-vim-command.h
+ *
+ * Copyright 2018-2019 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-core.h>
+
+G_BEGIN_DECLS
+
+#define GBP_TYPE_VIM_COMMAND (gbp_vim_command_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpVimCommand, gbp_vim_command, GBP, VIM_COMMAND, IdeObject)
+
+GbpVimCommand *gbp_vim_command_new (GtkWidget   *active_widget,
+                                    const gchar *typed_text,
+                                    const gchar *command,
+                                    const gchar *description);
+
+G_END_DECLS
diff --git a/src/plugins/vim/gbp-vim-preferences-addin.c b/src/plugins/vim/gbp-vim-preferences-addin.c
new file mode 100644
index 000000000..fcb4d238e
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-preferences-addin.c
@@ -0,0 +1,89 @@
+/* gbp-vim-preferences-addin.c
+ *
+ * Copyright 2018-2019 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-vim-preferences-addin"
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <libide-gui.h>
+
+#include "gbp-vim-preferences-addin.h"
+
+struct _GbpVimPreferencesAddin
+{
+  GObject parent_instance;
+  guint   keybinding_id;
+};
+
+static void
+gbp_vim_preferences_addin_load (IdePreferencesAddin *addin,
+                                DzlPreferences      *preferences)
+{
+  GbpVimPreferencesAddin *self = (GbpVimPreferencesAddin *)addin;
+
+  g_assert (GBP_IS_VIM_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  self->keybinding_id = dzl_preferences_add_radio (preferences,
+                                                   "keyboard",
+                                                   "mode",
+                                                   "org.gnome.builder.editor",
+                                                   "keybindings",
+                                                   NULL,
+                                                   "\"vim\"",
+                                                   _("Vim"),
+                                                   _("Emulates the Vim text editor"),
+                                                   NULL,
+                                                   30);
+}
+
+static void
+gbp_vim_preferences_addin_unload (IdePreferencesAddin *addin,
+                                  DzlPreferences      *preferences)
+{
+  GbpVimPreferencesAddin *self = (GbpVimPreferencesAddin *)addin;
+
+  g_assert (GBP_IS_VIM_PREFERENCES_ADDIN (self));
+  g_assert (DZL_IS_PREFERENCES (preferences));
+
+  dzl_preferences_remove_id (preferences, self->keybinding_id);
+}
+
+static void
+preferences_addin_iface_init (IdePreferencesAddinInterface *iface)
+{
+  iface->load = gbp_vim_preferences_addin_load;
+  iface->unload = gbp_vim_preferences_addin_unload;
+}
+
+G_DEFINE_TYPE_WITH_CODE (GbpVimPreferencesAddin, gbp_vim_preferences_addin, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (IDE_TYPE_PREFERENCES_ADDIN,
+                                                preferences_addin_iface_init))
+
+static void
+gbp_vim_preferences_addin_class_init (GbpVimPreferencesAddinClass *klass)
+{
+}
+
+static void
+gbp_vim_preferences_addin_init (GbpVimPreferencesAddin *self)
+{
+}
diff --git a/src/plugins/vim/gbp-vim-preferences-addin.h b/src/plugins/vim/gbp-vim-preferences-addin.h
new file mode 100644
index 000000000..7a8663cf4
--- /dev/null
+++ b/src/plugins/vim/gbp-vim-preferences-addin.h
@@ -0,0 +1,31 @@
+/* gbp-vim-preferences-addin.h
+ *
+ * Copyright 2018-2019 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_VIM_PREFERENCES_ADDIN (gbp_vim_preferences_addin_get_type())
+
+G_DECLARE_FINAL_TYPE (GbpVimPreferencesAddin, gbp_vim_preferences_addin, GBP, VIM_PREFERENCES_ADDIN, GObject)
+
+G_END_DECLS
diff --git a/src/plugins/vim/keybindings/vim.css b/src/plugins/vim/keybindings/vim.css
new file mode 100644
index 000000000..d339ec166
--- /dev/null
+++ b/src/plugins/vim/keybindings/vim.css
@@ -0,0 +1,2892 @@
+/* vim.css
+ *
+ * Copyright (C) 2015 Christian Hergert <christian hergert me>
+ *
+ * This file is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This file 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
+ * Lesser 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/>.
+ */
+
+/*
+ * Contributing:
+ *
+ * There are a lot of corner cases that vim handles. As you can see from the
+ * text below, we do not handle all of them. If you would like to contribute
+ * something that is missing, please do!
+ *
+ * In general, we use the selectors at the bottom to determine which binding
+ * sets are active in a mode.
+ *
+ * "set-mode" is used to move our way through the state machine. Take a look
+ * at the current uses to get an idea. "permanent" means that the mode will
+ * stay active until it has been released (by Escape or something). transient
+ * means that the the mode will disappear after a followup key press. However,
+ * transient mode may simply trigger another transient mode. (cip would be
+ * an example of this.
+ *
+ * If you need more advanced operations than you can perform, you might have
+ * to dig into IdeSourceView to add a new GSignal (with G_SIGNAL_ACTION flag).
+ * Any signal with G_SIGNAL_ACTION set in the following widget hierarchy
+ * should be callable from these bindings.
+ *
+ *   - GtkWidget
+ *     - GtkTextView
+ *       - GtkSourceView
+ *         - IdeSourceView
+ *
+ * For example, you could make the ficticious three-finger-salute keybinding
+ * to delete the entire buffer like so:
+ *
+ *   bind "<ctrl><alt>delete" { "movement" (first-line, 0, 0, 0)
+ *                              "movement" (last-line, 1, 0, 0)
+ *                              "movement" (last-char, 1, 0, 0)
+ *                              "copy-clipboard-extended" ()
+ *                              "delete-selection" () };
+ *
+ * The "movement" action takes three parameters.
+ *
+ *   1) If we want to extend the selection with the movement. Otherwise, it
+ *      will be cleared.
+ *   2) If the movement is exclusive. See :help exlusive in vim.
+ *   3) If the current count (digit prefix) should be applied to the movement.
+ *
+ * The first line will move the cursor to line and column 0:0. The second
+ * movement will extend the selection to the last line of the file (1
+ * indicates TRUE to the second action parameter "extend_selection").
+ * The third movement will move to the end of the current line (now the last
+ * line due to second movement). We then copy to the clipboard just to be
+ * nice, and then delete the whole thing from the buffer.
+ *
+ * NOTE: the exclusive/inclusive parameters are probably not right. They need
+ *       either careful study or battle testing.
+ *
+ * That's pretty much it, happy Vim'ing!
+ *
+ *   -- Christian
+ */
+
+@import url("resource:///org/gnome/builder/keybindings/shared.css");
+
+@binding-set builder-vim-source-view
+{
+  bind "Escape" { "end-macro" ()
+                  "set-overwrite" (0)
+                  "clear-count" ()
+                  "clear-selection" ()
+                  "clear-snippets" ()
+                  "hide-completion" ()
+                  "set-mode" ("vim-normal", permanent)
+                  "remove-cursors" () };
+  bind "<ctrl>bracketleft" { "end-macro" ()
+                             "set-overwrite" (0)
+                             "clear-count" ()
+                             "clear-selection" ()
+                             "clear-snippets" ()
+                             "hide-completion" ()
+                             "set-mode" ("vim-normal", permanent) };
+  bind "<ctrl>c" { "end-macro" ()
+                   "set-overwrite" (0)
+                   "clear-count" ()
+                   "clear-selection" ()
+                   "clear-snippets" ()
+                   "hide-completion" ()
+                   "set-mode" ("vim-normal", permanent) };
+
+  bind "<ctrl>k" { "action" ("frame", "show-list", "") };
+  bind "<ctrl>minus" { "decrease-font-size" () };
+  bind "<ctrl>plus" { "increase-font-size" () };
+  bind "<ctrl>equal" { "increase-font-size" () };
+  bind "<ctrl>0" { "reset-font-size" () };
+  bind "<ctrl><shift>e" { "add-cursor" (column) };
+  bind "<ctrl><shift>d" { "add-cursor" (match) };
+
+  bind "F4" { "action" ("win", "find-other-file", "") };
+}
+
+@binding-set builder-vim-source-view-normal-with-count
+{
+  bind "0" { "append-to-count" (0)
+             "set-mode" ("vim-normal-with-count", transient) };
+  bind "KP_0" { "append-to-count" (0)
+                "set-mode" ("vim-normal-with-count", transient) };
+  bind "percent" { "movement" (line-percentage, 0, 1, 1)
+                   "set-mode" ("vim-normal-with-count", transient) };
+}
+
+@binding-set builder-vim-source-view-normal
+{
+  bind "<ctrl>l" { "rebuild-highlight" () };
+
+  bind "1" { "append-to-count" (1)
+             "set-mode" ("vim-normal-with-count", transient) };
+  bind "2" { "append-to-count" (2)
+             "set-mode" ("vim-normal-with-count", transient) };
+  bind "3" { "append-to-count" (3)
+             "set-mode" ("vim-normal-with-count", transient) };
+  bind "4" { "append-to-count" (4)
+             "set-mode" ("vim-normal-with-count", transient) };
+  bind "5" { "append-to-count" (5)
+             "set-mode" ("vim-normal-with-count", transient) };
+  bind "6" { "append-to-count" (6)
+             "set-mode" ("vim-normal-with-count", transient) };
+  bind "7" { "append-to-count" (7)
+             "set-mode" ("vim-normal-with-count", transient) };
+  bind "8" { "append-to-count" (8)
+             "set-mode" ("vim-normal-with-count", transient) };
+  bind "9" { "append-to-count" (9)
+             "set-mode" ("vim-normal-with-count", transient) };
+
+  bind "KP_1" { "append-to-count" (1)
+                "set-mode" ("vim-normal-with-count", transient) };
+  bind "KP_2" { "append-to-count" (2)
+                "set-mode" ("vim-normal-with-count", transient) };
+  bind "KP_3" { "append-to-count" (3)
+                "set-mode" ("vim-normal-with-count", transient) };
+  bind "KP_4" { "append-to-count" (4)
+                "set-mode" ("vim-normal-with-count", transient) };
+  bind "KP_5" { "append-to-count" (5)
+                "set-mode" ("vim-normal-with-count", transient) };
+  bind "KP_6" { "append-to-count" (6)
+                "set-mode" ("vim-normal-with-count", transient) };
+  bind "KP_7" { "append-to-count" (7)
+                "set-mode" ("vim-normal-with-count", transient) };
+  bind "KP_8" { "append-to-count" (8)
+                "set-mode" ("vim-normal-with-count", transient) };
+  bind "KP_9" { "append-to-count" (9)
+                "set-mode" ("vim-normal-with-count", transient) };
+
+  bind "colon" { "action" ("win", "reveal-command-bar", "") };
+
+  /* cycle "tabs" */
+  bind "<ctrl><alt>Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl><alt>KP_Page_Up" { "action" ("frame", "previous-page", "") };
+  bind "<ctrl><alt>Page_Down" { "action" ("frame", "next-page", "") };
+  bind "<ctrl><alt>KP_Page_Down" { "action" ("frame", "next-page", "") };
+
+  /* replay the last recording */
+  bind "period" { "replay-macro" (1) };
+
+  /* start search backward */
+  /* TODO: use internal sourceview search */
+  bind "question" { "action" ("editor-page", "find", "") };
+
+  /* start search */
+  bind "slash" { "action" ("editor-search", "at-word-boundaries", "false")
+                 "action" ("editor-page", "find", "") };
+  bind "KP_Divide" { "action" ("editor-page", "find", "") };
+
+  /* insert at cursor */
+  bind "i" { "begin-macro" ()
+             "set-mode" ("vim-insert", permanent) };
+
+  /* insert after cursor */
+  bind "a" { "begin-macro" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (next-char, 0, 1, 0) };
+  bind "<shift>a" { "begin-macro" ()
+                    "set-mode" ("vim-insert", permanent)
+                    "movement" (last-char, 0, 0, 0) };
+
+  /* insert at first non-whitespace character */
+  bind "<shift>i" { "begin-macro" ()
+                    "set-mode" ("vim-insert", permanent)
+                    "movement" (first-nonspace-char, 0, 1, 0) };
+
+  /* insert line after current, insert mode */
+  bind "o" { "begin-macro" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (line-end, 0, 1, 0)
+             "insert-at-cursor" ("\n")
+             "reindent" ()
+             "movement" (last-char, 0, 0, 0) };
+
+  /* insert line before current */
+  bind "<shift>o" { "begin-macro" ()
+                    "set-mode" ("vim-insert", permanent)
+                    "movement" (first-char, 0, 0, 0)
+                    "insert-at-cursor" ("\n")
+                    "move-cursor" (display-lines, -1, 0)
+                    "reindent" ()
+                    "movement" (last-char, 0, 0, 0) };
+
+  bind "minus" { "movement" (previous-line, 0, 1, 1)
+                 "movement" (first-nonspace-char, 0, 1, 0)
+                 "clear-count" () };
+
+  bind "plus" { "movement" (next-line, 0, 1, 1)
+                 "movement" (first-nonspace-char, 0, 1, 0)
+                 "clear-count" () };
+  bind "KP_Enter" { "movement" (next-line, 0, 1, 1)
+                    "movement" (first-nonspace-char, 0, 1, 0)
+                    "clear-count" () };
+  bind "<shift>KP_Enter" { "movement" (next-line, 0, 1, 1)
+                           "movement" (first-nonspace-char, 0, 1, 0)
+                           "clear-count" () };
+  bind "Return" { "movement" (next-line, 0, 1, 1)
+                  "movement" (first-nonspace-char, 0, 1, 0)
+                  "clear-count" () };
+  bind "<shift>Return" { "movement" (next-line, 0, 1, 0)
+                         "movement" (first-nonspace-char, 0, 1, 0)
+                         "clear-count" () };
+
+  bind "<shift>k" { "clear-selection" ()
+                    "save-insert-mark" ()
+                    "movement" (previous-word-end, 0, 1, 1)
+                    "movement" (next-word-start, 0, 1, 0)
+                    "movement" (next-word-end, 1, 0, 1)
+                    "request-documentation" ()
+                    "clear-count" ()
+                    "clear-selection" ()
+                    "restore-insert-mark" () };
+
+  /* swallow the current character and go to insert */
+  bind "s" { "begin-macro" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (next-char, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" () };
+
+  /* overwrite the current character with a modifier */
+  bind "r" { "begin-macro" ()
+             "begin-user-action" ()
+             "capture-modifier" ()
+             "movement" (next-char, 1, 1, 1)
+             "delete-selection" ()
+             "insert-modifier" (1)
+             "clear-modifier" ()
+             "movement" (previous-char, 0, 1, 0)
+             "end-user-action" ()
+             "end-macro" () };
+
+  bind "Left"  { "movement" (previous-char, 0, 1, 1)
+                 "clear-count" () };
+  bind "Right" { "movement" (next-char, 0, 1, 1)
+                 "clear-count" () };
+  bind "Up"    { "movement" (previous-line, 0, 1, 1)
+                 "clear-count" () };
+  bind "Down"  { "movement" (next-line, 0, 1, 1)
+                 "clear-count" () };
+
+  bind "h"     { "movement" (previous-char, 0, 1, 1)
+                 "clear-count" () };
+  bind "l"     { "movement" (next-char, 0, 1, 1)
+                 "clear-count" () };
+  bind "k"     { "movement" (previous-line, 0, 1, 1)
+                 "clear-count" () };
+  bind "j"     { "movement" (next-line, 0, 1, 1)
+                 "clear-count" () };
+
+  /* move to special sub-mode 'g' */
+  bind "g" { "set-mode" ("vim-normal-g", transient ) };
+
+  /* move by word ends */
+  bind "e"        { "movement" (next-word-end, 0, 1, 1)
+                    "clear-count" () };
+  bind "<shift>e" { "movement" (next-full-word-end, 0, 1, 1)
+                    "clear-count" () };
+
+  /* move to by word start */
+  bind "w"        { "movement" (next-word-start, 0, 1, 1)
+                    "clear-count" () };
+  bind "<shift>w" { "movement" (next-full-word-start, 0, 1, 1)
+                    "clear-count" () };
+  bind "b"        { "movement" (previous-word-start, 0, 1, 1)
+                    "clear-count" () };
+  bind "<shift>b" { "movement" (previous-full-word-start, 0, 1, 1)
+                    "clear-count" () };
+
+  /* find matching char */
+  bind "f" { "save-command" ()
+             "capture-modifier" ()
+             "save-search-char" ()
+             "movement" (next-match-modifier, 0, 1, 1)
+             "clear-modifier" () };
+  bind "t" { "save-command" ()
+             "capture-modifier" ()
+             "save-search-char" ()
+             "movement" (next-match-modifier, 0, 1, 1)
+             "movement" (previous-char, 0, 1, 0)
+             "clear-modifier" () };
+  bind "<shift>f" { "save-command" ()
+                    "capture-modifier" ()
+                    "save-search-char" ()
+                    "movement" (previous-match-modifier, 0, 1, 1)
+                    "clear-modifier" () };
+  bind "<shift>t" { "save-command" ()
+                    "capture-modifier" ()
+                    "save-search-char" ()
+                    "movement" (previous-match-modifier, 0, 0, 1)
+                    "clear-modifier" () };
+  bind "comma" { "movement" (previous-match-search-char, 0, 0, 1)
+                 "clear-count" () };
+  bind "semicolon" { "movement" (next-match-search-char, 0, 0, 1)
+                     "clear-count" () };
+
+  bind "n" { "save-insert-mark" ()
+             "move-search" (tab-forward, 0, 0, 1, 1, 0)
+             "restore-insert-mark" () };
+  bind "<shift>n" { "save-insert-mark" ()
+                    "move-search" (tab-backward, 0, 0, 0, 1, 0)
+                    "restore-insert-mark" () };
+
+  bind "numbersign" { "save-insert-mark" ()
+                      "movement" (next-word-end, 0, 1, 0)
+                      "movement" (previous-word-start, 0, 1, 0)
+                      "movement" (next-word-end, 1, 0, 0)
+                      "set-search-text" ("", 1)
+                      "movement" (previous-char, 0, 1, 0)
+                      "move-search" (up, 0, 0, 0, 1, 1)
+                      "restore-insert-mark" () };
+
+  bind "asterisk" { "save-insert-mark" ()
+                    "movement" (next-word-end, 0, 1, 0)
+                    "movement" (previous-word-start, 0, 1, 0)
+                    "movement" (next-word-end, 1, 0, 0)
+                    "set-search-text" ("", 1)
+                    "move-search" (down, 0, 0, 1, 1, 1)
+                    "restore-insert-mark" () };
+
+  bind "KP_Multiply" { "save-insert-mark" ()
+                       "movement" (previous-word-end, 0, 1, 1)
+                       "movement" (next-word-start, 0, 1, 0)
+                       "movement" (next-word-end, 1, 0, 1)
+                       "set-search-text" ("", 1)
+                       "move-search" (down, 0, 0, 1, 1, 1)
+                       "restore-insert-mark" () };
+
+  /* page movements */
+  bind "<ctrl>b" { "movement" (page-up, 0, 0, 1)
+                   "clear-count" () };
+  bind "<ctrl>f" { "movement" (page-down, 0, 0, 1)
+                   "clear-count" () };
+  bind "<ctrl>u" { "movement" (half-page-up, 0, 0, 1)
+                   "clear-count" () };
+  bind "<ctrl>d" { "movement" (half-page-down, 0, 0, 1)
+                   "clear-count" () };
+  bind "Page_Up" { "movement" (page-up, 0, 0, 1)
+                   "clear-count" () };
+  bind "Page_Down" { "movement" (page-down, 0, 0, 1)
+                     "clear-count" () };
+
+  /* screen movements, keeping cursor locked to visible region */
+  bind "<ctrl>e" { "movement" (screen-up, 0, 0, 1)
+                   "clear-count" () };
+  bind "<ctrl>y" { "movement" (screen-down, 0, 0, 1)
+                   "clear-count" () };
+  bind "z" { "set-mode" ("vim-normal-z", transient) };
+  bind "<shift>z" { "set-mode" ("vim-normal-Z", transient) };
+
+  /* macro recording! */
+  bind "q" { "set-mode" ("vim-normal-q", transient) };
+
+  /* goto definition (or follow-link, really) */
+  bind "<ctrl>bracketright" { "goto-definition" () };
+
+  /* submode for bracket */
+  bind "bracketleft" { "set-mode" ("vim-normal-bracket", transient) };
+
+  /* move by paragraph */
+  bind "braceleft" { "movement" (paragraph-start, 0, 0, 1)
+                     "clear-count" () };
+  bind "braceright" { "movement" (paragraph-end, 0, 0, 1)
+                      "clear-count" () };
+
+  /* move by sentence */
+  bind "parenleft" { "movement" (sentence-start, 0, 0, 1)
+                     "clear-count" () };
+  bind "parenright" { "movement" (sentence-end, 0, 0, 1)
+                      "clear-count" () };
+
+  /* move to line offset of zero, and first non-whitespace char, end of line */
+  bind "0" { "movement" (first-char, 0, 1, 0)
+             "clear-count" () };
+  bind "KP_0" { "movement" (first-char, 0, 1, 0)
+                "clear-count" () };
+  bind "Home" { "movement" (first-char, 0, 1, 0)
+                "clear-count" () };
+  bind "asciicircum" { "movement" (first-nonspace-char, 0, 1, 0)
+                       "clear-count" () };
+
+  /* this is a count - 1 motion, we handle this specific case in C code */
+  bind "underscore" { "movement" (next-line, 0, 1, 1)
+                      "movement" (first-nonspace-char, 0, 1, 0)
+                      "clear-count" () };
+
+  bind "dollar" { "movement" (last-char, 0, 1, 0)
+                  "clear-count" () };
+  bind "End" { "movement" (last-char, 0, 1, 0)
+               "clear-count" () };
+  bind "bar" { "movement" (nth-char, 0, 1, 1)
+               "clear-count" () };
+
+  /* jump to match of brace/bracket/comment/etc */
+  bind "percent" { "movement" (match-special, 0, 1, 1)
+                   "clear-count" () };
+
+  /* move based on visible screen area */
+  bind "<shift>h" { "movement" (screen-top, 0, 0, 0)
+                    "clear-count" () };
+  bind "<shift>m" { "movement" (screen-middle, 0, 0, 0)
+                    "clear-count" () };
+  bind "<shift>l" { "movement" (screen-bottom, 0, 0, 0)
+                    "clear-count" () };
+
+  /* move to nth line, defaults to last */
+  bind "<shift>g" { "movement" (nth-line, 0, 1, 1)
+                    "movement" (first-nonspace-char, 0, 1, 0)
+                    "clear-count" () };
+
+  /* undo - todo: how do we land cursor on right spot? */
+  bind "u" { "undo" ()
+             "clear-count" ()
+             "clear-selection" ()};
+
+  /* redo */
+  bind "<ctrl>r" { "redo" ()
+                   "clear-count" ()
+                   "clear-selection" () };
+
+  bind "p" { "begin-macro" ()
+             "paste-clipboard-extended" (1, 1, 0)
+             "movement" (previous-char, 0, 1, 0)
+             "clear-count" ()
+             "end-macro" () };
+  bind "<shift>p" { "begin-macro" ()
+                    "paste-clipboard-extended" (1, 0, 0)
+                    "movement" (previous-char, 0, 1, 0)
+                    "clear-count" ()
+                    "end-macro" () };
+
+  /* overwrite */
+  bind "<shift>r" { "begin-macro" ()
+                    "set-mode" ("vim-replace", permanent)
+                    "set-overwrite" (1) };
+
+ /* jump to sub-mode */
+  bind "c" { "set-mode" ("vim-normal-c", transient) };
+  bind "d" { "set-mode" ("vim-normal-d", transient) };
+
+  /* delete to end of line */
+  bind "<shift>d" { "begin-macro" ()
+                    "movement" (last-char, 1, 0, 0)
+                    "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "clear-count" ()
+                    "end-macro" () };
+
+ /* delete to end of line and go to insert */
+  bind "<shift>c" { "begin-macro" ()
+                    "movement" (last-char, 1, 0, 0)
+                    "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "clear-count" ()
+                    "set-mode" ("vim-insert", permanent) };
+
+  /* delete current char */
+  bind "x" { "begin-macro" ()
+             "movement" (next-char, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "clear-count" ()
+             "end-macro" () };
+
+  /* delete previous char */
+  bind "<shift>x" { "begin-macro" ()
+                    "movement" (previous-char, 1, 1, 1)
+                    "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "clear-count" ()
+                    "end-macro" () };
+
+  bind "greater" { "set-mode" ("vim-normal-indent", transient) };
+  bind "less" { "set-mode" ("vim-normal-indent", transient) };
+
+  /* join selected lines */
+  /* todo: this actually grabs one more line than vim does when prefixed with
+   *       a count. 1J and 2J are both the same thing.
+   */
+  bind "<shift>j" { "begin-macro" ()
+                    "movement" (first-char, 0, 0, 0)
+                    "movement" (next-line, 1, 0, 1)
+                    "join-lines" ()
+                    "clear-count" ()
+                    "end-macro" () };
+
+  /* change number */
+  bind "<ctrl>a" { "begin-macro" ()
+                   "change-number" (1)
+                   "clear-count" ()
+                   "end-macro" () };
+  bind "<ctrl>x" { "begin-macro" ()
+                   "change-number" (-1)
+                   "clear-count" ()
+                   "end-macro" () };
+
+  /* toggle character case */
+  bind "asciitilde" { "begin-macro" ()
+                      "movement" (next-char, 1, 1, 1)
+                      "change-case" (toggle)
+                      "clear-count" ()
+                      "end-macro" () };
+
+  bind "BackSpace" { "movement" (previous-offset, 0, 1, 1)
+                     "clear-count" () };
+  bind "space" { "movement" (next-offset, 0, 1, 1)
+                 "clear-count" () };
+
+  /* copy */
+  bind "y" { "set-mode" ("vim-normal-y", transient) };
+  bind "<shift>y" { "save-insert-mark" ()
+                    "movement" (first-char, 0, 0, 0)
+                    "movement" (next-line, 1, 1, 1)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (expand)
+                    "clear-count" ()
+                    "clear-selection" ()
+                    "restore-insert-mark" () };
+
+  /* visual mode transition */
+  bind "v" { "begin-macro" ()
+             "movement" (next-char, 1, 1, 1)
+             "set-mode" ("vim-visual", permanent) };
+  bind "<shift>v" { "begin-macro" ()
+                    "movement" (first-char, 0, 1, 0)
+                    "movement" (next-line, 1, 0, 1)
+                    "set-mode" ("vim-visual-line", permanent) };
+  bind "<ctrl>v" { "set-mode" ("vim-visual-block", permanent) };
+
+  /* navigation */
+  bind "<ctrl>o" { "action" ("history", "move-previous-edit", "") };
+  bind "<ctrl>i" { "action" ("history", "move-next-edit", "") };
+
+  /* window controls */
+  bind "<ctrl>w" { "set-mode" ("vim-normal-ctrl-w", transient) };
+
+  /* reindent */
+  bind "equal" { "set-mode" ("vim-normal-equal", transient) };
+}
+
+@binding-set builder-vim-source-view-normal-equal
+{
+  bind "equal" { "reindent" () };
+}
+
+@binding-set builder-vim-source-view-normal-bracket
+{
+  bind "braceleft" { "movement" (previous-unmatched-brace, 0, 1, 1) };
+  bind "braceright" { "movement" (next-unmatched-brace, 0, 1, 1) };
+
+  bind "parenleft" { "movement" (previous-unmatched-paren, 0, 1, 1) };
+  bind "parenright" { "movement" (next-unmatched-paren, 0, 1, 1) };
+}
+
+@binding-set builder-vim-source-view-normal-c
+{
+  bind "1" { "append-to-count" (1)
+             "set-mode" ("vim-c-with-count", transient) };
+  bind "2" { "append-to-count" (2)
+             "set-mode" ("vim-c-with-count", transient) };
+  bind "3" { "append-to-count" (3)
+             "set-mode" ("vim-c-with-count", transient) };
+  bind "4" { "append-to-count" (4)
+             "set-mode" ("vim-c-with-count", transient) };
+  bind "5" { "append-to-count" (5)
+             "set-mode" ("vim-c-with-count", transient) };
+  bind "6" { "append-to-count" (6)
+             "set-mode" ("vim-c-with-count", transient) };
+  bind "7" { "append-to-count" (7)
+             "set-mode" ("vim-c-with-count", transient) };
+  bind "8" { "append-to-count" (8)
+             "set-mode" ("vim-c-with-count", transient) };
+  bind "9" { "append-to-count" (9)
+             "set-mode" ("vim-c-with-count", transient) };
+
+  bind "KP_1" { "append-to-count" (1)
+                "set-mode" ("vim-c-with-count", transient) };
+  bind "KP_2" { "append-to-count" (2)
+                "set-mode" ("vim-c-with-count", transient) };
+  bind "KP_3" { "append-to-count" (3)
+                "set-mode" ("vim-c-with-count", transient) };
+  bind "KP_4" { "append-to-count" (4)
+                "set-mode" ("vim-c-with-count", transient) };
+  bind "KP_5" { "append-to-count" (5)
+                "set-mode" ("vim-c-with-count", transient) };
+  bind "KP_6" { "append-to-count" (6)
+                "set-mode" ("vim-c-with-count", transient) };
+  bind "KP_7" { "append-to-count" (7)
+                "set-mode" ("vim-c-with-count", transient) };
+  bind "KP_8" { "append-to-count" (8)
+                "set-mode" ("vim-c-with-count", transient) };
+  bind "KP_9" { "append-to-count" (9)
+                "set-mode" ("vim-c-with-count", transient) };
+
+  bind "i" { "set-mode" ("vim-normal-c-i", transient) };
+
+  bind "a" { "set-mode" ("vim-normal-c-a", transient) };
+
+  bind "e" { "begin-macro" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (next-word-end, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" () };
+  bind "w" { "begin-macro" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (next-word-end, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" () };
+
+  bind "l" { "begin-macro" ()
+             "movement" (next-char, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "set-mode" ("vim-insert", permanent) };
+  bind "h" { "begin-macro" ()
+             "movement" (previous-char, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "set-mode" ("vim-insert", permanent) };
+  bind "k" { "begin-macro" ()
+             "movement" (last-char, 0, 0, 0)
+             "movement" (previous-line, 1, 0, 1)
+             "movement" (first-char, 1, 1, 0)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "set-mode" ("vim-insert", permanent) };
+  bind "j" { "begin-macro" ()
+             "movement" (first-char, 0, 1, 0)
+             "movement" (next-line, 1, 0, 1)
+             "movement" (last-char, 1, 0, 0)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "set-mode" ("vim-insert", permanent) };
+  bind "Right" { "begin-macro" ()
+                 "movement" (next-char, 1, 1, 1)
+                 "copy-clipboard-extended" ()
+                 "delete-selection" ()
+                 "set-mode" ("vim-insert", permanent) };
+  bind "Left" { "begin-macro" ()
+                "movement" (previous-char, 1, 1, 1)
+                "copy-clipboard-extended" ()
+                "delete-selection" ()
+                "set-mode" ("vim-insert", permanent) };
+  bind "Up" { "begin-macro" ()
+              "movement" (last-char, 0, 0, 0)
+              "movement" (previous-line, 1, 0, 1)
+              "movement" (first-char, 1, 1, 0)
+              "copy-clipboard-extended" ()
+              "delete-selection" ()
+              "set-mode" ("vim-insert", permanent) };
+  bind "Down" { "begin-macro" ()
+                "movement" (first-char, 0, 1, 0)
+                "movement" (next-line, 1, 0, 1)
+                "movement" (last-char, 1, 0, 0)
+                "copy-clipboard-extended" ()
+                "delete-selection" ()
+                "set-mode" ("vim-insert", permanent) };
+
+  bind "0" { "begin-macro" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (first-char, 1, 0, 0)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" () };
+  bind "KP_0" { "begin-macro" ()
+                "set-mode" ("vim-insert", permanent)
+                "movement" (first-char, 1, 0, 0)
+                "copy-clipboard-extended" ()
+                "delete-selection" ()
+                "end-macro" () };
+
+  bind "plus" { "begin-macro" ()
+                "movement" (first-char, 0, 1, 0)
+                "movement" (next-line, 1, 0, 1)
+                "movement" (last-char, 1, 0, 0)
+                "copy-clipboard-extended" ()
+                "delete-selection" ()
+                "reindent" ()
+                "clear-count" ()
+                "set-mode" ("vim-insert", permanent) };
+  bind "KP_Enter" { "begin-macro" ()
+                    "movement" (first-char, 0, 1, 0)
+                    "movement" (next-line, 1, 0, 1)
+                    "movement" (last-char, 1, 0, 0)
+                    "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "reindent" ()
+                    "clear-count" ()
+                    "set-mode" ("vim-insert", permanent) };
+  bind "<shift>KP_Enter" { "begin-macro" ()
+                           "movement" (first-char, 0, 1, 0)
+                           "movement" (next-line, 1, 0, 1)
+                           "movement" (last-char, 1, 0, 0)
+                           "copy-clipboard-extended" ()
+                           "delete-selection" ()
+                           "reindent" ()
+                           "clear-count" ()
+                           "set-mode" ("vim-insert", permanent) };
+  bind "Return" { "begin-macro" ()
+                  "movement" (first-char, 0, 1, 0)
+                  "movement" (next-line, 1, 0, 1)
+                  "movement" (last-char, 1, 0, 0)
+                  "copy-clipboard-extended" ()
+                  "delete-selection" ()
+                  "reindent" ()
+                  "clear-count" ()
+                  "set-mode" ("vim-insert", permanent) };
+  bind "<shift>Return" { "begin-macro" ()
+                         "movement" (first-char, 0, 1, 0)
+                         "movement" (next-line, 1, 0, 1)
+                         "movement" (last-char, 1, 0, 0)
+                         "copy-clipboard-extended" ()
+                         "delete-selection" ()
+                         "reindent" ()
+                         "clear-count" ()
+                         "set-mode" ("vim-insert", permanent) };
+
+  bind "<shift>asciicircum" { "begin-macro" ()
+                              "set-mode" ("vim-insert", permanent)
+                              "movement" (first-nonspace-char, 1, 1, 1)
+                              "copy-clipboard-extended" ()
+                              "delete-selection" ()
+                              "end-macro" () };
+
+  /* this is a count - 1 motion, we handle this specific case in C code */
+  bind "underscore" { "begin-macro" ()
+                      "movement" (first-char, 0, 1, 0)
+                      "movement" (next-line, 1, 0, 1)
+                      "movement" (last-char, 1, 0, 0)
+                      "copy-clipboard-extended" ()
+                      "delete-selection" ()
+                      "reindent" ()
+                      "clear-count" ()
+                      "set-mode" ("vim-insert", permanent) };
+
+  bind "dollar" { "begin-macro" ()
+                  "set-mode" ("vim-insert", permanent)
+                  "movement" (line-end, 1, 1, 0)
+                  "copy-clipboard-extended" ()
+                  "delete-selection" ()
+                  "end-macro" () };
+
+
+  bind "f" { "begin-macro" ()
+             "save-command" ()
+             "capture-modifier" ()
+             "save-search-char" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (next-match-modifier, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" ()
+             "clear-modifier" () };
+
+  bind "t" { "begin-macro" ()
+             "save-command" ()
+             "capture-modifier" ()
+             "save-search-char" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (next-match-modifier, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" ()
+             "clear-modifier" () };
+
+  bind "c" { "begin-macro" ()
+             "movement" (first-char, 0, 1, 0)
+             "movement" (last-char, 1, 0, 0)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "reindent" ()
+             "set-mode" ("vim-insert", permanent) };
+
+  bind "<shift>f" { "begin-macro" ()
+                    "save-command" ()
+                    "capture-modifier" ()
+                    "save-search-char" ()
+                    "set-mode" ("vim-insert", permanent)
+                    "movement" (previous-match-modifier, 1, 1, 1)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" ()
+                    "clear-modifier" () };
+
+  bind "<shift>t" { "begin-macro" ()
+                    "save-command" ()
+                    "capture-modifier" ()
+                    "save-search-char" ()
+                    "set-mode" ("vim-insert", permanent)
+                    "movement" (previous-match-modifier, 1, 0, 1)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" ()
+                    "clear-modifier" () };
+
+  bind "comma" { "begin-macro" ()
+                 "movement" (previous-match-search-char, 1, 0, 1)
+                 "set-mode" ("vim-insert", permanent)
+                 "copy-clipboard-extended" ()
+                 "selection-theatric" (shrink)
+                 "delete-selection" () };
+
+  bind "semicolon" { "begin-macro" ()
+                     "movement" (next-match-search-char, 1, 0, 1)
+                     "set-mode" ("vim-insert", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+}
+
+@binding-set builder-vim-source-view-c-with-count
+{
+  bind "0" { "append-to-count" (0)
+             "set-mode" ("vim-c-with-count", transient) };
+  bind "KP_0" { "append-to-count" (0)
+                "set-mode" ("vim-c-with-count", transient) };
+}
+
+@binding-set builder-vim-source-view-normal-c-i
+{
+  /* cip */
+  bind "p" { "begin-macro" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (paragraph-start, 1, 1, 1)
+             "swap-selection-bounds" ()
+             "movement" (paragraph-end, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" () };
+
+  /* cis */
+  bind "s" { "begin-macro" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (sentence-start, 1, 1, 1)
+             "swap-selection-bounds" ()
+             "movement" (sentence-end, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" () };
+
+  /* ciw */
+  bind "w" { "begin-macro" ()
+             "set-mode" ("vim-insert", permanent)
+             "movement" (previous-word-end, 0, 1, 1)
+             "movement" (next-word-start, 0, 1, 0)
+             "movement" (next-word-end, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" () };
+
+  /* ciW */
+  bind "<shift>w" { "begin-macro" ()
+                    "set-mode" ("vim-insert", permanent)
+                    "movement" (previous-full-word-end, 0, 1, 1)
+                    "movement" (next-full-word-start, 0, 1, 0)
+                    "movement" (next-full-word-end, 1, 0, 1)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" () };
+
+  /* ci( , ci) , cib */
+  bind "parenleft" { "begin-macro" ()
+                     "select-inner" ("(", ")", 1, 0)
+                     "set-mode" ("vim-insert", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  bind "parenright" { "begin-macro" ()
+                      "select-inner" ("(", ")", 1, 0)
+                      "set-mode" ("vim-insert", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+  bind "b" { "begin-macro" ()
+             "select-inner" ("(", ")", 1, 0)
+             "set-mode" ("vim-insert", permanent)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" () };
+
+  /* ci[ and ci] */
+  bind "bracketleft" { "begin-macro" ()
+                       "select-inner" ("[", "]", 1, 0)
+                       "set-mode" ("vim-insert", permanent)
+                       "copy-clipboard-extended" ()
+                       "selection-theatric" (shrink)
+                       "delete-selection" () };
+
+  bind "bracketright" { "begin-macro" ()
+                        "select-inner" ("[", "]", 1, 0)
+                        "set-mode" ("vim-insert", permanent)
+                        "copy-clipboard-extended" ()
+                        "selection-theatric" (shrink)
+                        "delete-selection" () };
+
+  /* ci{ , ci} , ciB */
+  bind "braceleft" { "begin-macro" ()
+                     "select-inner" ("{", "}", 1, 0)
+                     "set-mode" ("vim-insert", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  bind "braceright" { "begin-macro" ()
+                      "select-inner" ("{", "}", 1, 0)
+                      "set-mode" ("vim-insert", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+  bind "<shift>b" { "begin-macro" ()
+                    "select-inner" ("{", "}", 1, 0)
+                    "set-mode" ("vim-insert", permanent)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" () };
+
+  /* ci< and ci> */
+    bind "less" { "begin-macro" ()
+                  "select-inner" ("<", ">", 1, 0)
+                  "set-mode" ("vim-insert", permanent)
+                  "copy-clipboard-extended" ()
+                  "selection-theatric" (shrink)
+                  "delete-selection" () };
+
+    bind "greater" { "begin-macro" ()
+                     "select-inner" ("<", ">", 1, 0)
+                     "set-mode" ("vim-insert", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  /* ci" ci' ci` */
+    bind "quotedbl" { "begin-macro" ()
+                      "select-inner" ("\"", "\"", 1, 1)
+                      "set-mode" ("vim-insert", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+    bind "apostrophe" { "begin-macro" ()
+                        "select-inner" ("'", "'", 1, 1)
+                        "set-mode" ("vim-insert", permanent)
+                        "copy-clipboard-extended" ()
+                        "selection-theatric" (shrink)
+                        "delete-selection" () };
+
+    bind "grave" { "begin-macro" ()
+                   "select-inner" ("`", "`", 1, 1)
+                   "set-mode" ("vim-insert", permanent)
+                   "copy-clipboard-extended" ()
+                   "selection-theatric" (shrink)
+                   "delete-selection" () };
+
+  /* cit */
+    bind "t" { "begin-macro" ()
+               "select-tag" (1)
+               "set-mode" ("vim-insert", permanent)
+               "copy-clipboard-extended" ()
+               "selection-theatric" (shrink)
+               "delete-selection" () };
+}
+
+@binding-set builder-vim-source-view-normal-c-a
+{
+  /* ca( , ca) , cab */
+  bind "parenleft" { "begin-macro" ()
+                     "select-inner" ("(", ")", 0, 0)
+                     "set-mode" ("vim-insert", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  bind "parenright" { "begin-macro" ()
+                      "select-inner" ("(", ")", 0, 0)
+                      "set-mode" ("vim-insert", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+  bind "b" { "begin-macro" ()
+             "select-inner" ("(", ")", 0, 0)
+             "set-mode" ("vim-insert", permanent)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" () };
+
+  /* ca[ and ca] */
+  bind "bracketleft" { "begin-macro" ()
+                       "select-inner" ("[", "]", 0, 0)
+                       "set-mode" ("vim-insert", permanent)
+                       "copy-clipboard-extended" ()
+                       "selection-theatric" (shrink)
+                       "delete-selection" () };
+
+  bind "bracketright" { "begin-macro" ()
+                        "select-inner" ("[", "]", 0, 0)
+                        "set-mode" ("vim-insert", permanent)
+                        "copy-clipboard-extended" ()
+                        "selection-theatric" (shrink)
+                        "delete-selection" () };
+
+  /* ca{ , ca} , caB */
+  bind "braceleft" { "begin-macro" ()
+                     "select-inner" ("{", "}", 0, 0)
+                     "set-mode" ("vim-insert", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  bind "braceright" { "begin-macro" ()
+                      "select-inner" ("{", "}", 0, 0)
+                      "set-mode" ("vim-insert", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+  bind "<shift>b" { "begin-macro" ()
+                    "select-inner" ("{", "}", 0, 0)
+                    "set-mode" ("vim-insert", permanent)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" () };
+
+  /* ca< and ca> */
+    bind "less" { "begin-macro" ()
+                  "select-inner" ("<", ">", 0, 0)
+                  "set-mode" ("vim-insert", permanent)
+                  "copy-clipboard-extended" ()
+                  "selection-theatric" (shrink)
+                  "delete-selection" () };
+
+    bind "greater" { "begin-macro" ()
+                     "select-inner" ("<", ">", 0, 0)
+                     "set-mode" ("vim-insert", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  /* ca" ca' ca` */
+    bind "quotedbl" { "begin-macro" ()
+                      "select-inner" ("\"", "\"", 0, 1)
+                      "set-mode" ("vim-insert", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+    bind "apostrophe" { "begin-macro" ()
+                        "select-inner" ("'", "'", 0, 1)
+                        "set-mode" ("vim-insert", permanent)
+                        "copy-clipboard-extended" ()
+                        "selection-theatric" (shrink)
+                        "delete-selection" () };
+
+    bind "grave" { "begin-macro" ()
+                   "select-inner" ("`", "`", 0, 1)
+                   "set-mode" ("vim-insert", permanent)
+                   "copy-clipboard-extended" ()
+                   "selection-theatric" (shrink)
+                   "delete-selection" () };
+
+  /* cat */
+    bind "t" { "begin-macro" ()
+               "select-tag" (0)
+               "set-mode" ("vim-insert", permanent)
+               "copy-clipboard-extended" ()
+               "selection-theatric" (shrink)
+               "delete-selection" () };
+}
+
+@binding-set builder-vim-source-view-normal-d
+{
+  bind "Left"  { "begin-macro" ()
+                 "movement" (previous-char, 1, 1, 1)
+                 "copy-clipboard-extended" ()
+                 "delete-selection" ()
+                 "end-macro" () };
+  bind "h"     { "begin-macro" ()
+                 "movement" (previous-char, 1, 1, 1)
+                 "copy-clipboard-extended" ()
+                 "delete-selection" ()
+                 "end-macro" () };
+
+  bind "braceleft"  { "begin-macro" ()
+                      "movement" (paragraph-start, 1, 0, 1)
+                      "copy-clipboard-extended" ()
+                      "delete-selection" ()
+                      "end-macro" () };
+
+  bind "braceright" { "begin-macro" ()
+                      "movement" (paragraph-end, 1, 0, 1)
+                      "copy-clipboard-extended" ()
+                      "delete-selection" ()
+                      "end-macro" () };
+
+  bind "Right" { "begin-macro" ()
+                 "movement" (next-char, 1, 1, 1)
+                 "copy-clipboard-extended" ()
+                 "delete-selection" ()
+                 "end-macro" () };
+  bind "l"     { "begin-macro" ()
+                 "movement" (next-char, 1, 1, 1)
+                 "copy-clipboard-extended" ()
+                 "delete-selection" ()
+                 "end-macro" () };
+
+  bind "Up"    { "begin-macro" ()
+                 "movement" (line-end, 0, 0, 0)
+                 "movement" (previous-line, 1, 0, 0)
+                 "movement" (previous-line, 1, 0, 1)
+                 "copy-clipboard-extended" ()
+                 "delete-selection" ()
+                 "movement" (first-nonspace-char, 0, 1, 0)
+                 "end-macro" () };
+  bind "k"     { "begin-macro" ()
+                 "movement" (line-end, 0, 0, 0)
+                 "movement" (previous-line, 1, 0, 0)
+                 "movement" (previous-line, 1, 0, 1)
+                 "copy-clipboard-extended" ()
+                 "delete-selection" ()
+                 "movement" (first-nonspace-char, 0, 1, 0)
+                 "end-macro" () };
+
+  bind "Down"  { "begin-macro" ()
+                 "movement" (first-char, 0, 1, 0)
+                 "movement" (next-line, 1, 0, 0)
+                 "movement" (next-line, 1, 0, 1)
+                 "copy-clipboard-extended" ()
+                 "delete-selection" ()
+                 "movement" (first-nonspace-char, 0, 1, 0)
+                 "end-macro" () };
+  bind "j"     { "begin-macro" ()
+                 "movement" (first-char, 0, 1, 0)
+                 "movement" (next-line, 1, 0, 0)
+                 "movement" (next-line, 1, 0, 1)
+                 "copy-clipboard-extended" ()
+                 "delete-selection" ()
+                 "movement" (first-nonspace-char, 0, 1, 0)
+                 "end-macro" () };
+  bind "Return" { "begin-macro" ()
+                  "movement" (first-char, 0, 1, 0)
+                  "movement" (next-line, 1, 0, 0)
+                  "movement" (next-line, 1, 0, 1)
+                  "copy-clipboard-extended" ()
+                  "delete-selection" ()
+                  "movement" (first-nonspace-char, 0, 1, 0)
+                  "end-macro" () };
+
+  bind "<shift>g" { "begin-macro" ()
+                    "movement" (nth-line, 1, 0, 1)
+                    "movement" (last-char, 1, 0, 0)
+                    "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "movement" (last-char, 0, 0, 0)
+                    "end-macro" () };
+
+  bind "i" { "set-mode" ("vim-normal-d-i", transient) };
+  bind "a" { "set-mode" ("vim-normal-d-a", transient) };
+  bind "g" { "set-mode" ("vim-normal-d-g", transient) };
+
+  bind "f" { "begin-macro" ()
+             "save-command" ()
+             "capture-modifier" ()
+             "save-search-char" ()
+             "movement" (next-match-modifier, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" ()
+             "clear-modifier" ()
+             "end-macro" () };
+  bind "t" { "begin-macro" ()
+             "save-command" ()
+             "capture-modifier" ()
+             "save-search-char" ()
+             "movement" (next-match-modifier, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" ()
+             "clear-modifier" ()
+             "end-macro" () };
+
+  bind "<shift>f" { "begin-macro" ()
+                    "save-command" ()
+                    "capture-modifier" ()
+                    "save-search-char" ()
+                    "movement" (previous-match-modifier, 1, 1, 1)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" ()
+                    "clear-modifier" ()
+                    "end-macro" () };
+  bind "<shift>t" { "begin-macro" ()
+                    "save-command" ()
+                    "capture-modifier" ()
+                    "save-search-char" ()
+                    "movement" (previous-match-modifier, 1, 0, 1)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" ()
+                    "clear-modifier" ()
+                    "end-macro" () };
+  bind "comma" { "begin-macro" ()
+                 "movement" (previous-match-search-char, 1, 1, 1)
+                 "copy-clipboard-extended" ()
+                 "selection-theatric" (shrink)
+                 "delete-selection" ()
+                 "clear-count" ()
+                 "end-macro" () };
+
+  bind "semicolon" { "begin-macro" ()
+                     "movement" (next-match-search-char, 1, 0, 1)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" ()
+                     "clear-count" ()
+                     "end-macro" () };
+
+  bind "d" { "begin-macro" ()
+             "movement" (first-char, 0, 1, 0)
+             "movement" (next-line, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "movement" (first-nonspace-char, 0, 1, 0)
+             "end-macro" () };
+
+  bind "plus" { "begin-macro" ()
+                "movement" (first-char, 0, 1, 0)
+                "movement" (next-line, 1, 0, 0)
+                "movement" (next-line, 1, 0, 1)
+                "copy-clipboard-extended" ()
+                "delete-selection" ()
+                "movement" (first-nonspace-char, 0, 1, 0)
+                "clear-count" () };
+  bind "KP_Enter" { "begin-macro" ()
+                    "movement" (first-char, 0, 1, 0)
+                    "movement" (next-line, 1, 0, 0)
+                    "movement" (next-line, 1, 0, 1)
+                    "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "movement" (first-nonspace-char, 0, 1, 0)
+                    "clear-count" () };
+  bind "<shift>KP_Enter" { "begin-macro" ()
+                           "movement" (first-char, 0, 1, 0)
+                           "movement" (next-line, 1, 0, 0)
+                           "movement" (next-line, 1, 0, 1)
+                           "copy-clipboard-extended" ()
+                           "delete-selection" ()
+                           "movement" (first-nonspace-char, 0, 1, 0)
+                           "clear-count" () };
+  bind "Return" { "begin-macro" ()
+                  "movement" (first-char, 0, 1, 0)
+                  "movement" (next-line, 1, 0, 0)
+                  "movement" (next-line, 1, 0, 1)
+                  "copy-clipboard-extended" ()
+                  "delete-selection" ()
+                  "movement" (first-nonspace-char, 0, 1, 0)
+                  "clear-count" () };
+  bind "<shift>Return" { "begin-macro" ()
+                         "movement" (first-char, 0, 1, 0)
+                         "movement" (next-line, 1, 0, 0)
+                         "movement" (next-line, 1, 0, 1)
+                         "copy-clipboard-extended" ()
+                         "delete-selection" ()
+                         "movement" (first-nonspace-char, 0, 1, 0)
+                         "clear-count" () };
+
+  /* this is a count - 1 motion, we handle this specific case in C code */
+  bind "underscore" { "begin-macro" ()
+                      "movement" (first-char, 0, 1, 0)
+                      "movement" (next-line, 1, 0, 0)
+                      "movement" (next-line, 1, 0, 1)
+                      "copy-clipboard-extended" ()
+                      "delete-selection" ()
+                      "movement" (first-nonspace-char, 0, 1, 0)
+                      "clear-count" () };
+
+  bind "b" { "begin-macro" ()
+             "movement" (previous-word-start-newline-stop, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" () };
+
+  bind "<shift>b" { "begin-macro" ()
+                    "movement" (previous-full-word-start-newline-stop, 1, 1, 1)
+                    "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "end-macro" () };
+
+  bind "e" { "begin-macro" ()
+             "movement" (next-word-end-newline-stop, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" () };
+  bind "<shift>e" { "begin-macro" ()
+                    "movement" (next-full-word-end-newline-stop, 1, 0, 1)
+                    "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "end-macro" () };
+  bind "w" { "begin-macro" ()
+             "movement" (next-word-start-newline-stop, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" () };
+  bind "<shift>w" { "begin-macro" ()
+                    "movement" (next-full-word-start-newline-stop, 1, 1, 1)
+                    "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "end-macro" () };
+  bind "0" { "begin-macro" ()
+             "movement" (first-char, 1, 0, 0)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" () };
+  bind "KP_0" { "begin-macro" ()
+                "movement" (first-char, 1, 0, 0)
+                "copy-clipboard-extended" ()
+                "delete-selection" ()
+                "end-macro" () };
+  bind "<shift>asciicircum" { "begin-macro" ()
+                              "movement" (first-nonspace-char, 1, 1, 1)
+                              "copy-clipboard-extended" ()
+                              "delete-selection" ()
+                              "end-macro" () };
+  bind "dollar" { "begin-macro" ()
+                  "movement" (line-end, 1, 1, 0)
+                  "copy-clipboard-extended" ()
+                  "delete-selection" ()
+                  "end-macro" () };
+}
+
+@binding-set builder-vim-source-view-normal-indent
+{
+  bind "greater" { "begin-macro" ()
+                   "movement" (first-char, 0, 1, 0)
+                   "movement" (line-end, 1, 1, 0)
+                   "indent-selection" (1)
+                   "clear-selection" ()
+                   "movement" (first-nonspace-char, 0, 1, 0)
+                   "end-macro" () };
+  bind "less"    { "begin-macro" ()
+                   "movement" (first-char, 0, 1, 0)
+                   "movement" (line-end, 1, 1, 0)
+                   "indent-selection" (-1)
+                   "clear-selection" ()
+                   "movement" (first-nonspace-char, 0, 1, 0)
+                   "end-macro" () };
+}
+
+@binding-set builder-vim-source-view-normal-z
+{
+  bind "z" { "movement" (scroll-screen-center, 0, 0, 1) };
+  bind "period" { "movement" (scroll-screen-center, 0, 1, 1) };
+
+  bind "t" { "movement" (scroll-screen-top,    0, 0, 1) };
+  bind "Return" { "movement" (scroll-screen-top, 0, 1, 1) };
+  bind "KP_Enter" { "movement" (scroll-screen-top, 0, 1, 1) };
+
+  bind "b" { "movement" (scroll-screen-bottom, 0, 0, 1) };
+  bind "minus" { "movement" (scroll-screen-bottom, 0, 1, 1) };
+
+  bind "l" { "movement" (screen-left, 0, 0, 1) };
+  bind "Left" { "movement" (screen-left, 0, 0, 1) };
+
+  bind "<shift>l" { "movement" (half-page-left, 0, 0, 1)
+                    "clear-count" () };
+
+  bind "h" { "movement" (screen-right, 0, 0, 1) };
+  bind "Right" { "movement" (screen-right, 0, 0, 1) };
+
+  bind "<shift>h" { "movement" (half-page-right, 0, 0, 1)
+                    "clear-count" () };
+
+  bind "s" { "movement" (scroll-screen-left, 0, 0, 1) };
+  bind "e" { "movement" (scroll-screen-right, 0, 0, 1) };
+}
+
+@binding-set builder-vim-source-view-normal-Z
+{
+  bind "<shift>z" { "action" ("win", "save-all-quit", "") };
+}
+
+@binding-set builder-vim-source-view-visual-z
+{
+  bind "z" { "movement" (scroll-screen-center, 1, 0, 0)
+             "set-mode" ("vim-visual", permanent) };
+  bind "period" { "movement" (scroll-screen-center, 1, 1, 0)
+                  "set-mode" ("vim-visual", permanent) };
+
+  bind "t" { "movement" (scroll-screen-top, 1, 0, 0)
+             "set-mode" ("vim-visual", permanent) };
+  bind "Return" { "movement" (scroll-screen-top, 1, 1, 0)
+                  "set-mode" ("vim-visual", permanent) };
+  bind "KP_Enter" { "movement" (scroll-screen-top, 1, 1, 0)
+                    "set-mode" ("vim-visual", permanent) };
+
+  bind "b" { "movement" (scroll-screen-bottom, 1, 0, 0)
+             "set-mode" ("vim-visual", permanent) };
+  bind "minus" { "movement" (scroll-screen-bottom, 1, 1, 0)
+                 "set-mode" ("vim-visual", permanent) };
+
+  bind "l" { "movement" (screen-left, 1, 0, 1)
+             "set-mode" ("vim-visual", permanent) };
+  bind "Left" { "movement" (screen-left, 1, 0, 1)
+                "set-mode" ("vim-visual", permanent) };
+
+  bind "<shift>l" { "movement" (half-page-left, 1, 0, 1)
+                    "set-mode" ("vim-visual", permanent) };
+
+  bind "h" { "movement" (screen-right, 1, 0, 1)
+             "set-mode" ("vim-visual", permanent) };
+  bind "Right" { "movement" (screen-right, 1, 0, 1)
+                 "set-mode" ("vim-visual", permanent) };
+
+  bind "<shift>h" { "movement" (half-page-right, 1, 0, 1)
+                    "set-mode" ("vim-visual", permanent) };
+
+  bind "s" { "movement" (scroll-screen-left, 1, 0, 1)
+             "set-mode" ("vim-visual", permanent) };
+  bind "e" { "movement" (scroll-screen-right, 1, 0, 1)
+             "set-mode" ("vim-visual", permanent) };
+}
+
+@binding-set builder-vim-source-view-visual-line-z
+{
+  bind "z" { "movement" (scroll-screen-center, 1, 0, 0)
+             "set-mode" ("vim-visual-line", permanent) };
+  bind "period" { "movement" (scroll-screen-center, 1, 1, 0)
+                  "set-mode" ("vim-visual-line", permanent) };
+
+  bind "t" { "movement" (scroll-screen-top, 1, 0, 0)
+             "set-mode" ("vim-visual-line", permanent) };
+  bind "Return" { "movement" (scroll-screen-top, 1, 1, 0)
+                  "set-mode" ("vim-visual-line", permanent) };
+  bind "KP_Enter" { "movement" (scroll-screen-top, 1, 1, 0)
+                    "set-mode" ("vim-visual-line", permanent) };
+
+  bind "b" { "movement" (scroll-screen-bottom, 1, 0, 0)
+             "set-mode" ("vim-visual-line", permanent) };
+  bind "minus" { "movement" (scroll-screen-bottom, 1, 1, 0)
+                 "set-mode" ("vim-visual-line", permanent) };
+}
+
+@binding-set builder-vim-source-view-normal-y
+{
+  bind "y" { "save-insert-mark" ()
+             "movement" (first-char, 0, 1, 0)
+             "movement" (next-line, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (expand)
+             "clear-selection" ()
+             "restore-insert-mark" () };
+
+  bind "j" { "save-insert-mark" ()
+             "movement" (line-end, 0, 1, 0)
+             "movement" (first-char, 0, 1, 0)
+             "movement" (next-line, 1, 0, 0)
+             "movement" (next-line, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (expand)
+             "clear-selection" ()
+             "restore-insert-mark" () };
+
+  bind "k" { "movement" (line-end, 0, 0, 0)
+             "movement" (previous-line, 1, 0, 0)
+             "movement" (previous-line, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (expand)
+             "clear-selection" ()
+             "movement" (first-nonspace-char, 0, 1, 0) };
+
+  bind "w" { "save-insert-mark" ()
+             "movement" (next-word-start, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (expand)
+             "restore-insert-mark" ()
+             "clear-count" () };
+
+  bind "<shift>w" { "save-insert-mark" ()
+                    "movement" (next-full-word-start, 1, 1, 1)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (expand)
+                    "restore-insert-mark" ()
+                    "clear-count" () };
+
+  bind "f" { "begin-macro" ()
+             "save-insert-mark" ()
+             "save-command" ()
+             "capture-modifier" ()
+             "save-search-char" ()
+             "movement" (next-match-modifier, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (expand)
+             "clear-modifier" ()
+             "restore-insert-mark" ()
+             "clear-count" ()
+             "end-macro" () };
+
+  bind "t" { "begin-macro" ()
+             "save-insert-mark" ()
+             "save-command" ()
+             "capture-modifier" ()
+             "save-search-char" ()
+             "movement" (next-match-modifier, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (expand)
+             "clear-modifier" ()
+             "restore-insert-mark" ()
+             "clear-count" ()
+             "end-macro" () };
+
+  bind "dollar" { "begin-macro" ()
+                  "save-insert-mark" ()
+                  "movement" (line-end, 1, 1, 1)
+                  "copy-clipboard-extended" ()
+                  "selection-theatric" (expand)
+                  "restore-insert-mark" ()
+                  "clear-count" ()
+                  "end-macro" () };
+}
+
+@binding-set builder-vim-source-view-normal-g
+{
+  bind "<shift>i" { "set-mode" ("vim-insert", permanent)
+                    "movement" (first-char, 0, 1, 0) };
+  bind "d" { "goto-definition" () };
+  bind "e" { "movement" (previous-word-end, 0, 1, 1) };
+  bind "<shift>e" { "movement" (previous-full-word-end, 0, 1, 1) };
+  bind "g" { "movement" (first-line, 0, 1, 0 ) };
+  bind "j" { "movement" (next-line, 0, 1, 1) };
+
+  /* todo: this should actually be screen middle. this does middle of the text width */
+  bind "m" { "movement" (middle-char, 0, 1, 0) };
+
+  bind "u"        { "set-mode" ("vim-normal-g-u", transient) };
+  bind "<shift>u" { "set-mode" ("vim-normal-g-u", transient) };
+
+  /* cycle "tabs" */
+  bind "<shift>t" { "action" ("frame", "previous-page", "") };
+  bind "t" { "action" ("frame", "next-page", "") };
+}
+
+@binding-set builder-vim-source-view-normal-g-u
+{
+  bind "u" { "begin-macro" ()
+             "movement" (first-char, 0, 1, 0)
+             "movement" (next-line, 1, 0, 1)
+             "swap-selection-bounds" ()
+             "selection-theatric" (expand)
+             "change-case" (lower)
+             "clear-selection" ()
+             "movement" (first-nonspace-char, 0, 1, 0)
+             "end-macro" () };
+
+  bind "<shift>u" { "begin-macro" ()
+                    "movement" (first-char, 0, 1, 0)
+                    "movement" (next-line, 1, 0, 1)
+                    "swap-selection-bounds" ()
+                    "selection-theatric" (expand)
+                    "change-case" (upper)
+                    "clear-selection" ()
+                    "movement" (first-nonspace-char, 0, 1, 0)
+                    "end-macro" () };
+}
+
+@binding-set builder-vim-source-view-normal-d-g
+{
+  bind "e" { "begin-macro" ()
+             "movement" (previous-word-end, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" () };
+  bind "<shift>e" { "begin-macro" ()
+                    "movement" (previous-full-word-end, 1, 1, 1)
+                    "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "end-macro" () };
+  bind "g" { "begin-macro" ()
+             "movement" (first-line, 1, 1, 0)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" () };
+  bind "k" { "begin-macro" ()
+             "movement" (next-char, 1, 1, 0)
+             "movement" (previous-line, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" () };
+  bind "j" { "begin-macro" ()
+             "movement" (next-char, 1, 1, 0)
+             "movement" (next-line, 1, 1, 1)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" () };
+  bind "m" { "begin-macro" ()
+             "movement" (middle-char, 1, 1, 0)
+             "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" () };
+}
+
+@binding-set builder-vim-source-view-normal-d-i
+{
+  bind "p" { "begin-macro" ()
+             "movement" (paragraph-start, 1, 1, 1)
+             "movement" (first-char, 1, 1, 0)
+             "swap-selection-bounds" ()
+             "movement" (paragraph-end, 1, 1, 1)
+             "movement" (last-char, 1, 1, 0)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" ()
+             "end-macro" () };
+
+  /* diw */
+  bind "w" { "begin-macro" ()
+             "set-mode" ("vim-normal", permanent)
+             "movement" (previous-word-end, 0, 1, 1)
+             "movement" (next-word-start, 0, 1, 0)
+             "movement" (next-word-end, 1, 0, 1)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" () };
+
+  /* diW */
+  bind "<shift>w" { "begin-macro" ()
+                    "set-mode" ("vim-normal", permanent)
+                    "movement" (previous-full-word-end, 0, 1, 1)
+                    "movement" (next-full-word-start, 0, 1, 0)
+                    "movement" (next-full-word-end, 1, 0, 1)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" () };
+
+  /* di( , di) , dib */
+  bind "parenleft" { "begin-macro" ()
+                     "select-inner" ("(", ")", 1, 0)
+                     "set-mode" ("vim-normal", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  bind "parenright" { "begin-macro" ()
+                      "select-inner" ("(", ")", 1, 0)
+                      "set-mode" ("vim-normal", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+  bind "b" { "begin-macro" ()
+             "select-inner" ("(", ")", 1, 0)
+             "set-mode" ("vim-normal", permanent)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" () };
+
+  /* di[ and di] */
+  bind "bracketleft" { "begin-macro" ()
+                       "select-inner" ("[", "]", 1, 0)
+                       "set-mode" ("vim-normal", permanent)
+                       "copy-clipboard-extended" ()
+                       "selection-theatric" (shrink)
+                       "delete-selection" () };
+
+  bind "bracketright" { "begin-macro" ()
+                        "select-inner" ("[", "]", 1, 0)
+                        "set-mode" ("vim-normal", permanent)
+                        "copy-clipboard-extended" ()
+                        "selection-theatric" (shrink)
+                        "delete-selection" () };
+
+  /* di{ , di} , diB */
+  bind "braceleft" { "begin-macro" ()
+                     "select-inner" ("{", "}", 1, 0)
+                     "set-mode" ("vim-normal", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  bind "braceright" { "begin-macro" ()
+                      "select-inner" ("{", "}", 1, 0)
+                      "set-mode" ("vim-normal", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+  bind "<shift>b" { "begin-macro" ()
+                    "select-inner" ("{", "}", 1, 0)
+                    "set-mode" ("vim-normal", permanent)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" () };
+
+  /* di< and di> */
+    bind "less" { "begin-macro" ()
+                  "select-inner" ("<", ">", 1, 0)
+                  "set-mode" ("vim-normal", permanent)
+                  "copy-clipboard-extended" ()
+                  "selection-theatric" (shrink)
+                  "delete-selection" () };
+
+    bind "greater" { "begin-macro" ()
+                     "select-inner" ("<", ">", 1, 0)
+                     "set-mode" ("vim-normal", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  /* di" di' di` */
+    bind "quotedbl" { "begin-macro" ()
+                      "select-inner" ("\"", "\"", 1, 1)
+                      "set-mode" ("vim-normal", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+    bind "apostrophe" { "begin-macro" ()
+                        "select-inner" ("'", "'", 1, 1)
+                        "set-mode" ("vim-normal", permanent)
+                        "copy-clipboard-extended" ()
+                        "selection-theatric" (shrink)
+                        "delete-selection" () };
+
+    bind "grave" { "begin-macro" ()
+                   "select-inner" ("`", "`", 1, 1)
+                   "set-mode" ("vim-normal", permanent)
+                   "copy-clipboard-extended" ()
+                   "selection-theatric" (shrink)
+                   "delete-selection" () };
+
+  /* dit */
+    bind "t" { "begin-macro" ()
+               "select-tag" (1)
+               "set-mode" ("vim-normal", permanent)
+               "copy-clipboard-extended" ()
+               "selection-theatric" (shrink)
+               "delete-selection" () };
+}
+
+@binding-set builder-vim-source-view-normal-d-a
+{
+  /* da( , da) , dab */
+  bind "parenleft" { "begin-macro" ()
+                     "select-inner" ("(", ")", 0, 0)
+                     "set-mode" ("vim-normal", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  bind "parenright" { "begin-macro" ()
+                      "select-inner" ("(", ")", 0, 0)
+                      "set-mode" ("vim-normal", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+  bind "b" { "begin-macro" ()
+             "select-inner" ("(", ")", 0, 0)
+             "set-mode" ("vim-normal", permanent)
+             "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" () };
+
+  /* da[ and da] */
+  bind "bracketleft" { "begin-macro" ()
+                       "select-inner" ("[", "]", 0, 0)
+                       "set-mode" ("vim-normal", permanent)
+                       "copy-clipboard-extended" ()
+                       "selection-theatric" (shrink)
+                       "delete-selection" () };
+
+  bind "bracketright" { "begin-macro" ()
+                        "select-inner" ("[", "]", 0, 0)
+                        "set-mode" ("vim-normal", permanent)
+                        "copy-clipboard-extended" ()
+                        "selection-theatric" (shrink)
+                        "delete-selection" () };
+
+  /* da{ , da} , daB */
+  bind "braceleft" { "begin-macro" ()
+                     "select-inner" ("{", "}", 0, 0)
+                     "set-mode" ("vim-normal", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  bind "braceright" { "begin-macro" ()
+                      "select-inner" ("{", "}", 0, 0)
+                      "set-mode" ("vim-normal", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+  bind "<shift>b" { "begin-macro" ()
+                    "select-inner" ("{", "}", 0, 0)
+                    "set-mode" ("vim-normal", permanent)
+                    "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" () };
+
+  /* da< and da> */
+    bind "less" { "begin-macro" ()
+                  "select-inner" ("<", ">", 0, 0)
+                  "set-mode" ("vim-normal", permanent)
+                  "copy-clipboard-extended" ()
+                  "selection-theatric" (shrink)
+                  "delete-selection" () };
+
+    bind "greater" { "begin-macro" ()
+                     "select-inner" ("<", ">", 0, 0)
+                     "set-mode" ("vim-normal", permanent)
+                     "copy-clipboard-extended" ()
+                     "selection-theatric" (shrink)
+                     "delete-selection" () };
+
+  /* da" da' da` */
+    bind "quotedbl" { "begin-macro" ()
+                      "select-inner" ("\"", "\"", 0, 1)
+                      "set-mode" ("vim-normal", permanent)
+                      "copy-clipboard-extended" ()
+                      "selection-theatric" (shrink)
+                      "delete-selection" () };
+
+    bind "apostrophe" { "begin-macro" ()
+                        "select-inner" ("'", "'", 0, 1)
+                        "set-mode" ("vim-normal", permanent)
+                        "copy-clipboard-extended" ()
+                        "selection-theatric" (shrink)
+                        "delete-selection" () };
+
+    bind "grave" { "begin-macro" ()
+                   "select-inner" ("`", "`", 0, 1)
+                   "set-mode" ("vim-normal", permanent)
+                   "copy-clipboard-extended" ()
+                   "selection-theatric" (shrink)
+                   "delete-selection" () };
+
+  /* dat */
+    bind "t" { "begin-macro" ()
+               "select-tag" (0)
+               "set-mode" ("vim-normal", permanent)
+               "copy-clipboard-extended" ()
+               "selection-theatric" (shrink)
+               "delete-selection" () };
+}
+
+@binding-set builder-vim-source-view-visual-g
+{
+  bind "e" { "movement" (previous-word-end, 1, 1, 0)
+             "set-mode" ("vim-visual", permanent) };
+  bind "<shift>e" { "movement" (previous-full-word-end, 1, 1, 0)
+                    "set-mode" ("vim-visual", permanent) };
+  bind "g" { "movement" (first-line, 1, 1, 0)
+             "set-mode" ("vim-visual", permanent) };
+  bind "j" { "movement" (next-line, 1, 1, 0)
+             "set-mode" ("vim-visual", permanent) };
+  bind "m" { "movement" (middle-char, 1, 1, 0)
+             "set-mode" ("vim-visual", permanent) };
+}
+
+@binding-set builder-vim-source-view-normal-q
+{
+  /* this is wrong, you can store in any character for recording and then
+   * replay with @char.
+   */
+  bind "q" { "begin-macro" () };
+}
+
+@binding-set builder-vim-source-view-normal-ctrl-w
+{
+  bind "v" { "action" ("frame", "open-in-new-frame", "''") "grab_focus" () };
+  bind "<ctrl>v" { "action" ("frame", "open-in-new-frame", "''") "grab_focus" () };
+
+  bind "c" { "action" ("frame", "close-page", "") };
+
+  bind "s" { "action" ("frame", "split-page", "''") };
+
+  bind "w" { "action" ("grid", "focus-neighbor", "0") };
+  bind "<ctrl>w" { "action" ("grid", "focus-neighbor", "0") };
+
+  bind "l" { "action" ("grid", "focus-neighbor", "5") };
+  bind "Right" { "action" ("grid", "focus-neighbor", "5") };
+
+  bind "h" { "action" ("grid", "focus-neighbor", "4") };
+  bind "Left" { "action" ("grid", "focus-neighbor", "4") };
+
+  bind "j" { "action" ("grid", "focus-neighbor", "3") };
+  bind "Down" { "action" ("grid", "focus-neighbor", "3") };
+
+  bind "k" { "action" ("grid", "focus-neighbor", "2") };
+  bind "Up" { "action" ("grid", "focus-neighbor", "2") };
+}
+
+@binding-set builder-vim-source-view-visual-line-g
+{
+  /* XXX: This is a bit of a hack to just reuse the special
+   *      line handling case to wrap around our first selected
+   *      line. Otherwise, we lose that line. This type of stuff
+   *      really belongs in a special case keybinding context
+   *      once that subsystem lands.
+   */
+  bind "g" { "clear-count" ()
+             "append-to-count" (1)
+             "append-to-count" (0)
+             "append-to-count" (0)
+             "append-to-count" (0)
+             "append-to-count" (0)
+             "append-to-count" (0)
+             "append-to-count" (0)
+             "append-to-count" (0)
+             "movement" (previous-line, 1, 0, 1)
+             "set-mode" ("vim-visual-line", permanent) };
+  bind "j" { "movement" (next-line, 1, 1, 0)
+             "set-mode" ("vim-visual-line", permanent) };
+  bind "k" { "movement" (next-line, 1, 1, 0)
+             "set-mode" ("vim-visual-line", permanent) };
+
+  bind "q" { "format-selection" () };
+}
+
+@binding-set builder-vim-source-view-insert
+{
+  bind "<ctrl>u" { "movement" (line-chars, 1, 1, 0)
+                   "delete-selection" () };
+  bind "<ctrl>w" { "movement" (previous-word-start, 1, 1, 0)
+                   "delete-selection" () };
+
+  bind "<ctrl>e" { "movement" (screen-up, 0, 0, 1) };
+  bind "<ctrl>y" { "movement" (screen-down, 0, 0, 1) };
+
+  bind "<ctrl>n" { "cycle-completion" (down) };
+  bind "<ctrl>p" { "cycle-completion" (up) };
+
+  /* raw keycode (to some degree) */
+  bind "<ctrl>v" { "capture-modifier" ()
+                   "insert-modifier" (0)
+                   "clear-modifier" () };
+
+  bind "Escape" { "end-macro" ()
+                  "set-overwrite" (0)
+                  "clear-count" ()
+                  "clear-selection" ()
+                  "clear-snippets" ()
+                  "hide-completion" ()
+                  "movement" (previous-char, 0, 1, 0)
+                  "set-mode" ("vim-normal", permanent) 
+                  "remove-cursors" () };
+  bind "<ctrl>bracketleft" { "end-macro" ()
+                             "set-overwrite" (0)
+                             "clear-count" ()
+                             "clear-selection" ()
+                             "clear-snippets" ()
+                             "hide-completion" ()
+                             "movement" (previous-char, 0, 1, 0)
+                             "set-mode" ("vim-normal", permanent) };
+
+  /* Add back emoji */
+  bind "<ctrl>semicolon" { "insert-emoji" () };
+}
+
+@binding-set builder-vim-source-view-visual-with-count
+{
+  bind "0" { "append-to-count" (0)
+             "set-mode" ("vim-visual-with-count", transient) };
+  bind "KP_0" { "append-to-count" (0)
+                "set-mode" ("vim-visual-with-count", transient) };
+  bind "percent" { "movement" (line-percentage, 1, 1, 1)
+                   "set-mode" ("vim-visual-with-count", transient) };
+}
+
+@binding-set builder-vim-source-view-visual
+{
+  bind "i" { "set-mode" ("vim-visual-i", transient) };
+  bind "a" { "set-mode" ("vim-visual-a", transient) };
+
+  bind "colon" { "action" ("win", "reveal-command-bar", "") };
+
+  bind "percent" { "move-to-matching-bracket" (1) };
+
+  bind "1" { "append-to-count" (1)
+             "set-mode" ("vim-visual-with-count", transient) };
+  bind "2" { "append-to-count" (2)
+             "set-mode" ("vim-visual-with-count", transient) };
+  bind "3" { "append-to-count" (3)
+             "set-mode" ("vim-visual-with-count", transient) };
+  bind "4" { "append-to-count" (4)
+             "set-mode" ("vim-visual-with-count", transient) };
+  bind "5" { "append-to-count" (5)
+             "set-mode" ("vim-visual-with-count", transient) };
+  bind "6" { "append-to-count" (6)
+             "set-mode" ("vim-visual-with-count", transient) };
+  bind "7" { "append-to-count" (7)
+             "set-mode" ("vim-visual-with-count", transient) };
+  bind "8" { "append-to-count" (8)
+             "set-mode" ("vim-visual-with-count", transient) };
+  bind "9" { "append-to-count" (9)
+             "set-mode" ("vim-visual-with-count", transient) };
+
+  bind "x" { "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" ()
+             "set-mode" ("vim-normal", permanent) };
+
+  bind "c" { "copy-clipboard-extended" ()
+             "selection-theatric" (shrink)
+             "delete-selection" ()
+             "set-mode" ("vim-insert", permanent) };
+
+  bind "d" { "copy-clipboard-extended" ()
+             "delete-selection" ()
+             "end-macro" ()
+             "set-mode" ("vim-normal", permanent) };
+
+  bind "<shift>x" { "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "end-macro" ()
+                    "set-mode" ("vim-normal", permanent) };
+
+  bind "h" { "movement" (previous-char, 1, 1, 1) };
+  bind "l" { "movement" (next-char, 1, 1, 1) };
+  bind "k" { "movement" (previous-line, 1, 1, 1) };
+  bind "j" { "movement" (next-line, 1, 1, 1) };
+
+  bind "Left" { "movement" (previous-char, 1, 1, 1) };
+  bind "Right" { "movement" (next-char, 1, 1, 1) };
+  bind "Up" { "movement" (previous-line, 1, 1, 1) };
+  bind "Down" { "movement" (next-line, 1, 1, 1) };
+
+  bind "BackSpace" { "movement" (previous-offset, 1, 1, 1)
+                     "clear-count" () };
+  bind "space" { "movement" (next-offset, 1, 1, 1)
+                 "clear-count" () };
+
+  bind "equal" { "reindent" ()
+                 "set-mode" ("vim-normal", permanent) };
+
+  /* TODO: we really want to rollback the macro here */
+  bind "y" { "copy-clipboard-extended" ()
+             "selection-theatric" (expand)
+             "clear-selection" ()
+             "end-macro" ()
+             "set-mode" ("vim-normal", permanent) };
+
+  bind "slash" { "set-search-text" ("", 1)
+                 "action" ("editor-page", "find", "") };
+
+  bind "e" { "movement" (next-word-end, 1, 0, 1) };
+  bind "<shift>e" { "movement" (next-full-word-end, 1, 0, 1) };
+
+  bind "w" { "movement" (next-word-start, 1, 1, 1) };
+  bind "<shift>w" { "movement" (next-full-word-start, 1, 1, 1) };
+  bind "b" { "movement" (previous-word-start, 1, 1, 1) };
+  bind "<shift>b" { "movement" (previous-full-word-start, 1, 1, 1) };
+
+  bind "n" { "move-search" (tab-forward, 1, 0, 1, 1, 0) };
+  bind "<shift>n" { "move-search" (tab-backward, 1, 0, 0, 1, 0) };
+
+  bind "numbersign" { "save-insert-mark" ()
+                      "movement" (previous-word-end, 0, 1, 1)
+                      "movement" (next-word-start, 0, 1, 0)
+                      "movement" (next-word-end, 1, 0, 1)
+                      "set-search-text" ("", 1)
+                      "restore-insert-mark" ()
+                      "move-search" (up, 1, 0, 0, 1, 1) };
+
+  bind "asterisk" { "save-insert-mark" ()
+                    "movement" (previous-word-end, 0, 1, 1)
+                    "movement" (next-word-start, 0, 1, 0)
+                    "movement" (next-word-end, 1, 0, 1)
+                    "set-search-text" ("", 1)
+                    "restore-insert-mark" ()
+                    "move-search" (down, 1, 0, 1, 1, 1) };
+
+bind "KP_Multiply" { "save-insert-mark" ()
+                     "movement" (previous-word-end, 0, 1, 1)
+                     "movement" (next-word-start, 0, 1, 0)
+                     "movement" (next-word-end, 1, 0, 1)
+                     "set-search-text" ("", 1)
+                     "restore-insert-mark" ()
+                     "move-search" (down, 1, 0, 1, 1, 1) };
+
+  bind "<ctrl>b" { "movement" (page-up, 1, 0, 1) };
+  bind "Page_Up" { "movement" (page-up, 1, 0, 1) };
+  bind "<ctrl>f" { "movement" (page-down, 1, 0, 1) };
+  bind "Page_Down" { "movement" (page-down, 1, 0, 1) };
+  bind "<ctrl>u" { "movement" (half-page-up, 1, 0, 1) };
+  bind "<ctrl>d" { "movement" (half-page-down, 1, 0, 1) };
+
+  bind "greater" { "indent-selection" (1)
+                   "movement" (first-nonspace-char, 0, 1, 0)
+                   "end-macro" ()
+                   "set-mode" ("vim-normal", permanent) };
+  bind "less" { "indent-selection" (-1)
+                "movement" (first-nonspace-char, 0, 1, 0)
+                "end-macro" ()
+                "set-mode" ("vim-normal", permanent) };
+
+  bind "0" { "movement" (first-char, 1, 1, 0) };
+  bind "KP_0" { "movement" (first-char, 1, 1, 0) };
+  bind "Home" { "movement" (first-char, 1, 1, 0) };
+  bind "asciicircum" { "movement" (first-nonspace-char, 1, 0, 0) };
+  bind "dollar" { "movement" (last-char, 1, 0, 0) };
+  bind "End" { "movement" (last-char, 1, 0, 0) };
+  bind "bar" { "movement" (nth-char, 1, 1, 1) };
+
+  bind "<shift>h" { "movement" (screen-top, 1, 0, 0) };
+  bind "<shift>m" { "movement" (screen-middle, 1, 0, 0) };
+  bind "<shift>l" { "movement" (screen-bottom, 1, 0, 0) };
+
+  bind "braceleft" { "movement" (paragraph-start, 1, 1, 1) };
+  bind "braceright" { "movement" (paragraph-end, 1, 1, 1) };
+
+  bind "parenleft" { "movement" (sentence-start, 1, 1, 1) };
+  bind "parenright" { "movement" (sentence-end, 1, 1, 1) };
+
+  bind "<ctrl>e" { "movement" (screen-up, 1, 0, 1) };
+  bind "<ctrl>y" { "movement" (screen-down, 1, 0, 1) };
+
+  bind "<shift>j" { "join-lines" ()
+                    "selection-theatric" (expand)
+                    "clear-selection" ()
+                    "movement" (last-char, 0, 1, 0)
+                    "end-macro" ()
+                    "set-mode" ("vim-normal", permanent) };
+
+  bind "g" { "set-mode" ("vim-visual-g", transient) };
+  bind "z" { "set-mode" ("vim-visual-z", transient) };
+
+  bind "f" { "save-command" ()
+             "capture-modifier" ()
+             "save-search-char" ()
+             "movement" (next-match-modifier, 1, 0, 1)
+             "clear-modifier" () };
+  bind "t" { "save-command" ()
+             "capture-modifier" ()
+             "save-search-char" ()
+             "movement" (next-match-modifier, 1, 1, 1)
+             "clear-modifier" () };
+
+  bind "<shift>f" { "save-command" ()
+                    "capture-modifier" ()
+                    "save-search-char" ()
+                    "movement" (previous-match-modifier, 1, 1, 1)
+                    "clear-modifier" () };
+  bind "<shift>t" { "save-command" ()
+                    "capture-modifier" ()
+                    "save-search-char" ()
+                    "movement" (previous-match-modifier, 1, 0, 1)
+                    "clear-modifier" () };
+  bind "comma" { "movement" (previous-match-search-char, 1, 0, 1)
+                 "clear-count" () };
+  bind "semicolon" { "movement" (next-match-search-char, 1, 0, 1)
+                     "clear-count" () };
+
+  bind "asciitilde" { "selection-theatric" (expand)
+                      "change-case" (toggle)
+                      "clear-selection" ()
+                      "end-macro" ()
+                      "set-mode" ("vim-normal", permanent) };
+  bind "u" { "selection-theatric" (expand)
+             "change-case" (lower)
+             "clear-selection" ()
+             "end-macro" ()
+             "set-mode" ("vim-normal", permanent) };
+  bind "<shift>u" { "selection-theatric" (expand)
+                    "change-case" (upper)
+                    "clear-selection" ()
+                    "end-macro" ()
+                    "set-mode" ("vim-normal", permanent) };
+
+  bind "plus" { "begin-macro" ()
+                "movement" (next-line, 1, 0, 1)
+                "movement" (first-nonspace-char, 1, 1, 0)
+                "clear-count" () };
+  bind "KP_Enter" { "begin-macro" ()
+                    "movement" (next-line, 1, 0, 1)
+                    "movement" (first-nonspace-char, 1, 1, 0)
+                    "clear-count" () };
+  bind "<shift>KP_Enter" { "begin-macro" ()
+                           "movement" (next-line, 1, 0, 1)
+                           "movement" (first-nonspace-char, 1, 1, 0)
+                           "clear-count" () };
+  bind "Return" { "begin-macro" ()
+                  "movement" (next-line, 1, 0, 1)
+                  "movement" (first-nonspace-char, 1, 1, 0)
+                  "clear-count" () };
+  bind "<shift>Return" { "begin-macro" ()
+                         "movement" (next-line, 1, 0, 1)
+                         "movement" (first-nonspace-char, 1, 1, 0)
+                         "clear-count" () };
+
+  /* this is a count - 1 motion, we handle this specific case in C code */
+  bind "underscore" { "begin-macro" ()
+                      "movement" (next-line, 1, 0, 1)
+                      "movement" (first-nonspace-char, 1, 1, 0)
+                      "clear-count" () };
+}
+
+@binding-set builder-vim-source-view-visual-i
+{
+  /* vi( , vi) , vib */
+  bind "parenleft" { "begin-macro" ()
+                     "select-inner" ("(", ")", 1, 0)
+                     "set-mode" ("vim-visual", permanent) };
+
+  bind "parenright" { "begin-macro" ()
+                      "select-inner" ("(", ")", 1, 0)
+                      "set-mode" ("vim-visual", permanent) };
+
+  bind "b" { "begin-macro" ()
+             "select-inner" ("(", ")", 1, 0)
+             "set-mode" ("vim-visual", permanent) };
+
+  /* vi[ and vi] */
+  bind "bracketleft" { "begin-macro" ()
+                       "select-inner" ("[", "]", 1, 0)
+                       "set-mode" ("vim-visual", permanent) };
+
+  bind "bracketright" { "begin-macro" ()
+                        "select-inner" ("[", "]", 1, 0)
+                        "set-mode" ("vim-visual", permanent) };
+
+  /* vi{ , vi} , viB */
+  bind "braceleft" { "begin-macro" ()
+                     "select-inner" ("{", "}", 1, 0)
+                     "set-mode" ("vim-visual", permanent) };
+
+  bind "braceright" { "begin-macro" ()
+                      "select-inner" ("{", "}", 1, 0)
+                      "set-mode" ("vim-visual", permanent) };
+
+  bind "<shift>b" { "begin-macro" ()
+                    "select-inner" ("{", "}", 1, 0)
+                    "set-mode" ("vim-visual", permanent) };
+
+  /* vi< and vi> */
+  bind "less" { "begin-macro" ()
+                "select-inner" ("<", ">", 1, 0)
+                "set-mode" ("vim-visual", permanent) };
+
+  bind "greater" { "begin-macro" ()
+                   "select-inner" ("<", ">", 1, 0)
+                   "set-mode" ("vim-visual", permanent) };
+
+  /* vi" vi' vi` */
+  bind "quotedbl" { "begin-macro" ()
+                    "select-inner" ("\"", "\"", 1, 1)
+                    "set-mode" ("vim-visual", permanent) };
+
+  bind "apostrophe" { "begin-macro" ()
+                        "select-inner" ("'", "'", 1, 1)
+                        "set-mode" ("vim-visual", permanent) };
+
+  bind "grave" { "begin-macro" ()
+                 "select-inner" ("`", "`", 1, 1)
+                 "set-mode" ("vim-visual", permanent) };
+
+   /* vit */
+  bind "t" { "begin-macro" ()
+              "select-tag" (1)
+              "set-mode" ("vim-visual", permanent) };
+}
+
+@binding-set builder-vim-source-view-visual-a
+{
+  /* va( , va) , vab */
+  bind "parenleft" { "begin-macro" ()
+                     "select-inner" ("(", ")", 0, 0)
+                     "set-mode" ("vim-visual", permanent) };
+
+  bind "parenright" { "begin-macro" ()
+                      "select-inner" ("(", ")", 0, 0)
+                      "set-mode" ("vim-visual", permanent) };
+
+  bind "b" { "begin-macro" ()
+             "select-inner" ("(", ")", 0, 0)
+             "set-mode" ("vim-visual", permanent) };
+
+  /* va[ and va] */
+  bind "bracketleft" { "begin-macro" ()
+                       "select-inner" ("[", "]", 0, 0)
+                       "set-mode" ("vim-visual", permanent) };
+
+  bind "bracketright" { "begin-macro" ()
+                        "select-inner" ("[", "]", 0, 0)
+                        "set-mode" ("vim-visual", permanent) };
+
+  /* va{ , va} , vaB */
+  bind "braceleft" { "begin-macro" ()
+                     "select-inner" ("{", "}", 0, 0)
+                     "set-mode" ("vim-visual", permanent) };
+
+  bind "braceright" { "begin-macro" ()
+                      "select-inner" ("{", "}", 0, 0)
+                      "set-mode" ("vim-visual", permanent) };
+
+  bind "<shift>b" { "begin-macro" ()
+                    "select-inner" ("{", "}", 0, 0)
+                    "set-mode" ("vim-visual", permanent) };
+
+  /* va< and va> */
+    bind "less" { "begin-macro" ()
+                  "select-inner" ("<", ">", 0, 0)
+                  "set-mode" ("vim-visual", permanent) };
+
+    bind "greater" { "begin-macro" ()
+                     "select-inner" ("<", ">", 0, 0)
+                     "set-mode" ("vim-visual", permanent) };
+
+  /* va" va' va` */
+    bind "quotedbl" { "begin-macro" ()
+                      "select-inner" ("\"", "\"", 0, 1)
+                      "set-mode" ("vim-visual", permanent) };
+
+    bind "apostrophe" { "begin-macro" ()
+                        "select-inner" ("'", "'", 0, 1)
+                        "set-mode" ("vim-visual", permanent) };
+
+    bind "grave" { "begin-macro" ()
+                   "select-inner" ("`", "`", 0, 1)
+                   "set-mode" ("vim-visual", permanent) };
+
+  /* vat */
+    bind "t" { "begin-macro" ()
+               "select-tag" (0)
+               "set-mode" ("vim-visual", permanent) };
+}
+
+@binding-set builder-vim-source-view-visual-line-with-count
+{
+  bind "0" { "append-to-count" (0)
+             "set-mode" ("vim-visual-line-with-count", transient) };
+  bind "KP_0" { "append-to-count" (0)
+                "set-mode" ("vim-visual-line-with-count", transient) };
+  bind "percent" { "movement" (line-percentage, 0, 1, 1)
+                   "set-mode" ("vim-visual-line-with-count", transient) };
+}
+
+@binding-set builder-vim-source-view-visual-line
+{
+  bind "colon" { "action" ("win", "reveal-command-bar", "") };
+
+  bind "1" { "append-to-count" (1)
+             "set-mode" ("vim-visual-line-with-count", transient) };
+  bind "2" { "append-to-count" (2)
+             "set-mode" ("vim-visual-line-with-count", transient) };
+  bind "3" { "append-to-count" (3)
+             "set-mode" ("vim-visual-line-with-count", transient) };
+  bind "4" { "append-to-count" (4)
+             "set-mode" ("vim-visual-line-with-count", transient) };
+  bind "5" { "append-to-count" (5)
+             "set-mode" ("vim-visual-line-with-count", transient) };
+  bind "6" { "append-to-count" (6)
+             "set-mode" ("vim-visual-line-with-count", transient) };
+  bind "7" { "append-to-count" (7)
+             "set-mode" ("vim-visual-line-with-count", transient) };
+  bind "8" { "append-to-count" (8)
+             "set-mode" ("vim-visual-line-with-count", transient) };
+  bind "9" { "append-to-count" (9)
+             "set-mode" ("vim-visual-line-with-count", transient) };
+
+  bind "k" { "movement" (previous-line, 1, 0, 1) };
+  bind "j" { "movement" (next-line, 1, 0, 1) };
+
+  bind "g" { "set-mode" ("vim-visual-line-g", transient ) };
+
+  /* just to be nice */
+  bind "h" { "end-macro" ()
+             "clear-selection" ()
+             "set-mode" ("vim-normal", permanent) };
+  bind "l" { "end-macro" ()
+             "clear-selection" ()
+             "set-mode" ("vim-normal", permanent) };
+
+  bind "Up" { "movement" (previous-line, 1, 0, 1) };
+  bind "Down" { "movement" (next-line, 1, 0, 1) };
+
+  bind "equal" { "reindent" ()
+                 "set-mode" ("vim-normal", permanent) };
+
+  bind "z" { "set-mode" ("vim-visual-line-z", transient) };
+
+  bind "<shift>g" { "movement" (nth-line, 1, 0, 1)
+                    "movement" (last-char, 1, 0, 0) };
+
+  bind "x"        { "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "end-macro" ()
+                    "set-mode" ("vim-normal", permanent) };
+
+  bind "d"        { "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "end-macro" ()
+                    "set-mode" ("vim-normal", permanent) };
+
+  bind "c"        { "copy-clipboard-extended" ()
+                    "selection-theatric" (shrink)
+                    "delete-selection" ()
+                    "set-mode" ("vim-insert", permanent)
+                    "insert-at-cursor" ("\n")
+                    "move-cursor" (display-lines, -1, 0)
+                    "reindent" () };
+
+  bind "<shift>x" { "copy-clipboard-extended" ()
+                    "delete-selection" ()
+                    "delete-from-cursor" (chars, 1)
+                    "end-macro" ()
+                    "set-mode" ("vim-normal", permanent) };
+
+  /* TODO: this should actually cancel the macro */
+  bind "y"        { "copy-clipboard-extended" ()
+                    "selection-theatric" (expand)
+                    "clear-selection" ()
+                    "end-macro" ()
+                    "set-mode" ("vim-normal", permanent) };
+
+  /* TODO: this should actually cancel the macro */
+  bind "<shift>y" { "copy-clipboard-extended" ()
+                    "selection-theatric" (expand)
+                    "clear-selection" ()
+                    "set-mode" ("vim-normal", permanent) };
+
+  bind "<shift>j" { "join-lines" ()
+                    "selection-theatric" (expand)
+                    "clear-selection" ()
+                    "movement" (last-char, 0, 1, 0)
+                    "end-macro" ()
+                    "set-mode" ("vim-normal", permanent) };
+
+  bind "asciitilde" { "selection-theatric" (expand)
+                      "change-case" (toggle)
+                      "clear-selection" ()
+                      "end-macro" ()
+                      "set-mode" ("vim-normal", permanent) };
+
+  bind "<shift>u" { "selection-theatric" (expand)
+                    "change-case" (upper)
+                    "clear-selection" ()
+                    "end-macro" ()
+                    "set-mode" ("vim-normal", permanent) };
+  bind "u" { "selection-theatric" (expand)
+             "change-case" (lower)
+             "clear-selection" ()
+             "end-macro" ()
+             "set-mode" ("vim-normal", permanent) };
+
+  bind "braceleft" { "movement" (paragraph-start, 1, 1, 1)
+                     "movement" (last-char, 1, 1, 0) };
+  bind "braceright" { "movement" (paragraph-end, 1, 1, 1)
+                      "movement" (last-char, 1, 1, 0) };
+
+  bind "parenleft" { "movement" (sentence-start, 1, 1, 1)
+                     "movement" (last-char, 1, 1, 0) };
+  bind "parenright" { "movement" (sentence-end, 1, 1, 1)
+                      "movement" (last-char, 1, 1, 0) };
+
+  bind "greater" { "save-insert-mark" ()
+                   "indent-selection" (1)
+                   "clear-selection" ()
+                   "movement" (first-nonspace-char, 0, 1, 0)
+                   "restore-insert-mark" ()
+                   "movement" (first-nonspace-char, 0, 1, 0)
+                   "end-macro" ()
+                   "set-mode" ("vim-normal", permanent) };
+  bind "less" { "save-insert-mark" ()
+                "indent-selection" (-1)
+                "clear-selection" ()
+                "movement" (first-nonspace-char, 0, 1, 0)
+                "restore-insert-mark" ()
+                "movement" (first-nonspace-char, 0, 1, 0)
+                "end-macro" ()
+                "set-mode" ("vim-normal", permanent) };
+
+  bind "<ctrl>e" { "movement" (screen-up, 1, 0, 1) };
+  bind "<ctrl>y" { "movement" (screen-down, 1, 0, 1) };
+
+  /* page movements */
+  bind "<ctrl>b" { "movement" (page-up-lines, 1, 0, 1)
+                   "clear-count" () };
+  bind "Page_Up" { "movement" (page-up-lines, 1, 0, 1)
+                   "clear-count" () };
+  bind "<ctrl>f" { "movement" (page-down-lines, 1, 0, 1)
+                   "clear-count" () };
+  bind "Page_Down" { "movement" (page-down-lines, 1, 0, 1)
+                     "clear-count" () };
+  bind "<ctrl>u" { "movement" (half-page-up, 1, 0, 1)
+                   "clear-count" () };
+  bind "<ctrl>d" { "movement" (half-page-down, 1, 0, 1)
+                   "clear-count" () };
+}
+
+@binding-set builder-vim-source-view-visual-block
+{
+}
+
+@binding-set builder-vim-tree-view
+{
+  bind "<ctrl>n" { "move-cursor" (display-lines, 1) };
+  bind "<ctrl>p" { "move-cursor" (display-lines, -1) };
+  bind "slash" { "start-interactive-search" () };
+  bind "KP_Divide" { "start-interactive-search" () };
+}
+
+@binding-set builder-vim-list-box
+{
+  bind "<ctrl>n" { "move-cursor" (display-lines, 1) };
+  bind "<ctrl>p" { "move-cursor" (display-lines, -1) };
+}
+
+@binding-set builder-gb-project-tree-vim
+{
+  bind "colon" { "action" ("win", "reveal-command-bar", "") };
+}
+
+@binding-set builder-vim-workbench
+{
+  bind "<ctrl>period" { "action" ("win", "global-search", "") };
+}
+
+@binding-set builder-vim-global-search
+{
+  bind "Escape" { "unfocus" () };
+  bind "Return" { "activate-suggestion" () };
+
+  bind "Up" { "move-suggestion" (-1) };
+  bind "<ctrl>p" { "move-suggestion" (-1) };
+  bind "Page_Up" { "move-suggestion" (-10) };
+  bind "KP_Page_Up" { "move-suggestion" (-10) };
+  bind "Prior" { "move-suggestion" (-10) };
+
+  bind "Down" { "move-suggestion" (1) };
+  bind "<ctrl>n" { "move-suggestion" (1) };
+  bind "Page_Down" { "move-suggestion" (10) };
+  bind "KP_Page_Down" { "move-suggestion" (10) };
+  bind "Next" { "move-suggestion" (10) };
+}
+
+/*
+ * Sadly, this will draw from the middle, so it does not result in our
+ * cursor being over the actual character, but between two characters.
+ *
+ *   IdeSourceView {
+ *     -GtkWidget-cursor-aspect-ratio: 0.5;
+ *   }
+ */
+
+idesourceviewmode.default,
+idesourceviewmode.vim-normal {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-keep-mark-on-char: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal;
+}
+
+idesourceviewmode.vim-normal-with-count {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-with-count,
+                     builder-vim-source-view-normal;
+}
+
+idesourceviewmode.vim-normal-bracket {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-bracket;
+}
+
+idesourceviewmode.vim-normal-equal {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-equal;
+}
+
+idesourceviewmode.vim-normal-c {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-c;
+}
+
+idesourceviewmode.vim-c-with-count {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-default-mode: "vim-normal-c";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-c-with-count,
+                     builder-vim-source-view-normal-c;
+}
+
+idesourceviewmode.vim-normal-c-i {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-c-i;
+}
+
+idesourceviewmode.vim-normal-c-a {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-c-a;
+}
+
+idesourceviewmode.vim-normal-d {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Delete";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-d;
+}
+
+idesourceviewmode.vim-normal-d-g {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-d-g;
+}
+
+idesourceviewmode.vim-normal-d-i {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-d-i;
+}
+
+idesourceviewmode.vim-normal-d-a {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-d-a;
+}
+
+idesourceviewmode.vim-normal-g {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-g;
+}
+
+idesourceviewmode.vim-normal-g-u {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-g-u;
+}
+
+idesourceviewmode.vim-normal-q {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-q;
+}
+
+idesourceviewmode.vim-normal-indent {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-indent;
+}
+
+idesourceviewmode.vim-normal-ctrl-w {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "^w";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-ctrl-w;
+}
+
+idesourceviewmode.vim-normal-y {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-y;
+}
+
+idesourceviewmode.vim-normal-z {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-z;
+}
+
+idesourceviewmode.vim-normal-Z {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Z";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-normal-Z;
+}
+
+idesourceviewmode.vim-insert {
+  -IdeSourceViewMode-suppress-unbound: false;
+  -IdeSourceViewMode-block-cursor: false;
+  -IdeSourceViewMode-display-name: "Insert";
+
+  -gtk-key-bindings: builder-vim-source-view-insert,
+                     builder-vim-source-view;
+}
+
+idesourceviewmode.vim-replace {
+  -IdeSourceViewMode-suppress-unbound: false;
+  -IdeSourceViewMode-block-cursor: false;
+  -IdeSourceViewMode-display-name: "Replace";
+
+  -gtk-key-bindings: builder-vim-source-view-insert,
+                     builder-vim-source-view;
+}
+
+idesourceviewmode.vim-visual {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Visual";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-visual;
+}
+
+idesourceviewmode.vim-visual-i {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Visual";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-visual-i;
+}
+
+idesourceviewmode.vim-visual-a {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Visual";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-visual-a;
+}
+
+idesourceviewmode.vim-visual-with-count {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-default-mode: "vim-visual";
+  -IdeSourceViewMode-display-name: "Visual";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-visual-with-count,
+                     builder-vim-source-view-visual;
+}
+
+idesourceviewmode.vim-visual-g {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Visual";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-visual-g;
+}
+
+idesourceviewmode.vim-visual-z {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Visual";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-visual-z;
+}
+
+idesourceviewmode.vim-visual-line {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Visual Line";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-visual-line;
+}
+
+idesourceviewmode.vim-visual-line-with-count {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-default-mode: "vim-visual-line";
+  -IdeSourceViewMode-display-name: "Visual Line";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-visual-line-with-count,
+                     builder-vim-source-view-visual-line;
+}
+
+idesourceviewmode.vim-visual-line-g {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Visual Line";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-visual-line-g;
+}
+
+idesourceviewmode.vim-visual-line-z {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Visual Line";
+
+  -gtk-key-bindings: builder-vim-source-view,
+                     builder-vim-source-view-visual-line-z;
+}
+
+idesourceviewmode.vim-visual-block {
+  -IdeSourceViewMode-suppress-unbound: true;
+  -IdeSourceViewMode-block-cursor: true;
+  -IdeSourceViewMode-display-name: "Visual Block";
+
+  -gtk-key-bindings: builder-vim-source-view, builder-vim-source-view-visual-block;
+}
+
+treeview {
+  -gtk-key-bindings: builder-vim-tree-view;
+}
+
+treeview.project-tree {
+  -gtk-key-bindings: builder-vim-tree-view,
+                     builder-gb-project-tree-vim;
+}
+
+list {
+  -gtk-key-bindings: builder-vim-list-box;
+}
+
+window.workbench {
+  -gtk-key-bindings: builder-vim-workbench;
+}
+
+window.workbench entry.global-search {
+  -gtk-key-bindings: builder-vim-global-search;
+}
diff --git a/src/plugins/vim/meson.build b/src/plugins/vim/meson.build
new file mode 100644
index 000000000..8c20ea085
--- /dev/null
+++ b/src/plugins/vim/meson.build
@@ -0,0 +1,15 @@
+plugins_sources += files([
+  'vim-plugin.c',
+  'gb-vim.c',
+  'gbp-vim-command.c',
+  'gbp-vim-command-provider.c',
+  'gbp-vim-preferences-addin.c',
+])
+
+plugin_vim_resources = gnome.compile_resources(
+  'vim-resources',
+  'vim.gresource.xml',
+  c_name: 'gbp_vim',
+)
+
+plugins_sources += plugin_vim_resources[0]
diff --git a/src/plugins/vim/vim-plugin.c b/src/plugins/vim/vim-plugin.c
new file mode 100644
index 000000000..5a4cca438
--- /dev/null
+++ b/src/plugins/vim/vim-plugin.c
@@ -0,0 +1,41 @@
+/* vim-plugin.c
+ *
+ * Copyright 2018-2019 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 "vim-plugin"
+
+#include "config.h"
+
+#include <libpeas/peas.h>
+#include <libide-core.h>
+#include <libide-gui.h>
+
+#include "gbp-vim-command-provider.h"
+#include "gbp-vim-preferences-addin.h"
+
+_IDE_EXTERN void
+_gbp_vim_register_types (PeasObjectModule *module)
+{
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_PREFERENCES_ADDIN,
+                                              GBP_TYPE_VIM_PREFERENCES_ADDIN);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_COMMAND_PROVIDER,
+                                              GBP_TYPE_VIM_COMMAND_PROVIDER);
+}
diff --git a/src/plugins/vim/vim.gresource.xml b/src/plugins/vim/vim.gresource.xml
new file mode 100644
index 000000000..6d08eaaf2
--- /dev/null
+++ b/src/plugins/vim/vim.gresource.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/plugins/vim">
+    <file>vim.plugin</file>
+    <file>keybindings/vim.css</file>
+  </gresource>
+</gresources>
diff --git a/src/plugins/vim/vim.plugin b/src/plugins/vim/vim.plugin
new file mode 100644
index 000000000..5d9d0ef21
--- /dev/null
+++ b/src/plugins/vim/vim.plugin
@@ -0,0 +1,9 @@
+[Plugin]
+Authors=Christian Hergert <christian hergert me>
+Builtin=true
+Copyright=Copyright © 2015-2018 Christian Hergert
+Description=Emulation of various VIM features
+Embedded=_gbp_vim_register_types
+Hidden=true
+Module=vim
+Name=VIM Emulation
diff --git a/src/plugins/words/gbp-word-completion-provider.c 
b/src/plugins/words/gbp-word-completion-provider.c
index 3738ad1ee..bf4c4b78c 100644
--- a/src/plugins/words/gbp-word-completion-provider.c
+++ b/src/plugins/words/gbp-word-completion-provider.c
@@ -1,6 +1,6 @@
 /* gbp-word-completion-provider.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,13 +14,15 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-word-completion-provider"
 
-#include <ide.h>
+#include "config.h"
+
+#include <libide-sourceview.h>
 
 #include "gbp-word-completion-provider.h"
 #include "gbp-word-proposal.h"
diff --git a/src/plugins/words/gbp-word-completion-provider.h 
b/src/plugins/words/gbp-word-completion-provider.h
index a75d8f781..3a60a9192 100644
--- a/src/plugins/words/gbp-word-completion-provider.h
+++ b/src/plugins/words/gbp-word-completion-provider.h
@@ -1,6 +1,6 @@
 /* gbp-word-completion-provider.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <glib-object.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/words/gbp-word-proposal.c b/src/plugins/words/gbp-word-proposal.c
index 0c2711031..8118e181c 100644
--- a/src/plugins/words/gbp-word-proposal.c
+++ b/src/plugins/words/gbp-word-proposal.c
@@ -1,6 +1,6 @@
 /* gbp-word-proposal.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,13 +14,15 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-word-proposal"
 
-#include <ide.h>
+#include "config.h"
+
+#include <libide-sourceview.h>
 
 #include "gbp-word-proposal.h"
 
diff --git a/src/plugins/words/gbp-word-proposal.h b/src/plugins/words/gbp-word-proposal.h
index c1219f3ad..23065f172 100644
--- a/src/plugins/words/gbp-word-proposal.h
+++ b/src/plugins/words/gbp-word-proposal.h
@@ -1,6 +1,6 @@
 /* gbp-word-proposal.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/words/gbp-word-proposals.c b/src/plugins/words/gbp-word-proposals.c
index 0ed981ea7..ab6226705 100644
--- a/src/plugins/words/gbp-word-proposals.c
+++ b/src/plugins/words/gbp-word-proposals.c
@@ -1,7 +1,7 @@
 /* gbp-word-proposals.c
  *
  * Copyright 2017 Umang Jain <mailumangjain gmail com>
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -15,13 +15,15 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "gbp-word-proposals"
 
-#include "sourceview/ide-source-search-context.h"
+#include "config.h"
+
+#include <libide-sourceview.h>
 
 #include "gbp-word-proposal.h"
 #include "gbp-word-proposals.h"
@@ -325,7 +327,7 @@ gbp_word_proposals_populate_finish (GbpWordProposals  *self,
 
   if (old_len || self->items->len)
     g_list_model_items_changed (G_LIST_MODEL (self), 0, old_len, self->items->len);
-  
+
   return ide_task_propagate_boolean (IDE_TASK (result), error);
 }
 
diff --git a/src/plugins/words/gbp-word-proposals.h b/src/plugins/words/gbp-word-proposals.h
index 8bcf73dc9..f1786e7ca 100644
--- a/src/plugins/words/gbp-word-proposals.h
+++ b/src/plugins/words/gbp-word-proposals.h
@@ -1,6 +1,6 @@
 /* gbp-word-proposals.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-sourceview.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/words/meson.build b/src/plugins/words/meson.build
index 25c5f1651..0d8cfdb15 100644
--- a/src/plugins/words/meson.build
+++ b/src/plugins/words/meson.build
@@ -1,19 +1,18 @@
-if get_option('with_words')
+if get_option('plugin_words')
 
-words_resources = gnome.compile_resources(
-  'words-resources',
-  'words.gresource.xml',
-  c_name: 'gbp_words',
-)
-
-words_sources = [
+plugins_sources += files([
   'words-plugin.c',
   'gbp-word-completion-provider.c',
   'gbp-word-proposal.c',
   'gbp-word-proposals.c',
-]
+])
+
+plugin_words_resources = gnome.compile_resources(
+  'words-resources',
+  'words.gresource.xml',
+  c_name: 'gbp_words',
+)
 
-gnome_builder_plugins_sources += files(words_sources)
-gnome_builder_plugins_sources += words_resources[0]
+plugins_sources += plugin_words_resources[0]
 
 endif
diff --git a/src/plugins/words/words-plugin.c b/src/plugins/words/words-plugin.c
index 88285f15c..69220f01d 100644
--- a/src/plugins/words/words-plugin.c
+++ b/src/plugins/words/words-plugin.c
@@ -1,6 +1,6 @@
 /* words-plugin.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,15 +14,19 @@
  *
  * 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
  */
 
-#include <ide.h>
+#include "config.h"
+
+#include <libide-sourceview.h>
 #include <libpeas/peas.h>
 
 #include "gbp-word-completion-provider.h"
 
-void
-gbp_words_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_gbp_words_register_types (PeasObjectModule *module)
 {
   peas_object_module_register_extension_type (module,
                                               IDE_TYPE_COMPLETION_PROVIDER,
diff --git a/src/plugins/words/words.gresource.xml b/src/plugins/words/words.gresource.xml
index efa1f4a15..c6852157f 100644
--- a/src/plugins/words/words.gresource.xml
+++ b/src/plugins/words/words.gresource.xml
@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/words">
     <file>words.plugin</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/words/words.plugin b/src/plugins/words/words.plugin
index a92883c46..9676d2b76 100644
--- a/src/plugins/words/words.plugin
+++ b/src/plugins/words/words.plugin
@@ -1,9 +1,10 @@
 [Plugin]
-Module=words-plugin
-Name=Word Completion
-Description=Provides completiones based on words in the document
 Authors=Christian Hergert <christian hergert me>
+Builtin=true
 Copyright=Copyright © 2017-2018 Umang Jain, Christian Hergert
 Depends=editor;
-Builtin=true
-Embedded=gbp_words_register_types
+Description=Provides completiones based on words in the document
+Embedded=_gbp_words_register_types
+Hidden=true
+Module=words
+Name=Word Completion
diff --git a/src/plugins/xml-pack/ide-xml-analysis.c b/src/plugins/xml-pack/ide-xml-analysis.c
index e0e2df15d..71fe8bc1f 100644
--- a/src/plugins/xml-pack/ide-xml-analysis.c
+++ b/src/plugins/xml-pack/ide-xml-analysis.c
@@ -14,7 +14,10 @@
  *
  * 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
  */
+
 #include "ide-xml-analysis.h"
 
 G_DEFINE_BOXED_TYPE (IdeXmlAnalysis, ide_xml_analysis, ide_xml_analysis_ref, ide_xml_analysis_unref)
@@ -79,11 +82,7 @@ ide_xml_analysis_set_diagnostics (IdeXmlAnalysis *self,
   g_return_if_fail (self != NULL);
   g_return_if_fail (diagnostics != NULL);
 
-  if (diagnostics != self->diagnostics)
-    {
-      g_clear_pointer (&self->diagnostics, ide_diagnostics_unref);
-      self->diagnostics = ide_diagnostics_ref (diagnostics);
-    }
+  g_set_object (&self->diagnostics, diagnostics);
 }
 
 void
@@ -138,8 +137,7 @@ ide_xml_analysis_free (IdeXmlAnalysis *self)
   g_assert_cmpint (self->ref_count, ==, 0);
 
   g_clear_object (&self->root_node);
-  g_clear_pointer (&self->diagnostics, ide_diagnostics_unref);
-
+  g_clear_object (&self->diagnostics);
   g_slice_free (IdeXmlAnalysis, self);
 }
 
diff --git a/src/plugins/xml-pack/ide-xml-analysis.h b/src/plugins/xml-pack/ide-xml-analysis.h
index b3f6470c4..0eb019acc 100644
--- a/src/plugins/xml-pack/ide-xml-analysis.h
+++ b/src/plugins/xml-pack/ide-xml-analysis.h
@@ -14,14 +14,15 @@
  *
  * 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 "diagnostics/ide-diagnostics.h"
-#include "ide-xml-symbol-node.h"
+#include <libide-code.h>
 
-#include <glib-object.h>
+#include "ide-xml-symbol-node.h"
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/xml-pack/ide-xml-completion-attributes.c 
b/src/plugins/xml-pack/ide-xml-completion-attributes.c
index abd829eeb..6e14cfe21 100644
--- a/src/plugins/xml-pack/ide-xml-completion-attributes.c
+++ b/src/plugins/xml-pack/ide-xml-completion-attributes.c
@@ -14,10 +14,13 @@
  *
  * 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
  */
 
-#include "ide-xml-completion-attributes.h"
+#include <dazzle.h>
 
+#include "ide-xml-completion-attributes.h"
 #include "ide-xml-position.h"
 
 typedef struct _MatchingState
diff --git a/src/plugins/xml-pack/ide-xml-completion-attributes.h 
b/src/plugins/xml-pack/ide-xml-completion-attributes.h
index a849e1bb4..ca2f97665 100644
--- a/src/plugins/xml-pack/ide-xml-completion-attributes.h
+++ b/src/plugins/xml-pack/ide-xml-completion-attributes.h
@@ -14,13 +14,15 @@
  *
  * 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.h>
 
-#include <ide.h>
+#include <libide-code.h>
 
 #include "ide-xml-rng-define.h"
 #include "ide-xml-symbol-node.h"
diff --git a/src/plugins/xml-pack/ide-xml-completion-provider.c 
b/src/plugins/xml-pack/ide-xml-completion-provider.c
index b1d127fa1..b69005566 100644
--- a/src/plugins/xml-pack/ide-xml-completion-provider.c
+++ b/src/plugins/xml-pack/ide-xml-completion-provider.c
@@ -14,6 +14,8 @@
  *
  * 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 "xml-completion"
@@ -21,6 +23,7 @@
 #include <dazzle.h>
 #include <gtksourceview/gtksource.h>
 #include <libpeas/peas.h>
+#include <libide-sourceview.h>
 
 #include "ide-xml-completion-attributes.h"
 #include "ide-xml-completion-provider.h"
@@ -64,9 +67,9 @@ typedef struct _StateStackItem
 
 typedef struct
 {
-  IdeFile *ifile;
-  gint     line;
-  gint     line_offset;
+  GFile *file;
+  gint   line;
+  gint   line_offset;
 } PopulateState;
 
 typedef struct
@@ -89,7 +92,7 @@ populate_state_free (PopulateState *state)
 {
   g_assert (state != NULL);
 
-  g_clear_object (&state->ifile);
+  g_clear_object (&state->file);
   g_slice_free (PopulateState, state);
 }
 
@@ -995,7 +998,7 @@ populate_cb (GObject      *object,
           items = g_ptr_array_new_with_free_func ((GDestroyNotify)completion_item_free);
           if (child_pos != -1)
             {
-              candidate_node = ide_xml_symbol_node_new ("internal", NULL, "", IDE_SYMBOL_XML_ELEMENT);
+              candidate_node = ide_xml_symbol_node_new ("internal", NULL, "", IDE_SYMBOL_KIND_XML_ELEMENT);
               ide_xml_position_set_child_node (position, candidate_node);
             }
 
@@ -1041,20 +1044,20 @@ ide_xml_completion_provider_populate_async (IdeCompletionProvider *provider,
   ide_task_set_source_tag (task, ide_xml_completion_provider_populate_async);
 
   ide_context = ide_object_get_context (IDE_OBJECT (self));
-  service = ide_context_get_service_typed (ide_context, IDE_TYPE_XML_SERVICE);
+  service = ide_xml_service_from_context (ide_context);
 
   buffer = ide_completion_context_get_buffer (context);
   ide_completion_context_get_bounds (context, &iter, NULL);
 
   state = g_slice_new0 (PopulateState);
-  state->ifile = g_object_ref (ide_buffer_get_file (IDE_BUFFER (buffer)));
+  state->file = g_object_ref (ide_buffer_get_file (IDE_BUFFER (buffer)));
   state->line = gtk_text_iter_get_line (&iter) + 1;
   state->line_offset = gtk_text_iter_get_line_offset (&iter) + 1;
 
   ide_task_set_task_data (task, state, populate_state_free);
 
   ide_xml_service_get_position_from_cursor_async (service,
-                                                  state->ifile,
+                                                  state->file,
                                                   IDE_BUFFER (buffer),
                                                   state->line,
                                                   state->line_offset,
diff --git a/src/plugins/xml-pack/ide-xml-completion-provider.h 
b/src/plugins/xml-pack/ide-xml-completion-provider.h
index 10ad92bc8..5f33cff78 100644
--- a/src/plugins/xml-pack/ide-xml-completion-provider.h
+++ b/src/plugins/xml-pack/ide-xml-completion-provider.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/xml-pack/ide-xml-completion-values.c 
b/src/plugins/xml-pack/ide-xml-completion-values.c
index c307fc2d3..51e7c3216 100644
--- a/src/plugins/xml-pack/ide-xml-completion-values.c
+++ b/src/plugins/xml-pack/ide-xml-completion-values.c
@@ -14,8 +14,12 @@
  *
  * 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
  */
 
+#include <dazzle.h>
+
 #include "ide-xml-completion-values.h"
 #include "ide-xml-position.h"
 
diff --git a/src/plugins/xml-pack/ide-xml-completion-values.h 
b/src/plugins/xml-pack/ide-xml-completion-values.h
index 6168b70fd..4d1cd4244 100644
--- a/src/plugins/xml-pack/ide-xml-completion-values.h
+++ b/src/plugins/xml-pack/ide-xml-completion-values.h
@@ -14,13 +14,15 @@
  *
  * 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.h>
 
-#include <ide.h>
+#include <libide-code.h>
 
 #include "ide-xml-rng-define.h"
 #include "ide-xml-symbol-node.h"
diff --git a/src/plugins/xml-pack/ide-xml-diagnostic-provider.c 
b/src/plugins/xml-pack/ide-xml-diagnostic-provider.c
index 9f7659a69..d2e90eee7 100644
--- a/src/plugins/xml-pack/ide-xml-diagnostic-provider.c
+++ b/src/plugins/xml-pack/ide-xml-diagnostic-provider.c
@@ -14,6 +14,8 @@
  *
  * 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 "xml-diagnostic-provider"
@@ -53,15 +55,16 @@ ide_xml_diagnostic_provider_diagnose_cb (GObject      *object,
   else
     ide_task_return_pointer (task,
                              g_steal_pointer (&ret),
-                             (GDestroyNotify)ide_diagnostics_unref);
+                             g_object_unref);
 
   IDE_EXIT;
 }
 
 static void
 ide_xml_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
-                                            IdeFile               *file,
-                                            IdeBuffer             *buffer,
+                                            GFile                 *file,
+                                            GBytes                *contents,
+                                            const gchar           *lang_id,
                                             GCancellable          *cancellable,
                                             GAsyncReadyCallback    callback,
                                             gpointer               user_data)
@@ -74,19 +77,19 @@ ide_xml_diagnostic_provider_diagnose_async (IdeDiagnosticProvider *provider,
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_XML_DIAGNOSTIC_PROVIDER (self));
-  g_return_if_fail (IDE_IS_FILE (file));
-  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (G_IS_FILE (file));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_xml_diagnostic_provider_diagnose_async);
 
   context = ide_object_get_context (IDE_OBJECT (provider));
-  service = ide_context_get_service_typed (context, IDE_TYPE_XML_SERVICE);
+  service = ide_xml_service_from_context (context);
 
   ide_xml_service_get_diagnostics_async (service,
                                          file,
-                                         buffer,
+                                         contents,
+                                         lang_id,
                                          cancellable,
                                          ide_xml_diagnostic_provider_diagnose_cb,
                                          g_steal_pointer (&task));
diff --git a/src/plugins/xml-pack/ide-xml-diagnostic-provider.h 
b/src/plugins/xml-pack/ide-xml-diagnostic-provider.h
index dd3e9d773..d0169a774 100644
--- a/src/plugins/xml-pack/ide-xml-diagnostic-provider.h
+++ b/src/plugins/xml-pack/ide-xml-diagnostic-provider.h
@@ -14,12 +14,14 @@
  *
  * 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>
-#include <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/xml-pack/ide-xml-hash-table.c b/src/plugins/xml-pack/ide-xml-hash-table.c
index ce3038701..108920116 100644
--- a/src/plugins/xml-pack/ide-xml-hash-table.c
+++ b/src/plugins/xml-pack/ide-xml-hash-table.c
@@ -14,9 +14,12 @@
  *
  * 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
  */
 
-#include <ide.h>
+#include <dazzle.h>
+#include <libide-code.h>
 
 #include "ide-xml-hash-table.h"
 
diff --git a/src/plugins/xml-pack/ide-xml-hash-table.h b/src/plugins/xml-pack/ide-xml-hash-table.h
index 9a51cfb56..5d2d6240e 100644
--- a/src/plugins/xml-pack/ide-xml-hash-table.h
+++ b/src/plugins/xml-pack/ide-xml-hash-table.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-highlighter.c b/src/plugins/xml-pack/ide-xml-highlighter.c
index 3e8656459..ccac8a686 100644
--- a/src/plugins/xml-pack/ide-xml-highlighter.c
+++ b/src/plugins/xml-pack/ide-xml-highlighter.c
@@ -14,12 +14,14 @@
  *
  * 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
  */
 
-#include "config.h"
-
 #define G_LOG_DOMAIN "ide-xml-highlighter"
 
+#include "config.h"
+
 #include <dazzle.h>
 #include <glib/gi18n.h>
 
diff --git a/src/plugins/xml-pack/ide-xml-highlighter.h b/src/plugins/xml-pack/ide-xml-highlighter.h
index b3c43330b..41c7fc130 100644
--- a/src/plugins/xml-pack/ide-xml-highlighter.h
+++ b/src/plugins/xml-pack/ide-xml-highlighter.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/xml-pack/ide-xml-indenter.c b/src/plugins/xml-pack/ide-xml-indenter.c
index ef0b57a78..fedb05ce8 100644
--- a/src/plugins/xml-pack/ide-xml-indenter.c
+++ b/src/plugins/xml-pack/ide-xml-indenter.c
@@ -1,6 +1,6 @@
 /* ide-xml-indenter.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,12 +14,16 @@
  *
  * 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 "ide-xml-indenter"
 
-#include <libpeas/peas.h>
 #include <gtksourceview/gtksource.h>
+#include <libpeas/peas.h>
+#include <libide-code.h>
+#include <libide-sourceview.h>
 #include <string.h>
 
 #include "ide-xml-indenter.h"
diff --git a/src/plugins/xml-pack/ide-xml-indenter.h b/src/plugins/xml-pack/ide-xml-indenter.h
index e17f03221..23c5d112f 100644
--- a/src/plugins/xml-pack/ide-xml-indenter.h
+++ b/src/plugins/xml-pack/ide-xml-indenter.h
@@ -1,6 +1,6 @@
 /* ide-xml-indenter.h
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/xml-pack/ide-xml-parser-generic.c b/src/plugins/xml-pack/ide-xml-parser-generic.c
index bf7b07806..378b7e8c9 100644
--- a/src/plugins/xml-pack/ide-xml-parser-generic.c
+++ b/src/plugins/xml-pack/ide-xml-parser-generic.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <libxml/parser.h>
@@ -67,7 +69,7 @@ ide_xml_parser_generic_start_element_sax_cb (ParserState    *state,
   attr = collect_attributes (self, (const gchar **)attributes);
   label = g_strconcat ((const gchar *)name, attr, NULL);
 
-  node = ide_xml_symbol_node_new (label, NULL, (gchar *)name, IDE_SYMBOL_XML_ELEMENT);
+  node = ide_xml_symbol_node_new (label, NULL, (gchar *)name, IDE_SYMBOL_KIND_XML_ELEMENT);
   g_object_set (node, "use-markup", TRUE, NULL);
 
   state->attributes = (const gchar **)attributes;
@@ -85,7 +87,7 @@ ide_xml_parser_generic_comment_sax_cb (ParserState   *state,
   g_assert (IDE_IS_XML_PARSER (self));
 
   strip_name = g_strstrip (g_strdup ((const gchar *)name));
-  node = ide_xml_symbol_node_new (strip_name, NULL, NULL, IDE_SYMBOL_XML_COMMENT);
+  node = ide_xml_symbol_node_new (strip_name, NULL, NULL, IDE_SYMBOL_KIND_XML_COMMENT);
   ide_xml_parser_state_processing (self, state, "comment", node, IDE_XML_SAX_CALLBACK_TYPE_COMMENT, FALSE);
 }
 
@@ -99,7 +101,7 @@ ide_xml_parser_generic_cdata_sax_cb (ParserState   *state,
 
   g_assert (IDE_IS_XML_PARSER (self));
 
-  node = ide_xml_symbol_node_new ("cdata", NULL, NULL, IDE_SYMBOL_XML_CDATA);
+  node = ide_xml_symbol_node_new ("cdata", NULL, NULL, IDE_SYMBOL_KIND_XML_CDATA);
   ide_xml_parser_state_processing (self, state, "cdata", node, IDE_XML_SAX_CALLBACK_TYPE_CDATA, FALSE);
 }
 
diff --git a/src/plugins/xml-pack/ide-xml-parser-generic.h b/src/plugins/xml-pack/ide-xml-parser-generic.h
index 23a462e11..bfe08e213 100644
--- a/src/plugins/xml-pack/ide-xml-parser-generic.h
+++ b/src/plugins/xml-pack/ide-xml-parser-generic.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-parser-private.h b/src/plugins/xml-pack/ide-xml-parser-private.h
index 79c1fbf54..eec51c110 100644
--- a/src/plugins/xml-pack/ide-xml-parser-private.h
+++ b/src/plugins/xml-pack/ide-xml-parser-private.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-parser-ui.c b/src/plugins/xml-pack/ide-xml-parser-ui.c
index 446c45f71..5c77695f4 100644
--- a/src/plugins/xml-pack/ide-xml-parser-ui.c
+++ b/src/plugins/xml-pack/ide-xml-parser-ui.c
@@ -14,11 +14,14 @@
  *
  * 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 "ide-xml-parser-ui"
 
 #include <dazzle.h>
+#include <libide-code.h>
 
 #include "ide-xml-parser-ui.h"
 #include "ide-xml-parser.h"
@@ -67,7 +70,7 @@ ide_xml_parser_ui_start_element_sax_cb (ParserState    *state,
           dzl_str_equal0 (parent_name, "template"))
         {
           value = get_attribute (attributes, "name", NULL);
-          node = ide_xml_symbol_node_new (value, NULL, "property", IDE_SYMBOL_UI_PROPERTY);
+          node = ide_xml_symbol_node_new (value, NULL, "property", IDE_SYMBOL_KIND_UI_PROPERTY);
           is_internal = TRUE;
           state->build_state = BUILD_STATE_GET_CONTENT;
         }
@@ -79,7 +82,7 @@ ide_xml_parser_ui_start_element_sax_cb (ParserState    *state,
           dzl_str_equal0 (parent_name, "item"))
         {
           value = get_attribute (attributes, "name", NULL);
-          node = ide_xml_symbol_node_new (value, NULL, "attribute", IDE_SYMBOL_UI_MENU_ATTRIBUTE);
+          node = ide_xml_symbol_node_new (value, NULL, "attribute", IDE_SYMBOL_KIND_UI_MENU_ATTRIBUTE);
           is_internal = TRUE;
           state->build_state = BUILD_STATE_GET_CONTENT;
         }
@@ -87,7 +90,7 @@ ide_xml_parser_ui_start_element_sax_cb (ParserState    *state,
   else if (dzl_str_equal0 (name, "class") && dzl_str_equal0 (parent_name, "style"))
     {
       value = get_attribute (attributes, "name", NULL);
-      node = ide_xml_symbol_node_new (value, NULL, "class", IDE_SYMBOL_UI_STYLE_CLASS);
+      node = ide_xml_symbol_node_new (value, NULL, "class", IDE_SYMBOL_KIND_UI_STYLE_CLASS);
       is_internal = TRUE;
     }
   else if (dzl_str_equal0 (name, "child"))
@@ -108,7 +111,7 @@ ide_xml_parser_ui_start_element_sax_cb (ParserState    *state,
           g_string_append (string, value);
         }
 
-      node = ide_xml_symbol_node_new (string->str, NULL, "child", IDE_SYMBOL_UI_CHILD);
+      node = ide_xml_symbol_node_new (string->str, NULL, "child", IDE_SYMBOL_KIND_UI_CHILD);
       g_object_set (node, "use-markup", TRUE, NULL);
     }
   else if (dzl_str_equal0 (name, "object"))
@@ -126,7 +129,7 @@ ide_xml_parser_ui_start_element_sax_cb (ParserState    *state,
           g_string_append (string, value);
         }
 
-      node = ide_xml_symbol_node_new (string->str, NULL, "object", IDE_SYMBOL_UI_OBJECT);
+      node = ide_xml_symbol_node_new (string->str, NULL, "object", IDE_SYMBOL_KIND_UI_OBJECT);
       g_object_set (node, "use-markup", TRUE, NULL);
     }
   else if (dzl_str_equal0 (name, "template"))
@@ -142,16 +145,16 @@ ide_xml_parser_ui_start_element_sax_cb (ParserState    *state,
       g_string_append (string, label);
       g_string_append (string, value);
 
-      node = ide_xml_symbol_node_new (string->str, NULL, (const gchar *)name, IDE_SYMBOL_UI_TEMPLATE);
+      node = ide_xml_symbol_node_new (string->str, NULL, (const gchar *)name, IDE_SYMBOL_KIND_UI_TEMPLATE);
       g_object_set (node, "use-markup", TRUE, NULL);
     }
   else if (dzl_str_equal0 (name, "packing"))
     {
-      node = ide_xml_symbol_node_new ("packing", NULL, "packing", IDE_SYMBOL_UI_PACKING);
+      node = ide_xml_symbol_node_new ("packing", NULL, "packing", IDE_SYMBOL_KIND_UI_PACKING);
     }
   else if (dzl_str_equal0 (name, "style"))
     {
-      node = ide_xml_symbol_node_new ("style", NULL, "style", IDE_SYMBOL_UI_STYLE);
+      node = ide_xml_symbol_node_new ("style", NULL, "style", IDE_SYMBOL_KIND_UI_STYLE);
     }
   else if (dzl_str_equal0 (name, "menu"))
     {
@@ -160,7 +163,7 @@ ide_xml_parser_ui_start_element_sax_cb (ParserState    *state,
       g_string_append (string, label);
       g_string_append (string, value);
 
-      node = ide_xml_symbol_node_new (string->str, NULL, "menu", IDE_SYMBOL_UI_MENU);
+      node = ide_xml_symbol_node_new (string->str, NULL, "menu", IDE_SYMBOL_KIND_UI_MENU);
       g_object_set (node, "use-markup", TRUE, NULL);
     }
   else if (dzl_str_equal0 (name, "submenu"))
@@ -170,7 +173,7 @@ ide_xml_parser_ui_start_element_sax_cb (ParserState    *state,
       g_string_append (string, label);
       g_string_append (string, value);
 
-      node = ide_xml_symbol_node_new (string->str, NULL, "submenu", IDE_SYMBOL_UI_SUBMENU);
+      node = ide_xml_symbol_node_new (string->str, NULL, "submenu", IDE_SYMBOL_KIND_UI_SUBMENU);
       g_object_set (node, "use-markup", TRUE, NULL);
     }
   else if (dzl_str_equal0 (name, "section"))
@@ -180,12 +183,12 @@ ide_xml_parser_ui_start_element_sax_cb (ParserState    *state,
       g_string_append (string, label);
       g_string_append (string, value);
 
-      node = ide_xml_symbol_node_new (string->str, NULL, "section", IDE_SYMBOL_UI_SECTION);
+      node = ide_xml_symbol_node_new (string->str, NULL, "section", IDE_SYMBOL_KIND_UI_SECTION);
       g_object_set (node, "use-markup", TRUE, NULL);
     }
   else if (dzl_str_equal0 (name, "item"))
     {
-      node = ide_xml_symbol_node_new ("item", NULL, "item", IDE_SYMBOL_UI_ITEM);
+      node = ide_xml_symbol_node_new ("item", NULL, "item", IDE_SYMBOL_KIND_UI_ITEM);
     }
 
   state->attributes = (const gchar **)attributes;
@@ -205,7 +208,7 @@ get_menu_attribute_value (IdeXmlSymbolNode *node,
   for (gint i = 0; i < n_children; ++i)
     {
       child = IDE_XML_SYMBOL_NODE (ide_xml_symbol_node_get_nth_internal_child (node, i));
-      if (ide_symbol_node_get_kind (IDE_SYMBOL_NODE (child)) == IDE_SYMBOL_UI_MENU_ATTRIBUTE &&
+      if (ide_symbol_node_get_kind (IDE_SYMBOL_NODE (child)) == IDE_SYMBOL_KIND_UI_MENU_ATTRIBUTE &&
           dzl_str_equal0 (ide_symbol_node_get_name (IDE_SYMBOL_NODE (child)), name))
         {
           return ide_xml_symbol_node_get_value (child);
@@ -234,7 +237,7 @@ node_post_processing_collect_style_classes (IdeXmlParser      *self,
       const gchar *name;
 
       child = IDE_XML_SYMBOL_NODE (ide_xml_symbol_node_get_nth_internal_child (node, i));
-      if (ide_symbol_node_get_kind (IDE_SYMBOL_NODE (child)) == IDE_SYMBOL_UI_STYLE_CLASS)
+      if (ide_symbol_node_get_kind (IDE_SYMBOL_NODE (child)) == IDE_SYMBOL_KIND_UI_STYLE_CLASS)
         {
           name = ide_symbol_node_get_name (IDE_SYMBOL_NODE (child));
           if (dzl_str_empty0 (name))
diff --git a/src/plugins/xml-pack/ide-xml-parser-ui.h b/src/plugins/xml-pack/ide-xml-parser-ui.h
index 3ac91c2c8..da6904691 100644
--- a/src/plugins/xml-pack/ide-xml-parser-ui.h
+++ b/src/plugins/xml-pack/ide-xml-parser-ui.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-parser.c b/src/plugins/xml-pack/ide-xml-parser.c
index 6a15924d8..1b0cfa802 100644
--- a/src/plugins/xml-pack/ide-xml-parser.c
+++ b/src/plugins/xml-pack/ide-xml-parser.c
@@ -14,8 +14,11 @@
  *
  * 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
  */
 
+#include <dazzle.h>
 #include <glib/gi18n.h>
 #include <glib-object.h>
 
@@ -111,11 +114,9 @@ ide_xml_parser_create_diagnostic (ParserState            *state,
                                   IdeDiagnosticSeverity   severity)
 {
   IdeXmlParser *self = (IdeXmlParser *)state->self;
-  IdeContext *context;
+  g_autoptr(IdeLocation) start_loc = NULL;
+  g_autoptr(IdeLocation) end_loc = NULL;
   IdeDiagnostic *diagnostic;
-  g_autoptr(IdeSourceLocation) start_loc = NULL;
-  g_autoptr(IdeSourceLocation) end_loc = NULL;
-  g_autoptr(IdeFile) ifile = NULL;
   gint start_line;
   gint start_line_offset;
   gint end_line;
@@ -124,29 +125,25 @@ ide_xml_parser_create_diagnostic (ParserState            *state,
 
   g_assert (IDE_IS_XML_PARSER (self));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
   ide_xml_sax_get_location (state->sax_parser,
                             &start_line, &start_line_offset,
                             &end_line, &end_line_offset,
                             NULL,
                             &size);
 
-  ifile = ide_file_new (context, state->file);
-  start_loc = ide_source_location_new (ifile,
-                                       start_line - 1,
-                                       start_line_offset - 1,
-                                       0);
+  start_loc = ide_location_new (state->file,
+                                start_line - 1,
+                                start_line_offset - 1);
 
   if (size > 0)
     {
-      IdeSourceRange *range;
+      IdeRange *range;
 
-      end_loc = ide_source_location_new (ifile,
-                                         end_line - 1,
-                                         end_line_offset - 1,
-                                         0);
+      end_loc = ide_location_new (state->file,
+                                  end_line - 1,
+                                  end_line_offset - 1);
 
-      range = ide_source_range_new (start_loc, end_loc);
+      range = ide_range_new (start_loc, end_loc);
       diagnostic = ide_diagnostic_new (severity, msg, NULL);
       ide_diagnostic_take_range (diagnostic, range);
     }
@@ -210,7 +207,7 @@ ide_xml_parser_state_processing (IdeXmlParser          *self,
     {
       if (callback_type == IDE_XML_SAX_CALLBACK_TYPE_START_ELEMENT)
         {
-          node = ide_xml_symbol_node_new ("internal", NULL, element_name, IDE_SYMBOL_XML_ELEMENT);
+          node = ide_xml_symbol_node_new ("internal", NULL, element_name, IDE_SYMBOL_KIND_XML_ELEMENT);
           ide_xml_symbol_node_set_location (node, g_object_ref (state->file),
                                             start_line, start_line_offset,
                                             end_line, end_line_offset,
@@ -383,7 +380,7 @@ ide_xml_parser_error_sax_cb (ParserState    *state,
           if (prev >= base && *prev == '<')
             {
               /* '<' only case, no name tag, node not created, we need to do it ourself */
-              node = ide_xml_symbol_node_new ("internal", NULL, NULL, IDE_SYMBOL_XML_ELEMENT);
+              node = ide_xml_symbol_node_new ("internal", NULL, NULL, IDE_SYMBOL_KIND_XML_ELEMENT);
               ide_xml_symbol_node_set_state (node, IDE_XML_SYMBOL_NODE_STATE_NOT_CLOSED);
               ide_xml_symbol_node_take_internal_child (state->parent_node, node);
 
@@ -588,14 +585,19 @@ ide_xml_parser_get_analysis_worker (IdeTask      *task,
       return;
     }
 
-  diagnostics = ide_diagnostics_new (IDE_PTR_ARRAY_STEAL_FULL (&state->diagnostics_array));
+  diagnostics = ide_diagnostics_new ();
+  if (state->diagnostics_array)
+    {
+      for (guint i = 0; i < state->diagnostics_array->len; i++)
+        ide_diagnostics_add (diagnostics, g_ptr_array_index (state->diagnostics_array, i));
+    }
   ide_xml_analysis_set_diagnostics (analysis, diagnostics);
 
   if (state->file_is_ui)
     {
       entry = ide_xml_schema_cache_entry_new ();
       entry->kind = SCHEMA_KIND_RNG;
-      entry->file = g_file_new_for_uri 
("resource:///org/gnome/builder/plugins/xml-pack-plugin/schemas/gtkbuilder.rng");
+      entry->file = g_file_new_for_uri ("resource:///plugins/xml-pack/schemas/gtkbuilder.rng");
       g_object_set_data (G_OBJECT (entry->file), "kind", GUINT_TO_POINTER (entry->kind));
       g_ptr_array_add (state->schemas, entry);
     }
@@ -631,7 +633,7 @@ ide_xml_parser_get_analysis_async (IdeXmlParser        *self,
   state->file = g_object_ref (file);
   state->content = g_bytes_ref (content);
   state->sequence = sequence;
-  state->diagnostics_array = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_diagnostic_unref);
+  state->diagnostics_array = g_ptr_array_new_with_free_func (g_object_unref);
   state->schemas = g_ptr_array_new_with_free_func (g_object_unref);
   state->sax_parser = ide_xml_sax_new ();
   state->stack = ide_xml_stack_new ();
@@ -639,7 +641,7 @@ ide_xml_parser_get_analysis_async (IdeXmlParser        *self,
   state->build_state = BUILD_STATE_NORMAL;
 
   state->analysis = ide_xml_analysis_new (-1);
-  state->root_node = ide_xml_symbol_node_new ("root", NULL, "root", IDE_SYMBOL_NONE);
+  state->root_node = ide_xml_symbol_node_new ("root", NULL, "root", IDE_SYMBOL_KIND_NONE);
   ide_xml_analysis_set_root_node (state->analysis, state->root_node);
 
   state->parent_node = state->root_node;
diff --git a/src/plugins/xml-pack/ide-xml-parser.h b/src/plugins/xml-pack/ide-xml-parser.h
index 52b1ae590..937997a9b 100644
--- a/src/plugins/xml-pack/ide-xml-parser.h
+++ b/src/plugins/xml-pack/ide-xml-parser.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-path.c b/src/plugins/xml-pack/ide-xml-path.c
index 17126c265..f6645e848 100644
--- a/src/plugins/xml-pack/ide-xml-path.c
+++ b/src/plugins/xml-pack/ide-xml-path.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "ide-xml-path.h"
diff --git a/src/plugins/xml-pack/ide-xml-path.h b/src/plugins/xml-pack/ide-xml-path.h
index 3dd43d3d4..3b0bb611d 100644
--- a/src/plugins/xml-pack/ide-xml-path.h
+++ b/src/plugins/xml-pack/ide-xml-path.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-position.c b/src/plugins/xml-pack/ide-xml-position.c
index 15dc7a526..e2e98c078 100644
--- a/src/plugins/xml-pack/ide-xml-position.c
+++ b/src/plugins/xml-pack/ide-xml-position.c
@@ -14,8 +14,12 @@
  *
  * 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
  */
 
+#include <dazzle.h>
+
 #include "ide-xml-position.h"
 
 G_DEFINE_BOXED_TYPE (IdeXmlPosition, ide_xml_position, ide_xml_position_ref, ide_xml_position_unref)
diff --git a/src/plugins/xml-pack/ide-xml-position.h b/src/plugins/xml-pack/ide-xml-position.h
index 477c4c922..9fd9026e9 100644
--- a/src/plugins/xml-pack/ide-xml-position.h
+++ b/src/plugins/xml-pack/ide-xml-position.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-proposal.c b/src/plugins/xml-pack/ide-xml-proposal.c
index 20d9f8ac2..2814bdc25 100644
--- a/src/plugins/xml-pack/ide-xml-proposal.c
+++ b/src/plugins/xml-pack/ide-xml-proposal.c
@@ -1,6 +1,6 @@
 /* ide-xml-proposal.c
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,15 @@
  *
  * 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 "ide-xml-proposal"
+
 #include "config.h"
 
-#define G_LOG_DOMAIN "ide-xml-proposal"
+#include <libide-sourceview.h>
 
 #include "ide-xml-proposal.h"
 
@@ -61,7 +65,7 @@ ide_xml_proposal_new (const gchar *text,
                       const gchar *label)
 {
   IdeXmlProposal *self;
-  
+
   self = g_object_new (IDE_TYPE_XML_PROPOSAL, NULL);
   self->text = g_strdup (text);
   self->label = g_strdup (label);
diff --git a/src/plugins/xml-pack/ide-xml-proposal.h b/src/plugins/xml-pack/ide-xml-proposal.h
index 7b78fdc2c..59c717bf9 100644
--- a/src/plugins/xml-pack/ide-xml-proposal.h
+++ b/src/plugins/xml-pack/ide-xml-proposal.h
@@ -1,6 +1,6 @@
 /* ide-xml-proposal.h
  *
- * Copyright 2018 Christian Hergert <chergert redhat com>
+ * Copyright 2018-2019 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
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/xml-pack/ide-xml-rng-define.c b/src/plugins/xml-pack/ide-xml-rng-define.c
index 16c758cf7..e8d2ef2cf 100644
--- a/src/plugins/xml-pack/ide-xml-rng-define.c
+++ b/src/plugins/xml-pack/ide-xml-rng-define.c
@@ -14,8 +14,12 @@
  *
  * 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
  */
 
+#include <dazzle.h>
+
 #include "ide-xml-rng-define.h"
 
 G_DEFINE_BOXED_TYPE (IdeXmlRngDefine, ide_xml_rng_define, ide_xml_rng_define_ref, ide_xml_rng_define_unref)
diff --git a/src/plugins/xml-pack/ide-xml-rng-define.h b/src/plugins/xml-pack/ide-xml-rng-define.h
index 2a852a7df..41ad49804 100644
--- a/src/plugins/xml-pack/ide-xml-rng-define.h
+++ b/src/plugins/xml-pack/ide-xml-rng-define.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-rng-grammar.c b/src/plugins/xml-pack/ide-xml-rng-grammar.c
index 7dcbc919e..891f5051c 100644
--- a/src/plugins/xml-pack/ide-xml-rng-grammar.c
+++ b/src/plugins/xml-pack/ide-xml-rng-grammar.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "ide-xml-rng-grammar.h"
diff --git a/src/plugins/xml-pack/ide-xml-rng-grammar.h b/src/plugins/xml-pack/ide-xml-rng-grammar.h
index d88807c98..dd987b182 100644
--- a/src/plugins/xml-pack/ide-xml-rng-grammar.h
+++ b/src/plugins/xml-pack/ide-xml-rng-grammar.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-rng-parser.c b/src/plugins/xml-pack/ide-xml-rng-parser.c
index b646422cf..150178b66 100644
--- a/src/plugins/xml-pack/ide-xml-rng-parser.c
+++ b/src/plugins/xml-pack/ide-xml-rng-parser.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 /* Based on the relaxng.c libxml2 code.
@@ -21,6 +23,7 @@
  * Whole refactoring to match the GNOME Builder needs.
  */
 
+#include <dazzle.h>
 #include <libxml/tree.h>
 #include <libxml/uri.h>
 
diff --git a/src/plugins/xml-pack/ide-xml-rng-parser.h b/src/plugins/xml-pack/ide-xml-rng-parser.h
index 0e61fa1d0..ad039e68a 100644
--- a/src/plugins/xml-pack/ide-xml-rng-parser.h
+++ b/src/plugins/xml-pack/ide-xml-rng-parser.h
@@ -14,12 +14,14 @@
  *
  * 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.h>
-#include <ide.h>
+#include <libide-code.h>
 
 #include "ide-xml-schema.h"
 
diff --git a/src/plugins/xml-pack/ide-xml-sax.c b/src/plugins/xml-pack/ide-xml-sax.c
index 654e679a0..e76cc07f4 100644
--- a/src/plugins/xml-pack/ide-xml-sax.c
+++ b/src/plugins/xml-pack/ide-xml-sax.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <glib/gi18n.h>
diff --git a/src/plugins/xml-pack/ide-xml-sax.h b/src/plugins/xml-pack/ide-xml-sax.h
index e38f00b56..9a66ddccd 100644
--- a/src/plugins/xml-pack/ide-xml-sax.h
+++ b/src/plugins/xml-pack/ide-xml-sax.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-schema-cache-entry.c 
b/src/plugins/xml-pack/ide-xml-schema-cache-entry.c
index 0bb593e4d..338a0800d 100644
--- a/src/plugins/xml-pack/ide-xml-schema-cache-entry.c
+++ b/src/plugins/xml-pack/ide-xml-schema-cache-entry.c
@@ -14,6 +14,8 @@
  *
  * 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 "ide-xml-schema-cache-entry"
diff --git a/src/plugins/xml-pack/ide-xml-schema-cache-entry.h 
b/src/plugins/xml-pack/ide-xml-schema-cache-entry.h
index 0ac74ca9c..82343234d 100644
--- a/src/plugins/xml-pack/ide-xml-schema-cache-entry.h
+++ b/src/plugins/xml-pack/ide-xml-schema-cache-entry.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-schema.c b/src/plugins/xml-pack/ide-xml-schema.c
index 657d8b8d7..1075d6c47 100644
--- a/src/plugins/xml-pack/ide-xml-schema.c
+++ b/src/plugins/xml-pack/ide-xml-schema.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "ide-xml-schema.h"
diff --git a/src/plugins/xml-pack/ide-xml-schema.h b/src/plugins/xml-pack/ide-xml-schema.h
index c663039a2..764109466 100644
--- a/src/plugins/xml-pack/ide-xml-schema.h
+++ b/src/plugins/xml-pack/ide-xml-schema.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-service.c b/src/plugins/xml-pack/ide-xml-service.c
index ac76e069d..715871dd6 100644
--- a/src/plugins/xml-pack/ide-xml-service.c
+++ b/src/plugins/xml-pack/ide-xml-service.c
@@ -14,6 +14,8 @@
  *
  * 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 "ide-xml-service"
@@ -43,10 +45,7 @@ struct _IdeXmlService
   GCancellable      *cancellable;
 };
 
-static void service_iface_init (IdeServiceInterface *iface);
-
-G_DEFINE_TYPE_WITH_CODE (IdeXmlService, ide_xml_service, IDE_TYPE_OBJECT,
-                         G_IMPLEMENT_INTERFACE (IDE_TYPE_SERVICE, service_iface_init))
+G_DEFINE_TYPE (IdeXmlService, ide_xml_service, IDE_TYPE_OBJECT)
 
 static void
 ide_xml_service_build_tree_cb2 (GObject      *object,
@@ -75,19 +74,16 @@ ide_xml_service_build_tree_cb (DzlTaskCache  *cache,
                                gpointer       user_data)
 {
   IdeXmlService *self = user_data;
-  g_autofree gchar *path = NULL;
-  IdeFile *ifile = (IdeFile *)key;
-  GFile *gfile;
+  GFile *file = (GFile *)key;
 
   IDE_ENTRY;
 
   g_assert (DZL_IS_TASK_CACHE (cache));
   g_assert (IDE_IS_XML_SERVICE (self));
-  g_assert (IDE_IS_FILE (ifile));
+  g_assert (G_IS_FILE (file));
   g_assert (G_IS_TASK (task));
 
-  if (NULL == (gfile = ide_file_get_file (ifile)) ||
-      NULL == (path = g_file_get_path (gfile)))
+  if (!g_file_is_native (file))
     {
       g_task_return_new_error (task,
                                G_IO_ERROR,
@@ -97,7 +93,7 @@ ide_xml_service_build_tree_cb (DzlTaskCache  *cache,
     }
 
   ide_xml_tree_builder_build_tree_async (self->tree_builder,
-                                         gfile,
+                                         file,
                                          g_task_get_cancellable (task),
                                          ide_xml_service_build_tree_cb2,
                                          g_object_ref (task));
@@ -267,97 +263,29 @@ ide_xml_service_get_analysis_cb (GObject      *object,
     g_task_return_pointer (task, g_steal_pointer (&analysis), (GDestroyNotify)ide_xml_analysis_unref);
 }
 
-typedef struct
-{
-  IdeXmlService *self;
-  GTask         *task;
-  GCancellable  *cancellable;
-  IdeFile       *ifile;
-  IdeBuffer     *buffer;
-} TaskState;
-
-static void
-ide_xml_service__buffer_loaded_cb (IdeBuffer *buffer,
-                                   TaskState *state)
-{
-  IdeXmlService *self = (IdeXmlService *)state->self;
-
-  g_assert (IDE_IS_XML_SERVICE (self));
-  g_assert (G_IS_TASK (state->task));
-  g_assert (state->cancellable == NULL || G_IS_CANCELLABLE (state->cancellable));
-  g_assert (IDE_IS_FILE (state->ifile));
-  g_assert (IDE_IS_BUFFER (state->buffer));
-
-  g_signal_handlers_disconnect_by_func (buffer, ide_xml_service__buffer_loaded_cb, state);
-
-  dzl_task_cache_get_async (self->analyses,
-                            state->ifile,
-                            TRUE,
-                            state->cancellable,
-                            ide_xml_service_get_analysis_cb,
-                            g_steal_pointer (&state->task));
-
-  g_object_unref (state->buffer);
-  g_object_unref (state->ifile);
-  g_slice_free (TaskState, state);
-}
-
 static void
 ide_xml_service_get_analysis_async (IdeXmlService       *self,
-                                    IdeFile             *ifile,
-                                    IdeBuffer           *buffer,
+                                    GFile               *file,
+                                    GBytes              *contents,
                                     GCancellable        *cancellable,
                                     GAsyncReadyCallback  callback,
                                     gpointer             user_data)
 {
   g_autoptr(GTask) task = NULL;
-  IdeContext *context;
-  IdeBufferManager *manager;
-  GFile *gfile;
 
   g_assert (IDE_IS_XML_SERVICE (self));
-  g_assert (IDE_IS_FILE (ifile));
-  g_assert (IDE_IS_BUFFER (buffer));
+  g_assert (G_IS_FILE (file));
   g_assert (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
 
   task = g_task_new (self, cancellable, callback, user_data);
-  context = ide_object_get_context (IDE_OBJECT (self));
-  manager = ide_context_get_buffer_manager (context);
-  gfile = ide_file_get_file (ifile);
-
-  if (!ide_buffer_manager_has_file (manager, gfile))
-    {
-      TaskState *state;
+  g_task_set_source_tag (task, ide_xml_service_get_analysis_async);
 
-      if (!ide_buffer_get_loading (buffer))
-        {
-          g_task_return_new_error (task,
-                                   G_IO_ERROR,
-                                   G_IO_ERROR_NOT_SUPPORTED,
-                                   _("Buffer loaded but not in the buffer manager."));
-          return;
-        }
-
-      /* Wait for the buffer to be fully loaded */
-      state = g_slice_new0 (TaskState);
-      state->self = self;
-      state->task = g_steal_pointer (&task);
-      state->cancellable = cancellable;
-      state->ifile = g_object_ref (ifile);
-      state->buffer = g_object_ref (buffer);
-
-      g_signal_connect (buffer,
-                        "loaded",
-                        G_CALLBACK (ide_xml_service__buffer_loaded_cb),
-                        state);
-    }
-  else
-    dzl_task_cache_get_async (self->analyses,
-                              ifile,
-                              TRUE,
-                              cancellable,
-                              ide_xml_service_get_analysis_cb,
-                              g_steal_pointer (&task));
+  dzl_task_cache_get_async (self->analyses,
+                            file,
+                            TRUE,
+                            cancellable,
+                            ide_xml_service_get_analysis_cb,
+                            g_steal_pointer (&task));
 }
 
 static IdeXmlAnalysis *
@@ -414,8 +342,8 @@ ide_xml_service_get_root_node_cb (GObject      *object,
  */
 void
 ide_xml_service_get_root_node_async (IdeXmlService       *self,
-                                     IdeFile             *ifile,
-                                     IdeBuffer           *buffer,
+                                     GFile               *file,
+                                     GBytes              *contents,
                                      GCancellable        *cancellable,
                                      GAsyncReadyCallback  callback,
                                      gpointer             user_data)
@@ -424,8 +352,7 @@ ide_xml_service_get_root_node_async (IdeXmlService       *self,
   IdeXmlAnalysis *cached;
 
   g_return_if_fail (IDE_IS_XML_SERVICE (self));
-  g_return_if_fail (IDE_IS_FILE (ifile));
-  g_return_if_fail (IDE_IS_BUFFER (buffer));
+  g_return_if_fail (G_IS_FILE (file));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = g_task_new (self, cancellable, callback, user_data);
@@ -434,19 +361,17 @@ ide_xml_service_get_root_node_async (IdeXmlService       *self,
    * If we have a cached analysis with a valid root_node,
    * and it is new enough, then re-use it.
    */
-  if (NULL != (cached = dzl_task_cache_peek (self->analyses, ifile)))
+  if ((cached = dzl_task_cache_peek (self->analyses, file)))
     {
       IdeContext *context;
       IdeUnsavedFiles *unsaved_files;
       IdeUnsavedFile *uf;
       IdeXmlSymbolNode *root_node;
-      GFile *gfile;
 
-      gfile = ide_file_get_file (ifile);
       context = ide_object_get_context (IDE_OBJECT (self));
-      unsaved_files = ide_context_get_unsaved_files (context);
+      unsaved_files = ide_unsaved_files_from_context (context);
 
-      if (NULL != (uf = ide_unsaved_files_get_unsaved_file (unsaved_files, gfile)) &&
+      if (NULL != (uf = ide_unsaved_files_get_unsaved_file (unsaved_files, file)) &&
           ide_xml_analysis_get_sequence (cached) == ide_unsaved_file_get_sequence (uf))
         {
           root_node = g_object_ref (ide_xml_analysis_get_root_node (cached));
@@ -458,8 +383,8 @@ ide_xml_service_get_root_node_async (IdeXmlService       *self,
     }
 
   ide_xml_service_get_analysis_async (self,
-                                      ifile,
-                                      buffer,
+                                      file,
+                                      contents,
                                       cancellable,
                                       ide_xml_service_get_root_node_cb,
                                       g_steal_pointer (&task));
@@ -506,8 +431,8 @@ ide_xml_service_get_diagnostics_cb (GObject      *object,
     g_task_return_error (task, g_steal_pointer (&error));
   else
     {
-      diagnostics = ide_diagnostics_ref (ide_xml_analysis_get_diagnostics (analysis));
-      g_task_return_pointer (task, diagnostics, (GDestroyNotify)ide_diagnostics_unref);
+      diagnostics = g_object_ref (ide_xml_analysis_get_diagnostics (analysis));
+      g_task_return_pointer (task, diagnostics, g_object_unref);
     }
 }
 
@@ -528,8 +453,9 @@ ide_xml_service_get_diagnostics_cb (GObject      *object,
  */
 void
 ide_xml_service_get_diagnostics_async (IdeXmlService       *self,
-                                       IdeFile             *ifile,
-                                       IdeBuffer           *buffer,
+                                       GFile               *file,
+                                       GBytes              *contents,
+                                       const gchar         *lang_id,
                                        GCancellable        *cancellable,
                                        GAsyncReadyCallback  callback,
                                        gpointer             user_data)
@@ -539,8 +465,7 @@ ide_xml_service_get_diagnostics_async (IdeXmlService       *self,
 
   g_return_if_fail (IDE_IS_MAIN_THREAD ());
   g_return_if_fail (IDE_IS_XML_SERVICE (self));
-  g_return_if_fail (IDE_IS_FILE (ifile));
-  g_return_if_fail (IDE_IS_BUFFER (buffer) || buffer == NULL);
+  g_return_if_fail (G_IS_FILE (file));
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   task = g_task_new (self, cancellable, callback, user_data);
@@ -550,33 +475,31 @@ ide_xml_service_get_diagnostics_async (IdeXmlService       *self,
    * If we have a cached analysis with some diagnostics,
    * and it is new enough, then re-use it.
    */
-  if ((cached = dzl_task_cache_peek (self->analyses, ifile)))
+  if ((cached = dzl_task_cache_peek (self->analyses, file)))
     {
       IdeContext *context;
       IdeUnsavedFiles *unsaved_files;
       IdeUnsavedFile *uf;
       IdeDiagnostics *diagnostics;
-      GFile *gfile;
 
-      gfile = ide_file_get_file (ifile);
       context = ide_object_get_context (IDE_OBJECT (self));
-      unsaved_files = ide_context_get_unsaved_files (context);
+      unsaved_files = ide_unsaved_files_from_context (context);
 
-      if ((uf = ide_unsaved_files_get_unsaved_file (unsaved_files, gfile)) &&
+      if ((uf = ide_unsaved_files_get_unsaved_file (unsaved_files, file)) &&
           ide_xml_analysis_get_sequence (cached) == ide_unsaved_file_get_sequence (uf))
         {
           diagnostics = ide_xml_analysis_get_diagnostics (cached);
           g_assert (diagnostics != NULL);
           g_task_return_pointer (task,
-                                 ide_diagnostics_ref (diagnostics),
-                                 (GDestroyNotify)ide_diagnostics_unref);
+                                 g_object_ref (diagnostics),
+                                 g_object_unref);
           return;
         }
     }
 
   ide_xml_service_get_analysis_async (self,
-                                      ifile,
-                                      buffer,
+                                      file,
+                                      contents,
                                       cancellable,
                                       ide_xml_service_get_diagnostics_cb,
                                       g_steal_pointer (&task));
@@ -603,40 +526,72 @@ ide_xml_service_get_diagnostics_finish (IdeXmlService  *self,
 }
 
 static void
-ide_xml_service_context_loaded (IdeService *service)
+ide_xml_service_parent_set (IdeObject *object,
+                            IdeObject *parent)
 {
-  IdeXmlService *self = (IdeXmlService *)service;
-  IdeContext *context;
+  IdeXmlService *self = (IdeXmlService *)object;
 
   IDE_ENTRY;
 
+  g_assert (IDE_IS_MAIN_THREAD ());
   g_assert (IDE_IS_XML_SERVICE (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
 
-  context = ide_object_get_context (IDE_OBJECT (self));
+  if (parent == NULL)
+    return;
 
   if (self->tree_builder == NULL)
     self->tree_builder = g_object_new (IDE_TYPE_XML_TREE_BUILDER,
-                                       "context", context,
+                                       "parent", self,
                                        NULL);
 
+  self->analyses = dzl_task_cache_new ((GHashFunc)g_file_hash,
+                                       (GEqualFunc)g_file_equal,
+                                       g_object_ref,
+                                       g_object_unref,
+                                       (GBoxedCopyFunc)ide_xml_analysis_ref,
+                                       (GBoxedFreeFunc)ide_xml_analysis_unref,
+                                       DEFAULT_EVICTION_MSEC,
+                                       ide_xml_service_build_tree_cb,
+                                       self,
+                                       NULL);
+
+  dzl_task_cache_set_name (self->analyses, "xml analysis cache");
+
+  /* There's no eviction time on this cache */
+  self->schemas = dzl_task_cache_new ((GHashFunc)g_file_hash,
+                                      (GEqualFunc)g_file_equal,
+                                      g_object_ref,
+                                      g_object_unref,
+                                      (GBoxedCopyFunc)ide_xml_schema_cache_entry_ref,
+                                      (GBoxedFreeFunc)ide_xml_schema_cache_entry_unref,
+                                      0,
+                                      ide_xml_service_load_schema_cb,
+                                      self,
+                                      NULL);
+
+  dzl_task_cache_set_name (self->schemas, "xml schemas cache");
+
   IDE_EXIT;
 }
 
 typedef struct
 {
-  IdeFile   *ifile;
+  GFile     *file;
   IdeBuffer *buffer;
   gint       line;
   gint       line_offset;
 } PositionState;
 
 static void
-position_state_free (PositionState *state)
+position_state_free (gpointer data)
 {
-  g_assert (state != NULL);
+  PositionState *state = data;
+
   g_assert (IDE_IS_MAIN_THREAD ());
+  g_assert (state != NULL);
 
-  g_clear_object (&state->ifile);
+  g_clear_object (&state->file);
   g_clear_object (&state->buffer);
   g_slice_free (PositionState, state);
 }
@@ -999,7 +954,7 @@ ide_xml_service_get_position_from_cursor_cb (GObject      *object,
 
 void
 ide_xml_service_get_position_from_cursor_async (IdeXmlService       *self,
-                                                IdeFile             *ifile,
+                                                GFile               *file,
                                                 IdeBuffer           *buffer,
                                                 gint                 line,
                                                 gint                 line_offset,
@@ -1008,12 +963,13 @@ ide_xml_service_get_position_from_cursor_async (IdeXmlService       *self,
                                                 gpointer             user_data)
 {
   g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GBytes) content = NULL;
   PositionState *state;
 
   IDE_ENTRY;
 
   g_return_if_fail (IDE_IS_XML_SERVICE (self));
-  g_return_if_fail (IDE_IS_FILE (ifile));
+  g_return_if_fail (G_IS_FILE (file));
   g_return_if_fail (IDE_IS_BUFFER (buffer) || buffer == NULL);
   g_return_if_fail (!cancellable || G_IS_CANCELLABLE (cancellable));
 
@@ -1021,16 +977,18 @@ ide_xml_service_get_position_from_cursor_async (IdeXmlService       *self,
   ide_task_set_source_tag (task, ide_xml_service_get_position_from_cursor_async);
 
   state = g_slice_new0 (PositionState);
-  state->ifile = g_object_ref (ifile);
+  state->file = g_object_ref (file);
   state->buffer = g_object_ref (buffer);
   state->line = line;
   state->line_offset = line_offset;
 
   ide_task_set_task_data (task, state, position_state_free);
 
+  content = ide_buffer_dup_content (buffer);
+
   ide_xml_service_get_analysis_async (self,
-                                      ifile,
-                                      buffer,
+                                      file,
+                                      content,
                                       cancellable,
                                       ide_xml_service_get_position_from_cursor_cb,
                                       g_steal_pointer (&task));
@@ -1050,53 +1008,20 @@ ide_xml_service_get_position_from_cursor_finish (IdeXmlService  *self,
 }
 
 static void
-ide_xml_service_start (IdeService *service)
-{
-  IdeXmlService *self = (IdeXmlService *)service;
-
-  g_assert (IDE_IS_XML_SERVICE (self));
-
-  self->analyses = dzl_task_cache_new ((GHashFunc)ide_file_hash,
-                                       (GEqualFunc)ide_file_equal,
-                                       g_object_ref,
-                                       g_object_unref,
-                                       (GBoxedCopyFunc)ide_xml_analysis_ref,
-                                       (GBoxedFreeFunc)ide_xml_analysis_unref,
-                                       DEFAULT_EVICTION_MSEC,
-                                       ide_xml_service_build_tree_cb,
-                                       self,
-                                       NULL);
-
-  dzl_task_cache_set_name (self->analyses, "xml analysis cache");
-
-  /* There's no eviction time on this cache */
-  self->schemas = dzl_task_cache_new ((GHashFunc)g_file_hash,
-                                      (GEqualFunc)g_file_equal,
-                                      g_object_ref,
-                                      g_object_unref,
-                                      (GBoxedCopyFunc)ide_xml_schema_cache_entry_ref,
-                                      (GBoxedFreeFunc)ide_xml_schema_cache_entry_unref,
-                                      0,
-                                      ide_xml_service_load_schema_cb,
-                                      self,
-                                      NULL);
-
-  dzl_task_cache_set_name (self->schemas, "xml schemas cache");
-}
-
-static void
-ide_xml_service_stop (IdeService *service)
+ide_xml_service_destroy (IdeObject *object)
 {
-  IdeXmlService *self = (IdeXmlService *)service;
+  IdeXmlService *self = (IdeXmlService *)object;
 
   g_assert (IDE_IS_XML_SERVICE (self));
 
-  if (self->cancellable && !g_cancellable_is_cancelled (self->cancellable))
+  if (!g_cancellable_is_cancelled (self->cancellable))
     g_cancellable_cancel (self->cancellable);
 
   g_clear_object (&self->cancellable);
   g_clear_object (&self->analyses);
   g_clear_object (&self->schemas);
+
+  IDE_OBJECT_CLASS (ide_xml_service_parent_class)->destroy (object);
 }
 
 static void
@@ -1106,7 +1031,6 @@ ide_xml_service_finalize (GObject *object)
 
   IDE_ENTRY;
 
-  ide_xml_service_stop (IDE_SERVICE (self));
   g_clear_object (&self->tree_builder);
 
   G_OBJECT_CLASS (ide_xml_service_parent_class)->finalize (object);
@@ -1118,16 +1042,12 @@ static void
 ide_xml_service_class_init (IdeXmlServiceClass *klass)
 {
   GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
 
   object_class->finalize = ide_xml_service_finalize;
-}
 
-static void
-service_iface_init (IdeServiceInterface *iface)
-{
-  iface->context_loaded = ide_xml_service_context_loaded;
-  iface->start = ide_xml_service_start;
-  iface->stop = ide_xml_service_stop;
+  i_object_class->parent_set = ide_xml_service_parent_set;
+  i_object_class->destroy = ide_xml_service_destroy;
 }
 
 static void
@@ -1144,15 +1064,15 @@ ide_xml_service_init (IdeXmlService *self)
  */
 IdeXmlSymbolNode *
 ide_xml_service_get_cached_root_node (IdeXmlService *self,
-                                      GFile         *gfile)
+                                      GFile         *file)
 {
   IdeXmlAnalysis *analysis;
   IdeXmlSymbolNode *cached;
 
   g_return_val_if_fail (IDE_IS_XML_SERVICE (self), NULL);
-  g_return_val_if_fail (IDE_IS_FILE (gfile), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
 
-  if (NULL != (analysis = dzl_task_cache_peek (self->analyses, gfile)) &&
+  if (NULL != (analysis = dzl_task_cache_peek (self->analyses, file)) &&
       NULL != (cached = ide_xml_analysis_get_root_node (analysis)))
     return g_object_ref (cached);
 
@@ -1168,17 +1088,17 @@ ide_xml_service_get_cached_root_node (IdeXmlService *self,
  */
 IdeDiagnostics *
 ide_xml_service_get_cached_diagnostics (IdeXmlService *self,
-                                        GFile         *gfile)
+                                        GFile         *file)
 {
   IdeXmlAnalysis *analysis;
   IdeDiagnostics *cached;
 
   g_return_val_if_fail (IDE_IS_XML_SERVICE (self), NULL);
-  g_return_val_if_fail (IDE_IS_FILE (gfile), NULL);
+  g_return_val_if_fail (G_IS_FILE (file), NULL);
 
-  if (NULL != (analysis = dzl_task_cache_peek (self->analyses, gfile)) &&
+  if (NULL != (analysis = dzl_task_cache_peek (self->analyses, file)) &&
       NULL != (cached = ide_xml_analysis_get_diagnostics (analysis)))
-    return ide_diagnostics_ref (cached);
+    return g_object_ref (cached);
 
   return NULL;
 }
@@ -1197,3 +1117,22 @@ ide_xml_service_get_schemas_cache (IdeXmlService *self)
 
   return self->schemas;
 }
+
+/**
+ * ide_xml_service_from_context:
+ * @context: an #IdeContext
+ *
+ * Returns: (transfer none): an #IdeXmlService
+ *
+ * Since: 3.32
+ */
+IdeXmlService *
+ide_xml_service_from_context (IdeContext *context)
+{
+  g_autoptr(IdeXmlService) child = NULL;
+
+  g_return_val_if_fail (IDE_IS_CONTEXT (context), NULL);
+
+  child = ide_object_ensure_child_typed (IDE_OBJECT (context), IDE_TYPE_XML_SERVICE);
+  return ide_context_peek_child_typed (context, IDE_TYPE_XML_SERVICE);
+}
diff --git a/src/plugins/xml-pack/ide-xml-service.h b/src/plugins/xml-pack/ide-xml-service.h
index c0796be05..40ec05699 100644
--- a/src/plugins/xml-pack/ide-xml-service.h
+++ b/src/plugins/xml-pack/ide-xml-service.h
@@ -14,6 +14,8 @@
  *
  * 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
@@ -22,7 +24,7 @@
 #include <gtksourceview/gtksource.h>
 #include "ide-xml-position.h"
 #include "ide-xml-symbol-node.h"
-#include <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
@@ -30,6 +32,7 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeXmlService, ide_xml_service, IDE, XML_SERVICE, IdeObject)
 
+IdeXmlService      *ide_xml_service_from_context                       (IdeContext           *context);
 IdeDiagnostics     *ide_xml_service_get_cached_diagnostics             (IdeXmlService        *self,
                                                                         GFile                *gfile);
 IdeXmlSymbolNode   *ide_xml_service_get_cached_root_node               (IdeXmlService        *self,
@@ -38,13 +41,14 @@ IdeDiagnostics     *ide_xml_service_get_diagnostics_finish             (IdeXmlSe
                                                                         GAsyncResult         *result,
                                                                         GError              **error);
 void                ide_xml_service_get_diagnostics_async              (IdeXmlService        *self,
-                                                                        IdeFile              *ifile,
-                                                                        IdeBuffer            *buffer,
+                                                                        GFile                *file,
+                                                                        GBytes               *contents,
+                                                                        const gchar          *lang_id,
                                                                         GCancellable         *cancellable,
                                                                         GAsyncReadyCallback   callback,
                                                                         gpointer              user_data);
 void                ide_xml_service_get_position_from_cursor_async     (IdeXmlService        *self,
-                                                                        IdeFile              *ifile,
+                                                                        GFile                *file,
                                                                         IdeBuffer            *buffer,
                                                                         gint                  line,
                                                                         gint                  line_offset,
@@ -55,8 +59,8 @@ IdeXmlPosition     *ide_xml_service_get_position_from_cursor_finish    (IdeXmlSe
                                                                         GAsyncResult         *result,
                                                                         GError              **error);
 void                ide_xml_service_get_root_node_async                (IdeXmlService        *self,
-                                                                        IdeFile              *ifile,
-                                                                        IdeBuffer            *buffer,
+                                                                        GFile                *file,
+                                                                        GBytes               *contents,
                                                                         GCancellable         *cancellable,
                                                                         GAsyncReadyCallback   callback,
                                                                         gpointer              user_data);
diff --git a/src/plugins/xml-pack/ide-xml-stack.c b/src/plugins/xml-pack/ide-xml-stack.c
index 7813b8144..6b55607dd 100644
--- a/src/plugins/xml-pack/ide-xml-stack.c
+++ b/src/plugins/xml-pack/ide-xml-stack.c
@@ -14,10 +14,14 @@
  *
  * 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
  */
 
+#include <dazzle.h>
+#include <libide-code.h>
+
 #include "ide-xml-stack.h"
-#include <ide.h>
 
 typedef struct _StackItem
 {
diff --git a/src/plugins/xml-pack/ide-xml-stack.h b/src/plugins/xml-pack/ide-xml-stack.h
index 84a402cd6..466a1bb1b 100644
--- a/src/plugins/xml-pack/ide-xml-stack.h
+++ b/src/plugins/xml-pack/ide-xml-stack.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-symbol-node.c b/src/plugins/xml-pack/ide-xml-symbol-node.c
index 80308af09..6c1f982de 100644
--- a/src/plugins/xml-pack/ide-xml-symbol-node.c
+++ b/src/plugins/xml-pack/ide-xml-symbol-node.c
@@ -14,11 +14,15 @@
  *
  * 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 "ide-xml-symbol-node"
 
+#include <dazzle.h>
+
 #include "ide-xml-symbol-node.h"
 
 typedef struct _Attribute
@@ -79,9 +83,7 @@ ide_xml_symbol_node_get_location_async (IdeSymbolNode       *node,
 {
   IdeXmlSymbolNode *self = (IdeXmlSymbolNode *)node;
   g_autoptr(IdeTask) task = NULL;
-  IdeContext *context;
-  g_autoptr(IdeFile) ifile = NULL;
-  IdeSourceLocation *ret;
+  IdeLocation *ret;
 
   g_return_if_fail (IDE_IS_XML_SYMBOL_NODE (self));
   g_return_if_fail (G_IS_FILE (self->file));
@@ -90,18 +92,14 @@ ide_xml_symbol_node_get_location_async (IdeSymbolNode       *node,
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_source_tag (task, ide_xml_symbol_node_get_location_async);
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  ifile = ide_file_new (context, self->file);
-
-  ret = ide_source_location_new (ifile,
-                                 self->start_tag.start_line - 1,
-                                 self->start_tag.start_line_offset - 1,
-                                 0);
+  ret = ide_location_new (self->file,
+                          self->start_tag.start_line - 1,
+                          self->start_tag.start_line_offset - 1);
 
-  ide_task_return_pointer (task, ret, (GDestroyNotify)ide_source_location_unref);
+  ide_task_return_pointer (task, ret, g_object_unref);
 }
 
-static IdeSourceLocation *
+static IdeLocation *
 ide_xml_symbol_node_get_location_finish (IdeSymbolNode  *node,
                                          GAsyncResult   *result,
                                          GError        **error)
diff --git a/src/plugins/xml-pack/ide-xml-symbol-node.h b/src/plugins/xml-pack/ide-xml-symbol-node.h
index 29e7bc69e..3b1c5e462 100644
--- a/src/plugins/xml-pack/ide-xml-symbol-node.h
+++ b/src/plugins/xml-pack/ide-xml-symbol-node.h
@@ -14,6 +14,8 @@
  *
  * 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
@@ -21,7 +23,7 @@
 #include "ide-xml-types.h"
 #include "ide-xml-symbol-resolver.h"
 
-#include <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/xml-pack/ide-xml-symbol-resolver.c b/src/plugins/xml-pack/ide-xml-symbol-resolver.c
index a3a9f634f..52cc4a607 100644
--- a/src/plugins/xml-pack/ide-xml-symbol-resolver.c
+++ b/src/plugins/xml-pack/ide-xml-symbol-resolver.c
@@ -14,6 +14,8 @@
  *
  * 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 "xml-symbol-resolver"
@@ -35,7 +37,7 @@ G_DEFINE_TYPE_WITH_CODE (IdeXmlSymbolResolver, ide_xml_symbol_resolver, IDE_TYPE
 
 static void
 ide_xml_symbol_resolver_lookup_symbol_async (IdeSymbolResolver   *resolver,
-                                             IdeSourceLocation   *location,
+                                             IdeLocation   *location,
                                              GCancellable        *cancellable,
                                              GAsyncReadyCallback  callback,
                                              gpointer             user_data)
@@ -103,7 +105,7 @@ ide_xml_symbol_resolver_get_symbol_tree_cb (GObject      *object,
 static void
 ide_xml_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *resolver,
                                                GFile               *file,
-                                               IdeBuffer           *buffer,
+                                               GBytes              *contents,
                                                GCancellable        *cancellable,
                                                GAsyncReadyCallback  callback,
                                                gpointer             user_data)
@@ -112,7 +114,6 @@ ide_xml_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *resolver,
   g_autoptr(IdeTask) task = NULL;
   IdeContext *context;
   IdeXmlService *service;
-  g_autoptr(IdeFile) ifile = NULL;
 
   IDE_ENTRY;
 
@@ -121,17 +122,15 @@ ide_xml_symbol_resolver_get_symbol_tree_async (IdeSymbolResolver   *resolver,
   g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  service = ide_context_get_service_typed (context, IDE_TYPE_XML_SERVICE);
+  service = ide_xml_service_from_context (context);
 
   task = ide_task_new (self, cancellable, callback, user_data);
   ide_task_set_task_data (task, g_object_ref (file), g_object_unref);
   ide_task_set_source_tag (task, ide_xml_symbol_resolver_get_symbol_tree_async);
 
-  ifile = ide_file_new (context, file);
-
   ide_xml_service_get_root_node_async (service,
-                                       ifile,
-                                       buffer,
+                                       file,
+                                       contents,
                                        cancellable,
                                        ide_xml_symbol_resolver_get_symbol_tree_cb,
                                        g_object_ref (task));
diff --git a/src/plugins/xml-pack/ide-xml-symbol-resolver.h b/src/plugins/xml-pack/ide-xml-symbol-resolver.h
index 2c740017e..0d4f6e1fb 100644
--- a/src/plugins/xml-pack/ide-xml-symbol-resolver.h
+++ b/src/plugins/xml-pack/ide-xml-symbol-resolver.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 G_BEGIN_DECLS
 
diff --git a/src/plugins/xml-pack/ide-xml-symbol-tree.c b/src/plugins/xml-pack/ide-xml-symbol-tree.c
index 80df87e79..533a36653 100644
--- a/src/plugins/xml-pack/ide-xml-symbol-tree.c
+++ b/src/plugins/xml-pack/ide-xml-symbol-tree.c
@@ -14,6 +14,8 @@
  *
  * 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 "ide-xml-symbol-tree"
diff --git a/src/plugins/xml-pack/ide-xml-symbol-tree.h b/src/plugins/xml-pack/ide-xml-symbol-tree.h
index a26a372fa..86f4644a7 100644
--- a/src/plugins/xml-pack/ide-xml-symbol-tree.h
+++ b/src/plugins/xml-pack/ide-xml-symbol-tree.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 #include "ide-xml-symbol-node.h"
 
 G_BEGIN_DECLS
diff --git a/src/plugins/xml-pack/ide-xml-tree-builder-utils-private.h 
b/src/plugins/xml-pack/ide-xml-tree-builder-utils-private.h
index fb9ec71d5..5682ae420 100644
--- a/src/plugins/xml-pack/ide-xml-tree-builder-utils-private.h
+++ b/src/plugins/xml-pack/ide-xml-tree-builder-utils-private.h
@@ -14,11 +14,13 @@
  *
  * 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.h>
-#include <ide.h>
+#include <libide-code.h>
 
 #include "ide-xml-schema-cache-entry.h"
 #include "ide-xml-symbol-node.h"
diff --git a/src/plugins/xml-pack/ide-xml-tree-builder-utils.c 
b/src/plugins/xml-pack/ide-xml-tree-builder-utils.c
index dccb7ca32..4b10d9894 100644
--- a/src/plugins/xml-pack/ide-xml-tree-builder-utils.c
+++ b/src/plugins/xml-pack/ide-xml-tree-builder-utils.c
@@ -14,8 +14,11 @@
  *
  * 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
  */
 
+#include <dazzle.h>
 #include <string.h>
 
 #include "ide-xml-tree-builder-utils-private.h"
diff --git a/src/plugins/xml-pack/ide-xml-tree-builder.c b/src/plugins/xml-pack/ide-xml-tree-builder.c
index fce1a2b92..ee2d2d3c9 100644
--- a/src/plugins/xml-pack/ide-xml-tree-builder.c
+++ b/src/plugins/xml-pack/ide-xml-tree-builder.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 
@@ -83,17 +85,12 @@ create_diagnostic (IdeContext            *context,
                    gint                   col,
                    IdeDiagnosticSeverity  severity)
 {
-  g_autoptr(IdeSourceLocation) loc = NULL;
-  g_autoptr(IdeFile) ifile = NULL;
+  g_autoptr(IdeLocation) loc = NULL;
 
   g_assert (IDE_IS_CONTEXT (context));
   g_assert (G_IS_FILE (file));
 
-  ifile = ide_file_new (context, file);
-  loc = ide_source_location_new (ifile,
-                                 line - 1,
-                                 col - 1,
-                                 0);
+  loc = ide_location_new (file, line - 1, col - 1);
 
   return ide_diagnostic_new (severity, msg, loc);
 }
@@ -113,12 +110,12 @@ ide_xml_tree_builder_get_file_content (IdeXmlTreeBuilder *self,
   g_assert (G_IS_FILE (file));
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  manager = ide_context_get_buffer_manager (context);
+  manager = ide_buffer_manager_from_context (context);
   buffer = ide_buffer_manager_find_buffer (manager, file);
 
   if (buffer != NULL)
     {
-      content = ide_buffer_get_content (buffer);
+      content = ide_buffer_dup_content (buffer);
       sequence_tmp = ide_buffer_get_change_count (buffer);
     }
 
@@ -231,7 +228,7 @@ fetch_schemas_async (IdeXmlTreeBuilder   *self,
   schemas_copy = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_xml_schema_cache_entry_unref);
 
   context = ide_object_get_context (IDE_OBJECT (self));
-  service = ide_context_get_service_typed (context, IDE_TYPE_XML_SERVICE);
+  service = ide_xml_service_from_context (context);
   schemas_cache = ide_xml_service_get_schemas_cache (service);
 
   for (guint i = 0; i < schemas->len; i++)
@@ -554,40 +551,47 @@ ide_xml_tree_builder_build_tree_finish (IdeXmlTreeBuilder  *self,
 }
 
 static void
-ide_xml_tree_builder_finalize (GObject *object)
+ide_xml_tree_builder_destroy (IdeObject *object)
 {
   IdeXmlTreeBuilder *self = (IdeXmlTreeBuilder *)object;
 
-  g_clear_object (&self->parser);
-  g_clear_object (&self->validator);
+  ide_clear_and_destroy_object (&self->parser);
+  ide_clear_and_destroy_object (&self->validator);
 
-  G_OBJECT_CLASS (ide_xml_tree_builder_parent_class)->finalize (object);
+  IDE_OBJECT_CLASS (ide_xml_tree_builder_parent_class)->destroy (object);
 }
 
 static void
-ide_xml_tree_builder_constructed (GObject *object)
+ide_xml_tree_builder_parent_set (IdeObject *object,
+                                 IdeObject *parent)
 {
   IdeXmlTreeBuilder *self = (IdeXmlTreeBuilder *)object;
   IdeContext *context;
 
-  G_OBJECT_CLASS (ide_xml_tree_builder_parent_class)->constructed (object);
+  g_assert (IDE_IS_XML_TREE_BUILDER (self));
+  g_assert (!parent || IDE_IS_OBJECT (parent));
+
+  if (parent == NULL)
+    return;
 
   context = ide_object_get_context (IDE_OBJECT (self));
   g_assert (IDE_IS_CONTEXT (context));
 
   self->parser = g_object_new (IDE_TYPE_XML_PARSER,
-                               "context", context,
+                               "parent", self,
                                NULL);
-  self->validator = ide_xml_validator_new (context);
+  self->validator = g_object_new (IDE_TYPE_XML_VALIDATOR,
+                                  "parent", self,
+                                  NULL);
 }
 
 static void
 ide_xml_tree_builder_class_init (IdeXmlTreeBuilderClass *klass)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
 
-  object_class->constructed = ide_xml_tree_builder_constructed;
-  object_class->finalize = ide_xml_tree_builder_finalize;
+  i_object_class->parent_set = ide_xml_tree_builder_parent_set;
+  i_object_class->destroy = ide_xml_tree_builder_destroy;
 }
 
 static void
diff --git a/src/plugins/xml-pack/ide-xml-tree-builder.h b/src/plugins/xml-pack/ide-xml-tree-builder.h
index d728e27a7..5f5159403 100644
--- a/src/plugins/xml-pack/ide-xml-tree-builder.h
+++ b/src/plugins/xml-pack/ide-xml-tree-builder.h
@@ -14,11 +14,13 @@
  *
  * 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 <ide.h>
+#include <libide-code.h>
 
 #include "ide-xml-analysis.h"
 #include "ide-xml-symbol-node.h"
diff --git a/src/plugins/xml-pack/ide-xml-types.h b/src/plugins/xml-pack/ide-xml-types.h
index 21e7494ab..3ae82fb72 100644
--- a/src/plugins/xml-pack/ide-xml-types.h
+++ b/src/plugins/xml-pack/ide-xml-types.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-utils.c b/src/plugins/xml-pack/ide-xml-utils.c
index 950cec11a..0e20ea86e 100644
--- a/src/plugins/xml-pack/ide-xml-utils.c
+++ b/src/plugins/xml-pack/ide-xml-utils.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include "ide-xml-utils.h"
diff --git a/src/plugins/xml-pack/ide-xml-utils.h b/src/plugins/xml-pack/ide-xml-utils.h
index dfe78bb5d..a340faa0f 100644
--- a/src/plugins/xml-pack/ide-xml-utils.h
+++ b/src/plugins/xml-pack/ide-xml-utils.h
@@ -14,6 +14,8 @@
  *
  * 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
diff --git a/src/plugins/xml-pack/ide-xml-validator.c b/src/plugins/xml-pack/ide-xml-validator.c
index 9bc06047f..294b3db6e 100644
--- a/src/plugins/xml-pack/ide-xml-validator.c
+++ b/src/plugins/xml-pack/ide-xml-validator.c
@@ -14,6 +14,8 @@
  *
  * 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
  */
 
 #include <glib.h>
@@ -64,24 +66,17 @@ create_diagnostic (IdeXmlValidator        *self,
                    xmlError               *error,
                    IdeDiagnosticSeverity   severity)
 {
-  IdeContext *context;
-  IdeDiagnostic *diagnostic;
-  g_autoptr(IdeSourceLocation) loc = NULL;
-  g_autoptr(IdeFile) ifile = NULL;
+  g_autoptr(IdeLocation) loc = NULL;
   gint line;
 
   g_assert (IDE_IS_XML_VALIDATOR (self));
   g_assert (G_IS_FILE (file));
   g_assert (error != NULL);
 
-  context = ide_object_get_context (IDE_OBJECT (self));
-  ifile = ide_file_new (context, file);
-  line = (error->line > 0) ? error->line - 1 : 0;
-  loc = ide_source_location_new (ifile, line, 0, 0);
+  line = error->line - 1;
+  loc = ide_location_new (file, line, -1);
 
-  diagnostic = ide_diagnostic_new (severity, error->message, loc);
-
-  return diagnostic;
+  return ide_diagnostic_new (severity, error->message, loc);
 }
 
 static void
@@ -201,11 +196,10 @@ ide_xml_validator_validate (IdeXmlValidator   *self,
 
 end:
   if (diagnostics != NULL)
-    *diagnostics = ide_diagnostics_new (IDE_PTR_ARRAY_STEAL_FULL (&self->diagnostics_array));
-  else
-    g_clear_pointer (&self->diagnostics_array, g_ptr_array_unref);
+    *diagnostics = ide_diagnostics_new_from_array (self->diagnostics_array);
 
-  self->diagnostics_array = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_diagnostic_unref);
+  g_clear_pointer (&self->diagnostics_array, g_ptr_array_unref);
+  self->diagnostics_array = g_ptr_array_new_with_free_func (g_object_unref);
 
   return ret;
 }
@@ -262,14 +256,6 @@ ide_xml_validator_set_schema (IdeXmlValidator  *self,
   return ret;
 }
 
-IdeXmlValidator *
-ide_xml_validator_new (IdeContext *context)
-{
-  return g_object_new (IDE_TYPE_XML_VALIDATOR,
-                       "context", context,
-                       NULL);
-}
-
 static void
 ide_xml_validator_finalize (GObject *object)
 {
@@ -294,5 +280,5 @@ ide_xml_validator_class_init (IdeXmlValidatorClass *klass)
 static void
 ide_xml_validator_init (IdeXmlValidator *self)
 {
-  self->diagnostics_array = g_ptr_array_new_with_free_func ((GDestroyNotify)ide_diagnostic_unref);
+  self->diagnostics_array = g_ptr_array_new_with_free_func (g_object_unref);
 }
diff --git a/src/plugins/xml-pack/ide-xml-validator.h b/src/plugins/xml-pack/ide-xml-validator.h
index e4f4c4872..d4d967c94 100644
--- a/src/plugins/xml-pack/ide-xml-validator.h
+++ b/src/plugins/xml-pack/ide-xml-validator.h
@@ -14,6 +14,8 @@
  *
  * 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
@@ -21,7 +23,7 @@
 #include <glib-object.h>
 #include <libxml/parser.h>
 
-#include <ide.h>
+#include <libide-code.h>
 #include "ide-xml-schema-cache-entry.h"
 
 G_BEGIN_DECLS
@@ -30,7 +32,6 @@ G_BEGIN_DECLS
 
 G_DECLARE_FINAL_TYPE (IdeXmlValidator, ide_xml_validator, IDE, XML_VALIDATOR, IdeObject)
 
-IdeXmlValidator       *ide_xml_validator_new        (IdeContext       *context);
 IdeXmlSchemaKind       ide_xml_validator_get_kind   (IdeXmlValidator  *self);
 gboolean               ide_xml_validator_set_schema (IdeXmlValidator  *self,
                                                      IdeXmlSchemaKind  kind,
diff --git a/src/plugins/xml-pack/meson.build b/src/plugins/xml-pack/meson.build
index 5ae92897d..9338eae07 100644
--- a/src/plugins/xml-pack/meson.build
+++ b/src/plugins/xml-pack/meson.build
@@ -1,12 +1,6 @@
-if get_option('with_xml_pack')
+if get_option('plugin_xml_pack')
 
-xml_pack_resources = gnome.compile_resources(
-  'xml-pack-resources',
-  'xml-pack.gresource.xml',
-  c_name: 'ide_xml'
-)
-
-xml_pack_sources = [
+plugins_sources += files([
   'ide-xml-analysis.c',
   'ide-xml-completion-attributes.c',
   'ide-xml-completion-values.c',
@@ -38,9 +32,14 @@ xml_pack_sources = [
   'ide-xml-validator.c',
   'ide-xml.c',
   'xml-pack-plugin.c',
-]
+])
+
+plugin_xml_pack_resources = gnome.compile_resources(
+  'gbp-xml-pack-resources',
+  'xml-pack.gresource.xml',
+  c_name: 'gbp_xml_pack',
+)
 
-gnome_builder_plugins_sources += files(xml_pack_sources)
-gnome_builder_plugins_sources += xml_pack_resources[0]
+plugins_sources += plugin_xml_pack_resources[0]
 
 endif
diff --git a/src/plugins/xml-pack/xml-pack-plugin.c b/src/plugins/xml-pack/xml-pack-plugin.c
index 367d7119a..d2c8fb037 100644
--- a/src/plugins/xml-pack/xml-pack-plugin.c
+++ b/src/plugins/xml-pack/xml-pack-plugin.c
@@ -1,6 +1,6 @@
 /* xml-pack-plugin.c
  *
- * Copyright 2015 Christian Hergert <christian hergert me>
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
  *
  * 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
@@ -14,24 +14,38 @@
  *
  * 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
  */
 
+#include "config.h"
+
 #include <libpeas/peas.h>
+#include <libide-code.h>
+#include <libide-sourceview.h>
 
 #include "ide-xml-completion-provider.h"
 #include "ide-xml-diagnostic-provider.h"
 #include "ide-xml-highlighter.h"
 #include "ide-xml-indenter.h"
-#include "ide-xml-service.h"
 #include "ide-xml-symbol-resolver.h"
 
-void
-ide_xml_register_types (PeasObjectModule *module)
+_IDE_EXTERN void
+_ide_xml_register_types (PeasObjectModule *module)
 {
-  peas_object_module_register_extension_type (module, IDE_TYPE_COMPLETION_PROVIDER, 
IDE_TYPE_XML_COMPLETION_PROVIDER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_DIAGNOSTIC_PROVIDER, 
IDE_TYPE_XML_DIAGNOSTIC_PROVIDER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_HIGHLIGHTER, IDE_TYPE_XML_HIGHLIGHTER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_INDENTER, IDE_TYPE_XML_INDENTER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_SYMBOL_RESOLVER, 
IDE_TYPE_XML_SYMBOL_RESOLVER);
-  peas_object_module_register_extension_type (module, IDE_TYPE_SERVICE, IDE_TYPE_XML_SERVICE);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_COMPLETION_PROVIDER,
+                                              IDE_TYPE_XML_COMPLETION_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_DIAGNOSTIC_PROVIDER,
+                                              IDE_TYPE_XML_DIAGNOSTIC_PROVIDER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_HIGHLIGHTER,
+                                              IDE_TYPE_XML_HIGHLIGHTER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_INDENTER,
+                                              IDE_TYPE_XML_INDENTER);
+  peas_object_module_register_extension_type (module,
+                                              IDE_TYPE_SYMBOL_RESOLVER,
+                                              IDE_TYPE_XML_SYMBOL_RESOLVER);
 }
diff --git a/src/plugins/xml-pack/xml-pack.gresource.xml b/src/plugins/xml-pack/xml-pack.gresource.xml
index 510405d94..e54ca183d 100644
--- a/src/plugins/xml-pack/xml-pack.gresource.xml
+++ b/src/plugins/xml-pack/xml-pack.gresource.xml
@@ -1,9 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <gresources>
-  <gresource prefix="/org/gnome/builder/plugins">
+  <gresource prefix="/plugins/xml-pack">
     <file>xml-pack.plugin</file>
-  </gresource>
-  <gresource prefix="/org/gnome/builder/plugins/xml-pack-plugin">
     <file>schemas/gtkbuilder.rng</file>
   </gresource>
 </gresources>
diff --git a/src/plugins/xml-pack/xml-pack.plugin b/src/plugins/xml-pack/xml-pack.plugin
index 0b5530d7d..2a5e3d1f0 100644
--- a/src/plugins/xml-pack/xml-pack.plugin
+++ b/src/plugins/xml-pack/xml-pack.plugin
@@ -1,18 +1,18 @@
 [Plugin]
-Module=xml-pack-plugin
-Name=XML Auto-Indenter, completion, highlighter, resolver, diagnostics
-Description=Provides language support features for XML
 Authors=Christian Hergert <christian hergert me>
-Copyright=Copyright © 2015 Christian Hergert
 Builtin=true
-X-Indenter-Languages=xml,html
+Copyright=Copyright © 2015-2018 Christian Hergert
+Description=Provides language support features for XML
+Embedded=_ide_xml_register_types
+Module=xml-pack
+Name=XML Auto-Indenter, completion, highlighter, resolver, diagnostics
+X-Completion-Provider-Languages-Priority=0
+X-Completion-Provider-Languages=xml,html
+X-Diagnostic-Provider-Languages-Priority=0
+X-Diagnostic-Provider-Languages=xml,html
+X-Highlighter-Languages-Priority=0
+X-Highlighter-Languages=xml,html
 X-Indenter-Languages-Priority=0
-X-Symbol-Resolver-Languages=xml,html
+X-Indenter-Languages=xml,html
 X-Symbol-Resolver-Languages-Priority=0
-X-Highlighter-Languages=xml,html
-X-Highlighter-Languages-Priority=0
-X-Diagnostic-Provider-Languages=xml,html
-X-Diagnostic-Provider-Languages-Priority=0
-X-Completion-Provider-Languages=xml,html
-X-Completion-Provider-Languages-Priority=0
-Embedded=ide_xml_register_types
+X-Symbol-Resolver-Languages=xml,html
diff --git a/src/tests/data/test-ide-compile-commands.json b/src/tests/data/test-compile-commands.json
similarity index 100%
rename from src/tests/data/test-ide-compile-commands.json
rename to src/tests/data/test-compile-commands.json
diff --git a/src/tests/data/project1/project1.doap b/src/tests/data/test.doap
similarity index 100%
rename from src/tests/data/project1/project1.doap
rename to src/tests/data/test.doap
diff --git a/src/tests/meson.build b/src/tests/meson.build
index 901874f8c..0b409f2b3 100644
--- a/src/tests/meson.build
+++ b/src/tests/meson.build
@@ -10,7 +10,7 @@ typelib_dirs = [
   join_paths(gsv_libdir, 'girepository-1.0'),
 ]
 
-ide_test_env = [
+test_env = [
   'GI_TYPELIB_PATH="@0@:$(GI_TYPELIB_PATH)"'.format(':'.join(typelib_dirs)),
   'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()),
   'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()),
@@ -19,194 +19,86 @@ ide_test_env = [
   'GSETTINGS_SCHEMA_DIR=@0@/data/gsettings'.format(meson.build_root()),
   'PYTHONDONTWRITEBYTECODE=yes',
   'MALLOC_CHECK_=2',
-#  'MALLOC_PERTURB_=$((${RANDOM:-256} % 256))',
 ]
-ide_test_cflags = [
+
+test_cflags = [
   '-DTEST_DATA_DIR="@0@/data/"'.format(meson.current_source_dir()),
   '-I' + join_paths(meson.source_root(), 'src'),
 ]
 
-ide_test_deps = [
-  libide_dep,
-  libpeas_dep,
-  gnome_builder_plugins_dep,
-]
-
-
-ide_compile_commands = executable('test-ide-compile-commands', 'test-ide-compile-commands.c',
-        c_args: ide_test_cflags,
-  dependencies: [ ide_test_deps ],
-)
-test('test-ide-compile-commands', ide_compile_commands, env: ide_test_env)
-
-
-ide_context = executable('test-ide-context', 'test-ide-context.c',
-        c_args: ide_test_cflags,
-  dependencies: [ide_test_deps]
-)
-test('test-ide-context', ide_context, env: ide_test_env)
-
-
-ide_runtime = executable('test-ide-runtime', 'test-ide-runtime.c',
-        c_args: ide_test_cflags,
-  dependencies: [ide_test_deps]
-)
-test('test-ide-runtime', ide_runtime, env: ide_test_env)
-
-
-ide_buffer_manager = executable('test-ide-buffer-manager',
-  'test-ide-buffer-manager.c',
-  c_args: ide_test_cflags,
-  dependencies: [
-    ide_test_deps,
-  ],
-)
-test('test-ide-buffer-manager', ide_buffer_manager,
-  env: ide_test_env,
-)
-
-
-ide_buffer = executable('test-ide-buffer',
-  'test-ide-buffer.c',
-  c_args: ide_test_cflags,
-  dependencies: [
-    ide_test_deps,
-  ],
-)
-test('test-ide-buffer', ide_buffer,
-  env: ide_test_env,
-)
-
 
-ide_doap = executable('test-ide-doap',
-  'test-ide-doap.c',
-  c_args: ide_test_cflags,
-  dependencies: [
-    ide_test_deps,
-  ],
-)
-test('test-ide-doap', ide_doap,
-  env: ide_test_env,
+test_libide_core = executable('test-libide-core', 'test-libide-core.c',
+        c_args: test_cflags,
+  dependencies: [ libide_core_dep ],
 )
+test('test-libide-core', test_libide_core, env: test_env)
 
 
-ide_file_settings = executable('test-ide-file-settings',
-  'test-ide-file-settings.c',
-  c_args: ide_test_cflags,
-  dependencies: [
-    ide_test_deps,
-  ],
-)
-test('test-ide-file-settings', ide_file_settings,
-  env: ide_test_env,
+test_snippet_parser = executable('test-snippet-parser', 'test-snippet-parser.c',
+        c_args: test_cflags,
+  dependencies: [ libide_sourceview_dep ],
 )
+test('test-snippet-parser', test_snippet_parser, env: test_env)
 
 
-ide_indenter = executable('test-ide-indenter',
-  'test-ide-indenter.c',
-  c_args: ide_test_cflags,
-  dependencies: [
-    ide_test_deps,
-  ],
-)
-#test('test-ide-indenter', ide_indenter,
-  #env: ide_test_env,
-#)
-
-
-ide_vcs_uri = executable('test-ide-vcs-uri',
-  'test-ide-vcs-uri.c',
-  c_args: ide_test_cflags,
-  dependencies: [
-    ide_test_deps,
-  ],
-)
-test('test-ide-vcs-uri', ide_vcs_uri,
-  env: ide_test_env,
-)
-
-
-ide_uri = executable('test-ide-uri',
-  'test-ide-uri.c',
-  c_args: ide_test_cflags,
-  dependencies: [
-    ide_test_deps,
-  ],
-)
-test('test-ide-uri', ide_uri,
-  env: ide_test_env,
+test_line_reader = executable('test-line-reader', 'test-line-reader.c',
+        c_args: test_cflags,
+  dependencies: [ libide_io_dep ],
 )
+test('test-line-reader', test_line_reader, env: test_env)
 
 
-test_vim = executable('test-vim',
-  'test-vim.c',
-  c_args: ide_test_cflags,
-  dependencies: [
-    ide_test_deps,
-  ],
+test_text_iter = executable('test-text-iter', 'test-text-iter.c',
+        c_args: test_cflags,
+  dependencies: [ libide_sourceview_dep ],
 )
-#test('test-vim', test_vim,
-#  env: ide_test_env,
-#)
+test('test-text-iter', test_text_iter, env: test_env)
 
 
-test_snippet_parser = executable('test-snippet-parser',
-  'test-snippet-parser.c',
-  c_args: ide_test_cflags,
-  dependencies: [
-    ide_test_deps,
-  ],
+test_vcs_uri = executable('test-vcs-uri', 'test-vcs-uri.c',
+        c_args: test_cflags,
+  dependencies: [ libide_vcs_dep ],
 )
-#test('test-snippet-parser', test_snippet_parser,
-#  env: ide_test_env,
-#)
+test('test-vcs-uri', test_vcs_uri, env: test_env)
 
 
-test_ide_glib = executable('test-ide-glib', 'test-ide-glib.c',
-  c_args: ide_test_cflags,
-  dependencies: [ ide_test_deps ],
+test_task = executable('test-task', 'test-task.c',
+        c_args: test_cflags,
+  dependencies: [ libide_threading_dep ],
 )
-test('test-ide-glib', test_ide_glib, env: ide_test_env)
+test('test-task', test_task, env: test_env)
 
 
-test_line_reader = executable('test-line-reader', 'test-line-reader.c',
-  c_args: ide_test_cflags,
-  dependencies: [ ide_test_deps ],
+test_subprocess_launcher = executable('test-subprocess-launcher', 'test-subprocess-launcher.c',
+        c_args: test_cflags,
+  dependencies: [ libide_threading_dep ],
 )
-test('test-line-reader', test_line_reader, env: ide_test_env)
+test('test-subprocess-launcher', test_subprocess_launcher, env: test_env)
 
 
-test_ide_task = executable('test-ide-task', 'test-ide-task.c',
-  c_args: ide_test_cflags,
-  dependencies: [ ide_test_deps ],
+test_gfile = executable('test-gfile', 'test-gfile.c',
+        c_args: test_cflags,
+  dependencies: [ libide_io_dep ],
 )
-test('test-ide-task', test_ide_task, env: ide_test_env)
+test('test-gfile', test_gfile, env: test_env)
 
 
-test_iter = executable('test-iter', 'test-iter.c',
-  c_args: ide_test_cflags,
-  dependencies: [ ide_test_deps ],
+test_doap = executable('test-doap', 'test-doap.c',
+        c_args: test_cflags,
+  dependencies: [ libide_projects_dep ],
 )
-test('test-iter', test_iter, env: ide_test_env)
+test('test-doap', test_doap, env: test_env)
 
 
-test_backoff = executable('test-backoff', 'test-backoff.c',
-  c_args: ide_test_cflags,
-  dependencies: [ ide_test_deps ],
+test_compile_commands = executable('test-compile-commands', 'test-compile-commands.c',
+        c_args: test_cflags,
+  dependencies: [ libide_foundry_dep ],
 )
-test('test-backoff', test_backoff, env: ide_test_env)
+test('test-compile-commands', test_compile_commands, env: test_env)
 
 
-test_hdr_format = executable('test-hdr-format', [
-  'test-hdr-format.c',
-  '../plugins/c-pack/c-parse-helper.c',
-],
-        c_args: ide_test_cflags,
-  dependencies: [ ide_test_deps ],
-)
-
 test_completion_fuzzy = executable('test-completion-fuzzy', 'test-completion-fuzzy.c',
-        c_args: ide_test_cflags,
-  dependencies: [ ide_test_deps ],
+        c_args: test_cflags,
+  dependencies: [ libide_sourceview_dep ],
 )
-test('test-completion-fuzzy', test_completion_fuzzy, env: ide_test_env)
+test('test-completion-fuzzy', test_completion_fuzzy, env: test_env)
diff --git a/src/tests/test-compile-commands.c b/src/tests/test-compile-commands.c
new file mode 100644
index 000000000..2c50f2a0d
--- /dev/null
+++ b/src/tests/test-compile-commands.c
@@ -0,0 +1,80 @@
+/* test-ide-compile-commands.c
+ *
+ * Copyright 2017-2019 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
+ */
+
+#include <libide-foundry.h>
+
+static void
+test_compile_commands_basic (void)
+{
+  g_autoptr(IdeCompileCommands) commands = NULL;
+  g_autoptr(GFile) missing = g_file_new_for_path ("missing");
+  g_autoptr(GFile) data_file = NULL;
+  g_autoptr(GFile) expected_file = NULL;
+  g_autoptr(GFile) dir = NULL;
+  g_autoptr(GFile) vala = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *data_path = NULL;
+  g_autofree gchar *dir_path = NULL;
+  g_auto(GStrv) cmdstrv = NULL;
+  g_auto(GStrv) valastrv = NULL;
+  gboolean r;
+
+  commands = ide_compile_commands_new ();
+
+  /* Test missing info before we've loaded */
+  g_assert (NULL == ide_compile_commands_lookup (commands, missing, NULL, NULL, NULL));
+
+  /* Now load our test file */
+  data_path = g_build_filename (TEST_DATA_DIR, "test-compile-commands.json", NULL);
+  data_file = g_file_new_for_path (data_path);
+  r = ide_compile_commands_load (commands, data_file, NULL, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (r, ==, TRUE);
+
+  /* Now lookup a file that should exist in the database */
+  expected_file = g_file_new_for_path ("/build/gnome-builder/subprojects/libgd/libgd/gd-types-catalog.c");
+  cmdstrv = ide_compile_commands_lookup (commands, expected_file, NULL, &dir, &error);
+  g_assert_no_error (error);
+  g_assert (cmdstrv != NULL);
+  /* ccache cc should have been removed. */
+  /* relative -I paths should have been resolved */
+  g_assert_cmpstr (cmdstrv[0], ==, "-I/build/gnome-builder/build/subprojects/libgd/libgd/gd@sha");
+  dir_path = g_file_get_path (dir);
+  g_assert_cmpstr (dir_path, ==, "/build/gnome-builder/build");
+
+  /* Vala files don't need to match on exact filename, just something dot vala */
+  vala = g_file_new_for_path ("whatever.vala");
+  valastrv = ide_compile_commands_lookup (commands, vala, NULL, NULL, &error);
+  g_assert_no_error (error);
+  g_assert (valastrv != NULL);
+  g_assert_cmpstr (valastrv[0], ==, "--pkg");
+  g_assert_cmpstr (valastrv[1], ==, "json-glib-1.0");
+  g_assert_cmpstr (valastrv[2], ==, "--pkg");
+  g_assert_cmpstr (valastrv[3], ==, "gtksourceview-4");
+}
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  g_test_add_func ("/Ide/CompileCommands/basic", test_compile_commands_basic);
+  return g_test_run ();
+}
diff --git a/src/tests/test-completion-fuzzy.c b/src/tests/test-completion-fuzzy.c
index 00fa0262f..61a0088b5 100644
--- a/src/tests/test-completion-fuzzy.c
+++ b/src/tests/test-completion-fuzzy.c
@@ -18,7 +18,7 @@
  * SPDX-License-Identifier: GPL-3.0-or-later
  */
 
-#include <ide.h>
+#include <libide-sourceview.h>
 
 static void
 test_fuzzy_match (void)
diff --git a/src/tests/test-doap.c b/src/tests/test-doap.c
new file mode 100644
index 000000000..71765a146
--- /dev/null
+++ b/src/tests/test-doap.c
@@ -0,0 +1,78 @@
+/* test-doap.c
+ *
+ * Copyright 2015-2019 Christian Hergert <christian hergert me>
+ *
+ * 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
+ */
+
+#include <libide-projects.h>
+
+static void
+test_load_from_file (void)
+{
+  IdeDoap *doap;
+  GList *list;
+  IdeDoapPerson *person;
+  GError *error = NULL;
+  GFile *file;
+  gboolean ret;
+  gchar **langs;
+
+  doap = ide_doap_new ();
+  g_object_add_weak_pointer (G_OBJECT (doap), (gpointer *)&doap);
+
+  file = g_file_new_for_path (TEST_DATA_DIR"/test.doap");
+
+  ret = ide_doap_load_from_file (doap, file, NULL, &error);
+  g_assert_no_error (error);
+  g_assert_true (ret);
+
+  g_assert_cmpstr (ide_doap_get_name (doap), ==, "Project One");
+  g_assert_cmpstr (ide_doap_get_shortdesc (doap), ==, "Short Description of Project1");
+  g_assert_cmpstr (ide_doap_get_description (doap), ==, "Long Description");
+  g_assert_cmpstr (ide_doap_get_homepage (doap), ==, "https://example.org/";);
+  g_assert_cmpstr (ide_doap_get_download_page (doap), ==, "https://download.example.org/";);
+  g_assert_cmpstr (ide_doap_get_bug_database (doap), ==, "https://bugs.example.org/";);
+
+  langs = ide_doap_get_languages (doap);
+  g_assert (langs != NULL);
+  g_assert_cmpstr (langs [0], ==, "C");
+  g_assert_cmpstr (langs [1], ==, "JavaScript");
+  g_assert_cmpstr (langs [2], ==, "Python");
+
+  list = ide_doap_get_maintainers (doap);
+  g_assert (list != NULL);
+  g_assert (list->data != NULL);
+  g_assert (list->next == NULL);
+
+  person = list->data;
+  g_assert_cmpstr (ide_doap_person_get_name (person), ==, "Some Name");
+  g_assert_cmpstr (ide_doap_person_get_email (person), ==, "example example org");
+
+  g_object_unref (doap);
+  g_assert (doap == NULL);
+
+  g_clear_object (&file);
+}
+
+gint
+main (int argc,
+      char *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  g_test_add_func ("/Ide/Doap/load_from_file", test_load_from_file);
+  return g_test_run ();
+}
diff --git a/src/tests/test-gfile.c b/src/tests/test-gfile.c
new file mode 100644
index 000000000..0bfe78ba8
--- /dev/null
+++ b/src/tests/test-gfile.c
@@ -0,0 +1,42 @@
+#include <libide-io.h>
+
+static void
+test_uncanonical_file (void)
+{
+  static const struct {
+    const gchar *file;
+    const gchar *other;
+    const gchar *result;
+  } tests[] = {
+    { 
"/home/alberto/.var/app/org.gnome.Builder/cache/gnome-builder/projects/gtask-example/builds/org.gnome.Gtask-Example.json-0601fcfb2fbf01231dd228e0b218301c589ae573-local-flatpak-org.gnome.Platform-x86_64-master",
+      "/home/alberto/Projects/gtask-example/src/main.c",
+      
"/home/alberto/.var/app/org.gnome.Builder/cache/gnome-builder/projects/gtask-example/builds/org.gnome.Gtask-Example.json-0601fcfb2fbf01231dd228e0b218301c589ae573-local-flatpak-org.gnome.Platform-x86_64-master/../../../../../../../../../Projects/gtask-example/src/main.c"
 },
+    { "/home/xtian/foo",
+      "/home/xtian/foo/bar",
+      "/home/xtian/foo/bar" },
+    { "/home/xtian/foo",
+      "/home/xtian/bar",
+      "/home/xtian/foo/../bar" },
+    { "/home/xtian/foo",
+      "/",
+      "/home/xtian/foo/../../../" },
+  };
+
+  for (guint i = 0; i < G_N_ELEMENTS (tests); i++)
+    {
+      g_autoptr(GFile) file = g_file_new_for_path (tests[i].file);
+      g_autoptr(GFile) other = g_file_new_for_path (tests[i].other);
+      g_autofree gchar *result = ide_g_file_get_uncanonical_relative_path (file, other);
+
+      g_assert_cmpstr (tests[i].result, ==, result);
+    }
+}
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  g_test_add_func ("/Ide/GLib/uncanonical-file", test_uncanonical_file);
+  return g_test_run ();
+}
diff --git a/src/tests/test-libide-core.c b/src/tests/test-libide-core.c
new file mode 100644
index 000000000..703a3fc9e
--- /dev/null
+++ b/src/tests/test-libide-core.c
@@ -0,0 +1,297 @@
+/* test-libide-core.c
+ *
+ * Copyright 2018-2019 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
+ */
+
+#include <libide-core.h>
+
+#if 0
+static void
+dump_tree_foreach_cb (gpointer data,
+                      gpointer user_data)
+{
+  guint *depth = user_data;
+  g_autofree gchar *str = g_strnfill (*depth * 2, ' ');
+
+  g_assert (IDE_IS_OBJECT (data));
+  g_assert (depth != NULL);
+
+  (*depth)++;
+  g_printerr ("%s<%s at %p>\n", str, G_OBJECT_TYPE_NAME (data), data);
+  ide_object_foreach (data, dump_tree_foreach_cb, depth);
+  (*depth)--;
+}
+
+static void
+dump_tree (IdeObject *root)
+{
+  guint depth = 1;
+  g_printerr ("\n");
+  g_printerr ("<%s at %p>\n", G_OBJECT_TYPE_NAME (root), root);
+  ide_object_foreach (root, dump_tree_foreach_cb, &depth);
+}
+#endif
+
+static void
+test_ide_object_basic (void)
+{
+  IdeObject *root = ide_object_new (IDE_TYPE_OBJECT, NULL);
+  IdeObject *child1 = ide_object_new (IDE_TYPE_OBJECT, root);
+  IdeObject *child2 = ide_object_new (IDE_TYPE_OBJECT, root);
+  IdeObject *child3 = ide_object_new (IDE_TYPE_OBJECT, root);
+  IdeObject *toplevel = ide_object_ref_root (child3);
+  GCancellable *cancel1 = ide_object_ref_cancellable (child1);
+
+  g_object_add_weak_pointer (G_OBJECT (root), (gpointer *)&root);
+  g_object_add_weak_pointer (G_OBJECT (child1), (gpointer *)&child1);
+  g_object_add_weak_pointer (G_OBJECT (child2), (gpointer *)&child2);
+  g_object_add_weak_pointer (G_OBJECT (child3), (gpointer *)&child3);
+
+  g_assert (toplevel == root);
+  g_object_unref (toplevel);
+
+  g_object_unref (child1);
+  g_object_unref (child2);
+  g_object_unref (child3);
+
+  g_assert_false (g_cancellable_is_cancelled (cancel1));
+
+  g_assert_nonnull (root);
+  g_assert_nonnull (child1);
+  g_assert_nonnull (child2);
+  g_assert_nonnull (child3);
+
+  g_object_unref (root);
+
+  g_assert_null (root);
+  g_assert_null (child1);
+  g_assert_null (child2);
+  g_assert_null (child3);
+
+  g_assert_true (g_cancellable_is_cancelled (cancel1));
+
+  g_object_unref (cancel1);
+}
+
+static void
+test_ide_object_readd (void)
+{
+  g_autoptr(IdeObject) a = ide_object_new (IDE_TYPE_OBJECT, NULL);
+  g_autoptr(IdeObject) b = ide_object_new (IDE_TYPE_OBJECT, a);
+  g_autoptr(IdeObject) p = ide_object_ref_parent (b);
+
+  g_assert_nonnull (a);
+  g_assert_nonnull (b);
+  g_assert_nonnull (p);
+  g_assert (a == p);
+
+  g_clear_object (&p);
+
+  ide_object_remove (a, b);
+  p = ide_object_ref_parent (b);
+
+  g_assert_nonnull (a);
+  g_assert_nonnull (b);
+  g_assert_null (p);
+
+  g_clear_object (&p);
+
+  ide_object_append (a, b);
+  p = ide_object_ref_parent (b);
+
+  g_assert_nonnull (a);
+  g_assert_nonnull (b);
+  g_assert_nonnull (p);
+  g_assert (a == p);
+
+  g_clear_object (&p);
+
+  ide_object_destroy (a);
+  p = ide_object_ref_parent (b);
+
+  g_assert_nonnull (a);
+  g_assert_nonnull (b);
+  g_assert_null (p);
+}
+
+static void
+destroyed_cb (IdeObject *object,
+              guint     *location)
+{
+  g_assert (IDE_IS_OBJECT (object));
+  (*location)--;
+}
+
+static void
+test_ide_notification_basic (void)
+{
+  IdeObject *root = ide_object_new (IDE_TYPE_OBJECT, NULL);
+  IdeNotifications *messages = ide_object_new (IDE_TYPE_NOTIFICATIONS, root);
+  IdeNotification *message = ide_notification_new ();
+  GIcon *icon = g_icon_new_for_string ("system-run-symbolic", NULL);
+  g_autofree gchar *copy = NULL;
+  gint clear1 = 1;
+  gint clear2 = 1;
+  gint clear3 = 1;
+
+  ide_notifications_add_notification (messages, message);
+
+  g_signal_connect (root, "destroy", G_CALLBACK (destroyed_cb), &clear1);
+  g_signal_connect (messages, "destroy", G_CALLBACK (destroyed_cb), &clear2);
+  g_signal_connect (message, "destroy", G_CALLBACK (destroyed_cb), &clear3);
+
+  g_assert_cmpint (1, ==, G_OBJECT (root)->ref_count);
+  g_assert_cmpint (2, ==, G_OBJECT (messages)->ref_count);
+  g_assert_cmpint (2, ==, G_OBJECT (message)->ref_count);
+
+  g_object_add_weak_pointer (G_OBJECT (root), (gpointer *)&root);
+  g_object_add_weak_pointer (G_OBJECT (icon), (gpointer *)&icon);
+
+  g_assert_true (ide_object_is_root (IDE_OBJECT (root)));
+  g_assert_false (ide_object_is_root (IDE_OBJECT (messages)));
+  g_assert_false (ide_object_is_root (IDE_OBJECT (message)));
+
+  g_assert_null (ide_object_get_parent (root));
+  g_assert (ide_object_get_parent (IDE_OBJECT (messages)) == (gpointer)root);
+  g_assert (ide_object_get_parent (IDE_OBJECT (message)) == (gpointer)messages);
+
+  g_assert_cmpint (1, ==, G_OBJECT (root)->ref_count);
+  g_assert_cmpint (2, ==, G_OBJECT (messages)->ref_count);
+  g_assert_cmpint (2, ==, G_OBJECT (message)->ref_count);
+
+  ide_notification_set_title (message, "Foo");
+  copy = ide_notification_dup_title (message);
+  g_assert_cmpstr (copy, ==, "Foo");
+
+  g_assert_cmpint (1, ==, G_OBJECT (icon)->ref_count);
+  ide_notification_set_icon (message, icon);
+  g_assert_cmpint (2, ==, G_OBJECT (icon)->ref_count);
+  g_object_unref (icon);
+  g_assert_nonnull (icon);
+  g_assert_cmpint (1, ==, G_OBJECT (icon)->ref_count);
+
+  g_assert_cmpint (1, ==, G_OBJECT (root)->ref_count);
+  g_assert_cmpint (2, ==, G_OBJECT (messages)->ref_count);
+  g_assert_cmpint (2, ==, G_OBJECT (message)->ref_count);
+
+  g_object_unref (root);
+  g_assert_null (root);
+
+  g_assert_cmpint (1, ==, G_OBJECT (messages)->ref_count);
+  g_assert_cmpint (1, ==, G_OBJECT (message)->ref_count);
+
+  /* Make sure destruction propagated down the tree */
+  g_assert (ide_object_get_parent (IDE_OBJECT (messages)) == NULL);
+  g_assert (ide_object_get_parent (IDE_OBJECT (message)) == NULL);
+
+  g_assert_true (ide_object_is_root (IDE_OBJECT (messages)));
+  g_assert_true (ide_object_is_root (IDE_OBJECT (message)));
+
+  g_assert_cmpint (clear1, ==, 0);
+  g_assert_cmpint (clear2, ==, 0);
+  g_assert_cmpint (clear3, ==, 0);
+
+  g_object_add_weak_pointer (G_OBJECT (messages), (gpointer *)&messages);
+  g_object_unref (messages);
+  g_assert_null (messages);
+
+  g_assert_cmpint (1, ==, G_OBJECT (message)->ref_count);
+  g_assert (ide_object_get_parent (IDE_OBJECT (message)) == NULL);
+  g_assert_true (ide_object_is_root (IDE_OBJECT (message)));
+
+  g_object_add_weak_pointer (G_OBJECT (message), (gpointer *)&message);
+  g_object_unref (message);
+  g_assert_null (message);
+  g_assert_null (icon);
+
+  g_assert_cmpint (clear1, ==, 0);
+  g_assert_cmpint (clear2, ==, 0);
+  g_assert_cmpint (clear3, ==, 0);
+}
+
+static void
+test_ide_notification_destroy (void)
+{
+  IdeObject *root = ide_object_new (IDE_TYPE_OBJECT, NULL);
+  IdeNotifications *messages = ide_object_new (IDE_TYPE_NOTIFICATIONS, root);
+  IdeNotification *message = ide_notification_new ();
+  IdeObject *root_copy = root;
+  gint clear1 = 1;
+  gint clear2 = 1;
+  gint clear3 = 1;
+
+  ide_notifications_add_notification (messages, message);
+
+  g_signal_connect (root, "destroy", G_CALLBACK (destroyed_cb), &clear1);
+  g_signal_connect (messages, "destroy", G_CALLBACK (destroyed_cb), &clear2);
+  g_signal_connect (message, "destroy", G_CALLBACK (destroyed_cb), &clear3);
+
+  g_assert_cmpint (1, ==, G_OBJECT (root)->ref_count);
+  g_assert_cmpint (2, ==, G_OBJECT (messages)->ref_count);
+  g_assert_cmpint (2, ==, G_OBJECT (message)->ref_count);
+
+  g_object_add_weak_pointer (G_OBJECT (root), (gpointer *)&root);
+  g_object_add_weak_pointer (G_OBJECT (message), (gpointer *)&message);
+
+  g_assert_cmpint (1, ==, ide_object_get_n_children (root));
+  g_assert_cmpint (0, <, ide_object_get_n_children (IDE_OBJECT (messages)));
+
+  g_object_unref (message);
+
+  g_assert_cmpint (clear1, ==, 1);
+  g_assert_cmpint (clear2, ==, 1);
+  g_assert_cmpint (clear3, ==, 1);
+
+  ide_object_destroy (root);
+
+  g_assert_cmpint (0, ==, ide_object_get_n_children (root_copy));
+  g_assert_cmpint (0, ==, ide_object_get_n_children (IDE_OBJECT (messages)));
+
+  /* destroy should have caused this to dispose, thereby clearing
+   * any weak pointers.
+   */
+  g_assert_null (message);
+
+  g_object_add_weak_pointer (G_OBJECT (messages), (gpointer *)&messages);
+  g_object_unref (messages);
+
+  g_assert_cmpint (clear1, ==, 0);
+  g_assert_cmpint (clear2, ==, 0);
+  g_assert_cmpint (clear3, ==, 0);
+
+  g_assert_null (root); /* weak cleared from dispose */
+  g_assert_null (messages);
+  g_assert_null (message);
+
+  g_assert_cmpint (G_OBJECT (root_copy)->ref_count, ==, 1);
+  g_object_add_weak_pointer (G_OBJECT (root_copy), (gpointer *)&root_copy);
+  g_object_unref (root_copy);
+  g_assert_null (root_copy);
+}
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  g_test_add_func ("/libide-core/IdeObject/basic", test_ide_object_basic);
+  g_test_add_func ("/libide-core/IdeObject/re-add", test_ide_object_readd);
+  g_test_add_func ("/libide-core/IdeNotification/basic", test_ide_notification_basic);
+  g_test_add_func ("/libide-core/IdeNotification/destroy", test_ide_notification_destroy);
+  return g_test_run ();
+}
diff --git a/src/tests/test-line-reader.c b/src/tests/test-line-reader.c
index b29446d7d..df19f5ff7 100644
--- a/src/tests/test-line-reader.c
+++ b/src/tests/test-line-reader.c
@@ -16,11 +16,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
-#ifdef G_DISABLE_ASSERT
-# undef G_DISABLE_ASSERT
-#endif
-
-#include <ide.h>
+#include <libide-io.h>
 #include <string.h>
 
 static void
diff --git a/src/tests/test-snippet-parser.c b/src/tests/test-snippet-parser.c
index 53412dba5..93dfd605e 100644
--- a/src/tests/test-snippet-parser.c
+++ b/src/tests/test-snippet-parser.c
@@ -1,8 +1,6 @@
-#include <ide.h>
+#include <libide-sourceview.h>
 #include <stdlib.h>
 
-#include "snippets/ide-snippet-parser.h"
-
 gint
 main (gint   argc,
       gchar *argv[])
diff --git a/src/tests/test-subprocess-launcher.c b/src/tests/test-subprocess-launcher.c
new file mode 100644
index 000000000..c1cfe0413
--- /dev/null
+++ b/src/tests/test-subprocess-launcher.c
@@ -0,0 +1,186 @@
+/* test-subprocess-launcher.c
+ *
+ * Copyright 2016-2019 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
+ */
+
+#include <fcntl.h>
+#include <glib/gstdio.h>
+#include <libide-threading.h>
+#include <unistd.h>
+
+static void
+test_basic (void)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autoptr(IdeSubprocess) process = NULL;
+  g_autoptr(GError) error = NULL;
+
+  launcher = ide_subprocess_launcher_new (0);
+  g_assert (launcher != NULL);
+
+  ide_subprocess_launcher_push_argv (launcher, "true");
+
+  process = ide_subprocess_launcher_spawn (launcher, NULL, &error);
+  g_assert (process != NULL);
+  g_assert (error == NULL);
+  g_assert_cmpint (ide_subprocess_wait_check (process, NULL, &error), !=, 0);
+}
+
+static void
+test_communicate (void)
+{
+  IdeSubprocessLauncher *launcher;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *stdout_buf = NULL;
+  gboolean r;
+
+  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE);
+  ide_subprocess_launcher_push_argv (launcher, "ls");
+
+  subprocess = ide_subprocess_launcher_spawn (launcher, NULL, &error);
+  g_assert_no_error (error);
+  g_assert (subprocess != NULL);
+
+  r = ide_subprocess_communicate_utf8 (subprocess, NULL, NULL, &stdout_buf, NULL, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (r, ==, TRUE);
+
+  g_assert (stdout_buf != NULL);
+  g_assert (g_utf8_validate (stdout_buf, -1, NULL));
+}
+
+static void
+test_stdout_fd (void)
+{
+  IdeSubprocessLauncher *launcher;
+  g_autoptr(IdeSubprocess) subprocess = NULL;
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *pattern = NULL;
+  gchar buffer[4096];
+  gboolean r;
+  gint fd;
+
+  launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDERR_SILENCE);
+  ide_subprocess_launcher_push_argv (launcher, "ls");
+
+  pattern = g_build_filename (g_get_tmp_dir (), "makecache-XXXXXX", NULL);
+  fd = g_mkstemp (pattern);
+  g_assert_cmpint (fd, !=, -1);
+
+  ide_subprocess_launcher_take_stdout_fd (launcher, dup (fd));
+
+  subprocess = ide_subprocess_launcher_spawn (launcher, NULL, &error);
+  g_assert_no_error (error);
+  g_assert (subprocess != NULL);
+
+  r = ide_subprocess_wait (subprocess, NULL, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (r, ==, TRUE);
+
+  r = lseek (fd, 0, SEEK_SET);
+  g_assert_cmpint (r, ==, 0);
+
+  r = read (fd, buffer, sizeof buffer);
+  g_assert_cmpint (r, >, 0);
+
+  r = g_unlink (pattern);
+  g_assert_cmpint (r, ==, 0);
+
+  close (fd);
+}
+
+static int
+check_args (IdeSubprocessLauncher *launcher,
+            const gchar *argv0,
+            ...)
+{
+  va_list args;
+  const gchar * const * actual_argv;
+  guint num_args;
+  gchar *item;
+
+  g_assert (IDE_IS_SUBPROCESS_LAUNCHER (launcher));
+
+  actual_argv = ide_subprocess_launcher_get_argv (launcher);
+
+  if (actual_argv == NULL && argv0 == NULL)
+    return 1;
+  else if (actual_argv == NULL || argv0 == NULL)
+    return 0;
+
+  num_args = 0;
+  if (g_strcmp0 (argv0, actual_argv[num_args++]) != 0)
+    return 0;
+
+  va_start (args, argv0);
+  while (NULL != (item = va_arg (args, gchar *)))
+    {
+      const gchar *next_arg = NULL;
+      next_arg = actual_argv[num_args++];
+      if (g_strcmp0 (next_arg, item) != 0)
+        {
+          va_end (args);
+          return 0;
+        }
+    }
+  va_end (args);
+
+  if (actual_argv[num_args] == NULL)
+    return 1;
+  else
+    return 0;
+}
+
+static void
+test_argv_manipulation (void)
+{
+  g_autoptr(IdeSubprocessLauncher) launcher = NULL;
+  g_autofree gchar *popped = NULL;
+
+  launcher = ide_subprocess_launcher_new (0);
+  g_assert (launcher != NULL);
+  g_object_add_weak_pointer (G_OBJECT (launcher), (gpointer *)&launcher);
+
+  ide_subprocess_launcher_push_argv (launcher, "echo");
+  ide_subprocess_launcher_push_argv (launcher, "world");
+  ide_subprocess_launcher_insert_argv (launcher, 1, "hello");
+  g_assert_cmpint (check_args (launcher, "echo", "hello", "world", NULL), !=, 0);
+
+  ide_subprocess_launcher_replace_argv (launcher, 2, "universe");
+  g_assert_cmpint (check_args (launcher, "echo", "hello", "universe", NULL), !=, 0);
+
+  popped = ide_subprocess_launcher_pop_argv (launcher);
+  g_assert_cmpstr (popped, ==, "universe");
+  g_assert_cmpint (check_args (launcher, "echo", "hello", NULL), !=, 0);
+
+  g_object_unref (launcher);
+  g_assert (launcher == NULL);
+}
+
+gint
+main (gint   argc,
+      gchar *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  g_test_add_func ("/Ide/SubprocessLauncher/basic", test_basic);
+  g_test_add_func ("/Ide/SubprocessLauncher/communicate", test_communicate);
+  g_test_add_func ("/Ide/SubprocessLauncher/argv-manipulation", test_argv_manipulation);
+  g_test_add_func ("/Ide/SubprocessLauncher/take_stdout_fd", test_stdout_fd);
+  return g_test_run ();
+}
diff --git a/src/tests/test-task.c b/src/tests/test-task.c
new file mode 100644
index 000000000..881052efe
--- /dev/null
+++ b/src/tests/test-task.c
@@ -0,0 +1,704 @@
+/* test-task.c
+ *
+ * Copyright 2018-2019 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
+ */
+
+#include <libide-threading.h>
+
+static gboolean
+complete_int (gpointer data)
+{
+  IdeTask *task = data;
+  ide_task_return_int (task, -123);
+  return G_SOURCE_REMOVE;
+}
+
+static void
+check_int (GObject      *object,
+           GAsyncResult *result,
+           gpointer      user_data)
+{
+  g_autoptr(GMainLoop) main_loop = user_data;
+  g_autoptr(GError) error = NULL;
+  gssize ret;
+
+  g_assert (!object || G_IS_OBJECT (object));
+  g_assert (IDE_IS_TASK (result));
+  g_assert (main_loop != NULL);
+
+  ret = ide_task_propagate_int (IDE_TASK (result), &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (ret, ==, -123);
+
+  /* shoudln't switch to true until callback has exited */
+  g_assert_false (ide_task_get_completed (IDE_TASK (result)));
+
+  g_main_loop_quit (main_loop);
+}
+
+static void
+test_ide_task_chain (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  IdeTask *task = ide_task_new (NULL, NULL, NULL, NULL);
+  IdeTask *task2 = ide_task_new (NULL, NULL, check_int, g_main_loop_ref (main_loop));
+
+  /* tests that we can chain the result from the first task to the
+   * second task and get the same answer.
+   */
+
+  g_object_add_weak_pointer (G_OBJECT (task), (gpointer *)&task);
+  g_object_add_weak_pointer (G_OBJECT (task2), (gpointer *)&task2);
+
+  ide_task_chain (task, task2);
+
+  g_timeout_add (0, complete_int, task);
+  g_main_loop_run (main_loop);
+
+  g_assert_true (ide_task_get_completed (task));
+  g_assert_true (ide_task_get_completed (task2));
+
+  g_object_unref (task);
+  g_object_unref (task2);
+
+  g_assert_null (task);
+  g_assert_null (task2);
+}
+
+static void
+test_ide_task_basic (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  IdeTask *task = ide_task_new (NULL, NULL, check_int, g_main_loop_ref (main_loop));
+
+  ide_task_set_priority (task, G_PRIORITY_LOW);
+
+  ide_task_set_source_tag (task, test_ide_task_basic);
+  g_assert (ide_task_get_source_tag (task) == test_ide_task_basic);
+
+  g_object_add_weak_pointer (G_OBJECT (task), (gpointer *)&task);
+
+  g_timeout_add (0, complete_int, task);
+  g_main_loop_run (main_loop);
+
+  g_assert_true (ide_task_get_completed (task));
+  g_object_unref (task);
+
+  g_assert_null (task);
+}
+
+static void
+test_ide_task_no_release (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  IdeTask *task = ide_task_new (NULL, NULL, check_int, g_main_loop_ref (main_loop));
+
+  ide_task_set_release_on_propagate (task, FALSE);
+
+  g_object_add_weak_pointer (G_OBJECT (task), (gpointer *)&task);
+
+  g_timeout_add (0, complete_int, task);
+  g_main_loop_run (main_loop);
+
+  g_assert_true (ide_task_get_completed (task));
+  g_object_unref (task);
+
+  g_assert_null (task);
+}
+
+static void
+test_ide_task_serial (void)
+{
+  g_autoptr(IdeTask) task = NULL;
+  g_autoptr(GError) error = NULL;
+  gboolean r;
+
+  /*
+   * This tests creating a task, returning, and propagating a value
+   * serially without returning to the main loop. (the task will advance
+   * the main context to make this work.
+   */
+
+  task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_assert_false (ide_task_get_completed (task));
+  ide_task_return_boolean (task, TRUE);
+  g_assert_false (ide_task_get_completed (task));
+  r = ide_task_propagate_boolean (task, &error);
+  g_assert_true (ide_task_get_completed (task));
+  g_assert_no_error (error);
+  g_assert_true (r);
+}
+
+static void
+test_ide_task_delayed_chain (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(IdeTask) task2 = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GObject) obj2 = NULL;
+  g_autoptr(GError) error = NULL;
+
+  /* finish task 1, but it won't release resources since still need them
+   * for future chaining.
+   */
+  ide_task_set_release_on_propagate (task, FALSE);
+  ide_task_return_object (task, g_steal_pointer (&obj));
+  g_assert_null (obj);
+  obj = ide_task_propagate_object (task, &error);
+  g_assert_nonnull (obj);
+
+  /* try to chain a task, it should succeed since task still has the obj */
+  ide_task_chain (task, task2);
+  obj2 = ide_task_propagate_object (task2, &error);
+  g_assert_no_error (error);
+  g_assert_nonnull (obj2);
+}
+
+static void
+test_ide_task_delayed_chain_fail (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(IdeTask) task2 = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GObject) obj2 = NULL;
+  g_autoptr(GError) error = NULL;
+
+  /* complete a task with an object, with release_on_propagate set */
+  ide_task_return_object (task, g_steal_pointer (&obj));
+  g_assert_null (obj);
+  obj = ide_task_propagate_object (task, &error);
+  g_assert_nonnull (obj);
+
+  /* try to chain a task, it should fail since task released the obj */
+  ide_task_chain (task, task2);
+  obj2 = ide_task_propagate_object (task2, &error);
+  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_FAILED);
+  g_assert_null (obj2);
+}
+
+static void
+test_ide_task_null_object (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(IdeTask) task2 = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GObject) obj = NULL;
+  g_autoptr(GObject) obj2 = NULL;
+  g_autoptr(GError) error = NULL;
+
+  /* Create a task, return a NULL object for a result. Ensure we got
+   * NULL when propagating and no error.
+   */
+  ide_task_set_release_on_propagate (task, FALSE);
+  ide_task_return_object (task, NULL);
+  obj = ide_task_propagate_object (task, &error);
+  g_assert_null (obj);
+  g_assert_no_error (error);
+
+  /* Now try to chain it, and make sure it is the same */
+  ide_task_chain (task, task2);
+  obj2 = ide_task_propagate_object (task2, &error);
+  g_assert_no_error (error);
+  g_assert_null (obj2);
+}
+
+typedef gchar FooStr;
+GType foo_str_get_type (void);
+G_DEFINE_BOXED_TYPE (FooStr, foo_str, g_strdup, g_free)
+
+static void
+test_ide_task_boxed (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+  g_autofree gchar *ret = NULL;
+
+  ide_task_return_boxed (task, foo_str_get_type (), g_strdup ("Hi there"));
+  ret = ide_task_propagate_boxed (task, &error);
+  g_assert_no_error (error);
+  g_assert_cmpstr (ret, ==, "Hi there");
+}
+
+static void
+test_ide_task_get_cancellable (void)
+{
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (NULL, cancellable, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  g_assert (cancellable == ide_task_get_cancellable (task));
+  ide_task_return_int (task, 123);
+  g_assert (cancellable == ide_task_get_cancellable (task));
+  ide_task_propagate_int (task, &error);
+  g_assert_no_error (error);
+  g_assert (cancellable == ide_task_get_cancellable (task));
+}
+
+static void
+test_ide_task_is_valid (void)
+{
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(IdeTask) task2 = ide_task_new (obj, NULL, NULL, NULL);
+
+  g_assert (ide_task_is_valid (task, NULL));
+  g_assert (!ide_task_is_valid (task, obj));
+  g_assert (!ide_task_is_valid (task2, NULL));
+  g_assert (ide_task_is_valid (task2, obj));
+
+  ide_task_return_int (task, 123);
+  ide_task_return_int (task2, 123);
+}
+
+static void
+test_ide_task_source_object (void)
+{
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GObject) obj2 = NULL;
+  g_autoptr(IdeTask) task = ide_task_new (obj, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  obj2 = g_async_result_get_source_object (G_ASYNC_RESULT (task));
+  g_assert (obj == obj2);
+
+  ide_task_return_boolean (task, TRUE);
+  g_assert_true (ide_task_propagate_boolean (task, &error));
+  g_assert_no_error (error);
+
+  /* default release-on-propagate, source object released now */
+  g_assert_null (g_async_result_get_source_object (G_ASYNC_RESULT (task)));
+}
+
+static void
+test_ide_task_error (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  ide_task_return_new_error (task,
+                             G_IO_ERROR,
+                             G_IO_ERROR_NOT_CONNECTED,
+                             "Not connected");
+  g_assert_false (ide_task_propagate_boolean (task, &error));
+  g_assert_error (error,
+                  G_IO_ERROR,
+                  G_IO_ERROR_NOT_CONNECTED);
+}
+
+static void
+typical_cb (GObject      *object,
+            GAsyncResult *result,
+            gpointer      user_data)
+{
+  g_autoptr(GMainLoop) main_loop = user_data;
+  g_autoptr(GError) error = NULL;
+  gboolean r;
+
+  g_assert (object == NULL);
+  g_assert (IDE_IS_TASK (result));
+  g_assert (main_loop);
+
+  r = ide_task_propagate_boolean (IDE_TASK (result), &error);
+  g_assert_true (r);
+
+  g_main_loop_quit (main_loop);
+}
+
+static gboolean
+complete_in_main (gpointer data)
+{
+  g_autoptr(IdeTask) task = data;
+  g_assert (IDE_IS_TASK (task));
+  ide_task_return_boolean (task, TRUE);
+  return G_SOURCE_REMOVE;
+}
+
+static void
+test_ide_task_typical (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(IdeTask) task = NULL;
+  IdeTask *finalize_check = NULL;
+
+  task = ide_task_new (NULL, NULL, typical_cb, g_main_loop_ref (main_loop));
+
+  /* life-cycle tracking */
+  finalize_check = task;
+  g_object_add_weak_pointer (G_OBJECT (task), (gpointer *)&finalize_check);
+
+  /* simulate some async call */
+  g_timeout_add (0, complete_in_main, g_steal_pointer (&task));
+
+  g_main_loop_run (main_loop);
+
+  g_assert_null (finalize_check);
+}
+
+static void
+test_ide_task_thread_cb (IdeTask      *task,
+                         gpointer      source_object,
+                         gpointer      task_data,
+                         GCancellable *cancellable)
+{
+  g_assert_nonnull (task);
+  g_assert (IDE_IS_TASK (task));
+  g_assert_nonnull (source_object);
+  g_assert (G_IS_OBJECT (source_object));
+  g_assert (G_IS_CANCELLABLE (cancellable));
+
+  ide_task_return_int (task, -123);
+}
+
+static void
+test_ide_task_thread (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (obj, cancellable, check_int, g_main_loop_ref (main_loop));
+
+  ide_task_run_in_thread (task, test_ide_task_thread_cb);
+  g_main_loop_run (main_loop);
+}
+
+static void
+test_ide_task_thread_chained (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (obj, cancellable, check_int, g_main_loop_ref (main_loop));
+  g_autoptr(IdeTask) task2 = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+  gssize ret;
+
+  ide_task_chain (task, task2);
+  ide_task_run_in_thread (task, test_ide_task_thread_cb);
+  g_main_loop_run (main_loop);
+
+  ret = ide_task_propagate_int (task2, &error);
+  g_assert_no_error (error);
+  g_assert_cmpint (ret, ==, -123);
+}
+
+static void
+inc_completed (IdeTask    *task,
+               GParamSpec *pspec,
+               gpointer    user_data)
+{
+  g_autoptr(GMainContext) context = NULL;
+  guint *count = user_data;
+
+  g_assert (IDE_IS_TASK (task));
+  g_assert_nonnull (pspec);
+  g_assert_cmpstr (pspec->name, ==, "completed");
+  g_assert_nonnull (count);
+
+  context = g_main_context_ref_thread_default ();
+  g_assert (g_main_context_default () == context);
+
+  (*count)++;
+}
+
+static void
+test_ide_task_completed (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+  guint count = 0;
+
+  g_signal_connect (task, "notify::completed", G_CALLBACK (inc_completed), &count);
+  ide_task_return_boolean (task, TRUE);
+  g_assert_cmpint (count, ==, 0);
+  g_assert_true (ide_task_propagate_boolean (task, &error));
+  g_assert_no_error (error);
+  g_assert_cmpint (count, ==, 1);
+}
+
+static void
+test_ide_task_completed_threaded (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (obj, cancellable, check_int, g_main_loop_ref (main_loop));
+  guint count = 0;
+
+  g_signal_connect (task, "notify::completed", G_CALLBACK (inc_completed), &count);
+  ide_task_run_in_thread (task, test_ide_task_thread_cb);
+  g_main_loop_run (main_loop);
+  g_assert_cmpint (count, ==, 1);
+}
+
+static void
+test_ide_task_task_data (void)
+{
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+  gint *n = g_new0 (gint, 1);
+
+  ide_task_set_task_data (task, n, g_free);
+  g_assert (ide_task_get_task_data (task) == (gpointer)n);
+  ide_task_return_boolean (task, TRUE);
+  g_assert_nonnull (ide_task_get_task_data (task));
+  g_assert_true (ide_task_propagate_boolean (task, &error));
+  g_assert_no_error (error);
+  /* after propagation, task data should be freed */
+  g_assert_null (ide_task_get_task_data (task));
+}
+
+static void
+test_ide_task_task_data_threaded (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (obj, cancellable, check_int, g_main_loop_ref (main_loop));
+  gint *n = g_new0 (gint, 1);
+
+  ide_task_set_task_data (task, n, g_free);
+  ide_task_run_in_thread (task, test_ide_task_thread_cb);
+  g_main_loop_run (main_loop);
+  g_assert_null (ide_task_get_task_data (task));
+}
+
+static void
+set_in_thread_cb (GObject      *object,
+                  GAsyncResult *result,
+                  gpointer      user_data)
+{
+  g_autoptr(GMainLoop) main_loop = user_data;
+  g_autoptr(GError) error = NULL;
+  IdeTask *task = (IdeTask *)result;
+
+  g_assert (object == NULL);
+  g_assert (IDE_IS_TASK (task));
+  g_assert (main_loop != NULL);
+
+  g_assert (ide_task_get_task_data (task) == GINT_TO_POINTER (0x1234));
+  g_assert_true (ide_task_propagate_boolean (task, &error));
+  g_assert_no_error (error);
+
+  /* we should have this cleared until we return from this func */
+  g_assert_nonnull (ide_task_get_task_data (task));
+  g_assert (ide_task_get_task_data (task) == GINT_TO_POINTER (0x1234));
+
+  g_main_loop_quit (main_loop);
+}
+
+static void
+set_in_thread_worker (IdeTask      *task,
+                      gpointer      source_object,
+                      gpointer      task_data,
+                      GCancellable *cancellable)
+{
+  g_assert (IDE_IS_TASK (task));
+  g_assert (source_object == NULL);
+  g_assert (task_data == NULL);
+  g_assert (!cancellable || G_IS_CANCELLABLE (cancellable));
+
+  /* its invalid to call set_task_data() after return, but okay here.
+   * this obviously invalidates @task_data.
+   */
+  ide_task_set_task_data (task, GINT_TO_POINTER (0x1234), (GDestroyNotify)NULL);
+  ide_task_return_boolean (task, TRUE);
+}
+
+static void
+test_ide_task_task_data_set_in_thread (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(IdeTask) task = ide_task_new (NULL, NULL, set_in_thread_cb, g_main_loop_ref (main_loop));
+
+  ide_task_run_in_thread (task, set_in_thread_worker);
+  g_main_loop_run (main_loop);
+
+  /* and now it should be cleared */
+  g_assert_null (ide_task_get_task_data (task));
+}
+
+static void
+test_ide_task_get_source_object (void)
+{
+  g_autoptr(GObject) obj = g_object_new (G_TYPE_OBJECT, NULL);
+  g_autoptr(IdeTask) task = ide_task_new (obj, NULL, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  g_assert_nonnull (ide_task_get_source_object (task));
+  g_assert (obj == ide_task_get_source_object (task));
+
+  ide_task_return_boolean (task, TRUE);
+
+  g_assert_nonnull (ide_task_get_source_object (task));
+  g_assert (obj == ide_task_get_source_object (task));
+
+  g_assert_true (ide_task_propagate_boolean (task, &error));
+  g_assert_null (ide_task_get_source_object (task));
+}
+
+static void
+test_ide_task_check_cancellable (void)
+{
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (NULL, cancellable, NULL, NULL);
+  g_autoptr(IdeTask) task2 = ide_task_new (NULL, cancellable, NULL, NULL);
+  g_autoptr(GError) error = NULL;
+
+  ide_task_set_check_cancellable (task2, FALSE);
+
+  g_cancellable_cancel (cancellable);
+  ide_task_return_boolean (task, TRUE);
+  ide_task_return_boolean (task2, TRUE);
+  g_assert_false (ide_task_propagate_boolean (task, &error));
+  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+  g_clear_error (&error);
+  g_assert_true (ide_task_propagate_boolean (task2, &error));
+  g_assert_no_error (error);
+}
+
+G_LOCK_DEFINE (cancel_lock);
+
+static void
+test_ide_task_return_on_cancel_worker (IdeTask      *task,
+                                       gpointer      source_object,
+                                       gpointer      task_data,
+                                       GCancellable *cancellable)
+{
+  g_assert (IDE_IS_TASK (task));
+  g_assert (source_object == NULL);
+  g_assert (G_IS_CANCELLABLE (cancellable));
+
+  G_LOCK (cancel_lock);
+  ide_task_return_boolean (task, TRUE);
+  G_UNLOCK (cancel_lock);
+}
+
+static gboolean
+idle_main_loop_quit (gpointer data)
+{
+  GMainLoop *main_loop = data;
+  g_main_loop_quit (main_loop);
+  return G_SOURCE_REMOVE;
+}
+
+static void
+test_ide_task_return_on_cancel_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  g_autoptr(GMainLoop) main_loop = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert (object == NULL);
+  g_assert (IDE_IS_TASK (result));
+
+  g_assert_false (ide_task_propagate_boolean (IDE_TASK (result), &error));
+  g_assert_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
+
+  G_UNLOCK (cancel_lock);
+
+  /* sleep a bit to give the task a chance to hit error paths */
+  g_timeout_add_full (50,
+                      G_PRIORITY_DEFAULT,
+                      idle_main_loop_quit,
+                      g_steal_pointer (&main_loop),
+                      (GDestroyNotify)g_main_loop_unref);
+}
+
+static void
+test_ide_task_return_on_cancel (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+  g_autoptr(GCancellable) cancellable = g_cancellable_new ();
+  g_autoptr(IdeTask) task = ide_task_new (NULL, cancellable,
+                                          test_ide_task_return_on_cancel_cb,
+                                          g_main_loop_ref (main_loop));
+
+  G_LOCK (cancel_lock);
+
+  ide_task_set_return_on_cancel (task, TRUE);
+  ide_task_run_in_thread (task, test_ide_task_return_on_cancel_worker);
+
+  g_cancellable_cancel (cancellable);
+  g_main_loop_run (main_loop);
+}
+
+static void
+test_ide_task_report_new_error_cb (GObject      *object,
+                                   GAsyncResult *result,
+                                   gpointer      user_data)
+{
+  g_autoptr(GMainLoop) main_loop = user_data;
+  g_autoptr(GError) error = NULL;
+
+  g_assert_null (object);
+  g_assert (IDE_IS_TASK (result));
+
+  g_assert_false (ide_task_propagate_boolean (IDE_TASK (result), &error));
+  g_assert_error (error, G_IO_ERROR, 1234);
+
+  g_main_loop_quit (main_loop);
+}
+
+static void
+test_ide_task_report_new_error (void)
+{
+  g_autoptr(GMainLoop) main_loop = g_main_loop_new (NULL, FALSE);
+
+  ide_task_report_new_error (NULL,
+                             test_ide_task_report_new_error_cb,
+                             g_main_loop_ref (main_loop),
+                             test_ide_task_report_new_error,
+                             G_IO_ERROR,
+                             1234,
+                             "Failure message");
+  g_main_loop_run (main_loop);
+}
+
+gint
+main (gint   argc,
+      gchar *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_func ("/Ide/Task/typical", test_ide_task_typical);
+  g_test_add_func ("/Ide/Task/basic", test_ide_task_basic);
+  g_test_add_func ("/Ide/Task/get-cancellable", test_ide_task_get_cancellable);
+  g_test_add_func ("/Ide/Task/is-valid", test_ide_task_is_valid);
+  g_test_add_func ("/Ide/Task/source-object", test_ide_task_source_object);
+  g_test_add_func ("/Ide/Task/chain", test_ide_task_chain);
+  g_test_add_func ("/Ide/Task/delayed-chain", test_ide_task_delayed_chain);
+  g_test_add_func ("/Ide/Task/delayed-chain-fail", test_ide_task_delayed_chain_fail);
+  g_test_add_func ("/Ide/Task/no-release", test_ide_task_no_release);
+  g_test_add_func ("/Ide/Task/serial", test_ide_task_serial);
+  g_test_add_func ("/Ide/Task/null-object", test_ide_task_null_object);
+  g_test_add_func ("/Ide/Task/boxed", test_ide_task_boxed);
+  g_test_add_func ("/Ide/Task/error", test_ide_task_error);
+  g_test_add_func ("/Ide/Task/thread", test_ide_task_thread);
+  g_test_add_func ("/Ide/Task/thread-chained", test_ide_task_thread_chained);
+  g_test_add_func ("/Ide/Task/completed", test_ide_task_completed);
+  g_test_add_func ("/Ide/Task/completed-threaded", test_ide_task_completed_threaded);
+  g_test_add_func ("/Ide/Task/task-data", test_ide_task_task_data);
+  g_test_add_func ("/Ide/Task/task-data-threaded", test_ide_task_task_data_threaded);
+  g_test_add_func ("/Ide/Task/task-data-set-in-thread", test_ide_task_task_data_set_in_thread);
+  g_test_add_func ("/Ide/Task/get-source-object", test_ide_task_get_source_object);
+  g_test_add_func ("/Ide/Task/check-cancellable", test_ide_task_check_cancellable);
+  g_test_add_func ("/Ide/Task/return-on-cancel", test_ide_task_return_on_cancel);
+  g_test_add_func ("/Ide/Task/report-new-error", test_ide_task_report_new_error);
+
+  return g_test_run ();
+}
diff --git a/src/tests/test-text-iter.c b/src/tests/test-text-iter.c
new file mode 100644
index 000000000..24d6f7b2c
--- /dev/null
+++ b/src/tests/test-text-iter.c
@@ -0,0 +1,67 @@
+/* test-text-iter.c
+ *
+ * Copyright © 2018 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/>.
+ */
+
+#include <libide-sourceview.h>
+
+static void
+test_current_symbol (void)
+{
+  g_autoptr(GtkTextBuffer) buffer = GTK_TEXT_BUFFER (gtk_source_buffer_new (NULL));
+  GtkSourceLanguageManager *m = gtk_source_language_manager_get_default ();
+  GtkSourceLanguage *l = gtk_source_language_manager_get_language (m, "c");
+  static const gchar *expected[] = {
+    NULL, NULL, NULL, NULL, NULL,
+    "c", "co", "con", "cons", "const",
+    NULL,
+    "g", "gc", "gch", "gcha", "gchar",
+    NULL, NULL,
+    "s", "st", "str",
+    NULL, NULL, NULL,
+    "g", "g_", "g_s", "g_st", "g_str", "g_strd", "g_strdu", "g_strdup",
+    NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+  };
+
+  gtk_source_buffer_set_language (GTK_SOURCE_BUFFER (buffer), l);
+  gtk_source_buffer_set_highlight_syntax (GTK_SOURCE_BUFFER (buffer), TRUE);
+  gtk_text_buffer_set_text (buffer, "  { const gchar *str = g_strdup (\"something\"); }", -1);
+
+  /* Update syntax data immediately for gsv context classes */
+  while (g_main_context_pending (NULL))
+    g_main_context_iteration (NULL, FALSE);
+
+  for (guint i = 0; i < G_N_ELEMENTS (expected); i++)
+    {
+      g_autofree gchar *word = NULL;
+      GtkTextIter iter;
+
+      gtk_text_buffer_get_iter_at_line_offset (buffer, &iter, 0, i);
+      word = ide_text_iter_current_symbol (&iter, NULL);
+
+      g_assert_cmpstr (word, ==, expected[i]);
+    }
+}
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  gtk_init (&argc, &argv);
+  g_test_add_func ("/Ide/TextIter/current_symbol", test_current_symbol);
+  return g_test_run ();
+}
diff --git a/src/tests/test-vcs-uri.c b/src/tests/test-vcs-uri.c
new file mode 100644
index 000000000..85a1a56aa
--- /dev/null
+++ b/src/tests/test-vcs-uri.c
@@ -0,0 +1,113 @@
+/* test-vcs-uri.c
+ *
+ * Copyright 2015-2019 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
+ */
+
+#include <libide-vcs.h>
+
+typedef struct
+{
+  const gchar *uri;
+  const gchar *expected_scheme;
+  const gchar *expected_user;
+  const gchar *expected_host;
+  const gchar *expected_path;
+  guint        expected_port;
+  const gchar *canonical;
+} UriTest;
+
+static void
+test_sample_uris (void)
+{
+  static const UriTest sample_uris[] = {
+    { "ssh://user host xz:22/path/to/repo.git/", "ssh", "user", "host.xz", "/path/to/repo.git/", 22, 
"ssh://user host xz:22/path/to/repo.git/" },
+    { "ssh://user host xz/path/to/repo.git/", "ssh", "user", "host.xz", "/path/to/repo.git/", 0, "ssh://user 
host xz/path/to/repo.git/" },
+    { "ssh://host.xz:1234/path/to/repo.git/", "ssh", NULL,"host.xz", "/path/to/repo.git/", 1234, 
"ssh://host.xz:1234/path/to/repo.git/" },
+    { "ssh://host.xz/path/to/repo.git/", "ssh", NULL,"host.xz", "/path/to/repo.git/", 0, 
"ssh://host.xz/path/to/repo.git/" },
+    { "ssh://user host xz/path/to/repo.git/", "ssh", "user","host.xz", "/path/to/repo.git/", 0, "ssh://user 
host xz/path/to/repo.git/" },
+    { "ssh://host.xz/path/to/repo.git/", "ssh", NULL,"host.xz", "/path/to/repo.git/", 0, 
"ssh://host.xz/path/to/repo.git/" },
+    { "ssh://user host xz/~user/path/to/repo.git/", "ssh", "user", "host.xz", "~user/path/to/repo.git/", 0, 
"ssh://user host xz/~user/path/to/repo.git/" },
+    { "ssh://host.xz/~user/path/to/repo.git/", "ssh", NULL,"host.xz", "~user/path/to/repo.git/", 0, 
"ssh://host.xz/~user/path/to/repo.git/" },
+    { "ssh://user host xz/~/path/to/repo.git", "ssh", "user", "host.xz", "~/path/to/repo.git",0, "ssh://user 
host xz/~/path/to/repo.git" },
+    { "ssh://host.xz/~/path/to/repo.git", "ssh", NULL, "host.xz", "~/path/to/repo.git", 0, 
"ssh://host.xz/~/path/to/repo.git" },
+    { "user host xz:/path/to/repo.git/", "ssh", "user", "host.xz", "/path/to/repo.git/", 0, "user host 
xz:/path/to/repo.git/" },
+    { "host.xz:/path/to/repo.git/", "ssh", NULL, "host.xz", "/path/to/repo.git/", 0, 
"host.xz:/path/to/repo.git/" },
+    { "user host xz:~user/path/to/repo.git/", "ssh", "user", "host.xz", "~user/path/to/repo.git/", 0, "user 
host xz:~user/path/to/repo.git/" },
+    { "host.xz:~user/path/to/repo.git/", "ssh", NULL, "host.xz", "~user/path/to/repo.git/", 0, 
"host.xz:~user/path/to/repo.git/" },
+    { "user host xz:path/to/repo.git", "ssh", "user", "host.xz", "~/path/to/repo.git", 0, "user host 
xz:path/to/repo.git" },
+    { "host.xz:path/to/repo.git", "ssh", NULL, "host.xz", "~/path/to/repo.git", 0, 
"host.xz:path/to/repo.git" },
+    { "rsync://host.xz/path/to/repo.git/", "rsync", NULL, "host.xz", "/path/to/repo.git/", 0, 
"rsync://host.xz/path/to/repo.git/" },
+    { "git://host.xz/path/to/repo.git/", "git", NULL, "host.xz", "/path/to/repo.git/", 0, 
"git://host.xz/path/to/repo.git/" },
+    { "git://host.xz/~user/path/to/repo.git/", "git", NULL, "host.xz", "~user/path/to/repo.git/", 0, 
"git://host.xz/~user/path/to/repo.git/" },
+    { "http://host.xz/path/to/repo.git/";, "http", NULL, "host.xz", "/path/to/repo.git/", 0, 
"http://host.xz/path/to/repo.git/"; },
+    { "https://host.xz/path/to/repo.git/";, "https", NULL, "host.xz", "/path/to/repo.git/", 0, 
"https://host.xz/path/to/repo.git/"; },
+    { "/path/to/repo.git/", "file", NULL, NULL, "/path/to/repo.git/", 0, "/path/to/repo.git/" },
+    { "path/to/repo.git/", "file", NULL, NULL, "path/to/repo.git/", 0, "path/to/repo.git/" },
+    { "~/path/to/repo.git", "file", NULL, NULL, "~/path/to/repo.git", 0, "~/path/to/repo.git" },
+    { "file:///path/to/repo.git/", "file", NULL, NULL, "/path/to/repo.git/", 0, "file:///path/to/repo.git/" 
},
+    { "file://~/path/to/repo.git/", "file", NULL, NULL, "~/path/to/repo.git/", 0, 
"file://~/path/to/repo.git/" },
+    { "git github com:example/example.git", "ssh", "git", "github.com", "~/example/example.git", 0, "git 
github com:example/example.git" },
+    { NULL }
+  };
+  guint i;
+
+  for (i = 0; sample_uris [i].uri; i++)
+    {
+      g_autoptr(IdeVcsUri) uri = NULL;
+      g_autofree gchar *to_string = NULL;
+
+      uri = ide_vcs_uri_new (sample_uris [i].uri);
+
+      if (uri == NULL)
+        g_error ("Failed to parse %s\n", sample_uris [i].uri);
+
+#if 0
+      g_print ("\n%s (%u)\n"
+               "  scheme: %s\n"
+               "    user: %s\n"
+               "    host: %s\n"
+               "    port: %u\n"
+               "    path: %s\n",
+               sample_uris [i].uri, i,
+               ide_vcs_uri_get_scheme (uri),
+               ide_vcs_uri_get_user (uri),
+               ide_vcs_uri_get_host (uri),
+               ide_vcs_uri_get_port (uri),
+               ide_vcs_uri_get_path (uri));
+#endif
+
+      g_assert (uri != NULL);
+      g_assert_cmpstr (sample_uris [i].expected_scheme, ==, ide_vcs_uri_get_scheme (uri));
+      g_assert_cmpstr (sample_uris [i].expected_user, ==, ide_vcs_uri_get_user (uri));
+      g_assert_cmpstr (sample_uris [i].expected_host, ==, ide_vcs_uri_get_host (uri));
+      g_assert_cmpstr (sample_uris [i].expected_path, ==, ide_vcs_uri_get_path (uri));
+      g_assert_cmpint (sample_uris [i].expected_port, ==, ide_vcs_uri_get_port (uri));
+
+      to_string = ide_vcs_uri_to_string (uri);
+      g_assert_cmpstr (sample_uris [i].canonical, ==, to_string);
+    }
+}
+
+gint
+main (gint argc,
+      gchar *argv[])
+{
+  g_test_init (&argc, &argv, NULL);
+  g_test_add_func ("/Ide/VcsUri/sample_uris", test_sample_uris);
+  return g_test_run ();
+}


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